From d722aac36b8265996f13e059bc6d0521dcc3a035 Mon Sep 17 00:00:00 2001 From: Maxime Brunet Date: Sun, 27 Oct 2024 19:01:09 +0000 Subject: [PATCH] feat(ko): support annotations and user (#5227) Add support for the newly added annotations and user options in [ko v0.17.0](https://github.com/ko-build/ko/releases/tag/v0.17.0) --- internal/pipe/ko/ko.go | 42 ++++++++--- internal/pipe/ko/ko_test.go | 120 +++++++++++++++++++++----------- pkg/config/config.go | 2 + www/docs/customization/ko.md | 7 ++ www/docs/static/schema-pro.json | 9 +++ www/docs/static/schema.json | 9 +++ 6 files changed, 139 insertions(+), 50 deletions(-) diff --git a/internal/pipe/ko/ko.go b/internal/pipe/ko/ko.go index 4f45458d3..5ca2819e4 100644 --- a/internal/pipe/ko/ko.go +++ b/internal/pipe/ko/ko.go @@ -166,6 +166,8 @@ type buildOptions struct { platforms []string baseImage string labels map[string]string + annotations map[string]string + user string tags []string creationTime *v1.Time koDataCreationTime *v1.Time @@ -226,6 +228,12 @@ func (o *buildOptions) makeBuilder(ctx *context.Context) (*build.Caching, error) for k, v := range o.labels { buildOptions = append(buildOptions, build.WithLabel(k, v)) } + for k, v := range o.annotations { + buildOptions = append(buildOptions, build.WithAnnotation(k, v)) + } + if o.user != "" { + buildOptions = append(buildOptions, build.WithUser(o.user)) + } switch o.sbom { case "spdx": buildOptions = append(buildOptions, build.WithSPDX("devel")) @@ -356,6 +364,7 @@ func buildBuildOptions(ctx *context.Context, cfg config.Ko) (*buildOptions, erro platforms: cfg.Platforms, sbom: cfg.SBOM, imageRepo: cfg.Repository, + user: cfg.User, } tags, err := applyTemplate(ctx, cfg.Tags) @@ -381,14 +390,19 @@ func buildBuildOptions(ctx *context.Context, cfg config.Ko) (*buildOptions, erro } if len(cfg.Labels) > 0 { - opts.labels = make(map[string]string, len(cfg.Labels)) - for k, v := range cfg.Labels { - tv, err := tmpl.New(ctx).Apply(v) - if err != nil { - return nil, err - } - opts.labels[k] = tv + labels, err := applyTemplateToMapValues(ctx, cfg.Labels) + if err != nil { + return nil, err } + opts.labels = labels + } + + if len(cfg.Annotations) > 0 { + annotations, err := applyTemplateToMapValues(ctx, cfg.Annotations) + if err != nil { + return nil, err + } + opts.annotations = annotations } if len(cfg.Env) > 0 { @@ -429,7 +443,7 @@ func removeEmpty(strs []string) []string { } func applyTemplate(ctx *context.Context, templateable []string) ([]string, error) { - var templated []string + templated := make([]string, 0, len(templateable)) for _, t := range templateable { tlf, err := tmpl.New(ctx).Apply(t) if err != nil { @@ -440,6 +454,18 @@ func applyTemplate(ctx *context.Context, templateable []string) ([]string, error return templated, nil } +func applyTemplateToMapValues(ctx *context.Context, templateable map[string]string) (map[string]string, error) { + templated := make(map[string]string, len(templateable)) + for k, v := range templateable { + tv, err := tmpl.New(ctx).Apply(v) + if err != nil { + return nil, err + } + templated[k] = tv + } + return templated, nil +} + func getTimeFromTemplate(ctx *context.Context, t string) (*v1.Time, error) { epoch, err := tmpl.New(ctx).Apply(t) if err != nil { diff --git a/internal/pipe/ko/ko_test.go b/internal/pipe/ko/ko_test.go index c1f4465bd..23b6a2d42 100644 --- a/internal/pipe/ko/ko_test.go +++ b/internal/pipe/ko/ko_test.go @@ -2,6 +2,7 @@ package ko import ( "fmt" + "maps" "strconv" "strings" "testing" @@ -165,55 +166,63 @@ func TestPublishPipeSuccess(t *testing.T) { "org.opencontainers.image.vendor": "Chainguard", "org.opencontainers.image.created": ".*", } + baseImageAnnotations := map[string]string{ + "org.opencontainers.image.base.name": ".*", + "org.opencontainers.image.base.digest": ".*", + } table := []struct { - Name string - SBOM string - BaseImage string - Labels map[string]string - ExpectedLabels map[string]string - Platforms []string - Tags []string - CreationTime string - KoDataCreationTime string + Name string + SBOM string + BaseImage string + Labels map[string]string + ExpectedLabels map[string]string + Annotations map[string]string + ExpectedAnnotations map[string]string + User string + Platforms []string + Tags []string + CreationTime string + KoDataCreationTime string }{ { // Must be first as others add an SBOM for the same image - Name: "sbom-none", - SBOM: "none", - ExpectedLabels: chainguardStaticLabels, + Name: "sbom-none", + SBOM: "none", }, { - Name: "sbom-spdx", - SBOM: "spdx", - ExpectedLabels: chainguardStaticLabels, + Name: "sbom-spdx", + SBOM: "spdx", }, { Name: "base-image-is-not-index", BaseImage: "alpine:latest@sha256:c0d488a800e4127c334ad20d61d7bc21b4097540327217dfab52262adc02380c", }, { - Name: "multiple-platforms", - Platforms: []string{"linux/amd64", "linux/arm64"}, - ExpectedLabels: chainguardStaticLabels, + Name: "multiple-platforms", + Platforms: []string{"linux/amd64", "linux/arm64"}, }, { - Name: "labels", - Labels: map[string]string{"foo": "bar", "project": "{{.ProjectName}}"}, - ExpectedLabels: mapsMerge( - map[string]string{"foo": "bar", "project": "test"}, - chainguardStaticLabels, - ), + Name: "labels", + Labels: map[string]string{"foo": "bar", "project": "{{.ProjectName}}"}, + ExpectedLabels: map[string]string{"foo": "bar", "project": "test"}, }, { - Name: "creation-time", - CreationTime: "1672531200", - ExpectedLabels: chainguardStaticLabels, + Name: "annotations", + Annotations: map[string]string{"foo": "bar", "project": "{{.ProjectName}}"}, + ExpectedAnnotations: map[string]string{"foo": "bar", "project": "test"}, + }, + { + Name: "user", + User: "1234:1234", + }, + { + Name: "creation-time", + CreationTime: "1672531200", }, { Name: "kodata-creation-time", KoDataCreationTime: "1672531200", - ExpectedLabels: chainguardStaticLabels, }, { Name: "tag-templates", @@ -221,7 +230,6 @@ func TestPublishPipeSuccess(t *testing.T) { "{{if not .Prerelease }}{{.Version}}{{ end }}", " ", // empty }, - ExpectedLabels: chainguardStaticLabels, }, { Name: "tag-template-eval-empty", @@ -229,7 +237,6 @@ func TestPublishPipeSuccess(t *testing.T) { "{{.Version}}", "{{if .Prerelease }}latest{{ end }}", }, - ExpectedLabels: chainguardStaticLabels, }, } @@ -260,6 +267,8 @@ func TestPublishPipeSuccess(t *testing.T) { BaseImage: table.BaseImage, Repository: repository, Labels: table.Labels, + Annotations: table.Annotations, + User: table.User, Platforms: table.Platforms, Tags: table.Tags, CreationTime: table.CreationTime, @@ -270,6 +279,14 @@ func TestPublishPipeSuccess(t *testing.T) { }, }, testctx.WithVersion("1.2.0")) + if table.BaseImage == "" { + if table.User == "" { + table.User = "65532" + } + table.ExpectedLabels = mergeMaps(table.ExpectedLabels, chainguardStaticLabels) + } + table.ExpectedAnnotations = mergeMaps(table.ExpectedAnnotations, baseImageAnnotations) + require.NoError(t, Pipe{}.Default(ctx)) require.NoError(t, Pipe{}.Publish(ctx)) @@ -305,6 +322,8 @@ func TestPublishPipeSuccess(t *testing.T) { imf, err := index.IndexManifest() require.NoError(t, err) + compareMaps(t, table.ExpectedAnnotations, imf.Annotations) + platforms := make([]string, 0, len(imf.Manifests)) for _, mf := range imf.Manifests { platforms = append(platforms, mf.Platform.String()) @@ -351,16 +370,24 @@ func TestPublishPipeSuccess(t *testing.T) { } } + mf, err := image.Manifest() + require.NoError(t, err) + + expectedAnnotations := table.ExpectedAnnotations + if table.BaseImage == "" { + expectedAnnotations = mergeMaps( + expectedAnnotations, + chainguardStaticLabels, + ) + } + compareMaps(t, expectedAnnotations, mf.Annotations) + configFile, err := image.ConfigFile() require.NoError(t, err) require.GreaterOrEqual(t, len(configFile.History), 3) - require.Len(t, configFile.Config.Labels, len(table.ExpectedLabels)) - for k, v := range table.ExpectedLabels { - got, ok := configFile.Config.Labels[k] - require.True(t, ok, "missing label") - require.Regexp(t, v, got) - } + compareMaps(t, table.ExpectedLabels, configFile.Config.Labels) + require.Equal(t, table.User, configFile.Config.User) var creationTime time.Time if table.CreationTime != "" { @@ -603,13 +630,22 @@ func TestApplyTemplate(t *testing.T) { }) } -func mapsMerge(m1, m2 map[string]string) map[string]string { +func mergeMaps(ms ...map[string]string) map[string]string { result := map[string]string{} - for k, v := range m1 { - result[k] = v - } - for k, v := range m2 { - result[k] = v + for _, m := range ms { + if m != nil { + maps.Copy(result, m) + } } return result } + +func compareMaps(t *testing.T, expected, actual map[string]string) { + t.Helper() + require.Len(t, actual, len(expected), "expected: %v", expected) + for k, v := range expected { + got, ok := actual[k] + require.True(t, ok, "missing key: %s", k) + require.Regexp(t, v, got, "key: %s", k) + } +} diff --git a/pkg/config/config.go b/pkg/config/config.go index ebdb6b73c..80ebccff3 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -369,6 +369,8 @@ type Ko struct { WorkingDir string `yaml:"working_dir,omitempty" json:"working_dir,omitempty"` BaseImage string `yaml:"base_image,omitempty" json:"base_image,omitempty"` Labels map[string]string `yaml:"labels,omitempty" json:"labels,omitempty"` + Annotations map[string]string `yaml:"annotations,omitempty" json:"annotations,omitempty"` + User string `yaml:"user,omitempty" json:"user,omitempty"` Repository string `yaml:"repository,omitempty" json:"repository,omitempty"` Platforms []string `yaml:"platforms,omitempty" json:"platforms,omitempty"` Tags []string `yaml:"tags,omitempty" json:"tags,omitempty"` diff --git a/www/docs/customization/ko.md b/www/docs/customization/ko.md index 1ab0abef9..0587dd2bf 100644 --- a/www/docs/customization/ko.md +++ b/www/docs/customization/ko.md @@ -46,6 +46,13 @@ kos: labels: foo: bar + # Annotations for the OCI manifest. + annotations: + foo: bar + + # The default user the image should be run as. + user: "1234:1234" + # Repository to push to. # # Default: '$KO_DOCKER_REPO'. diff --git a/www/docs/static/schema-pro.json b/www/docs/static/schema-pro.json index 9263b03c2..e7135df22 100644 --- a/www/docs/static/schema-pro.json +++ b/www/docs/static/schema-pro.json @@ -1929,6 +1929,15 @@ }, "type": "object" }, + "annotations": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "user": { + "type": "string" + }, "repository": { "type": "string" }, diff --git a/www/docs/static/schema.json b/www/docs/static/schema.json index 3ed3706b1..693901284 100644 --- a/www/docs/static/schema.json +++ b/www/docs/static/schema.json @@ -1442,6 +1442,15 @@ }, "type": "object" }, + "annotations": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "user": { + "type": "string" + }, "repository": { "type": "string" },