Repository: google/ko
Pull request #419 (2021-08-18 23:28:40)

Enable setting environment variables in .ko.yaml
Matches the GoReleaser format.

Related: #340

halvards: Should I commit the changes in `doc/` too? The only changes are the dates in the `Auto generated` comments on each file, there are no changes to the content.
jonjohnsonjr: I think this just needs a rebase.
halvards: Rebased, but Travis CI complains about the s390x build. Other archs seem fine, and the other PR has the same problem.
jonjohnsonjr: > Rebased, but Travis CI complains about the s390x build. Other archs seem fine, and the other PR has the same problem.

Frustrating :/ just going to merge this anyway but we should probably just drop travis.

Patch:
diff --git a/README.md b/README.md
index 24e515ce..c44cecf8 100644
--- a/README.md
+++ b/README.md
@@ -135,6 +135,8 @@ configuration section in your `.ko.yaml`.
 builds:
 - id: foo
   main: ./foobar/foo
+  env:
+  - GOPRIVATE=git.internal.example.com,source.developers.google.com
   flags:
   - -tags
   - netgo
@@ -144,6 +146,8 @@ builds:
   - -X main.version={{.Env.VERSION}}
 - id: bar
   main: ./foobar/bar/main.go
+  env:
+  - GOCACHE=/workspace/.gocache
   ldflags:
   - -s
   - -w
