Skip to content

Commit c42ee5f

Browse files
nmittlerimjasonh
authored andcommitted
feat: add template params for platform info
This restructures the build logic in order to expand the buildArgs to include: - `Env`: the actual environment variables used to execute the build. This includes platform info (e.g. `GOOS`, `GOARCH`). - `GoEnv`: the map of variables from `go env`, but overridden with any platform-specific values defined in `Env`. Fixes #1301 Signed-off-by: Nathan Mittler <[email protected]>
1 parent 38a1feb commit c42ee5f

File tree

3 files changed

+152
-44
lines changed

3 files changed

+152
-44
lines changed

docs/configuration.md

+15-14
Original file line numberDiff line numberDiff line change
@@ -84,20 +84,21 @@ The `ko` builds supports templating of `flags` and `ldflags`, similar to the
8484

8585
The table below lists the supported template parameters.
8686

87-
| Template param | Description |
88-
|-----------------------|-------------------------------------------------------|
89-
| `Env` | Map of system environment variables from `os.Environ` |
90-
| `Date` | The UTC build date in RFC 3339 format |
91-
| `Timestamp` | The UTC build date as Unix epoc seconds |
92-
| `Git.Branch` | The current git branch |
93-
| `Git.Tag` | The current git tag |
94-
| `Git.ShortCommit` | The git commit short hash |
95-
| `Git.FullCommit` | The git commit full hash |
96-
| `Git.CommitDate` | The UTC commit date in RFC 3339 format |
97-
| `Git.CommitTimestamp` | The UTC commit date in Unix format |
98-
| `Git.IsDirty` | Whether or not current git state is dirty |
99-
| `Git.IsClean` | Whether or not current git state is clean. |
100-
| `Git.TreeState` | Either `clean` or `dirty` |
87+
| Template param | Description |
88+
|-----------------------|----------------------------------------------------------|
89+
| `Env` | Map of environment variables used for the build |
90+
| `GoEnv` | Map of `go env` environment variables used for the build |
91+
| `Date` | The UTC build date in RFC 3339 format |
92+
| `Timestamp` | The UTC build date as Unix epoc seconds |
93+
| `Git.Branch` | The current git branch |
94+
| `Git.Tag` | The current git tag |
95+
| `Git.ShortCommit` | The git commit short hash |
96+
| `Git.FullCommit` | The git commit full hash |
97+
| `Git.CommitDate` | The UTC commit date in RFC 3339 format |
98+
| `Git.CommitTimestamp` | The UTC commit date in Unix format |
99+
| `Git.IsDirty` | Whether or not current git state is dirty |
100+
| `Git.IsClean` | Whether or not current git state is clean. |
101+
| `Git.TreeState` | Either `clean` or `dirty` |
101102

102103
### Setting default platforms
103104

pkg/build/gobuild.go

+78-17
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package build
1616

