diff --git a/internal/container/start.go b/internal/container/start.go index bcda826..9defa11 100644 --- a/internal/container/start.go +++ b/internal/container/start.go @@ -96,6 +96,8 @@ func Start(ctx context.Context, rt runtime.Runtime, sink output.Sink, opts Start tel := opts.Telemetry + hostEnv := filterHostEnv(os.Environ()) + containers := make([]runtime.ContainerConfig, len(opts.Containers)) for i, c := range opts.Containers { image, err := c.Image() @@ -128,6 +130,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"}) @@ -445,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 518157e..c75d4ce 100644 --- a/internal/container/start_test.go +++ b/internal/container/start_test.go @@ -53,14 +53,26 @@ 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) { + 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 a95fd77..e67898e 100644 --- a/test/integration/start_test.go +++ b/test/integration/start_test.go @@ -226,14 +226,32 @@ 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) + + mockServer := createMockLicenseServer(true) + defer mockServer.Close() + + ctx := testContext(t) + _, stderr, err := runLstk(t, ctx, "", env.With(env.APIEndpoint, mockServer.URL). + With(env.CI, "true"). + With(env.DisableEvents, "1"), + "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"]) } // hasBindTarget checks if any bind mount targets the given container path. @@ -247,6 +265,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{})