@@ -156,7 +160,7 @@ with the intended import path.
 
 _Please note:_ Even though the configuration section is similar to the
 [GoReleaser `builds` section](https://goreleaser.com/customization/build/),
-only the `flags` and `ldflags` fields are currently supported. Also, the
+only the `env`, `flags` and `ldflags` fields are currently supported. Also, the
 templating support is currently limited to environment variables only.
 
 ## Naming Images
diff --git a/pkg/build/config.go b/pkg/build/config.go
index cb191648..5e1f4cf7 100644
--- a/pkg/build/config.go
+++ b/pkg/build/config.go
@@ -79,6 +79,9 @@ type Config struct {
 	Ldflags StringArray `yaml:",omitempty"`
 	Flags   FlagArray   `yaml:",omitempty"`
 
+	// Env allows setting environment variables for `go build`
+	Env []string `yaml:",omitempty"`
+
 	// Other GoReleaser fields that are not supported or do not make sense
 	// in the context of ko, for reference or for future use:
 	// Goos         []string    `yaml:",omitempty"`
@@ -87,7 +90,6 @@ type Config struct {
 	// Gomips       []string    `yaml:",omitempty"`
 	// Targets      []string    `yaml:",omitempty"`
 	// Binary       string      `yaml:",omitempty"`
-	// Env          []string    `yaml:",omitempty"`
 	// Lang         string      `yaml:",omitempty"`
 	// Asmflags     StringArray `yaml:",omitempty"`
 	// Gcflags      StringArray `yaml:",omitempty"`
diff --git a/pkg/build/gobuild.go b/pkg/build/gobuild.go
index 07017ec6..81d5d74b 100644
--- a/pkg/build/gobuild.go
+++ b/pkg/build/gobuild.go
@@ -378,7 +378,29 @@ func build(ctx context.Context, ip string, dir string, platform v1.Platform, con
 	cmd := exec.CommandContext(ctx, "go", args...)
 	cmd.Dir = dir
 
-	// Last one wins
+	env, err := buildEnv(platform, config.Env)
+	if err != nil {
+		return "", fmt.Errorf("could not create env for %s: %v", ip, err)
+	}
+	cmd.Env = env
+
+	var output bytes.Buffer
+	cmd.Stderr = &output
+	cmd.Stdout = &output
+
+	log.Printf("Building %s for %s", ip, platformToString(platform))
+	if err := cmd.Run(); err != nil {
+		os.RemoveAll(tmpDir)
+		log.Printf("Unexpected error running \"go build\": %v\n%v", err, output.String())
+		return "", err
+	}
+	return file, nil
+}
+
+// buildEnv creates the environment variables used by the `go build` command.
+// From `os/exec.Cmd`: If Env contains duplicate environment keys, only the last
+// value in the slice for each duplicate key is used.
+func buildEnv(platform v1.Platform, configEnv []string) ([]string, error) {
 	defaultEnv := []string{
 		"CGO_ENABLED=0",
 		"GOOS=" + platform.OS,
@@ -388,26 +410,16 @@ func build(ctx context.Context, ip string, dir string, platform v1.Platform, con
 	if strings.HasPrefix(platform.Architecture, "arm") && platform.Variant != "" {
 		goarm, err := getGoarm(platform)
 		if err != nil {
-			return "", fmt.Errorf("goarm failure for %s: %v", ip, err)
+			return nil, fmt.Errorf("goarm failure: %v", err)
 		}
 		if goarm != "" {
 			defaultEnv = append(defaultEnv, "GOARM="+goarm)
 		}
 	}
 
-	cmd.Env = append(defaultEnv, os.Environ()...)
-
-	var output bytes.Buffer
-	cmd.Stderr = &output
-	cmd.Stdout = &output
-
-	log.Printf("Building %s for %s", ip, platformToString(platform))
-	if err := cmd.Run(); err != nil {
-		os.RemoveAll(tmpDir)
-		log.Printf("Unexpected error running \"go build\": %v\n%v", err, output.String())
-		return "", err
-	}
-	return file, nil
+	env := append(defaultEnv, os.Environ()...)
+	env = append(env, configEnv...)
+	return env, nil
 }
 
 func appFilename(importpath string) string {


Test patch:
diff --git a/pkg/build/gobuild_test.go b/pkg/build/gobuild_test.go
index 8f8211f5..3d253ef5 100644
--- a/pkg/build/gobuild_test.go
+++ b/pkg/build/gobuild_test.go
@@ -232,6 +232,83 @@ func TestGoBuildIsSupportedRefWithModules(t *testing.T) {
 	}
 }
 
+func TestBuildEnv(t *testing.T) {
+	tests := []struct {
+		description  string
+		platform     v1.Platform
+		configEnv    []string
+		expectedEnvs map[string]string
+	}{
+		{
+			description: "defaults",
+			platform: v1.Platform{
+				OS:           "linux",
+				Architecture: "amd64",
+			},
+			expectedEnvs: map[string]string{
+				"GOOS":        "linux",
+				"GOARCH":      "amd64",
+				"CGO_ENABLED": "0",
+			},
+		},
+		{
+			description: "override a default value",
+			configEnv:   []string{"CGO_ENABLED=1"},
+			expectedEnvs: map[string]string{
+				"CGO_ENABLED": "1",
+			},
+		},
+		{
+			description: "override an envvar and add an envvar",
+			configEnv:   []string{"CGO_ENABLED=1", "GOPRIVATE=git.internal.example.com,source.developers.google.com"},
+			expectedEnvs: map[string]string{
+				"CGO_ENABLED": "1",
+				"GOPRIVATE":   "git.internal.example.com,source.developers.google.com",
+			},
+		},
+		{
+			description: "arm variant",
+			platform: v1.Platform{
+				Architecture: "arm",
+				Variant:      "v7",
+			},
+			expectedEnvs: map[string]string{
+				"GOARCH": "arm",
+				"GOARM":  "7",
+			},
+		},
+		{
+			description: "arm64 variant",
+			platform: v1.Platform{
+				Architecture: "arm64",
+				Variant:      "v8",
+			},
+			expectedEnvs: map[string]string{
+				"GOARCH": "arm64",
+				"GOARM":  "7",
+			},
+		},
+	}
+	for _, test := range tests {
+		t.Run(test.description, func(t *testing.T) {
+			env, err := buildEnv(test.platform, test.configEnv)
+			if err != nil {
+				t.Fatalf("unexpected error running buildEnv(): %v", err)
+			}
+			envs := map[string]string{}
+			for _, e := range env {
+				split := strings.SplitN(e, "=", 2)
+				envs[split[0]] = split[1]
+			}
+			for key, val := range test.expectedEnvs {
+				if envs[key] != val {
+					t.Errorf("buildEnv(): expected %s=%s, got %s=%s", key, val, key, envs[key])
+				}
+			}
+		})
+	}
+}
+
 // A helper method we use to substitute for the default "build" method.
 func writeTempFile(_ context.Context, s string, _ string, _ v1.Platform, _ Config) (string, error) {
 	tmpDir, err := ioutil.TempDir("", "ko")