1
0
mirror of https://github.com/ko-build/ko.git synced 2025-11-06 09:19:12 +02:00

Annotate images with base image information (#354)

* WIP: annotate base images

* remove TODO

* .

* Annotate with base index digest, if based on an index

* use correct new proposed annotation
This commit is contained in:
Jason Hall
2021-07-20 15:06:08 -05:00
committed by GitHub
parent 56282bf645
commit 26d03e92c2
1021 changed files with 87486 additions and 26763 deletions

View File

@@ -36,6 +36,7 @@ import (
"text/template"
"github.com/containerd/stargz-snapshotter/estargz"
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/empty"
"github.com/google/go-containerregistry/pkg/v1/mutate"
@@ -65,8 +66,13 @@ For more information see:
`
)
// GetBase takes an importpath and returns a base image.
type GetBase func(context.Context, string) (Result, error)
const (
baseDigestAnnotation = "org.opencontainers.image.base.digest"
baseRefAnnotation = "org.opencontainers.image.base.name"
)
// 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)
@@ -652,8 +658,8 @@ func (g *gobuild) configForImportPath(ip string) Config {
return config
}
func (g *gobuild) buildOne(ctx context.Context, s string, base v1.Image, platform *v1.Platform) (v1.Image, error) {
ref := newRef(s)
func (g *gobuild) buildOne(ctx context.Context, refStr string, baseRef name.Reference, baseDigest v1.Hash, base v1.Image, platform *v1.Platform) (v1.Image, error) {
ref := newRef(refStr)
cf, err := base.ConfigFile()
if err != nil {
@@ -728,6 +734,11 @@ func (g *gobuild) buildOne(ctx context.Context, s string, base v1.Image, platfor
return nil, err
}
withApp = mutate.Annotations(withApp, map[string]string{
baseRefAnnotation: baseRef.Name(),
baseDigestAnnotation: baseDigest.String(),
})
// Start from a copy of the base image's config file, and set
// the entrypoint to our app.
cfg, err := withApp.ConfigFile()
@@ -784,7 +795,7 @@ func updatePath(cf *v1.ConfigFile) {
// Build implements build.Interface
func (g *gobuild) Build(ctx context.Context, s string) (Result, error) {
// Determine the appropriate base image for this import path.
base, err := g.getBase(ctx, s)
baseRef, base, err := g.getBase(ctx, s)
if err != nil {
return nil, err
}
@@ -795,27 +806,33 @@ func (g *gobuild) Build(ctx context.Context, s string) (Result, error) {
return nil, err
}
// Take the digest of the base index or image, to annotate images we'll build later.
baseDigest, err := base.Digest()
if err != nil {
return nil, err
}
switch mt {
case types.OCIImageIndex, types.DockerManifestList:
base, ok := base.(v1.ImageIndex)
baseIndex, ok := base.(v1.ImageIndex)
if !ok {
return nil, fmt.Errorf("failed to interpret base as index: %v", base)
}
return g.buildAll(ctx, s, base)
return g.buildAll(ctx, s, baseRef, baseDigest, baseIndex)
case types.OCIManifestSchema1, types.DockerManifestSchema2:
base, ok := base.(v1.Image)
baseImage, ok := base.(v1.Image)
if !ok {
return nil, fmt.Errorf("failed to interpret base as image: %v", base)
}
return g.buildOne(ctx, s, base, nil)
return g.buildOne(ctx, s, baseRef, baseDigest, baseImage, nil)
default:
return nil, fmt.Errorf("base image media type: %s", mt)
}
}
// TODO(#192): Do these in parallel?
func (g *gobuild) buildAll(ctx context.Context, s string, base v1.ImageIndex) (v1.ImageIndex, error) {
im, err := base.IndexManifest()
func (g *gobuild) buildAll(ctx context.Context, ref string, baseRef name.Reference, baseDigest v1.Hash, baseIndex v1.ImageIndex) (v1.ImageIndex, error) {
im, err := baseIndex.IndexManifest()
if err != nil {
return nil, err
}
@@ -825,18 +842,18 @@ func (g *gobuild) buildAll(ctx context.Context, s string, base v1.ImageIndex) (v
for _, desc := range im.Manifests {
// Nested index is pretty rare. We could support this in theory, but return an error for now.
if desc.MediaType != types.OCIManifestSchema1 && desc.MediaType != types.DockerManifestSchema2 {
return nil, fmt.Errorf("%q has unexpected mediaType %q in base for %q", desc.Digest, desc.MediaType, s)
return nil, fmt.Errorf("%q has unexpected mediaType %q in base for %q", desc.Digest, desc.MediaType, ref)
}
if !g.platformMatcher.matches(desc.Platform) {
continue
}
base, err := base.Image(desc.Digest)
baseImage, err := baseIndex.Image(desc.Digest)
if err != nil {
return nil, err
}
img, err := g.buildOne(ctx, s, base, desc.Platform)
img, err := g.buildOne(ctx, ref, baseRef, baseDigest, baseImage, desc.Platform)
if err != nil {
return nil, err
}
@@ -851,7 +868,7 @@ func (g *gobuild) buildAll(ctx context.Context, s string, base v1.ImageIndex) (v
})
}
baseType, err := base.MediaType()
baseType, err := baseIndex.MediaType()
if err != nil {
return nil, err
}

View File

@@ -31,6 +31,7 @@ import (
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/empty"
"github.com/google/go-containerregistry/pkg/v1/mutate"
@@ -110,7 +111,7 @@ func TestGoBuildQualifyImport(t *testing.T) {
}
for _, test := range tests {
t.Run(test.description, func(t *testing.T) {
ng, err := NewGo(context.Background(), test.dir, WithBaseImages(func(context.Context, string) (Result, error) { return base, nil }))
ng, err := NewGo(context.Background(), test.dir, WithBaseImages(func(context.Context, string) (name.Reference, Result, error) { return nil, base, nil }))
if err != nil {
t.Fatalf("NewGo() = %v", err)
}
@@ -131,13 +132,15 @@ func TestGoBuildQualifyImport(t *testing.T) {
}
}
var baseRef = name.MustParseReference("all.your/base")
func TestGoBuildIsSupportedRef(t *testing.T) {
base, err := random.Image(1024, 3)
if err != nil {
t.Fatalf("random.Image() = %v", err)
}
ng, err := NewGo(context.Background(), "", WithBaseImages(func(context.Context, string) (Result, error) { return base, nil }))
ng, err := NewGo(context.Background(), "", WithBaseImages(func(context.Context, string) (name.Reference, Result, error) { return nil, base, nil }))
if err != nil {
t.Fatalf("NewGo() = %v", err)
}
@@ -186,7 +189,7 @@ func TestGoBuildIsSupportedRefWithModules(t *testing.T) {
}
opts := []Option{
WithBaseImages(func(context.Context, string) (Result, error) { return base, nil }),
WithBaseImages(func(context.Context, string) (name.Reference, Result, error) { return baseRef, base, nil }),
withModuleInfo(mods),
withBuildContext(stubBuildContext{
// make all referenced deps commands
@@ -259,7 +262,7 @@ func TestGoBuildNoKoData(t *testing.T) {
context.Background(),
"",
WithCreationTime(creationTime),
WithBaseImages(func(context.Context, string) (Result, error) { return base, nil }),
WithBaseImages(func(context.Context, string) (name.Reference, Result, error) { return baseRef, base, nil }),
withBuilder(writeTempFile),
)
if err != nil {
@@ -477,6 +480,21 @@ func validateImage(t *testing.T, img v1.Image, baseLayers int64, creationTime v1
t.Errorf("created = %v, want %v", actual, creationTime)
}
})
t.Run("check annotations", func(t *testing.T) {
mf, err := img.Manifest()
if err != nil {
t.Fatalf("Manifest() = %v", err)
}
t.Logf("Got annotations: %v", mf.Annotations)
if _, found := mf.Annotations[baseDigestAnnotation]; !found {
t.Errorf("image annotations did not contain base image digest")
}
want := baseRef.Name()
if got := mf.Annotations[baseRefAnnotation]; got != want {
t.Errorf("base image ref; got %q, want %q", got, want)
}
})
}
type stubBuildContext map[string]*gb.Package
@@ -502,7 +520,7 @@ func TestGoBuild(t *testing.T) {
context.Background(),
"",
WithCreationTime(creationTime),
WithBaseImages(func(context.Context, string) (Result, error) { return base, nil }),
WithBaseImages(func(context.Context, string) (name.Reference, Result, error) { return baseRef, base, nil }),
withBuilder(writeTempFile),
WithLabel("foo", "bar"),
WithLabel("hello", "world"),
@@ -575,7 +593,7 @@ func TestGoBuildIndex(t *testing.T) {
context.Background(),
"",
WithCreationTime(creationTime),
WithBaseImages(func(context.Context, string) (Result, error) { return base, nil }),
WithBaseImages(func(context.Context, string) (name.Reference, Result, error) { return baseRef, base, nil }),
WithPlatforms("all"),
withBuilder(writeTempFile),
)
@@ -648,7 +666,7 @@ func TestNestedIndex(t *testing.T) {
context.Background(),
"",
WithCreationTime(creationTime),
WithBaseImages(func(context.Context, string) (Result, error) { return nestedBase, nil }),
WithBaseImages(func(context.Context, string) (name.Reference, Result, error) { return baseRef, nestedBase, nil }),
withBuilder(writeTempFile),
)
if err != nil {

View File

@@ -50,7 +50,7 @@ var (
// getBaseImage returns a function that determines the base image for a given import path.
// If the `bo.BaseImage` parameter is non-empty, it overrides base image configuration from `.ko.yaml`.
func getBaseImage(platform string, bo *options.BuildOptions) build.GetBase {
return func(ctx context.Context, s string) (build.Result, error) {
return func(ctx context.Context, s string) (name.Reference, build.Result, error) {
s = strings.TrimPrefix(s, build.StrictScheme)
// Viper configuration file keys are case insensitive, and are
// returned as all lowercase. This means that import paths with
@@ -71,12 +71,13 @@ func getBaseImage(platform string, bo *options.BuildOptions) build.GetBase {
}
ref, err := name.ParseReference(baseImage, nameOpts...)
if err != nil {
return nil, fmt.Errorf("parsing base image (%q): %v", baseImage, err)
return nil, nil, fmt.Errorf("parsing base image (%q): %v", baseImage, err)
}
// For ko.local, look in the daemon.
if ref.Context().RegistryStr() == publish.LocalDomain {
return daemon.Image(ref)
img, err := daemon.Image(ref)
return ref, img, err
}
userAgent := ua()
@@ -108,7 +109,7 @@ func getBaseImage(platform string, bo *options.BuildOptions) build.GetBase {
p.Variant = parts[2]
}
if len(parts) > 3 {
return nil, fmt.Errorf("too many slashes in platform spec: %s", platform)
return nil, nil, fmt.Errorf("too many slashes in platform spec: %s", platform)
}
ropt = append(ropt, remote.WithPlatform(p))
}
@@ -116,16 +117,19 @@ func getBaseImage(platform string, bo *options.BuildOptions) build.GetBase {
log.Printf("Using base %s for %s", ref, s)
desc, err := remote.Get(ref, ropt...)
if err != nil {
return nil, err
return nil, nil, err
}
switch desc.MediaType {
case types.OCIImageIndex, types.DockerManifestList:
if multiplatform {
return desc.ImageIndex()
idx, err := desc.ImageIndex()
return ref, idx, err
}
return desc.Image()
img, err := desc.Image()
return ref, img, err
default:
return desc.Image()
img, err := desc.Image()
return ref, img, err
}
}
}

View File

@@ -32,7 +32,7 @@ func TestOverrideDefaultBaseImageUsingBuildOption(t *testing.T) {
}
baseFn := getBaseImage("all", bo)
res, err := baseFn(context.Background(), "ko://example.com/helloworld")
_, res, err := baseFn(context.Background(), "ko://example.com/helloworld")
if err != nil {
t.Fatalf("getBaseImage(): %v", err)
}