Skip to content

Commit

Permalink
feat(ko): support labels and creation times (#3852)
Browse files Browse the repository at this point in the history
* Add a `labels` key-value map to the `kos` config.
My interest is to be able to label the built images:
https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry#labelling-container-images
* Add creation times to allow using the commit timestamp as meaningful
creation time
  • Loading branch information
maxbrunet committed Mar 20, 2023
1 parent b96dba0 commit c47315f
Show file tree
Hide file tree
Showing 4 changed files with 192 additions and 28 deletions.
55 changes: 55 additions & 0 deletions internal/pipe/ko/ko.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,16 @@ import (
"fmt"
"io"
"path/filepath"
"strconv"
"sync"
"time"

"github.com/awslabs/amazon-ecr-credential-helper/ecr-login"
"github.com/chrismellard/docker-credential-acr-env/pkg/credhelper"
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/authn/github"
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/google"
"github.com/google/go-containerregistry/pkg/v1/remote"
"github.com/google/ko/pkg/build"
Expand Down Expand Up @@ -138,7 +141,10 @@ type buildOptions struct {
workingDir string
platforms []string
baseImage string
labels map[string]string
tags []string
creationTime *v1.Time
koDataCreationTime *v1.Time
sbom string
ldflags []string
bare bool
Expand Down Expand Up @@ -187,6 +193,15 @@ func (o *buildOptions) makeBuilder(ctx *context.Context) (*build.Caching, error)
return nil, nil, fmt.Errorf("unexpected base image media type: %s", desc.MediaType)
}),
}
if o.creationTime != nil {
buildOptions = append(buildOptions, build.WithCreationTime(*o.creationTime))
}
if o.koDataCreationTime != nil {
buildOptions = append(buildOptions, build.WithKoDataCreationTime(*o.koDataCreationTime))
}
for k, v := range o.labels {
buildOptions = append(buildOptions, build.WithLabel(k, v))
}
switch o.sbom {
case "spdx":
buildOptions = append(buildOptions, build.WithSPDX("devel"))
Expand Down Expand Up @@ -299,6 +314,33 @@ func buildBuildOptions(ctx *context.Context, cfg config.Ko) (*buildOptions, erro
}
opts.tags = tags

if cfg.CreationTime != "" {
creationTime, err := getTimeFromTemplate(ctx, cfg.CreationTime)
if err != nil {
return nil, err
}
opts.creationTime = creationTime
}

if cfg.KoDataCreationTime != "" {
koDataCreationTime, err := getTimeFromTemplate(ctx, cfg.KoDataCreationTime)
if err != nil {
return nil, err
}
opts.koDataCreationTime = koDataCreationTime
}

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
}
}

