mirror of https://github.com/k3d-io/k3d
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
238 lines
6.4 KiB
238 lines
6.4 KiB
1 year ago
|
// 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
|
||
|
}
|