diff options
Diffstat (limited to 'pkg/crane/optimize.go')
-rw-r--r-- | pkg/crane/optimize.go | 237 |
1 files changed, 237 insertions, 0 deletions
diff --git a/pkg/crane/optimize.go b/pkg/crane/optimize.go new file mode 100644 index 0000000..74c665d --- /dev/null +++ b/pkg/crane/optimize.go @@ -0,0 +1,237 @@ +// Copyright 2020 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 crane + +import ( + "errors" + "fmt" + + "github.com/containerd/stargz-snapshotter/estargz" + "github.com/google/go-containerregistry/pkg/logs" + "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" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/google/go-containerregistry/pkg/v1/tarball" + "github.com/google/go-containerregistry/pkg/v1/types" +) + +// Optimize optimizes a remote image or index from src to dst. +// THIS API IS EXPERIMENTAL AND SUBJECT TO CHANGE WITHOUT WARNING. +func Optimize(src, dst string, prioritize []string, opt ...Option) error { + pset := newStringSet(prioritize) + o := makeOptions(opt...) + srcRef, err := name.ParseReference(src, o.Name...) + if err != nil { + return fmt.Errorf("parsing reference %q: %w", src, err) + } + + dstRef, err := name.ParseReference(dst, o.Name...) + if err != nil { + return fmt.Errorf("parsing reference for %q: %w", dst, err) + } + + logs.Progress.Printf("Optimizing from %v to %v", srcRef, dstRef) + desc, err := remote.Get(srcRef, o.Remote...) + if err != nil { + return fmt.Errorf("fetching %q: %w", src, err) + } + + switch desc.MediaType { + case types.OCIImageIndex, types.DockerManifestList: + // Handle indexes separately. + if o.Platform != nil { + // If platform is explicitly set, don't optimize the whole index, just the appropriate image. + if err := optimizeAndPushImage(desc, dstRef, pset, o); err != nil { + return fmt.Errorf("failed to optimize image: %w", err) + } + } else { + if err := optimizeAndPushIndex(desc, dstRef, pset, o); err != nil { + return fmt.Errorf("failed to optimize index: %w", err) + } + } + + case types.DockerManifestSchema1, types.DockerManifestSchema1Signed: + return errors.New("docker schema 1 images are not supported") + + default: + // Assume anything else is an image, since some registries don't set mediaTypes properly. + if err := optimizeAndPushImage(desc, dstRef, pset, o); err != nil { + return fmt.Errorf("failed to optimize image: %w", err) + } + } + + return nil +} + +func optimizeAndPushImage(desc *remote.Descriptor, dstRef name.Reference, prioritize stringSet, o Options) error { + img, err := desc.Image() + if err != nil { + return err + } + + missing, oimg, err := optimizeImage(img, prioritize) + if err != nil { + return err + } + + if len(missing) > 0 { + return fmt.Errorf("the following prioritized files were missing from image: %v", missing.List()) + } + + return remote.Write(dstRef, oimg, o.Remote...) +} + +func optimizeImage(img v1.Image, prioritize stringSet) (stringSet, v1.Image, error) { + cfg, err := img.ConfigFile() + if err != nil { + return nil, nil, err + } + ocfg := cfg.DeepCopy() + ocfg.History = nil + ocfg.RootFS.DiffIDs = nil + + oimg, err := mutate.ConfigFile(empty.Image, ocfg) + if err != nil { + return nil, nil, err + } + + layers, err := img.Layers() + if err != nil { + return nil, nil, err + } + + missingFromImage := newStringSet(prioritize.List()) + olayers := make([]mutate.Addendum, 0, len(layers)) + for _, layer := range layers { + missingFromLayer := []string{} + olayer, err := tarball.LayerFromOpener(layer.Uncompressed, + tarball.WithEstargz, + tarball.WithEstargzOptions( + estargz.WithPrioritizedFiles(prioritize.List()), + estargz.WithAllowPrioritizeNotFound(&missingFromLayer), + )) + if err != nil { + return nil, nil, err + } + missingFromImage = missingFromImage.Intersection(newStringSet(missingFromLayer)) + + olayers = append(olayers, mutate.Addendum{ + Layer: olayer, + MediaType: types.DockerLayer, + }) + } + + oimg, err = mutate.Append(oimg, olayers...) + if err != nil { + return nil, nil, err + } + return missingFromImage, oimg, nil +} + +func optimizeAndPushIndex(desc *remote.Descriptor, dstRef name.Reference, prioritize stringSet, o Options) error { + idx, err := desc.ImageIndex() + if err != nil { + return err + } + + missing, oidx, err := optimizeIndex(idx, prioritize) + if err != nil { + return err + } + + if len(missing) > 0 { + return fmt.Errorf("the following prioritized files were missing from all images: %v", missing.List()) + } + + return remote.WriteIndex(dstRef, oidx, o.Remote...) +} + +func optimizeIndex(idx v1.ImageIndex, prioritize stringSet) (stringSet, v1.ImageIndex, error) { + im, err := idx.IndexManifest() + if err != nil { + return nil, nil, err + } + + missingFromIndex := newStringSet(prioritize.List()) + + // Build an image for each child from the base and append it to a new index to produce the result. + adds := make([]mutate.IndexAddendum, 0, len(im.Manifests)) + for _, desc := range im.Manifests { + img, err := idx.Image(desc.Digest) + if err != nil { + return nil, nil, err + } + + missingFromImage, oimg, err := optimizeImage(img, prioritize) + if err != nil { + return nil, nil, err + } + missingFromIndex = missingFromIndex.Intersection(missingFromImage) + adds = append(adds, mutate.IndexAddendum{ + Add: oimg, + Descriptor: v1.Descriptor{ + URLs: desc.URLs, + MediaType: desc.MediaType, + Annotations: desc.Annotations, + Platform: desc.Platform, + }, + }) + } + + idxType, err := idx.MediaType() + if err != nil { + return nil, nil, err + } + + return missingFromIndex, mutate.IndexMediaType(mutate.AppendManifests(empty.Index, adds...), idxType), nil +} + +type stringSet map[string]struct{} + +func newStringSet(in []string) stringSet { + ss := stringSet{} + for _, s := range in { + ss[s] = struct{}{} + } + return ss +} + +func (s stringSet) List() []string { + result := make([]string, 0, len(s)) + for k := range s { + result = append(result, k) + } + return result +} + +func (s stringSet) Intersection(rhs stringSet) stringSet { + // To appease ST1016 + lhs := s + + // Make sure len(lhs) >= len(rhs) + if len(lhs) < len(rhs) { + return rhs.Intersection(lhs) + } + + result := stringSet{} + for k := range lhs { + if _, ok := rhs[k]; ok { + result[k] = struct{}{} + } + } + return result +} |