diff --git a/e2e/tests/up/docker.go b/e2e/tests/up/docker.go index 4ca61d3bc..85f015988 100644 --- a/e2e/tests/up/docker.go +++ b/e2e/tests/up/docker.go @@ -429,6 +429,45 @@ var _ = DevPodDescribe("devpod up test suite", func() { framework.ExpectNoError(err) server.Close() }, ginkgo.SpecTimeout(framework.GetTimeout())) + + ginkgo.It("should start a new workspace with feature that uses environment variables", func(ctx context.Context) { + tempDir, err := framework.CopyToTempDir("tests/up/testdata/docker-feature-env") + framework.ExpectNoError(err) + ginkgo.DeferCleanup(framework.CleanupTempDir, initialDir, tempDir) + + f := framework.NewDefaultFramework(initialDir + "/bin") + _ = f.DevPodProviderAdd(ctx, "docker") + err = f.DevPodProviderUse(ctx, "docker") + framework.ExpectNoError(err) + + ginkgo.DeferCleanup(f.DevPodWorkspaceDelete, context.Background(), tempDir) + + // Wait for devpod workspace to come online (deadline: 30s) + err = f.DevPodUp(ctx, tempDir, "--debug") + framework.ExpectNoError(err) + + workspace, err := f.FindWorkspace(ctx, tempDir) + framework.ExpectNoError(err) + + projectName := workspace.ID + ids, err := dockerHelper.FindContainer(ctx, []string{ + fmt.Sprintf("%s=%s", config.DockerIDLabel, workspace.UID), + }) + framework.ExpectNoError(err) + gomega.Expect(ids).To(gomega.HaveLen(1), "1 container to be created") + + // Verify the environment variable was passed to the feature in postCreateCommand + featureEnvValue, _, err := f.ExecCommandCapture(ctx, []string{"ssh", "--command", "cat $HOME/feature-env-value.out", projectName}) + framework.ExpectNoError(err) + featureEnvValue = strings.TrimSpace(featureEnvValue) + gomega.Expect(featureEnvValue).To(gomega.Equal("RESULT_ENV=feature_value"), "Feature should have access to environment variable in postCreateCommand") + + // Verify standard DevPod environment variables are present + devpodEnv, _, err := f.ExecCommandCapture(ctx, []string{"ssh", "--command", "env | grep DEVPOD", projectName}) + framework.ExpectNoError(err) + gomega.Expect(devpodEnv).To(gomega.ContainSubstring("DEVPOD=true"), "DEVPOD environment variable should be set") + gomega.Expect(devpodEnv).To(gomega.ContainSubstring("DEVPOD_WORKSPACE_ID="), "DEVPOD_WORKSPACE_ID environment variable should be set") + }, ginkgo.SpecTimeout(framework.GetTimeout())) }) }) }) diff --git a/e2e/tests/up/testdata/docker-feature-env/.devcontainer.json b/e2e/tests/up/testdata/docker-feature-env/.devcontainer.json new file mode 100644 index 000000000..c559f8d20 --- /dev/null +++ b/e2e/tests/up/testdata/docker-feature-env/.devcontainer.json @@ -0,0 +1,17 @@ +{ + "name": "Docker with features that use environment variables", + "image": "mcr.microsoft.com/vscode/devcontainers/base:alpine", + "features": { + "./test-feature": { + "version": "1.0.0" + } + }, + "remoteEnv": { + "TEST_FEATURE_ENV": "feature_value" + }, + "postCreateCommand": [ + "sh", + "-c", + "cat /tmp/feature-entrypoint-env.txt > $HOME/feature-env-value.out" + ] +} diff --git a/e2e/tests/up/testdata/docker-feature-env/test-feature/devcontainer-feature.json b/e2e/tests/up/testdata/docker-feature-env/test-feature/devcontainer-feature.json new file mode 100644 index 000000000..c8de3fc6f --- /dev/null +++ b/e2e/tests/up/testdata/docker-feature-env/test-feature/devcontainer-feature.json @@ -0,0 +1,14 @@ +{ + "name": "Test Feature with Environment Variables", + "id": "test-feature", + "version": "1.0.0", + "description": "A test feature that captures environment variables during installation and in its entrypoint", + "options": { + "version": { + "type": "string", + "default": "1.0.0", + "description": "Version of the test feature" + } + }, + "entrypoint": "/usr/local/bin/test-feature-entrypoint.sh" +} \ No newline at end of file diff --git a/e2e/tests/up/testdata/docker-feature-env/test-feature/entrypoint.sh b/e2e/tests/up/testdata/docker-feature-env/test-feature/entrypoint.sh new file mode 100644 index 000000000..1a85cace1 --- /dev/null +++ b/e2e/tests/up/testdata/docker-feature-env/test-feature/entrypoint.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +# Write environment variable to file when entrypoint is executed +echo "RESULT_ENV=${TEST_FEATURE_ENV}" > /tmp/feature-entrypoint-env.txt + +# Continue with original entrypoint +exec $@ diff --git a/e2e/tests/up/testdata/docker-feature-env/test-feature/install.sh b/e2e/tests/up/testdata/docker-feature-env/test-feature/install.sh new file mode 100644 index 000000000..7affbab08 --- /dev/null +++ b/e2e/tests/up/testdata/docker-feature-env/test-feature/install.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +script_dir="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" + +install -D -m 755 "$script_dir/entrypoint.sh" /usr/local/bin/test-feature-entrypoint.sh + +echo "Test feature installed successfully" diff --git a/pkg/devcontainer/single.go b/pkg/devcontainer/single.go index cfb123bfe..f6f85172c 100644 --- a/pkg/devcontainer/single.go +++ b/pkg/devcontainer/single.go @@ -221,9 +221,20 @@ func (r *runner) getDockerlessRunOptions( "DOCKERLESS_DOCKERFILE": buildInfo.Dockerless.Dockerfile, "GODEBUG": "http2client=0", // https://github.com/GoogleContainerTools/kaniko/issues/875 } + + // Add containerEnv variables for k, v := range mergedConfig.ContainerEnv { env[k] = v } + + // Add remoteEnv variables to env to make them available to the container + for key, value := range mergedConfig.RemoteEnv { + if _, exists := env[key]; !exists { + env[key] = value + } + } + + if buildInfo.Dockerless.Target != "" { env["DOCKERLESS_TARGET"] = buildInfo.Dockerless.Target } @@ -294,6 +305,18 @@ func (r *runner) getRunOptions( return nil, errors.Wrap(err, "marshal config") } + // Ensure containerEnv is initialized + if mergedConfig.ContainerEnv == nil { + mergedConfig.ContainerEnv = make(map[string]string) + } + + // Add remoteEnv variables to containerEnv to make them available to the container + for key, value := range mergedConfig.RemoteEnv { + if _, exists := mergedConfig.ContainerEnv[key]; !exists { + mergedConfig.ContainerEnv[key] = value + } + } + // build labels & entrypoint entrypoint, cmd := GetContainerEntrypointAndArgs(mergedConfig, buildInfo.ImageDetails) labels := []string{