1717
import (
1818
"archive/tar"
19+
"bufio"
1920
"bytes"
2021
"context"
2122
"errors"
@@ -68,7 +69,7 @@ type buildContext struct {
6869
creationTime v1.Time
6970
ip string
7071
dir string
71-
env []string
72+
mergedEnv []string
7273
platform v1.Platform
7374
config Config
7475
}
@@ -267,6 +268,8 @@ func getGoBinary() string {
267268
}
268269

269270
func build(ctx context.Context, buildCtx buildContext) (string, error) {
271+
// Create the set of build arguments from the config flags/ldflags with any
272+
// template parameters applied.
270273
buildArgs, err := createBuildArgs(ctx, buildCtx)
271274
if err != nil {
272275
return "", err
@@ -275,12 +278,6 @@ func build(ctx context.Context, buildCtx buildContext) (string, error) {
275278
args := make([]string, 0, 4+len(buildArgs))
276279
args = append(args, "build")
277280
args = append(args, buildArgs...)
278-
279-
env, err := buildEnv(buildCtx.platform, os.Environ(), buildCtx.env, buildCtx.config.Env)
280-
if err != nil {
281-
return "", fmt.Errorf("could not create env for %s: %w", buildCtx.ip, err)
282-
}
283-
284281
tmpDir := ""
285282

286283
if dir := os.Getenv("KOCACHE"); dir != "" {
@@ -316,7 +313,7 @@ func build(ctx context.Context, buildCtx buildContext) (string, error) {
316313
gobin := getGoBinary()
317314
cmd := exec.CommandContext(ctx, gobin, args...)
318315
cmd.Dir = buildCtx.dir
319-
cmd.Env = env
316+
cmd.Env = buildCtx.mergedEnv
320317

321318
var output bytes.Buffer
322319
cmd.Stderr = &output
@@ -325,13 +322,49 @@ func build(ctx context.Context, buildCtx buildContext) (string, error) {
325322
log.Printf("Building %s for %s", buildCtx.ip, buildCtx.platform)
326323
if err := cmd.Run(); err != nil {
327324
if os.Getenv("KOCACHE") == "" {
328-
os.RemoveAll(tmpDir)
325+
_ = os.RemoveAll(tmpDir)
329326
}
330327
return "", fmt.Errorf("go build: %w: %s", err, output.String())
331328
}
332329
return file, nil
333330
}
334331

332+
func goenv(ctx context.Context) (map[string]string, error) {
333+
gobin := getGoBinary()
334+
cmd := exec.CommandContext(ctx, gobin, "env")
335+
var output bytes.Buffer
336+
cmd.Stdout = &output
337+
cmd.Stderr = &output
338+
if err := cmd.Run(); err != nil {
339+
return nil, fmt.Errorf("go env: %w: %s", err, output.String())
340+
}
341+
342+
env := make(map[string]string)
343+
scanner := bufio.NewScanner(bytes.NewReader(output.Bytes()))
344+
345+
line := 0
346+
for scanner.Scan() {
347+
line++
348+
kv := strings.SplitN(scanner.Text(), "=", 2)
349+
if len(kv) != 2 {
350+
return nil, fmt.Errorf("go env: failed parsing line: %d", line)
351+
}
352+
key := strings.TrimSpace(kv[0])
353+
value := strings.TrimSpace(kv[1])
354+
355+
// Unquote the value. Handle single or double quoted strings.
356+
if len(value) > 1 && ((value[0] == '\'' && value[len(value)-1] == '\'') ||
357+
(value[0] == '"' && value[len(value)-1] == '"')) {
358+
value = value[1 : len(value)-1]
359+
}
360+
env[key] = value
361+
}
362+
if err := scanner.Err(); err != nil {
363+
return nil, fmt.Errorf("go env: failed parsing: %w", err)
364+
}
365+
return env, nil
366+
}
367+
335368
func goversionm(ctx context.Context, file string, appPath string, appFileName string, se oci.SignedEntity, dir string) ([]byte, types.MediaType, error) {
336369
gobin := getGoBinary()
337370

@@ -724,15 +757,31 @@ func (g *gobuild) tarKoData(ref reference, platform *v1.Platform) (*bytes.Buffer
724757
return buf, walkRecursive(tw, root, chroot, creationTime, platform)
725758
}
726759

727-
func createTemplateData(ctx context.Context, buildCtx buildContext) map[string]interface{} {
760+
func createTemplateData(ctx context.Context, buildCtx buildContext) (map[string]interface{}, error) {
728761
envVars := map[string]string{
729762
"LDFLAGS": "",
730763
}
731-
for _, entry := range os.Environ() {
764+
for _, entry := range buildCtx.mergedEnv {
732765
kv := strings.SplitN(entry, "=", 2)
766+
if len(kv) != 2 {
767+
return nil, fmt.Errorf("invalid environment variable entry: %q", entry)
768+
}
733769
envVars[kv[0]] = kv[1]
734770
}
735771

772+
// Get the go environment.
773+
goEnv, err := goenv(ctx)
774+
if err != nil {
775+
return nil, err
776+
}
777+
778+
// Override go env with any matching values from the environment variables.
779+
for k, v := range envVars {
780+
if _, ok := goEnv[k]; ok {
781+
goEnv[k] = v
782+
}
783+
}
784+
736785
// Get the git information, if available.
737786
info, err := git.GetInfo(ctx, buildCtx.dir)
738787
if err != nil {
@@ -747,10 +796,11 @@ func createTemplateData(ctx context.Context, buildCtx buildContext) map[string]i
747796

748797
return map[string]interface{}{
749798
"Env": envVars,
799+
"GoEnv": goEnv,
750800
"Git": info.TemplateValue(),
751801
"Date": date.Format(time.RFC3339),
752802
"Timestamp": date.UTC().Unix(),
753-
}
803+
}, nil
754804
}
755805

756806
func applyTemplating(list []string, data map[string]interface{}) ([]string, error) {
@@ -775,7 +825,10 @@ func applyTemplating(list []string, data map[string]interface{}) ([]string, erro
775825
func createBuildArgs(ctx context.Context, buildCtx buildContext) ([]string, error) {
776826
var args []string
777827

778-
data := createTemplateData(ctx, buildCtx)
828+
data, err := createTemplateData(ctx, buildCtx)
829+
if err != nil {
830+
return nil, err
831+
}
779832

780833
if len(buildCtx.config.Flags) > 0 {
781834
flags, err := applyTemplating(buildCtx.config.Flags, data)
@@ -865,13 +918,21 @@ func (g *gobuild) buildOne(ctx context.Context, refStr string, base v1.Image, pl
865918
if !g.platformMatcher.matches(platform) {
866919
return nil, fmt.Errorf("base image platform %q does not match desired platforms %v", platform, g.platformMatcher.platforms)
867920
}
868-
// Do the build into a temporary file.
921+
869922
config := g.configForImportPath(ref.Path())
923+
924+
// Merge the system, global, and build config environment variables.
925+
mergedEnv, err := buildEnv(*platform, os.Environ(), g.env, config.Env)
926+
if err != nil {
927+
return nil, fmt.Errorf("could not create env for %s: %w", ref.Path(), err)
928+
}
929+
930+
// Do the build into a temporary file.
870931
file, err := g.build(ctx, buildContext{
871932
creationTime: g.creationTime,
872933
ip: ref.Path(),
873934
dir: g.dir,
874-
env: g.env,
935+
mergedEnv: mergedEnv,
875936
platform: *platform,
876937
config: config,
877938
})
@@ -1101,7 +1162,7 @@ func (g *gobuild) buildAll(ctx context.Context, ref string, baseRef name.Referen
11011162
return nil, err
11021163
}
11031164

1104-
matches := []v1.Descriptor{}
1165+
matches := make([]v1.Descriptor, 0)
11051166
for _, desc := range im.Manifests {
11061167
// Nested index is pretty rare. We could support this in theory, but return an error for now.
11071168
if desc.MediaType != types.OCIManifestSchema1 && desc.MediaType != types.DockerManifestSchema2 {
@@ -1226,7 +1287,7 @@ func parseSpec(spec []string) (*platformMatcher, error) {
12261287
return &platformMatcher{spec: spec}, nil
12271288
}
12281289

1229-
platforms := []v1.Platform{}
1290+
platforms := make([]v1.Platform, 0)
12301291
for _, s := range spec {
12311292
p, err := v1.ParsePlatform(s)
12321293
if err != nil {

pkg/build/gobuild_test.go

+59-13
Original file line numberDiff line numberDiff line change
@@ -315,16 +315,19 @@ func TestBuildEnv(t *testing.T) {
315315
}
316316
}
317317

318-
func TestCreateTemplateData(t *testing.T) {
319-
t.Run("env", func(t *testing.T) {
320-
t.Setenv("FOO", "bar")
321-
params := createTemplateData(context.TODO(), buildContext{dir: t.TempDir()})
322-
vars := params["Env"].(map[string]string)
323-
require.Equal(t, "bar", vars["FOO"])
324-
})
318+
func TestGoEnv(t *testing.T) {
319+
goVars, err := goenv(context.TODO())
320+
require.NoError(t, err)
325321

322+
// Just check some basic values.
323+
require.Equal(t, runtime.GOOS, goVars["GOOS"])
324+
require.Equal(t, runtime.GOARCH, goVars["GOARCH"])
325+
}
326+
327+
func TestCreateTemplateData(t *testing.T) {
326328
t.Run("empty creation time", func(t *testing.T) {
327-
params := createTemplateData(context.TODO(), buildContext{dir: t.TempDir()})
329+
params, err := createTemplateData(context.TODO(), buildContext{dir: t.TempDir()})
330+
require.NoError(t, err)
328331

329332
// Make sure the date was set to time.Now().
330333
actualDateStr := params["Date"].(string)
@@ -346,10 +349,11 @@ func TestCreateTemplateData(t *testing.T) {
346349
expectedTime, err := time.Parse(time.RFC3339, "2012-11-01T22:08:00Z")
347350
require.NoError(t, err)
348351

349-
params := createTemplateData(context.TODO(), buildContext{
352+
params, err := createTemplateData(context.TODO(), buildContext{
350353
creationTime: v1.Time{Time: expectedTime},
351354
dir: t.TempDir(),
352355
})
356+
require.NoError(t, err)
353357

354358
// Check the date.
355359
actualDateStr := params["Date"].(string)
@@ -365,9 +369,10 @@ func TestCreateTemplateData(t *testing.T) {
365369

366370
t.Run("no git available", func(t *testing.T) {
367371
dir := t.TempDir()
368-
params := createTemplateData(context.TODO(), buildContext{dir: dir})
369-
gitParams := params["Git"].(map[string]interface{})
372+
params, err := createTemplateData(context.TODO(), buildContext{dir: dir})
373+
require.NoError(t, err)
370374

375+
gitParams := params["Git"].(map[string]interface{})
371376
require.Equal(t, "", gitParams["Branch"])
372377
require.Equal(t, "", gitParams["Tag"])
373378
require.Equal(t, "", gitParams["ShortCommit"])
@@ -384,13 +389,54 @@ func TestCreateTemplateData(t *testing.T) {
384389
gittesting.GitCommit(t, dir, "commit1")
385390
gittesting.GitTag(t, dir, "v0.0.1")
386391

387-
params := createTemplateData(context.TODO(), buildContext{dir: dir})
388-
gitParams := params["Git"].(map[string]interface{})
392+
params, err := createTemplateData(context.TODO(), buildContext{dir: dir})
393+
require.NoError(t, err)
389394

395+
gitParams := params["Git"].(map[string]interface{})
390396
require.Equal(t, "main", gitParams["Branch"])
391397
require.Equal(t, "v0.0.1", gitParams["Tag"])
392398
require.Equal(t, "clean", gitParams["TreeState"])
393399
})
400+
401+
t.Run("env", func(t *testing.T) {
402+
params, err := createTemplateData(context.TODO(), buildContext{
403+
dir: t.TempDir(),
404+
mergedEnv: []string{"FOO=bar"},
405+
})
406+
require.NoError(t, err)
407+
vars := params["Env"].(map[string]string)
408+
require.Equal(t, "bar", vars["FOO"])
409+
})
410+
411+
t.Run("bad env", func(t *testing.T) {
412+
_, err := createTemplateData(context.TODO(), buildContext{
413+
dir: t.TempDir(),
414+
mergedEnv: []string{"bad var"},
415+
})
416+
require.Error(t, err)
417+
})
418+
419+
t.Run("default go env", func(t *testing.T) {
420+
params, err := createTemplateData(context.TODO(), buildContext{dir: t.TempDir()})
421+
require.NoError(t, err)
422+
vars := params["GoEnv"].(map[string]string)
423+
require.Equal(t, runtime.GOOS, vars["GOOS"])
424+
require.Equal(t, runtime.GOARCH, vars["GOARCH"])
425+
})
426+
427+
t.Run("env overrides go env", func(t *testing.T) {
428+
params, err := createTemplateData(context.TODO(), buildContext{
429+
dir: t.TempDir(),
430+
mergedEnv: []string{
431+
"GOOS=testgoos",
432+
"GOARCH=testgoarch",
433+
},
434+
})
435+
require.NoError(t, err)
436+
vars := params["GoEnv"].(map[string]string)
437+
require.Equal(t, "testgoos", vars["GOOS"])
438+
require.Equal(t, "testgoarch", vars["GOARCH"])
439+
})
394440
}
395441

396442
func TestBuildConfig(t *testing.T) {

0 commit comments

Comments
 (0)