Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions router-tests/testenv/testenv.go
Original file line number Diff line number Diff line change
Expand Up @@ -828,6 +828,7 @@ func configureRouter(listenerAddr string, testConfig *Config, routerConfig *node
core.WithGraphApiToken(graphApiToken),
core.WithDevelopmentMode(true),
core.WithPlayground(true),
core.WithPlaygroundConfig(config.PlaygroundConfig{Enabled: true}),
core.WithEngineExecutionConfig(engineExecutionConfig),
core.WithSecurityConfig(cfg.SecurityConfiguration),
core.WithCacheControlPolicy(cfg.CacheControl),
Expand Down
1 change: 1 addition & 0 deletions router/cmd/instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ func NewRouter(ctx context.Context, params Params, additionalOptions ...core.Opt
core.WithGraphQLPath(cfg.GraphQLPath),
core.WithModulesConfig(cfg.Modules),
core.WithGracePeriod(cfg.GracePeriod),
core.WithPlaygroundConfig(cfg.PlaygroundConfig),
core.WithPlaygroundPath(cfg.PlaygroundPath),
core.WithHealthCheckPath(cfg.HealthCheckPath),
core.WithLivenessCheckPath(cfg.LivenessCheckPath),
Expand Down
6 changes: 3 additions & 3 deletions router/core/graph_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,8 +235,8 @@ func newGraphServer(ctx context.Context, r *Router, routerConfig *nodev1.RouterC

// We mount the playground once here when we don't have a conflict with the websocket handler
// If we have a conflict, we mount the playground during building the individual muxes
if s.playgroundHandler != nil && s.graphqlPath != s.playgroundPath {
httpRouter.Get(r.playgroundPath, s.playgroundHandler(nil).ServeHTTP)
if s.playgroundHandler != nil && s.graphqlPath != s.playgroundConfig.Path {
httpRouter.Get(r.playgroundConfig.Path, s.playgroundHandler(nil).ServeHTTP)
}

httpRouter.Get(s.healthCheckPath, r.healthcheck.Liveness())
Expand Down Expand Up @@ -1049,7 +1049,7 @@ func (s *graphServer) buildGraphMux(ctx context.Context,

// When the playground path is equal to the graphql path, we need to handle
// ws upgrades and html requests on the same route.
if s.playground && s.graphqlPath == s.playgroundPath {
if s.playgroundConfig.Enabled && s.graphqlPath == s.playgroundConfig.Path {
httpRouter.Use(s.playgroundHandler, wsMiddleware)
} else {
httpRouter.Use(wsMiddleware)
Expand Down
35 changes: 27 additions & 8 deletions router/core/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ type (
healthCheckPath string
readinessCheckPath string
livenessCheckPath string
playgroundConfig config.PlaygroundConfig
cacheControlPolicy config.CacheControlPolicy
routerConfigPollerConfig *RouterConfigPollerConfig
cdnConfig config.CDNConfiguration
Expand Down Expand Up @@ -258,8 +259,18 @@ func NewRouter(opts ...Option) (*Router, error) {
r.graphqlWebURL = r.graphqlPath
}

if r.playgroundPath == "" {
r.playgroundPath = "/"
// this is set via the deprecated method
if !r.playground {
r.playgroundConfig.Enabled = r.playground
r.logger.Warn("The playground_enabled option is deprecated. Use the playground.enabled option in the config instead.")
}
if r.playgroundPath != "" && r.playgroundPath != "/" {
r.playgroundConfig.Path = r.playgroundPath
r.logger.Warn("The playground_path option is deprecated. Use the playground.path option in the config instead.")
}

if r.playgroundConfig.Path == "" {
r.playgroundConfig.Path = "/"
}

if r.instanceID == "" {
Expand Down Expand Up @@ -598,7 +609,7 @@ func (r *Router) newServer(ctx context.Context, cfg *nodev1.RouterConfig) error
func (r *Router) listenAndServe(cfg *nodev1.RouterConfig) error {
r.logger.Info("Server listening and serving",
zap.String("listen_addr", r.listenAddr),
zap.Bool("playground", r.playground),
zap.Bool("playground", r.playgroundConfig.Enabled),
zap.Bool("introspection", r.introspection),
zap.String("config_version", cfg.GetVersion()),
)
Expand Down Expand Up @@ -854,15 +865,16 @@ func (r *Router) bootstrap(ctx context.Context) error {
debug.ReportMemoryUsage(ctx, r.logger)
}

if r.playground {
playgroundUrl, err := url.JoinPath(r.baseURL, r.playgroundPath)
if r.playgroundConfig.Enabled {
playgroundUrl, err := url.JoinPath(r.baseURL, r.playgroundConfig.Path)
if err != nil {
return fmt.Errorf("failed to join playground url: %w", err)
}
r.logger.Info("Serving GraphQL playground", zap.String("url", playgroundUrl))
r.playgroundHandler = graphiql.NewPlayground(&graphiql.PlaygroundOptions{
Html: graphiql.PlaygroundHTML(),
GraphqlURL: r.graphqlWebURL,
Html: graphiql.PlaygroundHTML(),
GraphqlURL: r.graphqlWebURL,
ConcurrencyLimit: int64(r.playgroundConfig.ConcurrencyLimit),
})
}

Expand Down Expand Up @@ -1128,7 +1140,7 @@ func (r *Router) Start(ctx context.Context) error {
return err
}

if r.playground {
if r.playgroundConfig.Enabled {
graphqlEndpointURL, err := url.JoinPath(r.baseURL, r.graphqlPath)
if err != nil {
return fmt.Errorf("failed to join graphql endpoint url: %w", err)
Expand Down Expand Up @@ -1381,6 +1393,13 @@ func WithPlaygroundPath(p string) Option {
}
}

// WithPlaygroundPath sets the path where the GraphQL Playground is served.
func WithPlaygroundConfig(c config.PlaygroundConfig) Option {
return func(r *Router) {
r.playgroundConfig = c
}
}

// WithConfigPoller sets the poller client to fetch the router config. If not set, WithConfigPollerConfig should be set.
func WithConfigPoller(cf configpoller.ConfigPoller) Option {
return func(r *Router) {
Expand Down
44 changes: 34 additions & 10 deletions router/internal/graphiql/playgroundhandler.go
Original file line number Diff line number Diff line change
@@ -1,30 +1,52 @@
package graphiql

import (
"bytes"
"golang.org/x/sync/semaphore"
"net/http"
"strconv"
"strings"
)

type PlaygroundOptions struct {
Html string
GraphqlURL string
Html string
GraphqlURL string
ConcurrencyLimit int64
}

type Playground struct {
next http.Handler
opts *PlaygroundOptions
next http.Handler
opts *PlaygroundOptions
templateBytes []byte
sem *semaphore.Weighted
}

var (
defaultLimitUsage = int64(10)
)

func NewPlayground(opts *PlaygroundOptions) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return &Playground{
limit := opts.ConcurrencyLimit
if limit == 0 {
limit = defaultLimitUsage
}
p := &Playground{
next: next,
opts: opts,
sem: semaphore.NewWeighted(limit),
}
p.initPlayground()
return p
}
}

func (p *Playground) initPlayground() {
tpl := strings.Replace(p.opts.Html, "{{graphqlURL}}", p.opts.GraphqlURL, -1)
play := []byte(tpl)
p.templateBytes = play
}

func (p *Playground) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Only serve the playground if the request is for text/html
// if not, just pass through to the next handler
Expand All @@ -35,12 +57,14 @@ func (p *Playground) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}

tpl := strings.Replace(p.opts.Html, "{{graphqlURL}}", p.opts.GraphqlURL, -1)
resp := []byte(tpl)

if err := p.sem.Acquire(r.Context(), 1); err != nil {
http.Error(w, "Too many requests", http.StatusTooManyRequests)
return
}
defer p.sem.Release(1) // Ensure the semaphore slot is released
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Header().Set("Content-Length", strconv.Itoa(len(resp)))
w.Header().Set("Content-Length", strconv.Itoa(len(p.templateBytes)))

w.WriteHeader(http.StatusOK)
_, _ = w.Write(resp)
_, _ = bytes.NewBuffer(p.templateBytes).WriteTo(w)
}
7 changes: 7 additions & 0 deletions router/pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -805,6 +805,7 @@ type Config struct {

ListenAddr string `yaml:"listen_addr" envDefault:"localhost:3002" env:"LISTEN_ADDR"`
ControlplaneURL string `yaml:"controlplane_url" envDefault:"https://cosmo-cp.wundergraph.com" env:"CONTROLPLANE_URL"`
PlaygroundConfig PlaygroundConfig `yaml:"playground,omitempty"`
PlaygroundEnabled bool `yaml:"playground_enabled" envDefault:"true" env:"PLAYGROUND_ENABLED"`
IntrospectionEnabled bool `yaml:"introspection_enabled" envDefault:"true" env:"INTROSPECTION_ENABLED"`
QueryPlansEnabled bool `yaml:"query_plans_enabled" envDefault:"true" env:"QUERY_PLANS_ENABLED"`
Expand Down Expand Up @@ -851,6 +852,12 @@ type Config struct {
ClientHeader ClientHeader `yaml:"client_header"`
}

type PlaygroundConfig struct {
Enabled bool `yaml:"enabled" envDefault:"true" env:"PLAYGROUND_ENABLED"`
Path string `yaml:"path" envDefault:"/" env:"PLAYGROUND_PATH"`
ConcurrencyLimit int `yaml:"concurrency_limit,omitempty" envDefault:"10" env:"PLAYGROUND_CONCURRENCY_LIMIT"`
}

type LoadResult struct {
Config Config
DefaultLoaded bool
Expand Down
32 changes: 30 additions & 2 deletions router/pkg/config/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1075,10 +1075,36 @@
"default": "https://cosmo-cp.wundergraph.com",
"format": "http-url"
},
"playground": {
"type": "object",
"description": "The configuration for the playground. The playground is a web-based GraphQL IDE that allows you to interact with the GraphQL API.",
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean",
"description": "Enable the GraphQL Playground. The GraphQL Playground is a web-based GraphQL IDE that allows you to interact with the GraphQL API. The default value is true. If the value is false, the GraphQL Playground is disabled.",
"default": true
},
"path": {
"type": "string",
"format": "x-uri",
"default": "/",
"description": "The path of the GraphQL Playground. The GraphQL Playground is a web-based GraphQL IDE that allows you to interact with the GraphQL API. The default value is '/'."
},
"concurrency_limit": {
"type": "integer",
"description": "The concurrency limit for loading the playground. This shouldn't impact normal usage.",
"default": 10,
"minimum": 1
}
}
},
"playground_enabled": {
"type": "boolean",
"description": "Enable the GraphQL Playground. The GraphQL Playground is a web-based GraphQL IDE that allows you to interact with the GraphQL API. The default value is true. If the value is false, the GraphQL Playground is disabled.",
"default": true
"default": true,
"deprecated": true,
"deprecationMessage": "playground_enabled is deprecated. Please use the playground.enabled configuration instead."
},
"introspection_enabled": {
"type": "boolean",
Expand Down Expand Up @@ -1159,7 +1185,9 @@
"type": "string",
"format": "x-uri",
"default": "/",
"description": "The path of the GraphQL Playground. The GraphQL Playground is a web-based GraphQL IDE that allows you to interact with the GraphQL API. The default value is '/'."
"description": "The path of the GraphQL Playground. The GraphQL Playground is a web-based GraphQL IDE that allows you to interact with the GraphQL API. The default value is '/'.",
"deprecated": true,
"deprecationMessage": "playground_path is deprecated. Please use the playground.path configuration instead."
},
"file_upload": {
"type": "object",
Expand Down
4 changes: 4 additions & 0 deletions router/pkg/config/fixtures/full.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ listen_addr: "localhost:3002"
controlplane_url: "https://cosmo-cp.wundergraph.com"
playground_enabled: true
playground_path: "/"
playground:
enabled: false
path: "/my-playground"
concurrency_limit: 1500
introspection_enabled: true
json_log: true
shutdown_delay: 15s
Expand Down
5 changes: 5 additions & 0 deletions router/pkg/config/testdata/config_defaults.json
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,11 @@
},
"ListenAddr": "localhost:3002",
"ControlplaneURL": "https://cosmo-cp.wundergraph.com",
"PlaygroundConfig": {
"Enabled": true,
"Path": "/",
"ConcurrencyLimit": 10
},
"PlaygroundEnabled": true,
"IntrospectionEnabled": true,
"QueryPlansEnabled": true,
Expand Down
5 changes: 5 additions & 0 deletions router/pkg/config/testdata/config_full.json
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,11 @@
},
"ListenAddr": "localhost:3002",
"ControlplaneURL": "https://cosmo-cp.wundergraph.com",
"PlaygroundConfig": {
"Enabled": false,
"Path": "/my-playground",
"ConcurrencyLimit": 1500
},
"PlaygroundEnabled": true,
"IntrospectionEnabled": true,
"QueryPlansEnabled": true,
Expand Down