diff --git a/e2e/tests/up/provider_docker.go b/e2e/tests/up/provider_docker.go index 8cd5aaf9a..56e95a7b2 100644 --- a/e2e/tests/up/provider_docker.go +++ b/e2e/tests/up/provider_docker.go @@ -175,6 +175,50 @@ var _ = ginkgo.Describe( gomega.Expect(customImage).To(gomega.Equal("alpine:latest")) }, ginkgo.SpecTimeout(framework.GetTimeout())) + ginkgo.It("variable substitution with defaults", func(ctx context.Context) { + tempDir, err := dtc.setupAndUp( + ctx, "tests/up/testdata/docker-variables-defaults", + ) + framework.ExpectNoError(err) + + workspace, err := dtc.f.FindWorkspace(ctx, tempDir) + framework.ExpectNoError(err) + + ids, err := dtc.findWorkspaceContainer(ctx, workspace) + framework.ExpectNoError(err) + gomega.Expect(ids).To(gomega.HaveLen(1)) + + // Unset var uses simple default + withDefault, err := dtc.execSSHCapture( + ctx, workspace.ID, + "cat $HOME/with-default.out", + ) + framework.ExpectNoError(err) + gomega.Expect(withDefault).To( + gomega.Equal("my_default_value"), + ) + + // Unset var uses default containing colons + colonDefault, err := dtc.execSSHCapture( + ctx, workspace.ID, + "cat $HOME/colon-default.out", + ) + framework.ExpectNoError(err) + gomega.Expect(colonDefault).To( + gomega.Equal("http://proxy:8080"), + ) + + // Set var ignores default + setVar, err := dtc.execSSHCapture( + ctx, workspace.ID, + "cat $HOME/set-var.out", + ) + framework.ExpectNoError(err) + gomega.Expect(setVar).To( + gomega.Equal(os.Getenv("HOME")), + ) + }, ginkgo.SpecTimeout(framework.GetTimeout())) + ginkgo.It("mounts", func(ctx context.Context) { tempDir, err := dtc.setupAndUp(ctx, "tests/up/testdata/docker-mounts", "--debug") framework.ExpectNoError(err) diff --git a/e2e/tests/up/testdata/docker-variables-defaults/.devcontainer.json b/e2e/tests/up/testdata/docker-variables-defaults/.devcontainer.json new file mode 100644 index 000000000..76663147e --- /dev/null +++ b/e2e/tests/up/testdata/docker-variables-defaults/.devcontainer.json @@ -0,0 +1,14 @@ +{ + "name": "variable-defaults-test", + "image": "ghcr.io/devsy-org/test-images/go:1", + "remoteEnv": { + "WITH_DEFAULT": "${localEnv:VARSUB_NONEXISTENT:my_default_value}", + "COLON_DEFAULT": "${localEnv:VARSUB_NONEXISTENT2:http://proxy:8080}", + "SET_VAR_IGNORES_DEFAULT": "${localEnv:HOME:fallback}" + }, + "postCreateCommand": [ + "sh", + "-c", + "echo -n $WITH_DEFAULT > $HOME/with-default.out && echo -n $COLON_DEFAULT > $HOME/colon-default.out && echo -n $SET_VAR_IGNORES_DEFAULT > $HOME/set-var.out" + ] +} diff --git a/pkg/devcontainer/config/substitute.go b/pkg/devcontainer/config/substitute.go index e25f1fba7..2a15dfea3 100644 --- a/pkg/devcontainer/config/substitute.go +++ b/pkg/devcontainer/config/substitute.go @@ -155,7 +155,7 @@ func lookupValue(isWindows bool, env map[string]string, args []string, match str } if len(args) > 1 { - defaultValue := args[1] + defaultValue := strings.Join(args[1:], ":") return defaultValue } diff --git a/pkg/devcontainer/config/substitute_test.go b/pkg/devcontainer/config/substitute_test.go new file mode 100644 index 000000000..d1efceb52 --- /dev/null +++ b/pkg/devcontainer/config/substitute_test.go @@ -0,0 +1,77 @@ +package config + +import "testing" + +func TestLookupValue(t *testing.T) { + tests := []struct { + name string + env map[string]string + args []string + want string + }{ + { + name: "var set no default", + env: map[string]string{"HOME": "/root"}, + args: []string{"HOME"}, + want: "/root", + }, + { + name: "var unset no default", + env: map[string]string{}, + args: []string{"MISSING"}, + want: "", + }, + { + name: "var unset simple default", + env: map[string]string{}, + args: []string{"MISSING", "fallback"}, + want: "fallback", + }, + { + name: "var unset default with colons", + env: map[string]string{}, + args: []string{ + "MISSING", "http", + "//proxy.example.com", "8080", + }, + want: "http://proxy.example.com:8080", + }, + { + name: "var set default ignored", + env: map[string]string{"VAR": "real"}, + args: []string{"VAR", "default"}, + want: "real", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := lookupValue( + false, tt.env, tt.args, "${match}", + ) + if got != tt.want { + t.Errorf( + "lookupValue() = %q, want %q", + got, tt.want, + ) + } + }) + } +} + +func TestResolveStringDefaultWithColons(t *testing.T) { + replace := func(_, variable string, args []string) string { + env := map[string]string{} + return lookupValue(false, env, args, "${"+variable+"}") + } + + got := ResolveString( + "${localEnv:MISSING:http://x:8080}", replace, + ) + want := "http://x:8080" + if got != want { + t.Errorf( + "ResolveString() = %q, want %q", got, want, + ) + } +}