feat(server/v2): add swagger server component (#23486)

This commit is contained in:
Julien Robert 2025-01-24 10:24:26 +01:00 committed by GitHub
parent 5e58330196
commit 0cc73ba1b2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 1310 additions and 1349 deletions

View File

@ -80,14 +80,6 @@
}
}
},
{
"url": "./tmp-swagger-gen/cosmos/params/v1beta1/query.swagger.json",
"operationIds": {
"rename": {
"Params": "Params"
}
}
},
{
"url": "./tmp-swagger-gen/cosmos/slashing/v1beta1/query.swagger.json",
"operationIds": {
@ -174,6 +166,15 @@
},
{
"url": "./tmp-swagger-gen/cosmos/app/v1alpha1/query.swagger.json"
},
{
"url": "./tmp-swagger-gen/cosmos/protocolpool/v1/query.swagger.json",
"operationIds": {
"rename": {
"Params": "ProtocolPoolParams",
"CommunityPool": "ProtocolPoolCommunityPool"
}
}
}
]
}
}

View File

@ -1,6 +1,19 @@
package docs
import "embed"
import (
"embed"
"io/fs"
)
//go:embed swagger-ui
var SwaggerUI embed.FS
// GetSwaggerFS returns the embedded Swagger UI filesystem
func GetSwaggerFS() fs.FS {
root, err := fs.Sub(SwaggerUI, "swagger-ui")
if err != nil {
panic(err)
}
return root
}

View File

