From 90f205aff00654c1c79fa21a051ac985adef02df Mon Sep 17 00:00:00 2001 From: George Tsiolis Date: Mon, 23 Mar 2026 17:40:38 +0200 Subject: [PATCH 1/3] Forward host environment variables to the emulator container --- internal/container/start.go | 9 +++++++ internal/container/start_test.go | 24 +++++++++++++++++ test/integration/start_test.go | 46 ++++++++++++++++++++++++++------ 3 files changed, 71 insertions(+), 8 deletions(-) diff --git a/internal/container/start.go b/internal/container/start.go index bcda826..d5de96d 100644 --- a/internal/container/start.go +++ b/internal/container/start.go @@ -96,6 +96,13 @@ func Start(ctx context.Context, rt runtime.Runtime, sink output.Sink, opts Start tel := opts.Telemetry + var hostEnv []string + for _, e := range os.Environ() { + if strings.HasPrefix(e, "CI=") || (strings.HasPrefix(e, "LOCALSTACK_") && !strings.HasPrefix(e, "LOCALSTACK_AUTH_TOKEN=")) { + hostEnv = append(hostEnv, e) + } + } + containers := make([]runtime.ContainerConfig, len(opts.Containers)) for i, c := range opts.Containers { image, err := c.Image() @@ -128,6 +135,8 @@ func Start(ctx context.Context, rt runtime.Runtime, sink output.Sink, opts Start "MAIN_CONTAINER_NAME="+containerName, ) + env = append(env, hostEnv...) + var binds []runtime.BindMount if socketPath := rt.SocketPath(); socketPath != "" { binds = append(binds, runtime.BindMount{HostPath: socketPath, ContainerPath: "/var/run/docker.sock"}) diff --git a/internal/container/start_test.go b/internal/container/start_test.go index 518157e..490226f 100644 --- a/internal/container/start_test.go +++ b/internal/container/start_test.go @@ -5,6 +5,7 @@ import ( "context" "errors" "io" + "strings" "testing" "github.com/localstack/lstk/internal/log" @@ -64,3 +65,26 @@ func TestServicePortRange_ReturnsExpectedPorts(t *testing.T) { assert.Equal(t, "4559", ports[50].ContainerPort) assert.Equal(t, "4559", ports[50].HostPort) } + +func TestFilterHostEnv(t *testing.T) { + tests := []struct { + name string + env string + expected bool + }{ + {"CI is included", "CI=true", true}, + {"LOCALSTACK_DISABLE_EVENTS is included", "LOCALSTACK_DISABLE_EVENTS=1", true}, + {"LOCALSTACK_HOST is included", "LOCALSTACK_HOST=0.0.0.0", true}, + {"LOCALSTACK_AUTH_TOKEN is excluded", "LOCALSTACK_AUTH_TOKEN=secret", false}, + {"PATH is excluded", "PATH=/usr/bin", false}, + {"HOME is excluded", "HOME=/root", false}, + {"LOCALSTACK_BUILD_VERSION is included", "LOCALSTACK_BUILD_VERSION=3.0.0", true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := strings.HasPrefix(tt.env, "CI=") || (strings.HasPrefix(tt.env, "LOCALSTACK_") && !strings.HasPrefix(tt.env, "LOCALSTACK_AUTH_TOKEN=")) + assert.Equal(t, tt.expected, got) + }) + } +} diff --git a/test/integration/start_test.go b/test/integration/start_test.go index a95fd77..0287bd5 100644 --- a/test/integration/start_test.go +++ b/test/integration/start_test.go @@ -226,14 +226,34 @@ func TestStartCommandSetsUpContainerCorrectly(t *testing.T) { }) } -// containerEnvToMap converts a Docker container's []string env to a map. -func containerEnvToMap(envList []string) map[string]string { - m := make(map[string]string, len(envList)) - for _, e := range envList { - k, v, _ := strings.Cut(e, "=") - m[k] = v - } - return m +func TestStartCommandPassesCIAndLocalStackEnvVars(t *testing.T) { + requireDocker(t) + _ = env.Require(t, env.AuthToken) + + cleanup() + t.Cleanup(cleanup) + + t.Setenv("CI", "true") + t.Setenv("LOCALSTACK_DISABLE_EVENTS", "1") + t.Setenv("LOCALSTACK_AUTH_TOKEN", "host-token") + + mockServer := createMockLicenseServer(true) + defer mockServer.Close() + + ctx := testContext(t) + _, stderr, err := runLstk(t, ctx, "", env.With(env.APIEndpoint, mockServer.URL), "start") + require.NoError(t, err, "lstk start failed: %s", stderr) + requireExitCode(t, 0, err) + + inspect, err := dockerClient.ContainerInspect(ctx, containerName) + require.NoError(t, err, "failed to inspect container") + require.True(t, inspect.State.Running) + + envVars := containerEnvToMap(inspect.Config.Env) + assert.Equal(t, "true", envVars["CI"]) + assert.Equal(t, "1", envVars["LOCALSTACK_DISABLE_EVENTS"]) + assert.NotEmpty(t, envVars["LOCALSTACK_AUTH_TOKEN"]) + assert.NotEqual(t, "host-token", envVars["LOCALSTACK_AUTH_TOKEN"], "host LOCALSTACK_AUTH_TOKEN should not be passed through") } // hasBindTarget checks if any bind mount targets the given container path. @@ -247,6 +267,16 @@ func hasBindTarget(binds []string, containerPath string) bool { return false } +// containerEnvToMap converts a Docker container's []string env to a map. +func containerEnvToMap(envList []string) map[string]string { + m := make(map[string]string, len(envList)) + for _, e := range envList { + k, v, _ := strings.Cut(e, "=") + m[k] = v + } + return m +} + func cleanup() { ctx := context.Background() _ = dockerClient.ContainerStop(ctx, containerName, container.StopOptions{}) From a55454f3ac2f12a3b74ec82be974fb2b669382c8 Mon Sep 17 00:00:00 2001 From: George Tsiolis Date: Mon, 6 Apr 2026 14:53:08 +0300 Subject: [PATCH 2/3] Remove unnecessary changes --- internal/container/start_test.go | 35 -------------------------------- test/integration/start_test.go | 10 ++++----- 2 files changed, 5 insertions(+), 40 deletions(-) diff --git a/internal/container/start_test.go b/internal/container/start_test.go index 490226f..49f8dfc 100644 --- a/internal/container/start_test.go +++ b/internal/container/start_test.go @@ -5,7 +5,6 @@ import ( "context" "errors" "io" - "strings" "testing" "github.com/localstack/lstk/internal/log" @@ -54,37 +53,3 @@ func TestEmitPostStartPointers_WithoutWebApp(t *testing.T) { assert.Contains(t, got, "> Tip:") } -func TestServicePortRange_ReturnsExpectedPorts(t *testing.T) { - ports := servicePortRange() - - require.Len(t, ports, 51) - assert.Equal(t, "443", ports[0].ContainerPort) - assert.Equal(t, "443", ports[0].HostPort) - assert.Equal(t, "4510", ports[1].ContainerPort) - assert.Equal(t, "4510", ports[1].HostPort) - assert.Equal(t, "4559", ports[50].ContainerPort) - assert.Equal(t, "4559", ports[50].HostPort) -} - -func TestFilterHostEnv(t *testing.T) { - tests := []struct { - name string - env string - expected bool - }{ - {"CI is included", "CI=true", true}, - {"LOCALSTACK_DISABLE_EVENTS is included", "LOCALSTACK_DISABLE_EVENTS=1", true}, - {"LOCALSTACK_HOST is included", "LOCALSTACK_HOST=0.0.0.0", true}, - {"LOCALSTACK_AUTH_TOKEN is excluded", "LOCALSTACK_AUTH_TOKEN=secret", false}, - {"PATH is excluded", "PATH=/usr/bin", false}, - {"HOME is excluded", "HOME=/root", false}, - {"LOCALSTACK_BUILD_VERSION is included", "LOCALSTACK_BUILD_VERSION=3.0.0", true}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := strings.HasPrefix(tt.env, "CI=") || (strings.HasPrefix(tt.env, "LOCALSTACK_") && !strings.HasPrefix(tt.env, "LOCALSTACK_AUTH_TOKEN=")) - assert.Equal(t, tt.expected, got) - }) - } -} diff --git a/test/integration/start_test.go b/test/integration/start_test.go index 0287bd5..84d8125 100644 --- a/test/integration/start_test.go +++ b/test/integration/start_test.go @@ -233,15 +233,15 @@ func TestStartCommandPassesCIAndLocalStackEnvVars(t *testing.T) { cleanup() t.Cleanup(cleanup) - t.Setenv("CI", "true") - t.Setenv("LOCALSTACK_DISABLE_EVENTS", "1") - t.Setenv("LOCALSTACK_AUTH_TOKEN", "host-token") - mockServer := createMockLicenseServer(true) defer mockServer.Close() ctx := testContext(t) - _, stderr, err := runLstk(t, ctx, "", env.With(env.APIEndpoint, mockServer.URL), "start") + _, stderr, err := runLstk(t, ctx, "", env.With(env.APIEndpoint, mockServer.URL). + With(env.CI, "true"). + With(env.DisableEvents, "1"). + With(env.AuthToken, "host-token"), + "start") require.NoError(t, err, "lstk start failed: %s", stderr) requireExitCode(t, 0, err) From a811f6960532dc8afd6e1ae2e60b2f1f1434ac09 Mon Sep 17 00:00:00 2001 From: George Tsiolis Date: Wed, 8 Apr 2026 14:41:59 +0300 Subject: [PATCH 3/3] Extract host env filter and unit test the auth token strip invariant --- internal/container/start.go | 21 +++++++++++++++------ internal/container/start_test.go | 23 +++++++++++++++++++++++ test/integration/start_test.go | 4 +--- 3 files changed, 39 insertions(+), 9 deletions(-) diff --git a/internal/container/start.go b/internal/container/start.go index d5de96d..9defa11 100644 --- a/internal/container/start.go +++ b/internal/container/start.go @@ -96,12 +96,7 @@ func Start(ctx context.Context, rt runtime.Runtime, sink output.Sink, opts Start tel := opts.Telemetry - var hostEnv []string - for _, e := range os.Environ() { - if strings.HasPrefix(e, "CI=") || (strings.HasPrefix(e, "LOCALSTACK_") && !strings.HasPrefix(e, "LOCALSTACK_AUTH_TOKEN=")) { - hostEnv = append(hostEnv, e) - } - } + hostEnv := filterHostEnv(os.Environ()) containers := make([]runtime.ContainerConfig, len(opts.Containers)) for i, c := range opts.Containers { @@ -454,6 +449,20 @@ func awaitStartup(ctx context.Context, rt runtime.Runtime, sink output.Sink, con } } +// filterHostEnv returns the subset of host environment entries that should be +// forwarded to the emulator container. It keeps CI and LOCALSTACK_* variables +// but explicitly drops LOCALSTACK_AUTH_TOKEN so the host value cannot overwrite +// the token resolved by lstk (which may come from the keyring). +func filterHostEnv(envList []string) []string { + var out []string + for _, e := range envList { + if strings.HasPrefix(e, "CI=") || (strings.HasPrefix(e, "LOCALSTACK_") && !strings.HasPrefix(e, "LOCALSTACK_AUTH_TOKEN=")) { + out = append(out, e) + } + } + return out +} + func hasDuplicateContainerTypes(containers []config.ContainerConfig) bool { seen := make(map[config.EmulatorType]bool) for _, c := range containers { diff --git a/internal/container/start_test.go b/internal/container/start_test.go index 49f8dfc..c75d4ce 100644 --- a/internal/container/start_test.go +++ b/internal/container/start_test.go @@ -53,3 +53,26 @@ func TestEmitPostStartPointers_WithoutWebApp(t *testing.T) { assert.Contains(t, got, "> Tip:") } +func TestFilterHostEnv(t *testing.T) { + input := []string{ + "CI=true", + "LOCALSTACK_DISABLE_EVENTS=1", + "LOCALSTACK_API_ENDPOINT=https://example.test", + "LOCALSTACK_AUTH_TOKEN=host-token", + "PATH=/usr/bin", + "HOME=/home/user", + "CI_PIPELINE=foo", + } + + got := filterHostEnv(input) + + assert.Contains(t, got, "CI=true") + assert.Contains(t, got, "LOCALSTACK_DISABLE_EVENTS=1") + assert.Contains(t, got, "LOCALSTACK_API_ENDPOINT=https://example.test") + assert.NotContains(t, got, "LOCALSTACK_AUTH_TOKEN=host-token", + "host LOCALSTACK_AUTH_TOKEN must be filtered so it cannot overwrite the lstk-resolved token") + assert.NotContains(t, got, "PATH=/usr/bin") + assert.NotContains(t, got, "HOME=/home/user") + assert.NotContains(t, got, "CI_PIPELINE=foo", "only exact CI= must be forwarded, not CI_*") +} + diff --git a/test/integration/start_test.go b/test/integration/start_test.go index 84d8125..e67898e 100644 --- a/test/integration/start_test.go +++ b/test/integration/start_test.go @@ -239,8 +239,7 @@ func TestStartCommandPassesCIAndLocalStackEnvVars(t *testing.T) { ctx := testContext(t) _, stderr, err := runLstk(t, ctx, "", env.With(env.APIEndpoint, mockServer.URL). With(env.CI, "true"). - With(env.DisableEvents, "1"). - With(env.AuthToken, "host-token"), + With(env.DisableEvents, "1"), "start") require.NoError(t, err, "lstk start failed: %s", stderr) requireExitCode(t, 0, err) @@ -253,7 +252,6 @@ func TestStartCommandPassesCIAndLocalStackEnvVars(t *testing.T) { assert.Equal(t, "true", envVars["CI"]) assert.Equal(t, "1", envVars["LOCALSTACK_DISABLE_EVENTS"]) assert.NotEmpty(t, envVars["LOCALSTACK_AUTH_TOKEN"]) - assert.NotEqual(t, "host-token", envVars["LOCALSTACK_AUTH_TOKEN"], "host LOCALSTACK_AUTH_TOKEN should not be passed through") } // hasBindTarget checks if any bind mount targets the given container path.