From 43956db7fd75c488a82c70cf231f44287300a75d Mon Sep 17 00:00:00 2001
From: Maksim Nabokikh <maksim.nabokikh@flant.com>
Date: Wed, 7 Aug 2024 19:31:01 +0200
Subject: [PATCH] Change workdir for gomplate (#3684)

Workaround to run gomplate from a non-root directory in distroless images, because gomplate tries to access CWD on start.
See: https://github.com/hairyhenderson/gomplate/pull/2202

Signed-off-by: m.nabokikh <maksim.nabokikh@flant.com>
---
 cmd/docker-entrypoint/main.go      | 46 ++++++++++-------
 cmd/docker-entrypoint/main_test.go | 79 +++++++++++++-----------------
 2 files changed, 62 insertions(+), 63 deletions(-)

diff --git a/cmd/docker-entrypoint/main.go b/cmd/docker-entrypoint/main.go
index b0f8e277..14d837e5 100644
--- a/cmd/docker-entrypoint/main.go
+++ b/cmd/docker-entrypoint/main.go
@@ -22,20 +22,13 @@ func main() {
 		os.Exit(1)
 	}
 
-	if err := run(args, realExec, realWhich); err != nil {
+	if err := run(args, realExec, realWhich, realGomplate); err != nil {
 		fmt.Println("error:", err.Error())
 		os.Exit(1)
 	}
 }
 
-func realExec(fork bool, args ...string) error {
-	if fork {
-		if output, err := exec.Command(args[0], args[1:]...).CombinedOutput(); err != nil {
-			return fmt.Errorf("cannot fork/exec command %s: %w (output: %q)", args, err, string(output))
-		}
-		return nil
-	}
-
+func realExec(args ...string) error {
 	argv0, err := exec.LookPath(args[0])
 	if err != nil {
 		return fmt.Errorf("cannot lookup path for command %s: %w", args[0], err)
@@ -56,34 +49,49 @@ func realWhich(path string) string {
 	return fullPath
 }
 
-func run(args []string, execFunc func(bool, ...string) error, whichFunc func(string) string) error {
+func realGomplate(path string) (string, error) {
+	tmpFile, err := os.CreateTemp("/tmp", "dex.config.yaml-*")
+	if err != nil {
+		return "", fmt.Errorf("cannot create temp file: %w", err)
+	}
+
+	cmd := exec.Command("gomplate", "-f", path, "-o", tmpFile.Name())
+	// TODO(nabokihms): Workaround to run gomplate from a non-root directory in distroless images
+	//   gomplate tries to access CWD on start, see: https://github.com/hairyhenderson/gomplate/pull/2202
+	cmd.Dir = "/etc/dex"
+
+	output, err := cmd.CombinedOutput()
+	if err != nil {
+		return "", fmt.Errorf("error executing gomplate: %w, (output: %q)", err, string(output))
+	}
+
+	return tmpFile.Name(), nil
+}
+
+func run(args []string, execFunc func(...string) error, whichFunc func(string) string, gomplateFunc func(string) (string, error)) error {
 	if args[0] != "dex" && args[0] != whichFunc("dex") {
-		return execFunc(false, args...)
+		return execFunc(args...)
 	}
 
 	if args[1] != "serve" {
-		return execFunc(false, args...)
+		return execFunc(args...)
 	}
 
 	newArgs := []string{}
 	for _, tplCandidate := range args {
 		if hasSuffixes(tplCandidate, ".tpl", ".tmpl", ".yaml") {
-			tmpFile, err := os.CreateTemp("/tmp", "dex.config.yaml-*")
+			fileName, err := gomplateFunc(tplCandidate)
 			if err != nil {
-				return fmt.Errorf("cannot create temp file: %w", err)
-			}
-
-			if err := execFunc(true, "gomplate", "-f", tplCandidate, "-o", tmpFile.Name()); err != nil {
 				return err
 			}
 
-			newArgs = append(newArgs, tmpFile.Name())
+			newArgs = append(newArgs, fileName)
 		} else {
 			newArgs = append(newArgs, tplCandidate)
 		}
 	}
 
-	return execFunc(false, newArgs...)
+	return execFunc(newArgs...)
 }
 
 func hasSuffixes(s string, suffixes ...string) bool {
diff --git a/cmd/docker-entrypoint/main_test.go b/cmd/docker-entrypoint/main_test.go
index c8aef169..49da3b5f 100644
--- a/cmd/docker-entrypoint/main_test.go
+++ b/cmd/docker-entrypoint/main_test.go
@@ -6,7 +6,7 @@ import (
 )
 
 type execArgs struct {
-	fork        bool
+	gomplate    bool
 	argPrefixes []string
 }
 
@@ -16,98 +16,89 @@ func TestRun(t *testing.T) {
 		args         []string
 		execReturns  error
 		whichReturns string
-		wantExecArgs []execArgs
+		wantExecArgs execArgs
 		wantErr      error
 	}{
 		{
 			name:         "executable not dex",
 			args:         []string{"tuna", "fish"},
-			wantExecArgs: []execArgs{{fork: false, argPrefixes: []string{"tuna", "fish"}}},
+			wantExecArgs: execArgs{gomplate: false, argPrefixes: []string{"tuna", "fish"}},
 		},
 		{
 			name:         "executable is full path to dex",
 			args:         []string{"/usr/local/bin/dex", "marshmallow", "zelda"},
 			whichReturns: "/usr/local/bin/dex",
-			wantExecArgs: []execArgs{{fork: false, argPrefixes: []string{"/usr/local/bin/dex", "marshmallow", "zelda"}}},
+			wantExecArgs: execArgs{gomplate: false, argPrefixes: []string{"/usr/local/bin/dex", "marshmallow", "zelda"}},
 		},
 		{
 			name:         "command is not serve",
 			args:         []string{"dex", "marshmallow", "zelda"},
-			wantExecArgs: []execArgs{{fork: false, argPrefixes: []string{"dex", "marshmallow", "zelda"}}},
+			wantExecArgs: execArgs{gomplate: false, argPrefixes: []string{"dex", "marshmallow", "zelda"}},
 		},
 		{
 			name:         "no templates",
 			args:         []string{"dex", "serve", "config.yaml.not-a-template"},
-			wantExecArgs: []execArgs{{fork: false, argPrefixes: []string{"dex", "serve", "config.yaml.not-a-template"}}},
+			wantExecArgs: execArgs{gomplate: false, argPrefixes: []string{"dex", "serve", "config.yaml.not-a-template"}},
 		},
 		{
 			name:         "no templates",
 			args:         []string{"dex", "serve", "config.yaml.not-a-template"},
-			wantExecArgs: []execArgs{{fork: false, argPrefixes: []string{"dex", "serve", "config.yaml.not-a-template"}}},
+			wantExecArgs: execArgs{gomplate: false, argPrefixes: []string{"dex", "serve", "config.yaml.not-a-template"}},
 		},
 		{
-			name: ".tpl template",
-			args: []string{"dex", "serve", "config.tpl"},
-			wantExecArgs: []execArgs{
-				{fork: true, argPrefixes: []string{"gomplate", "-f", "config.tpl", "-o", "/tmp/dex.config.yaml-"}},
-				{fork: false, argPrefixes: []string{"dex", "serve", "/tmp/dex.config.yaml-"}},
-			},
+			name:         ".tpl template",
+			args:         []string{"dex", "serve", "config.tpl"},
+			wantExecArgs: execArgs{gomplate: true, argPrefixes: []string{"dex", "serve", "/tmp/dex.config.yaml-"}},
 		},
 		{
-			name: ".tmpl template",
-			args: []string{"dex", "serve", "config.tmpl"},
-			wantExecArgs: []execArgs{
-				{fork: true, argPrefixes: []string{"gomplate", "-f", "config.tmpl", "-o", "/tmp/dex.config.yaml-"}},
-				{fork: false, argPrefixes: []string{"dex", "serve", "/tmp/dex.config.yaml-"}},
-			},
+			name:         ".tmpl template",
+			args:         []string{"dex", "serve", "config.tmpl"},
+			wantExecArgs: execArgs{gomplate: true, argPrefixes: []string{"dex", "serve", "/tmp/dex.config.yaml-"}},
 		},
 		{
-			name: ".yaml template",
-			args: []string{"dex", "serve", "some/path/config.yaml"},
-			wantExecArgs: []execArgs{
-				{fork: true, argPrefixes: []string{"gomplate", "-f", "some/path/config.yaml", "-o", "/tmp/dex.config.yaml-"}},
-				{fork: false, argPrefixes: []string{"dex", "serve", "/tmp/dex.config.yaml-"}},
-			},
+			name:         ".yaml template",
+			args:         []string{"dex", "serve", "some/path/config.yaml"},
+			wantExecArgs: execArgs{gomplate: true, argPrefixes: []string{"dex", "serve", "/tmp/dex.config.yaml-"}},
 		},
 	}
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
-			var gotExecForks []bool
-			var gotExecArgs [][]string
-			fakeExec := func(fork bool, args ...string) error {
-				gotExecForks = append(gotExecForks, fork)
-				gotExecArgs = append(gotExecArgs, args)
+			var gotExecArgs []string
+			var runsGomplate bool
+
+			fakeExec := func(args ...string) error {
+				gotExecArgs = append(args, gotExecArgs...)
 				return test.execReturns
 			}
 
 			fakeWhich := func(_ string) string { return test.whichReturns }
 
-			gotErr := run(test.args, fakeExec, fakeWhich)
+			fakeGomplate := func(file string) (string, error) {
+				runsGomplate = true
+				return "/tmp/dex.config.yaml-", nil
+			}
+
+			gotErr := run(test.args, fakeExec, fakeWhich, fakeGomplate)
 			if (test.wantErr == nil) != (gotErr == nil) {
 				t.Errorf("wanted error %s, got %s", test.wantErr, gotErr)
 			}
-			if !execArgsMatch(test.wantExecArgs, gotExecForks, gotExecArgs) {
-				t.Errorf("wanted exec args %+v, got %+v %+v", test.wantExecArgs, gotExecForks, gotExecArgs)
+
+			if !execArgsMatch(test.wantExecArgs, runsGomplate, gotExecArgs) {
+				t.Errorf("wanted exec args %+v (running gomplate: %+v), got %+v (running gomplate: %+v)",
+					test.wantExecArgs.argPrefixes, test.wantExecArgs.gomplate, gotExecArgs, runsGomplate)
 			}
 		})
 	}
 }
 
-func execArgsMatch(wantExecArgs []execArgs, gotForks []bool, gotExecArgs [][]string) bool {
-	if len(wantExecArgs) != len(gotForks) {
+func execArgsMatch(wantExecArgs execArgs, gomplate bool, gotExecArgs []string) bool {
+	if wantExecArgs.gomplate != gomplate {
 		return false
 	}
-
-	for i := range wantExecArgs {
-		if wantExecArgs[i].fork != gotForks[i] {
+	for i := range wantExecArgs.argPrefixes {
+		if !strings.HasPrefix(gotExecArgs[i], wantExecArgs.argPrefixes[i]) {
 			return false
 		}
-		for j := range wantExecArgs[i].argPrefixes {
-			if !strings.HasPrefix(gotExecArgs[i][j], wantExecArgs[i].argPrefixes[j]) {
-				return false
-			}
-		}
 	}
-
 	return true
 }
-- 
GitLab