@ -1,10 +1,12 @@
<!doctype html> <!-- Important: must specify -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"> <!-- Important: rapi-doc uses utf8 characters -->
<meta charset="utf-8" />
<!-- Important: rapi-doc uses utf8 characters -->
<script type="module" src="rapidoc-min.js"></script>
</head>
<body>
<rapi-doc spec-url = "swagger.yaml"> </rapi-doc>
<rapi-doc spec-url="swagger.yaml" server-url="http://localhost:1317">
</rapi-doc>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@ -22,6 +22,8 @@ Each entry must include the Github issue reference in the following format:
## [Unreleased]
* [#23486](https://github.com/cosmos/cosmos-sdk/pull/23486) Add `server/v2/api/swagger` server component.
## [v2.0.0-beta.2](https://github.com/cosmos/cosmos-sdk/releases/tag/server/v2.0.0-beta.2)
* [#23411](https://github.com/cosmos/cosmos-sdk/pull/23411) Use only command name in `IsAppRequired()` instead of command usage

View File

@ -23,7 +23,6 @@ import (
"google.golang.org/protobuf/reflect/protoreflect"
"cosmossdk.io/core/transaction"
"cosmossdk.io/log"
"cosmossdk.io/server/v2/appmanager"
)
@ -47,7 +46,7 @@ type queryMetadata struct {
// mountHTTPRoutes registers handlers for from proto HTTP annotations to the http.ServeMux, using runtime.ServeMux as a fallback/
// last ditch effort router.
func mountHTTPRoutes[T transaction.Tx](logger log.Logger, httpMux *http.ServeMux, fallbackRouter *runtime.ServeMux, am appmanager.AppManager[T]) error {
func mountHTTPRoutes[T transaction.Tx](httpMux *http.ServeMux, fallbackRouter *runtime.ServeMux, am appmanager.AppManager[T]) error {
annotationMapping, err := newHTTPAnnotationMapping()
if err != nil {
return err
@ -56,12 +55,12 @@ func mountHTTPRoutes[T transaction.Tx](logger log.Logger, httpMux *http.ServeMux
if err != nil {
return err
}
registerMethods[T](logger, httpMux, am, fallbackRouter, annotationToMetadata)
registerMethods[T](httpMux, am, fallbackRouter, annotationToMetadata)
return nil
}
// registerMethods registers the endpoints specified in the annotation mapping to the http.ServeMux.
func registerMethods[T transaction.Tx](logger log.Logger, mux *http.ServeMux, am appmanager.AppManager[T], fallbackRouter *runtime.ServeMux, annotationToMetadata map[string]queryMetadata) {
func registerMethods[T transaction.Tx](mux *http.ServeMux, am appmanager.AppManager[T], fallbackRouter *runtime.ServeMux, annotationToMetadata map[string]queryMetadata) {
// register the fallback handler. this will run if the mux isn't able to get a match from the registrations below.
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fallbackRouter.ServeHTTP(w, r)

View File

@ -76,7 +76,7 @@ func New[T transaction.Tx](
s.logger = logger.With(log.ModuleKey, s.Name())
s.config = serverCfg
mux := http.NewServeMux()
err := mountHTTPRoutes[T](logger, mux, s.GRPCGatewayRouter, appManager)
err := mountHTTPRoutes(mux, s.GRPCGatewayRouter, appManager)
if err != nil {
return nil, fmt.Errorf("failed to register gRPC gateway annotations: %w", err)
}

View File

@ -0,0 +1,34 @@
package swagger
// Config defines the configuration for the Swagger UI server
type Config struct {
// Enable enables/disables the Swagger UI server
Enable bool `mapstructure:"enable" toml:"enable" comment:"Enable enables/disables the Swagger UI server"`
// Address defines the server address to bind to
Address string `mapstructure:"address" toml:"address" comment:"Address defines the server address to bind to"`
}
// DefaultConfig returns the default configuration
func DefaultConfig() *Config {
return &Config{
Enable: true,
Address: "localhost:8090",
}
}
// CfgOption defines a function for configuring the settings
type CfgOption func(*Config)
// OverwriteDefaultConfig overwrites the default config with the new config.
func OverwriteDefaultConfig(newCfg *Config) CfgOption {
return func(cfg *Config) {
*cfg = *newCfg
}
}
// Disable the grpc server by default (default enabled).
func Disable() CfgOption {
return func(cfg *Config) {
cfg.Enable = false
}
}

View File

@ -0,0 +1,14 @@
/*
Package swagger provides Swagger UI server/v2 component.
Example usage in commands.go:
swaggerServer, err := swaggerv2.New[T](
logger.With(log.ModuleKey, "swagger"),
deps.GlobalConfig,
swaggerv2.CfgOption(func(cfg *swaggerv2.Config) {
cfg.SwaggerUI = docs.SwaggerUI
}),
)
*/
package swagger

View File

@ -0,0 +1,22 @@
package swagger
import (
"io/fs"
"net/http"
)
type swaggerHandler struct {
swaggerFS fs.FS
}
func (h *swaggerHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// CORS headers
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, OPTIONS")
if r.Method == http.MethodOptions {
return
}
http.StripPrefix("/swagger/", http.FileServer(http.FS(h.swaggerFS))).ServeHTTP(w, r)
}

View File

@ -0,0 +1,110 @@
package swagger
import (
"context"
"fmt"
"io/fs"
"net/http"
"cosmossdk.io/core/server"
"cosmossdk.io/core/transaction"
"cosmossdk.io/log"
serverv2 "cosmossdk.io/server/v2"
)
var (
_ serverv2.ServerComponent[transaction.Tx] = (*Server[transaction.Tx])(nil)
_ serverv2.HasConfig = (*Server[transaction.Tx])(nil)
)
const ServerName = "swagger"
// Server represents a Swagger UI server
type Server[T transaction.Tx] struct {
logger log.Logger
config *Config
cfgOptions []CfgOption
server *http.Server
}
// New creates a new Swagger UI server
func New[T transaction.Tx](
logger log.Logger,
swaggerUI fs.FS,
config server.ConfigMap,
cfgOptions ...CfgOption,
) (*Server[T], error) {
s := &Server[T]{
logger: logger.With(log.ModuleKey, ServerName),
cfgOptions: cfgOptions,
}
serverCfg := s.Config().(*Config)
if len(config) > 0 {
if err := serverv2.UnmarshalSubConfig(config, s.Name(), &serverCfg); err != nil {
return s, fmt.Errorf("failed to unmarshal config: %w", err)
}
}
s.config = serverCfg
mux := http.NewServeMux()
mux.Handle("/swagger/", &swaggerHandler{
swaggerFS: swaggerUI,
})
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/swagger/", http.StatusMovedPermanently)
})
s.server = &http.Server{
Addr: s.config.Address,
Handler: mux,
}
return s, nil
}
// Name returns the server's name
func (s *Server[T]) Name() string {
return ServerName
}
// Config returns the server configuration
func (s *Server[T]) Config() any {
if s.config == nil || s.config.Address == "" {
cfg := DefaultConfig()
// overwrite the default config with the provided options
for _, opt := range s.cfgOptions {
opt(cfg)
}
return cfg
}
return s.config
}
// Start starts the server
func (s *Server[T]) Start(ctx context.Context) error {
if !s.config.Enable {
s.logger.Info(fmt.Sprintf("%s server is disabled via config", s.Name()))
return nil
}
s.logger.Info("starting swagger server...", "address", s.config.Address)
if err := s.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
return fmt.Errorf("failed to start swagger server: %w", err)
}
return nil
}
// Stop stops the server
func (s *Server[T]) Stop(ctx context.Context) error {
if !s.config.Enable {
return nil
}
s.logger.Info("stopping swagger server...", "address", s.config.Address)
return s.server.Shutdown(ctx)
}

View File

@ -29,7 +29,7 @@ type Server[T transaction.Tx] struct {
}
// New creates a new telemetry server.
func New[T transaction.Tx](cfg server.ConfigMap, logger log.Logger, enableTelemetry func(), cfgOptions ...CfgOption) (*Server[T], error) {
func New[T transaction.Tx](logger log.Logger, enableTelemetry func(), cfg server.ConfigMap, cfgOptions ...CfgOption) (*Server[T], error) {
srv := &Server[T]{}
serverCfg := srv.Config().(*Config)
if len(cfg) > 0 {

View File

@ -15,6 +15,7 @@ import (
grpcserver "cosmossdk.io/server/v2/api/grpc"
"cosmossdk.io/server/v2/api/grpcgateway"
"cosmossdk.io/server/v2/api/rest"
"cosmossdk.io/server/v2/api/swagger"
"cosmossdk.io/server/v2/api/telemetry"
"cosmossdk.io/server/v2/cometbft"
serverstore "cosmossdk.io/server/v2/store"
@ -24,6 +25,7 @@ import (
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/config"
"github.com/cosmos/cosmos-sdk/client/debug"
"github.com/cosmos/cosmos-sdk/client/docs"
"github.com/cosmos/cosmos-sdk/client/grpc/cmtservice"
nodeservice "github.com/cosmos/cosmos-sdk/client/grpc/node"
"github.com/cosmos/cosmos-sdk/client/keys"
@ -85,6 +87,7 @@ func InitRootCmd[T transaction.Tx](
&telemetry.Server[T]{},
&rest.Server[T]{},
&grpcgateway.Server[T]{},
&swagger.Server[T]{},
)
}
@ -122,7 +125,12 @@ func InitRootCmd[T transaction.Tx](
return nil, err
}
telemetryServer, err := telemetry.New[T](deps.GlobalConfig, logger, sdktelemetry.EnableTelemetry)
telemetryServer, err := telemetry.New[T](logger, sdktelemetry.EnableTelemetry, deps.GlobalConfig)
if err != nil {
return nil, err
}
swaggerServer, err := swagger.New[T](logger, docs.GetSwaggerFS(), deps.GlobalConfig)
if err != nil {
return nil, err
}
@ -168,6 +176,7 @@ func InitRootCmd[T transaction.Tx](
telemetryServer,
restServer,
grpcgatewayServer,
swaggerServer,
)
}

View File

@ -1,32 +1,44 @@
[comet]
# min-retain-blocks defines the minimum block height offset from the current block being committed, such that all blocks past this offset are pruned from CometBFT. A value of 0 indicates that no blocks should be pruned.
min-retain-blocks = 0
# halt-height contains a non-zero block height at which a node will gracefully halt and shutdown that can be used to assist upgrades and testing.
halt-height = 0
# halt-time contains a non-zero minimum block time (in Unix seconds) at which a node will gracefully halt and shutdown that can be used to assist upgrades and testing.
halt-time = 0
# address defines the CometBFT RPC server address to bind to.
address = 'tcp://127.0.0.1:26658'
# transport defines the CometBFT RPC server transport protocol: socket, grpc
transport = 'socket'
# trace enables the CometBFT RPC server to output trace information about its internal operations.
trace = false
# standalone starts the application without the CometBFT node. The node should be started separately.
standalone = false
# index-abci-events defines the set of events in the form {eventType}.{attributeKey}, which informs CometBFT what to index. If empty, all events will be indexed.
index-abci-events = []
# disable-index-abci-events disables the ABCI event indexing done by CometBFT. Useful when relying on the SDK indexer for event indexing, but still want events to be included in FinalizeBlockResponse.
disable-index-abci-events = false
# disable-abci-events disables all ABCI events. Useful when relying on the SDK indexer for event indexing.
disable-abci-events = false
# mempool defines the configuration for the SDK built-in app-side mempool implementations.
[comet.mempool]
# max-txs defines the maximum number of transactions that can be in the mempool. A value of 0 indicates an unbounded mempool, a negative value disables the app-side mempool.
max-txs = -1
# indexer defines the configuration for the SDK built-in indexer implementation.
[comet.indexer]
# Buffer size of the channels used for buffering data sent to indexer go routines.
channel_buffer_size = 1024
@ -34,76 +46,133 @@ channel_buffer_size = 1024
[comet.indexer.target]
[grpc]
# Enable defines if the gRPC server should be enabled.
enable = true
# Address defines the gRPC server address to bind to.
address = 'localhost:9090'
# MaxRecvMsgSize defines the max message size in bytes the server can receive.
# The default value is 10MB.
max-recv-msg-size = 10485760
# MaxSendMsgSize defines the max message size in bytes the server can send.
# The default value is math.MaxInt32.
max-send-msg-size = 2147483647
[grpc-gateway]
# Enable defines if the gRPC-gateway should be enabled.
# Enable defines if the gRPC-Gateway should be enabled.
enable = true
# Address defines the address the gRPC-gateway server binds to.
# Address defines the address the gRPC-Gateway server binds to.
address = 'localhost:1317'
[rest]
# Enable defines if the REST server should be enabled.
enable = true
# Address defines the REST server address to bind to.
address = 'localhost:8080'
[server]
# minimum-gas-prices defines the price which a validator is willing to accept for processing a transaction. A transaction's fees must meet the minimum of any denomination specified in this config (e.g. 0.25token1;0.0001token2).
minimum-gas-prices = '0stake'
[store]
# The type of database for application and snapshots databases.
app-db-backend = 'goleveldb'
[store.options]
# State commitment database type. Currently we support: "iavl" and "iavl-v2"
sc-type = 'iavl'
# Pruning options for state commitment
[store.options.sc-pruning-option]
# Number of recent heights to keep on disk.
keep-recent = 2
# Height interval at which pruned heights are removed from disk.
interval = 100
[store.options.iavl-config]
# CacheSize set the size of the iavl tree cache.
cache-size = 100000
cache-size = 500000
# If true, the tree will work like no fast storage and always not upgrade fast storage.
skip-fast-storage-upgrade = true
[store.options.iavl-v2-config]
# CheckpointInterval set the interval of the checkpoint.
checkpoint-interval = 0
# CheckpointMemory set the memory of the checkpoint.
checkpoint-memory = 0
# StateStorage set the state storage.
state-storage = false
# HeightFilter set the height filter.
height-filter = 0
# EvictionDepth set the eviction depth.
eviction-depth = 0
# PruneRatio set the prune ratio.
prune-ratio = 0.0
# MinimumKeepVersions set the minimum keep versions.
minimum-keep-versions = 0
[swagger]
# Enable enables/disables the Swagger UI server
enable = true
# Address defines the server address to bind to
address = 'localhost:8090'
[telemetry]
# Enable enables the application telemetry functionality. When enabled, an in-memory sink is also enabled by default. Operators may also enabled other sinks such as Prometheus.
enable = true
# Address defines the metrics server address to bind to.
address = 'localhost:7180'
# Prefixed with keys to separate services.
service-name = ''
# Enable prefixing gauge values with hostname.
enable-hostname = false
# Enable adding hostname to labels.
enable-hostname-label = false
# Enable adding service to labels.
enable-service-label = false
# PrometheusRetentionTime, when positive, enables a Prometheus metrics sink. It defines the retention duration in seconds.
prometheus-retention-time = 0
prometheus-retention-time = 600
# GlobalLabels defines a global set of name/value label tuples applied to all metrics emitted using the wrapper functions defined in telemetry package.
# Example:
# [["chain_id", "cosmoshub-1"]]
global-labels = []
# MetricsSink defines the type of metrics backend to use. Default is in memory
metrics-sink = ''
# StatsdAddr defines the address of a statsd server to send metrics to. Only utilized if MetricsSink is set to "statsd" or "dogstatsd".
stats-addr = ''
# DatadogHostname defines the hostname to use when emitting metrics to Datadog. Only utilized if MetricsSink is set to "dogstatsd".
data-dog-hostname = ''