Skip to content

Commit

Permalink
feat: Add global env
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
nmittler committed May 14, 2024
1 parent bde269b commit 067f9f7
Show file tree
Hide file tree
Showing 8 changed files with 111 additions and 17 deletions.
25 changes: 25 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
43 changes: 31 additions & 12 deletions pkg/build/gobuild.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
}
Expand All @@ -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 := ""
Expand All @@ -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)
}
Expand All @@ -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)
Expand Down Expand Up @@ -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",
Expand All @@ -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
}
Expand Down Expand Up @@ -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)
}
Expand Down
26 changes: 21 additions & 5 deletions pkg/build/gobuild_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}{{
Expand All @@ -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",
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
9 changes: 9 additions & 0 deletions pkg/build/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.:
Expand Down
8 changes: 8 additions & 0 deletions pkg/commands/options/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
15 changes: 15 additions & 0 deletions pkg/commands/options/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions pkg/commands/options/testdata/config/.ko.yaml
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
defaultBaseImage: alpine
defaultPlatforms: all
env: FOO=bar
1 change: 1 addition & 0 deletions pkg/commands/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}
Expand Down

0 comments on commit 067f9f7

Please sign in to comment.