diff --git a/pkg/build/scratch.go b/pkg/build/scratch.go new file mode 100644 index 0000000000..36e5089e78 --- /dev/null +++ b/pkg/build/scratch.go @@ -0,0 +1,59 @@ +/* +Copyright 2023 Google LLC 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. +*/ + +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) { + idx := mutate.IndexMediaType(empty.Index, types.OCIImageIndex) + for _, pf := range platforms { + 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) + } + idx = mutate.AppendManifests(idx, mutate.IndexAddendum{ + Add: img, + Descriptor: v1.Descriptor{ + Platform: p, + }, + }) + } + return idx, nil +} diff --git a/pkg/build/scratch_test.go b/pkg/build/scratch_test.go new file mode 100644 index 0000000000..20c4c651d3 --- /dev/null +++ b/pkg/build/scratch_test.go @@ -0,0 +1,66 @@ +/* +Copyright 2023 Google LLC 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. +*/ + +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 { + 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) + } + } + +} diff --git a/pkg/commands/config.go b/pkg/commands/config.go index f327aee974..43fbdbf99d 100644 --- a/pkg/commands/config.go +++ b/pkg/commands/config.go @@ -94,6 +94,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) @@ -103,6 +104,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 + } + if v, ok := cache.Load(ref.String()); ok { return ref, v.(build.Result), nil }