From d586ccdce92400af208a49e5b9be0f92027ba111 Mon Sep 17 00:00:00 2001 From: Halvard Skogsrud Date: Wed, 8 Dec 2021 09:20:00 +1100 Subject: [PATCH] feat(ko): Enable templating of `labels` and `env` This change enables `ko` builder users to substitute environment variable values in the `labels` and `env` config fields. These fields are used for image labels and build-time environment variables, respectively. Envvar templating of image labels can be used to add information such as the Git commit SHA to the image, see #6916. Also, environment variable expansion of `flags` and `ldflags` in the `ko` builder configuration now supports Skaffold's templating syntax, for consistency. For backwards compatibility, `ko`'s templating syntax still works with `flags` and `ldflags`: Skaffold: `{{.FOO}}` `ko`: `{{.Env.FOO}}` Tracking: #6041 Fixes: #6916 --- .../content/en/docs/environment/templating.md | 1 + .../en/docs/pipeline-stages/builders/ko.md | 39 +++- pkg/skaffold/build/ko/builder.go | 78 +++++-- pkg/skaffold/build/ko/builder_test.go | 190 +++++++++--------- 4 files changed, 184 insertions(+), 124 deletions(-) diff --git a/docs/content/en/docs/environment/templating.md b/docs/content/en/docs/environment/templating.md index 82f676c41f2..b4f8223f9f7 100644 --- a/docs/content/en/docs/environment/templating.md +++ b/docs/content/en/docs/environment/templating.md @@ -16,6 +16,7 @@ will be `gcr.io/k8s-skaffold/example:v1`. List of fields that support templating: * `build.artifacts.[].docker.buildArgs` (see [builders]({{< relref "/docs/pipeline-stages/builders" >}})) +* `build.artifacts.[].ko.{env,flags,labels,ldflags}` (see [`ko` builder]({{< relref "/docs/pipeline-stages/builders/ko" >}})) * `build.tagPolicy.envTemplate.template` (see [envTemplate tagger]({{< relref "/docs/pipeline-stages/taggers#envtemplate-using-values-of-environment-variables-as-tags)" >}})) * `deploy.helm.releases.setValueTemplates` (see [Deploying with helm]({{< relref "/docs/pipeline-stages/deployers#deploying-with-helm)" >}})) * `deploy.helm.releases.name` (see [Deploying with helm]({{< relref "/docs/pipeline-stages/deployers#deploying-with-helm)" >}})) diff --git a/docs/content/en/docs/pipeline-stages/builders/ko.md b/docs/content/en/docs/pipeline-stages/builders/ko.md index 0ba5656a6c6..95783ee259c 100644 --- a/docs/content/en/docs/pipeline-stages/builders/ko.md +++ b/docs/content/en/docs/pipeline-stages/builders/ko.md @@ -88,12 +88,15 @@ is `linux/amd64`, but you can configure a list of platforms using the You can also supply `["all"]` as the value of `platforms`. `all` means that the ko builder builds images for all platforms supported by the base image. -### Labels / annotations +### Image labels Use the `labels` configuration field to add -[annotations](https://github.com/opencontainers/image-spec/blob/main/annotations.md) +[image labels](https://github.com/opencontainers/image-spec/blob/main/config.md#properties) (a.k.a. [`Dockerfile` `LABEL`s](https://docs.docker.com/engine/reference/builder/#label)), -e.g.: + +For example, you can add labels based on the +[pre-defined annotations keys](https://github.com/opencontainers/image-spec/blob/main/annotations.md#pre-defined-annotation-keys) +from the Open Container Initiative (OCI) Image Format Specification: ```yaml ko: @@ -102,6 +105,16 @@ e.g.: org.opencontainers.image.source: https://github.com/GoogleContainerTools/skaffold ``` +The `labels` section supports templating of values based on environment +variables, e.g.: + +```yaml + ko: + labels: + org.opencontainers.image.revision: "{{.GITHUB_SHA}}" + org.opencontainers.image.source: "{{.GITHUB_SERVER_URL}}/{{.GITHUB_REPOSITORY}}" +``` + ### Build time environment variables Use the `env` configuration field to specify build-time environment variables. @@ -115,6 +128,15 @@ Example: - GOPRIVATE=git.internal.example.com,source.developers.google.com ``` +The `env` field supports templating of values using environment variables, for +example: + +```yaml + ko: + env: + - GOPROXY={{.GOPROXY}} +``` + ### Dependencies The `dependencies` section configures what files Skaffold should watch for @@ -168,17 +190,20 @@ Use the `ldflags` configuration field to provide linker flag arguments, e.g.: - -w ``` -`ko` supports templating of `flags` and `ldflags` using environment variables, +The `flags` and `ldflags` fields support templating using environment +variables, e.g.: ```yaml ko: ldflags: - - -X main.version={{.Env.VERSION}} + - -X main.version={{.VERSION}} ``` -These templates are passed through to `ko` and are expanded using -[`ko`'s template expansion implementation](https://github.com/google/ko/blob/v0.9.3/pkg/build/gobuild.go#L632-L660). +These templates are evaluated by Skaffold. Note that the syntax is slightly +different to +[`ko`'s template expansion](https://github.com/google/ko/blob/v0.9.3/pkg/build/gobuild.go#L632-L660), +specifically, there's no `.Env` prefix. ### Source file locations diff --git a/pkg/skaffold/build/ko/builder.go b/pkg/skaffold/build/ko/builder.go index 8cc91b313fe..e74eca5da85 100644 --- a/pkg/skaffold/build/ko/builder.go +++ b/pkg/skaffold/build/ko/builder.go @@ -28,6 +28,7 @@ import ( "github.com/GoogleContainerTools/skaffold/pkg/skaffold/config" latestV1 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest/v1" + "github.com/GoogleContainerTools/skaffold/pkg/skaffold/util" "github.com/GoogleContainerTools/skaffold/pkg/skaffold/version" ) @@ -44,12 +45,16 @@ func buildOptions(a *latestV1.Artifact, runMode config.RunMode) (*options.BuildO if err != nil { return nil, fmt.Errorf("could not create ko build config: %v", err) } + imageLabels, err := labels(a) + if err != nil { + return nil, fmt.Errorf("could not expand image labels: %v", err) + } return &options.BuildOptions{ BaseImage: a.KoArtifact.BaseImage, BuildConfigs: buildconfig, ConcurrentBuilds: 1, // we could plug in Skaffold's max builds here, but it'd be incorrect if users build more than one artifact DisableOptimizations: runMode == config.RunModes.Debug, - Labels: labels(a), + Labels: imageLabels, Platform: strings.Join(a.KoArtifact.Platforms, ","), Trimpath: runMode != config.RunModes.Debug, UserAgent: version.UserAgentWithClient(), @@ -63,20 +68,33 @@ func buildOptions(a *latestV1.Artifact, runMode config.RunMode) (*options.BuildO // In this case, ko falls back to build configs provided in `.ko.yaml`, or to the default zero config. func buildConfig(a *latestV1.Artifact) (map[string]build.Config, error) { buildconfigs := map[string]build.Config{} - if koArtifactSpecifiesBuildConfig(*a.KoArtifact) { - koImportpath, err := getImportPath(a) - if err != nil { - return nil, fmt.Errorf("could not determine import path of image %s: %v", a.ImageName, err) - } - importpath := strings.TrimPrefix(koImportpath, build.StrictScheme) - buildconfigs[importpath] = build.Config{ - ID: a.ImageName, - Dir: ".", - Env: a.KoArtifact.Env, - Flags: a.KoArtifact.Flags, - Ldflags: a.KoArtifact.Ldflags, - Main: a.KoArtifact.Main, - } + if !koArtifactSpecifiesBuildConfig(*a.KoArtifact) { + return buildconfigs, nil + } + koImportpath, err := getImportPath(a) + if err != nil { + return nil, fmt.Errorf("could not determine import path of image %s: %v", a.ImageName, err) + } + env, err := expand(a.KoArtifact.Env) + if err != nil { + return nil, fmt.Errorf("could not expand env: %v", err) + } + flags, err := expand(a.KoArtifact.Flags) + if err != nil { + return nil, fmt.Errorf("could not expand build flags: %v", err) + } + ldflags, err := expand(a.KoArtifact.Ldflags) + if err != nil { + return nil, fmt.Errorf("could not expand linker flags: %v", err) + } + importpath := strings.TrimPrefix(koImportpath, build.StrictScheme) + buildconfigs[importpath] = build.Config{ + ID: a.ImageName, + Dir: ".", + Env: env, + Flags: flags, + Ldflags: ldflags, + Main: a.KoArtifact.Main, } return buildconfigs, nil } @@ -100,10 +118,32 @@ func koArtifactSpecifiesBuildConfig(k latestV1.KoArtifact) bool { return false } -func labels(a *latestV1.Artifact) []string { - var labels []string +func labels(a *latestV1.Artifact) ([]string, error) { + rawLabels := map[string]*string{} for k, v := range a.KoArtifact.Labels { - labels = append(labels, fmt.Sprintf("%s=%s", k, v)) + rawLabels[k] = util.StringPtr(v) + } + expandedLabels, err := util.EvaluateEnvTemplateMapWithEnv(rawLabels, nil) + if err != nil { + return nil, fmt.Errorf("unable to expand image labels: %w", err) + } + var labels []string + for k, v := range expandedLabels { + labels = append(labels, fmt.Sprintf("%s=%s", k, *v)) + } + return labels, nil +} + +func expand(dryValues []string) ([]string, error) { + var expandedValues []string + for _, rawValue := range dryValues { + // support ko-style envvar templating syntax, see https://github.com/GoogleContainerTools/skaffold/issues/6916 + rawValue = strings.ReplaceAll(rawValue, "{{.Env.", "{{.") + expandedValue, err := util.ExpandEnvTemplate(rawValue, nil) + if err != nil { + return nil, fmt.Errorf("could not expand %s", rawValue) + } + expandedValues = append(expandedValues, expandedValue) } - return labels + return expandedValues, nil } diff --git a/pkg/skaffold/build/ko/builder_test.go b/pkg/skaffold/build/ko/builder_test.go index bbe5b8c1f5f..6852604289d 100644 --- a/pkg/skaffold/build/ko/builder_test.go +++ b/pkg/skaffold/build/ko/builder_test.go @@ -17,10 +17,14 @@ limitations under the License. package ko import ( + "fmt" + "os" "path/filepath" "testing" "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/ko/pkg/build" + "github.com/google/ko/pkg/commands/options" "github.com/GoogleContainerTools/skaffold/pkg/skaffold/config" latestV1 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest/v1" @@ -28,16 +32,26 @@ import ( "github.com/GoogleContainerTools/skaffold/testutil" ) +const ( + testKoBuildOptionsEnvVar = "TEST_KO_BUILDER_IMAGE_LABEL_ENV_VAR" +) + +var ( + baseBo = options.BuildOptions{ + BuildConfigs: map[string]build.Config{}, + ConcurrentBuilds: 1, + Trimpath: true, + UserAgent: version.UserAgentWithClient(), + } +) + func TestBuildOptions(t *testing.T) { tests := []struct { - description string - artifact latestV1.Artifact - runMode config.RunMode - wantDebugOptions bool - wantLabels []string - wantPlatform string - wantWorkingDirectory string - wantImportPath string + description string + artifact latestV1.Artifact + envVarValue string + runMode config.RunMode + wantBo options.BuildOptions }{ { description: "all zero value", @@ -46,128 +60,108 @@ func TestBuildOptions(t *testing.T) { KoArtifact: &latestV1.KoArtifact{}, }, }, - }, - { - description: "base image", - artifact: latestV1.Artifact{ - ArtifactType: latestV1.ArtifactType{ - KoArtifact: &latestV1.KoArtifact{ - BaseImage: "gcr.io/distroless/base:nonroot", - }, - }, - ImageName: "ko://example.com/foo", - }, - }, - { - description: "empty platforms", - artifact: latestV1.Artifact{ - ArtifactType: latestV1.ArtifactType{ - KoArtifact: &latestV1.KoArtifact{ - Platforms: []string{}, - }, - }, - ImageName: "ko://example.com/foo", + wantBo: options.BuildOptions{ + ConcurrentBuilds: 1, + Trimpath: true, + UserAgent: version.UserAgentWithClient(), }, }, { - description: "multiple platforms", + description: "all options", artifact: latestV1.Artifact{ ArtifactType: latestV1.ArtifactType{ KoArtifact: &latestV1.KoArtifact{ + BaseImage: "gcr.io/distroless/base:nonroot", + Dir: "gomoddir", + Env: []string{ + "FOO=BAR", + fmt.Sprintf("frob={{.%s}}", testKoBuildOptionsEnvVar), + }, + Flags: []string{ + "-v", + fmt.Sprintf("-flag-{{.%s}}", testKoBuildOptionsEnvVar), + }, + Labels: map[string]string{ + "foo": "bar", + "frob": fmt.Sprintf("{{.%s}}", testKoBuildOptionsEnvVar), + }, + Ldflags: []string{ + "-s", + fmt.Sprintf("-ldflag-{{.%s}}", testKoBuildOptionsEnvVar), + }, + Main: "cmd/app", Platforms: []string{"linux/amd64", "linux/arm64"}, }, }, ImageName: "ko://example.com/foo", + Workspace: "workdir", }, - wantPlatform: "linux/amd64,linux/arm64", - }, - { - description: "workspace", - artifact: latestV1.Artifact{ - ArtifactType: latestV1.ArtifactType{ - KoArtifact: &latestV1.KoArtifact{}, - }, - ImageName: "ko://example.com/foo", - Workspace: "my-app-subdirectory", - }, - wantWorkingDirectory: "my-app-subdirectory", - }, - { - description: "source dir", - artifact: latestV1.Artifact{ - ArtifactType: latestV1.ArtifactType{ - KoArtifact: &latestV1.KoArtifact{ - Dir: "my-go-mod-is-here", + envVarValue: "baz", + runMode: config.RunModes.Debug, + wantBo: options.BuildOptions{ + BaseImage: "gcr.io/distroless/base:nonroot", + BuildConfigs: map[string]build.Config{ + "example.com/foo": { + ID: "ko://example.com/foo", + Dir: ".", + Env: []string{"FOO=BAR", "frob=baz"}, + Flags: build.FlagArray{"-v", "-flag-baz"}, + Ldflags: build.StringArray{"-s", "-ldflag-baz"}, + Main: "cmd/app", }, }, - ImageName: "ko://example.com/foo", + ConcurrentBuilds: 1, + DisableOptimizations: true, + Labels: []string{"foo=bar", "frob=baz"}, + Platform: "linux/amd64,linux/arm64", + Trimpath: false, + UserAgent: version.UserAgentWithClient(), + WorkingDirectory: "workdir" + string(filepath.Separator) + "gomoddir", }, - wantWorkingDirectory: "my-go-mod-is-here", - wantImportPath: "example.com/foo", }, { - description: "workspace and source dir", + description: "compatibility with ko envvar expansion syntax for flags and ldflags", artifact: latestV1.Artifact{ ArtifactType: latestV1.ArtifactType{ KoArtifact: &latestV1.KoArtifact{ - Dir: "my-go-mod-is-here", + Flags: []string{ + "-v", + fmt.Sprintf("-flag-{{.Env.%s}}", testKoBuildOptionsEnvVar), + }, + Ldflags: []string{ + "-s", + fmt.Sprintf("-ldflag-{{.Env.%s}}", testKoBuildOptionsEnvVar), + }, }, }, ImageName: "ko://example.com/foo", - Workspace: "my-app-subdirectory", }, - wantWorkingDirectory: "my-app-subdirectory" + string(filepath.Separator) + "my-go-mod-is-here", - wantImportPath: "example.com/foo", - }, - { - description: "remove trimpath flag and add flags that disable compiler optimizations for debug", - artifact: latestV1.Artifact{ - ArtifactType: latestV1.ArtifactType{ - KoArtifact: &latestV1.KoArtifact{}, - }, - ImageName: "ko://example.com/foo", - }, - runMode: config.RunModes.Debug, - wantDebugOptions: true, - }, - { - description: "labels", - artifact: latestV1.Artifact{ - ArtifactType: latestV1.ArtifactType{ - KoArtifact: &latestV1.KoArtifact{ - Labels: map[string]string{ - "foo": "bar", - "frob": "baz", - }, + envVarValue: "xyzzy", + wantBo: options.BuildOptions{ + BuildConfigs: map[string]build.Config{ + "example.com/foo": { + ID: "ko://example.com/foo", + Dir: ".", + Flags: build.FlagArray{"-v", "-flag-xyzzy"}, + Ldflags: build.StringArray{"-s", "-ldflag-xyzzy"}, }, }, - ImageName: "ko://example.com/foo", + ConcurrentBuilds: 1, + Trimpath: true, + UserAgent: version.UserAgentWithClient(), }, - wantLabels: []string{"foo=bar", "frob=baz"}, }, } for _, test := range tests { testutil.Run(t, test.description, func(t *testutil.T) { - bo, err := buildOptions(&test.artifact, test.runMode) + os.Setenv(testKoBuildOptionsEnvVar, test.envVarValue) + gotBo, err := buildOptions(&test.artifact, test.runMode) + os.Unsetenv(testKoBuildOptionsEnvVar) t.CheckErrorAndFailNow(false, err) - t.CheckDeepEqual(test.artifact.KoArtifact.BaseImage, bo.BaseImage) - if bo.ConcurrentBuilds < 1 { - t.Errorf("ConcurrentBuilds must always be >= 1 for the ko builder") - } - t.CheckDeepEqual(test.wantPlatform, bo.Platform) - t.CheckDeepEqual(version.UserAgentWithClient(), bo.UserAgent) - t.CheckDeepEqual(test.wantWorkingDirectory, bo.WorkingDirectory) - t.CheckDeepEqual(test.wantDebugOptions, bo.DisableOptimizations) - t.CheckDeepEqual(test.wantDebugOptions, !bo.Trimpath) - t.CheckDeepEqual(test.wantLabels, bo.Labels, + t.CheckDeepEqual(test.wantBo, *gotBo, + cmpopts.EquateEmpty(), cmpopts.SortSlices(func(x, y string) bool { return x < y }), - cmpopts.EquateEmpty()) - if test.wantImportPath != "" && len(bo.BuildConfigs) != 1 { - t.Fatalf("expected exactly one build config, got %d", len(bo.BuildConfigs)) - } - for importpath := range bo.BuildConfigs { - t.CheckDeepEqual(test.wantImportPath, importpath) - } + ) }) } }