Skip to content

Commit

Permalink
support building from a scratch image
Browse files Browse the repository at this point in the history
  • Loading branch information
tzneal committed Aug 11, 2023
1 parent ecc9812 commit c3ab49c
Show file tree
Hide file tree
Showing 5 changed files with 229 additions and 5 deletions.
4 changes: 4 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ baseImageOverrides:
github.com/my-user/my-repo/cmd/app: registry.example.com/base/for/app
github.com/my-user/my-repo/cmd/foo: registry.example.com/base/for/foo
```
#### Scratch Base Image

If the base image name `scratch` is used, `ko` will construct an empty base image with support for the platforms
specified. In this mode it is not possible to specify `all` platforms.

### Overriding Go build settings

Expand Down
84 changes: 79 additions & 5 deletions pkg/build/gobuild_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ func TestGoBuildQualifyImport(t *testing.T) {
}

var baseRef = name.MustParseReference("all.your/base")
var scratchBaseRef = name.MustParseReference("scratch")

func TestGoBuildIsSupportedRef(t *testing.T) {
base, err := random.Image(1024, 3)
Expand Down Expand Up @@ -514,7 +515,7 @@ func TestGoBuildNoKoData(t *testing.T) {
})
}

func validateImage(t *testing.T, img oci.SignedImage, baseLayers int64, creationTime v1.Time, checkAnnotations bool, expectSBOM bool) {
func validateImage(t *testing.T, img oci.SignedImage, baseLayers int64, base name.Reference, creationTime v1.Time, checkAnnotations bool, expectSBOM bool) {
t.Helper()

ls, err := img.Layers()
Expand Down Expand Up @@ -665,7 +666,7 @@ func validateImage(t *testing.T, img oci.SignedImage, baseLayers int64, creation
if _, found := mf.Annotations[specsv1.AnnotationBaseImageDigest]; !found {
t.Errorf("image annotations did not contain base image digest")
}
want := baseRef.Name()
want := base.Name()
if got := mf.Annotations[specsv1.AnnotationBaseImageName]; got != want {
t.Errorf("base image ref; got %q, want %q", got, want)
}
Expand Down Expand Up @@ -734,7 +735,7 @@ func TestGoBuild(t *testing.T) {
t.Fatalf("Build() not a SignedImage: %T", result)
}

validateImage(t, img, baseLayers, creationTime, true, true)
validateImage(t, img, baseLayers, baseRef, creationTime, true, true)

// Check that rebuilding the image again results in the same image digest.
t.Run("check determinism", func(t *testing.T) {
Expand Down Expand Up @@ -848,7 +849,7 @@ func TestGoBuildWithoutSBOM(t *testing.T) {
t.Fatalf("Build() not a SignedImage: %T", result)
}

validateImage(t, img, baseLayers, creationTime, true, false)
validateImage(t, img, baseLayers, baseRef, creationTime, true, false)
}

func TestGoBuildIndex(t *testing.T) {
Expand Down Expand Up @@ -894,7 +895,7 @@ func TestGoBuildIndex(t *testing.T) {
if err != nil {
t.Fatalf("idx.Image(%s) = %v", desc.Digest, err)
}
validateImage(t, img, baseLayers, creationTime, false, true)
validateImage(t, img, baseLayers, baseRef, creationTime, false, true)
}

if want, got := images, int64(len(im.Manifests)); want != got {
Expand Down Expand Up @@ -1233,3 +1234,76 @@ func TestGoBuildConsistentMediaTypes(t *testing.T) {
})
}
}

func TestGoBuildScratch(t *testing.T) {
importpath := "github.com/google/ko"

creationTime := v1.Time{Time: time.Unix(5000, 0)}
ng, err := NewGo(
context.Background(),
"",
WithCreationTime(creationTime),
WithBaseImages(func(context.Context, string) (name.Reference, Result, error) {
img, err := ScratchImage([]string{"linux/s390x"})
return scratchBaseRef, img, err
}),
withBuilder(writeTempFile),
withSBOMber(fauxSBOM),
WithLabel("foo", "bar"),
WithLabel("hello", "world"),
WithPlatforms("all"),
)
if err != nil {
t.Fatalf("NewGo() = %v", err)
}

result, err := ng.Build(context.Background(), StrictScheme+filepath.Join(importpath, "test"))
if err != nil {
t.Fatalf("Build() = %v", err)
}

img, ok := result.(oci.SignedImage)
if !ok {
t.Fatalf("Build() not a SignedImage: %T", result)
}

baseLayers := int64(0) // scratch image has no layers
validateImage(t, img, baseLayers, scratchBaseRef, creationTime, true, true)

// Check that rebuilding the image again results in the same image digest.
t.Run("check determinism", func(t *testing.T) {
result2, err := ng.Build(context.Background(), StrictScheme+filepath.Join(importpath, "test"))
if err != nil {
t.Fatalf("Build() = %v", err)
}

d1, err := result.Digest()
if err != nil {
t.Fatalf("Digest() = %v", err)
}
d2, err := result2.Digest()
if err != nil {
t.Fatalf("Digest() = %v", err)
}

if d1 != d2 {
t.Errorf("Digest mismatch: %s != %s", d1, d2)
}
})

t.Run("check labels", func(t *testing.T) {
cfg, err := img.ConfigFile()
if err != nil {
t.Fatalf("ConfigFile() = %v", err)
}

want := map[string]string{
"foo": "bar",
"hello": "world",
}
got := cfg.Config.Labels
if d := cmp.Diff(got, want); d != "" {
t.Fatalf("Labels diff (-got,+want): %s", d)
}
})
}
62 changes: 62 additions & 0 deletions pkg/build/scratch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Copyright 2023 Google LLC All Rights Reserved.

Check failure on line 1 in pkg/build/scratch.go

View workflow job for this annotation

GitHub Actions / Boilerplate Check (go)

[Go headers] reported by reviewdog 🐶 missing boilerplate: Raw Output: pkg/build/scratch.go:1: missing boilerplate: // Copyright 2023 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package build

import (
"fmt"

v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/empty"
"github.com/google/go-containerregistry/pkg/v1/mutate"
"github.com/google/go-containerregistry/pkg/v1/types"
)

// ScratchImage returns a scratch image manifest with scratch images for each of the specified platforms
func ScratchImage(platforms []string) (Result, error) {
var manifests []mutate.IndexAddendum
for _, pf := range platforms {
if pf == "all" {
return nil, fmt.Errorf("'all' is not supported for building a scratch image, the platform list must be provided")
}
p, err := v1.ParsePlatform(pf)
if err != nil {
return nil, err
}
img, err := mutate.ConfigFile(empty.Image, &v1.ConfigFile{
RootFS: v1.RootFS{
// Some clients check this.
Type: "layers",
},
Architecture: p.Architecture,
OS: p.OS,
Variant: p.Variant,
OSVersion: p.OSVersion,
OSFeatures: p.OSFeatures,
},
)
if err != nil {
return nil, fmt.Errorf("setting config file on empty image, %w", err)
}
manifests = append(manifests, mutate.IndexAddendum{
Add: img,
Descriptor: v1.Descriptor{
Platform: p,
},
})
}
idx := mutate.IndexMediaType(empty.Index, types.OCIImageIndex)
idx = mutate.AppendManifests(idx, manifests...)
return idx, nil
}
74 changes: 74 additions & 0 deletions pkg/build/scratch_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Copyright 2023 Google LLC All Rights Reserved.

Check failure on line 1 in pkg/build/scratch_test.go

View workflow job for this annotation

GitHub Actions / Boilerplate Check (go)

[Go headers] reported by reviewdog 🐶 missing boilerplate: Raw Output: pkg/build/scratch_test.go:1: missing boilerplate: // Copyright 2023 ko Build Authors All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package build

import (
"testing"

v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/types"
)

func TestScratchImage(t *testing.T) {
img, err := ScratchImage([]string{"linux/s390x", "plan9/386"})
if err != nil {
t.Fatalf("expected to create the image, %s", err)
}
mt, err := img.MediaType()
if err != nil {
t.Fatalf("expected to get a mediatype, %s", err)
}
expMT := types.OCIImageIndex
if mt != expMT {
t.Errorf("expected media type = %s, got %s", expMT, mt)
}

imgIdx, ok := img.(v1.ImageIndex)
if !ok {
t.Fatalf("expected to have an image index")
}

mf, err := imgIdx.IndexManifest()
if mt != expMT {
t.Errorf("expected a manifest, got %s", err)
}
if len(mf.Manifests) != 2 {
t.Fatalf("expected two manifests, got %d", len(mf.Manifests))
}
for _, m := range mf.Manifests {
img, err := imgIdx.Image(m.Digest)
if err != nil {
t.Fatalf("expected no error when getting image for digest %s, got %s", m.Digest, err)
}
ls, err := img.Layers()
if len(ls) != 0 {
t.Errorf("expected no layers, found %d", len(ls))
}

switch m.Platform.OS {
case "linux":
if m.Platform.Architecture != "s390x" {
t.Errorf("expected arch = s390x, got %s", m.Platform.Architecture)
}
case "plan9":
if m.Platform.Architecture != "386" {
t.Errorf("expected arch = 386, got %s", m.Platform.Architecture)
}
default:
t.Errorf("unexpected OS %s", m.Platform.OS)
}
}

}
10 changes: 10 additions & 0 deletions pkg/commands/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ func getBaseImage(bo *options.BuildOptions) build.GetBase {
if !ok || baseImage == "" {
baseImage = bo.BaseImage
}

var nameOpts []name.Option
if bo.InsecureRegistry {
nameOpts = append(nameOpts, name.Insecure)
Expand All @@ -112,6 +113,15 @@ func getBaseImage(bo *options.BuildOptions) build.GetBase {
return nil, nil, fmt.Errorf("parsing base image (%q): %w", baseImage, err)
}

if baseImage == "scratch" {
log.Printf("Using base %s for %s", ref, s)
si, err := build.ScratchImage(bo.Platforms)
if err != nil {
return nil, nil, fmt.Errorf("constructing scratch image: %w", err)
}
return ref, si, nil
}

result, err := cache.get(ctx, ref, fetch)
if err != nil {
return ref, result, err
Expand Down

0 comments on commit c3ab49c

Please sign in to comment.