From fa04def63264a06b6a9b0525044a20b60d1514ec Mon Sep 17 00:00:00 2001 From: x1unix Date: Mon, 14 Mar 2022 06:23:56 +0100 Subject: [PATCH 1/7] server: add environment variables allow list for Go compiler --- cmd/playground/main.go | 26 ++++-- pkg/compiler/compiler.go | 37 +++++++-- pkg/compiler/compiler_test.go | 41 ++++++++- pkg/compiler/{env.go => goenv.go} | 0 pkg/compiler/{env_test.go => goenv_test.go} | 0 pkg/util/cmdutil/flags.go | 40 +++++++++ pkg/util/osutil/env.go | 71 ++++++++++++++++ pkg/util/osutil/env_test.go | 92 +++++++++++++++++++++ pkg/util/set.go | 41 +++++++++ pkg/util/set_test.go | 30 +++++++ tools/cover.txt | 1 + 11 files changed, 360 insertions(+), 19 deletions(-) rename pkg/compiler/{env.go => goenv.go} (100%) rename pkg/compiler/{env_test.go => goenv_test.go} (100%) create mode 100644 pkg/util/cmdutil/flags.go create mode 100644 pkg/util/osutil/env.go create mode 100644 pkg/util/osutil/env_test.go create mode 100644 pkg/util/set.go create mode 100644 pkg/util/set_test.go diff --git a/cmd/playground/main.go b/cmd/playground/main.go index ac87d112..3fdf96ab 100644 --- a/cmd/playground/main.go +++ b/cmd/playground/main.go @@ -4,7 +4,6 @@ import ( "context" "flag" "fmt" - "github.com/x1unix/go-playground/pkg/langserver/webutil" "net/http" "os" "path/filepath" @@ -18,6 +17,9 @@ import ( "github.com/x1unix/go-playground/pkg/compiler/storage" "github.com/x1unix/go-playground/pkg/goplay" "github.com/x1unix/go-playground/pkg/langserver" + "github.com/x1unix/go-playground/pkg/langserver/webutil" + "github.com/x1unix/go-playground/pkg/util/cmdutil" + "github.com/x1unix/go-playground/pkg/util/osutil" "go.uber.org/zap" ) @@ -33,8 +35,9 @@ type appArgs struct { buildDir string cleanupInterval string assetsDirectory string - connectTimeout time.Duration googleAnalyticsID string + bypassEnvVarsList []string + connectTimeout time.Duration } func (a appArgs) getCleanDuration() (time.Duration, error) { @@ -59,10 +62,11 @@ func main() { flag.StringVar(&args.assetsDirectory, "static-dir", filepath.Join(wd, "public"), "Path to web page assets (HTML, JS, etc)") flag.DurationVar(&args.connectTimeout, "timeout", 15*time.Second, "Go Playground server connect timeout") flag.StringVar(&args.googleAnalyticsID, "gtag-id", "", "Google Analytics tag ID (optional)") + flag.Var(cmdutil.NewStringsListValue(&args.bypassEnvVarsList), "permit-env-vars", "Comma-separated allow list of environment variables passed to Go compiler tool") + flag.Parse() l := getLogger(args.debug) defer l.Sync() //nolint:errcheck - flag.Parse() goRoot, err := compiler.GOROOT() if err != nil { @@ -118,18 +122,24 @@ func start(goRoot string, args appArgs) error { wg := &sync.WaitGroup{} go store.StartCleaner(ctx, cleanInterval, nil) - r := mux.NewRouter() + // Initialize services pgClient := goplay.NewClient(args.playgroundURL, goplay.DefaultUserAgent, args.connectTimeout) goTipClient := goplay.NewClient(args.goTipPlaygroundURL, goplay.DefaultUserAgent, args.connectTimeout) clients := &langserver.PlaygroundServices{ Default: pgClient, GoTip: goTipClient, } - // API routes - svcCfg := langserver.ServiceConfig{ - Version: Version, + buildCfg := compiler.BuildEnvironmentConfig{ + IncludedEnvironmentVariables: osutil.SelectEnvironmentVariables(args.bypassEnvVarsList...), } - langserver.New(svcCfg, clients, packages, compiler.NewBuildService(zap.S(), store)). + zap.L().Info("Loaded list of environment variables used by compiler", + zap.Any("vars", buildCfg.IncludedEnvironmentVariables)) + buildSvc := compiler.NewBuildService(zap.S(), buildCfg, store) + + // Initialize API endpoints + r := mux.NewRouter() + svcCfg := langserver.ServiceConfig{Version: Version} + langserver.New(svcCfg, clients, packages, buildSvc). Mount(r.PathPrefix("/api").Subrouter()) // Web UI routes diff --git a/pkg/compiler/compiler.go b/pkg/compiler/compiler.go index e208e98b..2d30652d 100644 --- a/pkg/compiler/compiler.go +++ b/pkg/compiler/compiler.go @@ -3,17 +3,20 @@ package compiler import ( "bytes" "context" - "github.com/x1unix/go-playground/pkg/compiler/storage" - "go.uber.org/zap" "io" "os" + + "github.com/x1unix/go-playground/pkg/compiler/storage" + "github.com/x1unix/go-playground/pkg/util/osutil" + "go.uber.org/zap" ) -var buildArgs = []string{ - "CGO_ENABLED=0", - "GOOS=js", - "GOARCH=wasm", - "HOME=" + os.Getenv("HOME"), +// predefinedBuildVars is list of environment vars which contain build values +var predefinedBuildVars = osutil.EnvironmentVariables{ + "CGO_ENABLED": "0", + "GOOS": "js", + "GOARCH": "wasm", + "HOME": os.Getenv("HOME"), } // Result is WASM build result @@ -22,23 +25,31 @@ type Result struct { FileName string } +// BuildEnvironmentConfig is BuildService environment configuration. +type BuildEnvironmentConfig struct { + // IncludedEnvironmentVariables is a list included environment variables for build. + IncludedEnvironmentVariables osutil.EnvironmentVariables +} + // BuildService is WASM build service type BuildService struct { log *zap.SugaredLogger + config BuildEnvironmentConfig storage storage.StoreProvider } // NewBuildService is BuildService constructor -func NewBuildService(log *zap.SugaredLogger, store storage.StoreProvider) BuildService { +func NewBuildService(log *zap.SugaredLogger, cfg BuildEnvironmentConfig, store storage.StoreProvider) BuildService { return BuildService{ log: log.Named("builder"), + config: cfg, storage: store, } } func (s BuildService) buildSource(ctx context.Context, outputLocation, sourceLocation string) error { cmd := newGoToolCommand(ctx, "build", "-o", outputLocation, sourceLocation) - cmd.Env = buildArgs + cmd.Env = s.getEnvironmentVariables() buff := &bytes.Buffer{} cmd.Stderr = buff @@ -56,6 +67,14 @@ func (s BuildService) buildSource(ctx context.Context, outputLocation, sourceLoc return nil } +func (s BuildService) getEnvironmentVariables() []string { + if len(s.config.IncludedEnvironmentVariables) == 0 { + return predefinedBuildVars.Join() + } + + return s.config.IncludedEnvironmentVariables.Concat(predefinedBuildVars).Join() +} + // GetArtifact returns artifact by id func (s BuildService) GetArtifact(id storage.ArtifactID) (io.ReadCloser, error) { return s.storage.GetItem(id) diff --git a/pkg/compiler/compiler_test.go b/pkg/compiler/compiler_test.go index fee948e0..20133799 100644 --- a/pkg/compiler/compiler_test.go +++ b/pkg/compiler/compiler_test.go @@ -12,6 +12,7 @@ import ( "github.com/stretchr/testify/require" "github.com/x1unix/go-playground/pkg/compiler/storage" "github.com/x1unix/go-playground/pkg/testutil" + "github.com/x1unix/go-playground/pkg/util/osutil" "go.uber.org/zap/zaptest" ) @@ -78,7 +79,7 @@ func TestBuildService_GetArtifact(t *testing.T) { for k, v := range cases { t.Run(k, func(t *testing.T) { ts := v.beforeRun(t) - bs := NewBuildService(zaptest.NewLogger(t).Sugar(), ts) + bs := NewBuildService(zaptest.NewLogger(t).Sugar(), BuildEnvironmentConfig{}, ts) got, err := bs.GetArtifact(v.artifactID) if v.wantErr != "" { require.Error(t, err) @@ -176,7 +177,7 @@ func TestBuildService_Build(t *testing.T) { }() } - bs := NewBuildService(zaptest.NewLogger(t).Sugar(), store) + bs := NewBuildService(zaptest.NewLogger(t).Sugar(), BuildEnvironmentConfig{}, store) got, err := bs.Build(context.TODO(), c.data) if c.wantErr != "" { if c.onErrorCheck != nil { @@ -193,6 +194,42 @@ func TestBuildService_Build(t *testing.T) { } } +func TestBuildService_getEnvironmentVariables(t *testing.T) { + cases := map[string]struct { + includedVars osutil.EnvironmentVariables + check func(t *testing.T, included osutil.EnvironmentVariables, result []string) + }{ + "include vars": { + includedVars: osutil.EnvironmentVariables{ + "FOOBAR": "BAZ", + "GOOS": "stub-value", + }, + check: func(t *testing.T, included osutil.EnvironmentVariables, result []string) { + got := osutil.SplitEnvironmentValues(result) + expect := included.Concat(predefinedBuildVars) + require.Equal(t, expect, got) + }, + }, + "ignore vars if empty": { + check: func(t *testing.T, _ osutil.EnvironmentVariables, result []string) { + got := osutil.SplitEnvironmentValues(result) + require.Equal(t, predefinedBuildVars, got) + }, + }, + } + + for n, c := range cases { + t.Run(n, func(t *testing.T) { + cfg := BuildEnvironmentConfig{ + IncludedEnvironmentVariables: c.includedVars, + } + svc := NewBuildService(zaptest.NewLogger(t).Sugar(), cfg, nil) + got := svc.getEnvironmentVariables() + c.check(t, c.includedVars, got) + }) + } +} + func mustArtifactID(t *testing.T, data []byte) storage.ArtifactID { t.Helper() a, err := storage.GetArtifactID(data) diff --git a/pkg/compiler/env.go b/pkg/compiler/goenv.go similarity index 100% rename from pkg/compiler/env.go rename to pkg/compiler/goenv.go diff --git a/pkg/compiler/env_test.go b/pkg/compiler/goenv_test.go similarity index 100% rename from pkg/compiler/env_test.go rename to pkg/compiler/goenv_test.go diff --git a/pkg/util/cmdutil/flags.go b/pkg/util/cmdutil/flags.go new file mode 100644 index 00000000..17e81b9b --- /dev/null +++ b/pkg/util/cmdutil/flags.go @@ -0,0 +1,40 @@ +package cmdutil + +import ( + "strconv" + "strings" +) + +const csvSeparator = "," + +// StringsListValue is comma-separated list of values that implements flag.Value interface. +type StringsListValue []string + +// String implements flag.Value +func (s StringsListValue) String() string { + if len(s) == 0 { + return "" + } + + return strconv.Quote(strings.Join(s, csvSeparator)) +} + +// Set implements flag.Value +func (s *StringsListValue) Set(s2 string) error { + vals := strings.Split(s2, csvSeparator) + for i, v := range vals { + v = strings.TrimSpace(v) + if v == "" { + continue + } + vals[i] = v + } + + *s = vals + return nil +} + +// NewStringsListValue returns a new StringsListValue +func NewStringsListValue(p *[]string) *StringsListValue { + return (*StringsListValue)(p) +} diff --git a/pkg/util/osutil/env.go b/pkg/util/osutil/env.go new file mode 100644 index 00000000..3878e89a --- /dev/null +++ b/pkg/util/osutil/env.go @@ -0,0 +1,71 @@ +package osutil + +import ( + "os" + "strings" + + "github.com/x1unix/go-playground/pkg/util" +) + +const envVarsDelimiter = "=" + +// EnvironmentVariables is a key-value pair of environment variable and value. +type EnvironmentVariables map[string]string + +// Join returns slice of environment variable and values joined by delimiter (=). +func (s EnvironmentVariables) Join() []string { + r := make([]string, 0, len(s)) + for key, value := range s { + r = append(r, strings.Join([]string{key, value}, envVarsDelimiter)) + } + return r +} + +// Append appends new values to existing item. +func (s EnvironmentVariables) Append(items EnvironmentVariables) { + if s == nil { + return + } + if len(items) == 0 { + return + } + for k, v := range items { + s[k] = v + } +} + +// Concat joins two items together into a new one. +func (s EnvironmentVariables) Concat(newItems EnvironmentVariables) EnvironmentVariables { + newList := make(EnvironmentVariables, len(s)+len(newItems)) + newList.Append(s) + newList.Append(newItems) + return s +} + +// SplitEnvironmentValues splits slice of '='-separated key-value items and returns key-value pair. +// +// Second optional parameter allows filter items. +func SplitEnvironmentValues(vals []string, filterKeys ...string) EnvironmentVariables { + allowList := util.NewStringSet(filterKeys...) + out := make(EnvironmentVariables, len(vals)) + for _, val := range vals { + chunks := strings.SplitN(val, "=", 2) + key := chunks[0] + + if len(allowList) > 0 && !allowList.Has(key) { + continue + } + + val := "" + if len(chunks) == 2 { + val = chunks[1] + } + out[key] = val + } + return out +} + +// SelectEnvironmentVariables selects environment variables as key-value pair with whitelist filter. +func SelectEnvironmentVariables(filterKeys ...string) EnvironmentVariables { + return SplitEnvironmentValues(os.Environ(), filterKeys...) +} diff --git a/pkg/util/osutil/env_test.go b/pkg/util/osutil/env_test.go new file mode 100644 index 00000000..c58de715 --- /dev/null +++ b/pkg/util/osutil/env_test.go @@ -0,0 +1,92 @@ +package osutil + +import ( + "os" + "sort" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestSplitEnvironmentValues(t *testing.T) { + cases := map[string]struct { + allowList []string + input []string + expect EnvironmentVariables + }{ + "with ignore list": { + allowList: []string{ + "FOO", + "BAR", + "EMPTY", + }, + input: []string{ + "FOO=BAR", + "BAR=BAZ=BBB", + "EMPTY=", + "VALUE_SHOULD_BE_IGNORED", + "ALSO_IGNORE=ME=PLEASE", + }, + expect: EnvironmentVariables{ + "FOO": "BAR", + "BAR": "BAZ=BBB", + "EMPTY": "", + }, + }, + "without ignore list": { + input: []string{ + "FOO=BAR", + "BAR=BAZ=BBB", + "EMPTY=", + }, + expect: EnvironmentVariables{ + "FOO": "BAR", + "BAR": "BAZ=BBB", + "EMPTY": "", + }, + }, + } + + for n, c := range cases { + t.Run(n, func(t *testing.T) { + got := SplitEnvironmentValues(c.input, c.allowList...) + require.Equal(t, c.expect, got) + }) + } +} + +func TestSelectEnvironmentVariables(t *testing.T) { + allowList := []string{"GOOS", "GOARCH", "GOPATH"} + expect := SplitEnvironmentValues(os.Environ(), allowList...) + got := SelectEnvironmentVariables(allowList...) + require.Equal(t, expect, got) +} + +func TestEnvironmentVariables_Join(t *testing.T) { + expect := []string{ + "B=2", + "A=1", + } + got := EnvironmentVariables{ + "A": "1", + "B": "2", + }.Join() + + // sort items to mitigate random map iteration issue + sort.Strings(got) + sort.Strings(expect) + require.Equal(t, expect, got) +} + +func TestEnvironmentVariables_Append(t *testing.T) { + expect := EnvironmentVariables{ + "FOO": "BAR", + "BAR": "BAZ", + } + got := EnvironmentVariables{"BAR": "BAZ"} + EnvironmentVariables.Append(nil, nil) + EnvironmentVariables.Append(got, nil) + EnvironmentVariables.Append(got, EnvironmentVariables{"FOO": "BAR"}) + + require.Equal(t, expect, got) +} diff --git a/pkg/util/set.go b/pkg/util/set.go new file mode 100644 index 00000000..12a98446 --- /dev/null +++ b/pkg/util/set.go @@ -0,0 +1,41 @@ +package util + +type void = struct{} + +// StringSet is a list of unique strings. +type StringSet map[string]void + +// Has checks if set contains a value. +func (s StringSet) Has(val string) bool { + if s == nil { + return false + } + + _, ok := s[val] + return ok +} + +// Concat joins two string sets together into a new set. +func (s StringSet) Concat(items StringSet) StringSet { + newList := make(StringSet, len(s)+len(items)) + for k, v := range s { + newList[k] = v + } + for k, v := range items { + newList[k] = v + } + return newList +} + +// NewStringSet creates a new string set from strings slice. +func NewStringSet(items ...string) StringSet { + if len(items) == 0 { + return StringSet{} + } + + s := make(StringSet, len(items)) + for _, v := range items { + s[v] = void{} + } + return s +} diff --git a/pkg/util/set_test.go b/pkg/util/set_test.go new file mode 100644 index 00000000..e8ae9cde --- /dev/null +++ b/pkg/util/set_test.go @@ -0,0 +1,30 @@ +package util + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestNewStringSet(t *testing.T) { + s := NewStringSet("foo", "bar") + require.Equal(t, StringSet{ + "foo": void{}, + "bar": void{}, + }, s) +} + +func TestStringSet_Has(t *testing.T) { + s := NewStringSet("foo") + require.True(t, s.Has("foo")) + require.False(t, s.Has("bar")) + require.False(t, StringSet.Has(nil, "foo")) +} + +func TestStringSet_Concat(t *testing.T) { + a := NewStringSet("foo", "bar") + b := NewStringSet("bar", "baz") + expect := NewStringSet("foo", "bar", "baz") + got := a.Concat(b) + require.Equal(t, expect, got) +} diff --git a/tools/cover.txt b/tools/cover.txt index 2c76937e..b643e02c 100644 --- a/tools/cover.txt +++ b/tools/cover.txt @@ -2,3 +2,4 @@ ./pkg/analyzer/... ./pkg/goplay/... ./pkg/langserver/webutil +./pkg/utils/... \ No newline at end of file From 7ae50fff5bb8639ea229800ee090b3cdcefc2a82 Mon Sep 17 00:00:00 2001 From: x1unix Date: Mon, 14 Mar 2022 06:40:04 +0100 Subject: [PATCH 2/7] server: enforce wasm mime type --- pkg/langserver/server.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/langserver/server.go b/pkg/langserver/server.go index f3541c41..a0153358 100644 --- a/pkg/langserver/server.go +++ b/pkg/langserver/server.go @@ -276,6 +276,7 @@ func (s *Service) HandleArtifactRequest(w http.ResponseWriter, r *http.Request) return err } + w.Header().Set("Content-Type", wasmMimeType) n, err := io.Copy(w, data) defer data.Close() if err != nil { @@ -286,7 +287,6 @@ func (s *Service) HandleArtifactRequest(w http.ResponseWriter, r *http.Request) return err } - w.Header().Set("Content-Type", wasmMimeType) w.Header().Set("Content-Length", strconv.FormatInt(n, 10)) return nil } From 289ccb4df7ff1e3c6b1f7975d163115bb67ebe5c Mon Sep 17 00:00:00 2001 From: x1unix Date: Mon, 14 Mar 2022 06:40:19 +0100 Subject: [PATCH 3/7] server: fix environment variables expansion --- pkg/util/osutil/env.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/util/osutil/env.go b/pkg/util/osutil/env.go index 3878e89a..55941e87 100644 --- a/pkg/util/osutil/env.go +++ b/pkg/util/osutil/env.go @@ -39,7 +39,7 @@ func (s EnvironmentVariables) Concat(newItems EnvironmentVariables) EnvironmentV newList := make(EnvironmentVariables, len(s)+len(newItems)) newList.Append(s) newList.Append(newItems) - return s + return newList } // SplitEnvironmentValues splits slice of '='-separated key-value items and returns key-value pair. From c662a00869b91597407b9b47a538206526e30f1c Mon Sep 17 00:00:00 2001 From: x1unix Date: Mon, 14 Mar 2022 06:40:38 +0100 Subject: [PATCH 4/7] server: change env var log line severity --- cmd/playground/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/playground/main.go b/cmd/playground/main.go index 3fdf96ab..43cb267e 100644 --- a/cmd/playground/main.go +++ b/cmd/playground/main.go @@ -132,7 +132,7 @@ func start(goRoot string, args appArgs) error { buildCfg := compiler.BuildEnvironmentConfig{ IncludedEnvironmentVariables: osutil.SelectEnvironmentVariables(args.bypassEnvVarsList...), } - zap.L().Info("Loaded list of environment variables used by compiler", + zap.L().Debug("Loaded list of environment variables used by compiler", zap.Any("vars", buildCfg.IncludedEnvironmentVariables)) buildSvc := compiler.NewBuildService(zap.S(), buildCfg, store) From 59f77fc26bc345d7de401869d0e1475fbada08d9 Mon Sep 17 00:00:00 2001 From: x1unix Date: Mon, 14 Mar 2022 06:40:50 +0100 Subject: [PATCH 5/7] make: add option to pass extra args --- Makefile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index c9ee5746..1d6c8ce7 100644 --- a/Makefile +++ b/Makefile @@ -22,7 +22,8 @@ run: -static-dir="$(UI)/build" \ -gtag-id="$(GTAG)" \ -debug=$(DEBUG) \ - -addr $(LISTEN_ADDR) + -addr $(LISTEN_ADDR) \ + $(EXTRA_ARGS) .PHONY:ui ui: From 5d324f63d4c84d8648e64c0d5cf82ddd85868418 Mon Sep 17 00:00:00 2001 From: x1unix Date: Mon, 14 Mar 2022 06:55:58 +0100 Subject: [PATCH 6/7] cmd: fix empty items filter in flags list --- pkg/util/cmdutil/flags.go | 7 ++++--- pkg/util/cmdutil/flags_test.go | 28 ++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 3 deletions(-) create mode 100644 pkg/util/cmdutil/flags_test.go diff --git a/pkg/util/cmdutil/flags.go b/pkg/util/cmdutil/flags.go index 17e81b9b..0424645a 100644 --- a/pkg/util/cmdutil/flags.go +++ b/pkg/util/cmdutil/flags.go @@ -22,15 +22,16 @@ func (s StringsListValue) String() string { // Set implements flag.Value func (s *StringsListValue) Set(s2 string) error { vals := strings.Split(s2, csvSeparator) - for i, v := range vals { + filteredVals := make([]string, 0, len(vals)) + for _, v := range vals { v = strings.TrimSpace(v) if v == "" { continue } - vals[i] = v + filteredVals = append(filteredVals, v) } - *s = vals + *s = filteredVals return nil } diff --git a/pkg/util/cmdutil/flags_test.go b/pkg/util/cmdutil/flags_test.go new file mode 100644 index 00000000..ef538ecb --- /dev/null +++ b/pkg/util/cmdutil/flags_test.go @@ -0,0 +1,28 @@ +package cmdutil + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestStringsListValue_String(t *testing.T) { + require.Equal(t, StringsListValue.String(nil), "") + vals := []string{"foo", "bar"} + require.Equal(t, StringsListValue(vals).String(), `"foo,bar"`) +} + +func TestStringsListValue_Set(t *testing.T) { + input := "foo, , bar," + expect := []string{"foo", "bar"} + + val := make(StringsListValue, 0) + require.NoError(t, val.Set(input)) + require.Equal(t, expect, ([]string)(val)) +} + +func TestNewStringsListValue(t *testing.T) { + val := []string{"foo"} + got := NewStringsListValue(&val) + require.Equal(t, (*[]string)(got), &val) +} From bd05b343f3f8a7a7d9a5d3e6f4b28a89f842a17e Mon Sep 17 00:00:00 2001 From: x1unix Date: Mon, 14 Mar 2022 06:59:07 +0100 Subject: [PATCH 7/7] docker: expose -permit-env-vars flag as $APP_PERMIT_ENV_VARS env var. --- build/Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/build/Dockerfile b/build/Dockerfile index 88b73f11..79562361 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -39,4 +39,5 @@ ENTRYPOINT /opt/playground/server \ -playground-url="${APP_PLAYGROUND_URL}" \ -gotip-url="${APP_GOTIP_URL}" \ -gtag-id="${APP_GTAG_ID}" \ + -permit-env-vars="${APP_PERMIT_ENV_VARS}" \ -addr=:8000 \ No newline at end of file