From 067f9f78996c9d32d0c8924dfca3d0a36fd2294f Mon Sep 17 00:00:00 2001 From: Nathan Mittler Date: Tue, 14 May 2024 05:32:08 -0700 Subject: [PATCH] feat: Add global env Supports configuring global `env` variables that will be applied to all builds. Modifies the `builder` function to accept a `buildContext` structure. This will simplify similar modifications in the future. Fixes #1305 Signed-off-by: Nathan Mittler --- docs/configuration.md | 25 +++++++++++ pkg/build/gobuild.go | 43 +++++++++++++------ pkg/build/gobuild_test.go | 26 ++++++++--- pkg/build/options.go | 9 ++++ pkg/commands/options/build.go | 8 ++++ pkg/commands/options/build_test.go | 15 +++++++ pkg/commands/options/testdata/config/.ko.yaml | 1 + pkg/commands/resolver.go | 1 + 8 files changed, 111 insertions(+), 17 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 1b7439d2e6..0e7a089d07 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -104,6 +104,31 @@ You can also use the `KO_DEFAULTPLATFORMS` environment variable to set the defau KO_DEFAULTPLATFORMS=linux/arm64,linux/amd64 ``` +### Setting build environment variables + +By default, `ko` builds use the ambient environment from the system (i.e. `os.Environ()`). +These values can be overridden globally or per-build (or both). + +```yaml +env: +- FOO=foo +builds: +- id: foo + dir: . + main: ./foobar/foo + env: + - FOO=bar # Overrides the global value. +- id: bar + dir: ./bar + main: . +``` + +For a given build, the environment variables are merged in the following order: + +- System `os.Environ` (lowest precedence) +- Global `env` +- Build `env` (highest precedence) + ### Environment Variables (advanced) For ease of use, backward compatibility and advanced use cases, `ko` supports the following environment variables to diff --git a/pkg/build/gobuild.go b/pkg/build/gobuild.go index 172e9e3163..f7ff43dfb1 100644 --- a/pkg/build/gobuild.go +++ b/pkg/build/gobuild.go @@ -61,7 +61,16 @@ const ( // GetBase takes an importpath and returns a base image reference and base image (or index). type GetBase func(context.Context, string) (name.Reference, Result, error) -type builder func(context.Context, string, string, v1.Platform, Config) (string, error) +// buildContext provides parameters for a builder function. +type buildContext struct { + ip string + dir string + env []string + platform v1.Platform + config Config +} + +type builder func(context.Context, buildContext) (string, error) type sbomber func(context.Context, string, string, string, oci.SignedEntity, string) ([]byte, types.MediaType, error) @@ -81,6 +90,7 @@ type gobuild struct { disableOptimizations bool trimpath bool buildConfigs map[string]Config + env []string platformMatcher *platformMatcher dir string labels map[string]string @@ -103,6 +113,7 @@ type gobuildOpener struct { disableOptimizations bool trimpath bool buildConfigs map[string]Config + env []string platforms []string labels map[string]string dir string @@ -131,6 +142,7 @@ func (gbo *gobuildOpener) Open() (Interface, error) { disableOptimizations: gbo.disableOptimizations, trimpath: gbo.trimpath, buildConfigs: gbo.buildConfigs, + env: gbo.env, labels: gbo.labels, dir: gbo.dir, platformMatcher: matcher, @@ -251,8 +263,8 @@ func getGoBinary() string { return defaultGoBin } -func build(ctx context.Context, ip string, dir string, platform v1.Platform, config Config) (string, error) { - buildArgs, err := createBuildArgs(config) +func build(ctx context.Context, buildCtx buildContext) (string, error) { + buildArgs, err := createBuildArgs(buildCtx.config) if err != nil { return "", err } @@ -261,9 +273,9 @@ func build(ctx context.Context, ip string, dir string, platform v1.Platform, con args = append(args, "build") args = append(args, buildArgs...) - env, err := buildEnv(platform, os.Environ(), config.Env) + env, err := buildEnv(buildCtx.platform, os.Environ(), buildCtx.env, buildCtx.config.Env) if err != nil { - return "", fmt.Errorf("could not create env for %s: %w", ip, err) + return "", fmt.Errorf("could not create env for %s: %w", buildCtx.ip, err) } tmpDir := "" @@ -282,7 +294,7 @@ func build(ctx context.Context, ip string, dir string, platform v1.Platform, con } // TODO(#264): if KOCACHE is unset, default to filepath.Join(os.TempDir(), "ko"). - tmpDir = filepath.Join(dir, "bin", ip, platform.String()) + tmpDir = filepath.Join(dir, "bin", buildCtx.ip, buildCtx.platform.String()) if err := os.MkdirAll(tmpDir, os.ModePerm); err != nil { return "", fmt.Errorf("creating KOCACHE bin dir: %w", err) } @@ -296,18 +308,18 @@ func build(ctx context.Context, ip string, dir string, platform v1.Platform, con file := filepath.Join(tmpDir, "out") args = append(args, "-o", file) - args = append(args, ip) + args = append(args, buildCtx.ip) gobin := getGoBinary() cmd := exec.CommandContext(ctx, gobin, args...) - cmd.Dir = dir + cmd.Dir = buildCtx.dir cmd.Env = env var output bytes.Buffer cmd.Stderr = &output cmd.Stdout = &output - log.Printf("Building %s for %s", ip, platform) + log.Printf("Building %s for %s", buildCtx.ip, buildCtx.platform) if err := cmd.Run(); err != nil { if os.Getenv("KOCACHE") == "" { os.RemoveAll(tmpDir) @@ -440,7 +452,7 @@ func cycloneDX() sbomber { // 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, userEnv, configEnv []string) ([]string, error) { +func buildEnv(platform v1.Platform, osEnv, globalEnv, configEnv []string) ([]string, error) { // Default env env := []string{ "CGO_ENABLED=0", @@ -464,7 +476,8 @@ func buildEnv(platform v1.Platform, userEnv, configEnv []string) ([]string, erro } } - env = append(env, userEnv...) + env = append(env, osEnv...) + env = append(env, globalEnv...) env = append(env, configEnv...) return env, nil } @@ -836,7 +849,13 @@ func (g *gobuild) buildOne(ctx context.Context, refStr string, base v1.Image, pl } // Do the build into a temporary file. config := g.configForImportPath(ref.Path()) - file, err := g.build(ctx, ref.Path(), g.dir, *platform, config) + file, err := g.build(ctx, buildContext{ + ip: ref.Path(), + dir: g.dir, + env: g.env, + platform: *platform, + config: config, + }) if err != nil { return nil, fmt.Errorf("build: %w", err) } diff --git a/pkg/build/gobuild_test.go b/pkg/build/gobuild_test.go index 923a9bd3d2..6dd278b59b 100644 --- a/pkg/build/gobuild_test.go +++ b/pkg/build/gobuild_test.go @@ -217,7 +217,8 @@ func TestBuildEnv(t *testing.T) { tests := []struct { description string platform v1.Platform - userEnv []string + osEnv []string + globalEnv []string configEnv []string expectedEnvs map[string]string }{{ @@ -233,13 +234,28 @@ func TestBuildEnv(t *testing.T) { }, }, { description: "override a default value", + osEnv: []string{"CGO_ENABLED=0"}, + configEnv: []string{"CGO_ENABLED=1"}, + expectedEnvs: map[string]string{ + "CGO_ENABLED": "1", + }, + }, { + description: "global override a default value", + osEnv: []string{"CGO_ENABLED=0"}, + globalEnv: []string{"CGO_ENABLED=1"}, + expectedEnvs: map[string]string{ + "CGO_ENABLED": "1", + }, + }, { + description: "override a global value", + globalEnv: []string{"CGO_ENABLED=0"}, configEnv: []string{"CGO_ENABLED=1"}, expectedEnvs: map[string]string{ "CGO_ENABLED": "1", }, }, { description: "override an envvar and add an envvar", - userEnv: []string{"CGO_ENABLED=0"}, + osEnv: []string{"CGO_ENABLED=0"}, configEnv: []string{"CGO_ENABLED=1", "GOPRIVATE=git.internal.example.com,source.developers.google.com"}, expectedEnvs: map[string]string{ "CGO_ENABLED": "1", @@ -279,7 +295,7 @@ func TestBuildEnv(t *testing.T) { }} for _, test := range tests { t.Run(test.description, func(t *testing.T) { - env, err := buildEnv(test.platform, test.userEnv, test.configEnv) + env, err := buildEnv(test.platform, test.osEnv, test.globalEnv, test.configEnv) if err != nil { t.Fatalf("unexpected error running buildEnv(): %v", err) } @@ -401,7 +417,7 @@ func fauxSBOM(context.Context, string, string, string, oci.SignedEntity, string) } // A helper method we use to substitute for the default "build" method. -func writeTempFile(_ context.Context, s string, _ string, _ v1.Platform, _ Config) (string, error) { +func writeTempFile(_ context.Context, buildCtx buildContext) (string, error) { tmpDir, err := os.MkdirTemp("", "ko") if err != nil { return "", err @@ -412,7 +428,7 @@ func writeTempFile(_ context.Context, s string, _ string, _ v1.Platform, _ Confi return "", err } defer file.Close() - if _, err := file.WriteString(filepath.ToSlash(s)); err != nil { + if _, err := file.WriteString(filepath.ToSlash(buildCtx.ip)); err != nil { return "", err } return file.Name(), nil diff --git a/pkg/build/options.go b/pkg/build/options.go index 4cedf4d0fb..c4b4425aec 100644 --- a/pkg/build/options.go +++ b/pkg/build/options.go @@ -85,6 +85,15 @@ func WithConfig(buildConfigs map[string]Config) Option { } } +// WithEnv is a functional option for providing a global set of environment +// variables across all builds. +func WithEnv(env []string) Option { + return func(gbo *gobuildOpener) error { + gbo.env = env + return nil + } +} + // WithPlatforms is a functional option for building certain platforms for // multi-platform base images. To build everything from the base, use "all", // otherwise use a list of platform specs, i.e.: diff --git a/pkg/commands/options/build.go b/pkg/commands/options/build.go index a906d3282e..0fe4c789a7 100644 --- a/pkg/commands/options/build.go +++ b/pkg/commands/options/build.go @@ -47,6 +47,9 @@ type BuildOptions struct { // DefaultPlatforms defines the default platforms when Platforms is not explicitly defined DefaultPlatforms []string + // Env allows setting environment variables globally and applying them to each build. + Env []string + // WorkingDirectory allows for setting the working directory for invocations of the `go` tool. // Empty string means the current working directory. WorkingDirectory string @@ -138,6 +141,11 @@ func (bo *BuildOptions) LoadConfig() error { bo.DefaultPlatforms = dp } + env := v.GetStringSlice("env") + if len(env) > 0 { + bo.Env = env + } + if bo.BaseImage == "" { ref := v.GetString("defaultBaseImage") if _, err := name.ParseReference(ref); err != nil { diff --git a/pkg/commands/options/build_test.go b/pkg/commands/options/build_test.go index 2b25488e70..8a6a5fa15a 100644 --- a/pkg/commands/options/build_test.go +++ b/pkg/commands/options/build_test.go @@ -67,6 +67,21 @@ func TestDefaultPlatformsAll(t *testing.T) { } } +func TestEnv(t *testing.T) { + bo := &BuildOptions{ + WorkingDirectory: "testdata/config", + } + err := bo.LoadConfig() + if err != nil { + t.Fatal(err) + } + + wantEnv := []string{"FOO=bar"} // matches value in ./testdata/config/.ko.yaml + if !reflect.DeepEqual(bo.Env, wantEnv) { + t.Fatalf("wanted Env %s, got %s", wantEnv, bo.Env) + } +} + func TestBuildConfigWithWorkingDirectoryAndDirAndMain(t *testing.T) { bo := &BuildOptions{ WorkingDirectory: "testdata/paths", diff --git a/pkg/commands/options/testdata/config/.ko.yaml b/pkg/commands/options/testdata/config/.ko.yaml index c502a0b562..d1062112d2 100644 --- a/pkg/commands/options/testdata/config/.ko.yaml +++ b/pkg/commands/options/testdata/config/.ko.yaml @@ -1,2 +1,3 @@ defaultBaseImage: alpine defaultPlatforms: all +env: FOO=bar diff --git a/pkg/commands/resolver.go b/pkg/commands/resolver.go index 06fb29f140..185577efee 100644 --- a/pkg/commands/resolver.go +++ b/pkg/commands/resolver.go @@ -86,6 +86,7 @@ func gobuildOptions(bo *options.BuildOptions) ([]build.Option, error) { opts := []build.Option{ build.WithBaseImages(getBaseImage(bo)), + build.WithEnv(bo.Env), build.WithPlatforms(bo.Platforms...), build.WithJobs(bo.ConcurrentBuilds), }