if len(cfg.Env) > 0 {
env, err := applyTemplate(ctx, cfg.Env)
if err != nil {
Expand Down Expand Up @@ -336,3 +378,16 @@ func applyTemplate(ctx *context.Context, templateable []string) ([]string, error
}
return templated, nil
}

func getTimeFromTemplate(ctx *context.Context, t string) (*v1.Time, error) {
epoch, err := tmpl.New(ctx).Apply(t)
if err != nil {
return nil, err
}

seconds, err := strconv.ParseInt(epoch, 10, 64)
if err != nil {
return nil, err
}
return &v1.Time{Time: time.Unix(seconds, 0)}, nil
}
111 changes: 98 additions & 13 deletions internal/pipe/ko/ko_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ package ko

import (
"fmt"
"strconv"
"strings"
"testing"
"time"

_ "github.com/distribution/distribution/v3/registry/auth/htpasswd"
_ "github.com/distribution/distribution/v3/registry/storage/driver/inmemory"
Expand Down Expand Up @@ -123,10 +125,14 @@ func TestPublishPipeSuccess(t *testing.T) {
testlib.StartRegistry(t, "ko_registry", registryPort)

table := []struct {
Name string
SBOM string
BaseImage string
Platforms []string
Name string
SBOM string
BaseImage string
Labels map[string]string
ExpectedLabels map[string]string
Platforms []string
CreationTime string
KoDataCreationTime string
}{
{
// Must be first as others add an SBOM for the same image
Expand All @@ -153,6 +159,19 @@ func TestPublishPipeSuccess(t *testing.T) {
Name: "multiple-platforms",
Platforms: []string{"linux/amd64", "linux/arm64"},
},
{
Name: "labels",
Labels: map[string]string{"foo": "bar", "project": "{{.ProjectName}}"},
ExpectedLabels: map[string]string{"foo": "bar", "project": "test"},
},
{
Name: "creation-time",
CreationTime: "1672531200",
},
{
Name: "kodata-creation-time",
KoDataCreationTime: "1672531200",
},
}

repository := fmt.Sprintf("%sgoreleasertest/testapp", registry)
Expand All @@ -173,15 +192,18 @@ func TestPublishPipeSuccess(t *testing.T) {
},
Kos: []config.Ko{
{
ID: "default",
Build: "foo",
WorkingDir: "./testdata/app/",
BaseImage: table.BaseImage,
Repository: repository,
Platforms: table.Platforms,
Tags: []string{table.Name},
SBOM: table.SBOM,
Bare: true,
ID: "default",
Build: "foo",
WorkingDir: "./testdata/app/",
BaseImage: table.BaseImage,
Repository: repository,
Labels: table.Labels,
Platforms: table.Platforms,
Tags: []string{table.Name},
CreationTime: table.CreationTime,
KoDataCreationTime: table.KoDataCreationTime,
SBOM: table.SBOM,
Bare: true,
},
},
})
Expand Down Expand Up @@ -250,6 +272,30 @@ func TestPublishPipeSuccess(t *testing.T) {
require.Fail(t, "unknown SBOM type", table.SBOM)
}
}

configFile, err := image.ConfigFile()
require.NoError(t, err)
require.GreaterOrEqual(t, len(configFile.History), 3)

require.Equal(t, table.ExpectedLabels, configFile.Config.Labels)

var creationTime time.Time
if table.CreationTime != "" {
ct, err := strconv.ParseInt(table.CreationTime, 10, 64)
require.NoError(t, err)
creationTime = time.Unix(ct, 0).UTC()

require.Equal(t, creationTime, configFile.Created.Time)
}
require.Equal(t, creationTime, configFile.History[len(configFile.History)-1].Created.Time)

var koDataCreationTime time.Time
if table.KoDataCreationTime != "" {
kdct, err := strconv.ParseInt(table.KoDataCreationTime, 10, 64)
require.NoError(t, err)
koDataCreationTime = time.Unix(kdct, 0).UTC()
}
require.Equal(t, koDataCreationTime, configFile.History[len(configFile.History)-2].Created.Time)
})
}
}
Expand Down Expand Up @@ -282,6 +328,13 @@ func TestPublishPipeError(t *testing.T) {
require.EqualError(t, Pipe{}.Publish(ctx), `build: could not parse reference: not a valid image hopefully`)
})

t.Run("invalid label tmpl", func(t *testing.T) {
ctx := makeCtx()
ctx.Config.Kos[0].Labels = map[string]string{"nope": "{{.Nope}}"}
require.NoError(t, Pipe{}.Default(ctx))
testlib.RequireTemplateError(t, Pipe{}.Publish(ctx))
})

