Repository: coder/envbuilder
Pull request #206 (2024-05-23 02:57:35)

feat: Accept custom LookupEnv function in SubstituteVars
I need this change for an off-brand usage of the devcontainer package in custom code.


Patch:
diff --git a/devcontainer/devcontainer.go b/devcontainer/devcontainer.go
index 8f2780c..9fd2740 100644
--- a/devcontainer/devcontainer.go
+++ b/devcontainer/devcontainer.go
@@ -73,7 +73,7 @@ type Compiled struct {
 	RemoteEnv    map[string]string
 }
 
-func SubstituteVars(s string, workspaceFolder string) string {
+func SubstituteVars(s string, workspaceFolder string, lookupEnv func(string) (string, bool)) string {
 	var buf string
 	for {
 		beforeOpen, afterOpen, ok := strings.Cut(s, "${")
@@ -85,14 +85,14 @@ func SubstituteVars(s string, workspaceFolder string) string {
 			return buf + s
 		}
 
-		buf += beforeOpen + substitute(varExpr, workspaceFolder)
+		buf += beforeOpen + substitute(varExpr, workspaceFolder, lookupEnv)
 		s = afterClose
 	}
 }
 
 // Spec for variable substitutions:
 // https://containers.dev/implementors/json_reference/#variables-in-devcontainerjson
-func substitute(varExpr string, workspaceFolder string) string {
+func substitute(varExpr string, workspaceFolder string, lookupEnv func(string) (string, bool)) string {
 	parts := strings.Split(varExpr, ":")
 	if len(parts) == 1 {
 		switch varExpr {
@@ -101,12 +101,16 @@ func substitute(varExpr string, workspaceFolder string) string {
 		case "localWorkspaceFolderBasename", "containerWorkspaceFolderBasename":
 			return filepath.Base(workspaceFolder)
 		default:
-			return os.Getenv(varExpr)
+			val, ok := lookupEnv(varExpr)
+			if ok {
+				return val
+			}
+			return ""
 		}
 	}
 	switch parts[0] {
 	case "env", "localEnv", "containerEnv":
-		if val, ok := os.LookupEnv(parts[1]); ok {
+		if val, ok := lookupEnv(parts[1]); ok {
 			return val
 		}
 		if len(parts) == 3 {
@@ -131,7 +135,7 @@ func (s Spec) HasDockerfile() bool {
 // devcontainerDir is the path to the directory where the devcontainer.json file
 // is located. scratchDir is the path to the directory where the Dockerfile will
 // be written to if one doesn't exist.
-func (s *Spec) Compile(fs billy.Filesystem, devcontainerDir, scratchDir string, fallbackDockerfile, workspaceFolder string, useBuildContexts bool) (*Compiled, error) {
+func (s *Spec) Compile(fs billy.Filesystem, devcontainerDir, scratchDir string, fallbackDockerfile, workspaceFolder string, useBuildContexts bool, lookupEnv func(string) (string, bool)) (*Compiled, error) {
 	params := &Compiled{
 		User:         s.ContainerUser,
 		ContainerEnv: s.ContainerEnv,
@@ -178,7 +182,7 @@ func (s *Spec) Compile(fs billy.Filesystem, devcontainerDir, scratchDir string,
 
 	buildArgs := make([]string, 0)
 	for _, key := range buildArgkeys {
-		val := SubstituteVars(s.Build.Args[key], workspaceFolder)
+		val := SubstituteVars(s.Build.Args[key], workspaceFolder, lookupEnv)
 		buildArgs = append(buildArgs, key+"="+val)
 	}
 	params.BuildArgs = buildArgs
diff --git a/envbuilder.go b/envbuilder.go
index 7ea7791..7e88cbe 100644
--- a/envbuilder.go
+++ b/envbuilder.go
@@ -290,7 +290,7 @@ func Run(ctx context.Context, options Options) error {
 					options.Logger(notcodersdk.LogLevelInfo, "No Dockerfile or image specified; falling back to the default image...")
 					fallbackDockerfile = defaultParams.DockerfilePath
 				}
-				buildParams, err = devContainer.Compile(options.Filesystem, devcontainerDir, MagicDir, fallbackDockerfile, options.WorkspaceFolder, false)
+				buildParams, err = devContainer.Compile(options.Filesystem, devcontainerDir, MagicDir, fallbackDockerfile, options.WorkspaceFolder, false, os.LookupEnv)
 				if err != nil {
 					return fmt.Errorf("compile devcontainer.json: %w", err)
 				}
@@ -669,7 +669,7 @@ func Run(ctx context.Context, options Options) error {
 		}
 		sort.Strings(envKeys)
 		for _, envVar := range envKeys {
-			value := devcontainer.SubstituteVars(env[envVar], options.WorkspaceFolder)
+			value := devcontainer.SubstituteVars(env[envVar], options.WorkspaceFolder, os.LookupEnv)
 			os.Setenv(envVar, value)
 		}
 	}


Test patch:
diff --git a/devcontainer/devcontainer_test.go b/devcontainer/devcontainer_test.go
index da00322..c864e11 100644
--- a/devcontainer/devcontainer_test.go
+++ b/devcontainer/devcontainer_test.go
@@ -87,7 +87,7 @@ func TestCompileWithFeatures(t *testing.T) {
 	dc, err := devcontainer.Parse([]byte(raw))
 	require.NoError(t, err)
 	fs := memfs.New()
-	params, err := dc.Compile(fs, "", magicDir, "", "", false)
+	params, err := dc.Compile(fs, "", magicDir, "", "", false, os.LookupEnv)
 	require.NoError(t, err)
 
 	// We have to SHA because we get a different MD5 every time!
@@ -118,7 +118,7 @@ func TestCompileDevContainer(t *testing.T) {
 		dc := &devcontainer.Spec{
 			Image: "localhost:5000/envbuilder-test-ubuntu:latest",
 		}
-		params, err := dc.Compile(fs, "", magicDir, "", "", false)
+		params, err := dc.Compile(fs, "", magicDir, "", "", false, os.LookupEnv)
 		require.NoError(t, err)
 		require.Equal(t, filepath.Join(magicDir, "Dockerfile"), params.DockerfilePath)
 		require.Equal(t, magicDir, params.BuildContext)
@@ -144,7 +144,7 @@ func TestCompileDevContainer(t *testing.T) {
 		_, err = io.WriteString(file, "FROM localhost:5000/envbuilder-test-ubuntu:latest")
 		require.NoError(t, err)
 		_ = file.Close()
-		params, err := dc.Compile(fs, dcDir, magicDir, "", "/var/workspace", false)
+		params, err := dc.Compile(fs, dcDir, magicDir, "", "/var/workspace", false, os.LookupEnv)
 		require.NoError(t, err)
 		require.Equal(t, "ARG1=value1", params.BuildArgs[0])
 		require.Equal(t, "ARG2=workspace", params.BuildArgs[1])