t.Run("invalid sbom", func(t *testing.T) {
ctx := makeCtx()
ctx.Config.Kos[0].SBOM = "nope"
Expand All @@ -303,6 +356,38 @@ func TestPublishPipeError(t *testing.T) {
testlib.RequireTemplateError(t, Pipe{}.Publish(ctx))
})

t.Run("invalid creation time", func(t *testing.T) {
ctx := makeCtx()
ctx.Config.Kos[0].CreationTime = "nope"
require.NoError(t, Pipe{}.Default(ctx))
err := Pipe{}.Publish(ctx)
require.Error(t, err)
require.Contains(t, err.Error(), `strconv.ParseInt: parsing "nope": invalid syntax`)
})

t.Run("invalid creation time tmpl", func(t *testing.T) {
ctx := makeCtx()
ctx.Config.Kos[0].CreationTime = "{{.Nope}}"
require.NoError(t, Pipe{}.Default(ctx))
testlib.RequireTemplateError(t, Pipe{}.Publish(ctx))
})

t.Run("invalid kodata creation time", func(t *testing.T) {
ctx := makeCtx()
ctx.Config.Kos[0].KoDataCreationTime = "nope"
require.NoError(t, Pipe{}.Default(ctx))
err := Pipe{}.Publish(ctx)
require.Error(t, err)
require.Contains(t, err.Error(), `strconv.ParseInt: parsing "nope": invalid syntax`)
})

t.Run("invalid kodata creation time tmpl", func(t *testing.T) {
ctx := makeCtx()
ctx.Config.Kos[0].KoDataCreationTime = "{{.Nope}}"
require.NoError(t, Pipe{}.Default(ctx))
testlib.RequireTemplateError(t, Pipe{}.Publish(ctx))
})

t.Run("invalid env tmpl", func(t *testing.T) {
ctx := makeCtx()
ctx.Config.Builds[0].Env = []string{"{{.Nope}}"}
Expand Down
33 changes: 18 additions & 15 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,21 +202,24 @@ type Krew struct {

// Ko contains the ko section
type Ko struct {
ID string `yaml:"id,omitempty" json:"id,omitempty"`
Build string `yaml:"build,omitempty" json:"build,omitempty"`
Main string `yaml:"main,omitempty" json:"main,omitempty"`
WorkingDir string `yaml:"working_dir,omitempty" json:"working_dir,omitempty"`
BaseImage string `yaml:"base_image,omitempty" json:"base_image,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"`
SBOM string `yaml:"sbom,omitempty" json:"sbom,omitempty"`
Ldflags []string `yaml:"ldflags,omitempty" json:"ldflags,omitempty"`
Flags []string `yaml:"flags,omitempty" json:"flags,omitempty"`
Env []string `yaml:"env,omitempty" json:"env,omitempty"`
Bare bool `yaml:"bare,omitempty" json:"bare,omitempty"`
PreserveImportPaths bool `yaml:"preserve_import_paths,omitempty" json:"preserve_import_paths,omitempty"`
BaseImportPaths bool `yaml:"base_import_paths,omitempty" json:"base_import_paths,omitempty"`
ID string `yaml:"id,omitempty" json:"id,omitempty"`
Build string `yaml:"build,omitempty" json:"build,omitempty"`
Main string `yaml:"main,omitempty" json:"main,omitempty"`
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"`
Repository string `yaml:"repository,omitempty" json:"repository,omitempty"`
Platforms []string `yaml:"platforms,omitempty" json:"platforms,omitempty"`
Tags []string `yaml:"tags,omitempty" json:"tags,omitempty"`
CreationTime string `yaml:"creation_time,omitempty" json:"creation_time,omitempty"`
KoDataCreationTime string `yaml:"ko_data_creation_time,omitempty" json:"ko_data_creation_time,omitempty"`
SBOM string `yaml:"sbom,omitempty" json:"sbom,omitempty"`
Ldflags []string `yaml:"ldflags,omitempty" json:"ldflags,omitempty"`
Flags []string `yaml:"flags,omitempty" json:"flags,omitempty"`
Env []string `yaml:"env,omitempty" json:"env,omitempty"`
Bare bool `yaml:"bare,omitempty" json:"bare,omitempty"`
PreserveImportPaths bool `yaml:"preserve_import_paths,omitempty" json:"preserve_import_paths,omitempty"`
BaseImportPaths bool `yaml:"base_import_paths,omitempty" json:"base_import_paths,omitempty"`
}

// Scoop contains the scoop.sh section.
Expand Down
21 changes: 21 additions & 0 deletions www/docs/customization/ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ kos:
# Defaults to cgr.dev/chainguard/static.
base_image: alpine

# Labels for the image.
#
# Defaults to null.
# Since v1.17.
labels:
foo: bar

# Repository to push to.
#
# Defaults to the value of $KO_DOCKER_REPO.
Expand All @@ -58,6 +65,20 @@ kos:
- latest
- '{{.Tag}}'

# Creation time given to the image
# in seconds since the Unix epoch as a string.
#
# Defaults to empty string.
# Since v1.17.
creation_time: '{{.CommitTimestamp}}'

# Creation time given to the files in the kodata directory
# in seconds since the Unix epoch as a string.
#
# Defaults to empty string.
# Since v1.17.
ko_data_creation_time: '{{.CommitTimestamp}}'

# SBOM format to use.
#
# Defaults to spdx.
Expand Down

0 comments on commit c47315f

Please sign in to comment.