diff --git a/src/cmd/linuxkit/cache.go b/src/cmd/linuxkit/cache.go index 405a45333..ea07f509e 100644 --- a/src/cmd/linuxkit/cache.go +++ b/src/cmd/linuxkit/cache.go @@ -26,5 +26,7 @@ func cacheCmd() *cobra.Command { cmd.AddCommand(cacheLsCmd()) cmd.AddCommand(cacheExportCmd()) cmd.AddCommand(cacheImportCmd()) + cmd.AddCommand(cachePullCmd()) + cmd.AddCommand(cachePushCmd()) return cmd } diff --git a/src/cmd/linuxkit/cache/pull.go b/src/cmd/linuxkit/cache/pull.go index 61aeef57e..fcb0865b9 100644 --- a/src/cmd/linuxkit/cache/pull.go +++ b/src/cmd/linuxkit/cache/pull.go @@ -5,10 +5,18 @@ import ( "fmt" "github.com/containerd/containerd/reference" + "github.com/google/go-containerregistry/pkg/authn" + namepkg "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/layout" + "github.com/google/go-containerregistry/pkg/v1/match" "github.com/google/go-containerregistry/pkg/v1/partial" + "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/google/go-containerregistry/pkg/v1/validate" lktspec "github.com/linuxkit/linuxkit/src/cmd/linuxkit/spec" + "github.com/linuxkit/linuxkit/src/cmd/linuxkit/util" + imagespec "github.com/opencontainers/image-spec/specs-go/v1" + log "github.com/sirupsen/logrus" ) const ( @@ -118,3 +126,77 @@ func validateManifestContents(index v1.ImageIndex, digest v1.Hash) error { } return nil } + +// Pull pull a reference, whether it points to an arch-specific image or to an index. +// If an index, optionally, try to pull its individual named references as well. +func (p *Provider) Pull(name string, withArchReferences bool) error { + var ( + err error + ) + fullname := util.ReferenceExpand(name, util.ReferenceWithTag()) + ref, err := namepkg.ParseReference(fullname) + if err != nil { + return err + } + v1ref, err := reference.Parse(ref.String()) + if err != nil { + return err + } + + // before we even try to push, let us see if it exists remotely + remoteOptions := []remote.Option{remote.WithAuthFromKeychain(authn.DefaultKeychain)} + + desc, err := remote.Get(ref, remoteOptions...) + if err != nil { + return fmt.Errorf("error getting manifest for trusted image %s: %v", name, err) + } + + // use the original image name in the annotation + annotations := map[string]string{ + imagespec.AnnotationRefName: fullname, + } + + // first attempt as an index + ii, err := desc.ImageIndex() + if err == nil { + log.Debugf("ImageWrite retrieved %s is index, saving", fullname) + + if err := p.cache.WriteIndex(ii); err != nil { + return fmt.Errorf("unable to write index: %v", err) + } + if _, err := p.DescriptorWrite(&v1ref, desc.Descriptor); err != nil { + return fmt.Errorf("unable to write index descriptor to cache: %v", err) + } + if withArchReferences { + im, err := ii.IndexManifest() + if err != nil { + return fmt.Errorf("unable to get IndexManifest: %v", err) + } + for _, m := range im.Manifests { + if m.MediaType.IsImage() && m.Platform != nil && m.Platform.Architecture != unknown && m.Platform.OS != unknown { + archSpecific := fmt.Sprintf("%s-%s", ref.String(), m.Platform.Architecture) + archRef, err := reference.Parse(archSpecific) + if err != nil { + return fmt.Errorf("unable to parse arch-specific reference %s: %v", archSpecific, err) + } + if _, err := p.DescriptorWrite(&archRef, m); err != nil { + return fmt.Errorf("unable to write index descriptor to cache: %v", err) + } + } + } + } + } else { + var im v1.Image + // try an image + im, err = desc.Image() + if err != nil { + return fmt.Errorf("provided image is neither an image nor an index: %s", name) + } + log.Debugf("ImageWrite retrieved %s is image, saving", fullname) + if err = p.cache.ReplaceImage(im, match.Name(fullname), layout.WithAnnotations(annotations)); err != nil { + return fmt.Errorf("unable to save image to cache: %v", err) + } + } + + return nil +} diff --git a/src/cmd/linuxkit/cache_pull.go b/src/cmd/linuxkit/cache_pull.go new file mode 100644 index 000000000..42e3eca0a --- /dev/null +++ b/src/cmd/linuxkit/cache_pull.go @@ -0,0 +1,37 @@ +package main + +import ( + cachepkg "github.com/linuxkit/linuxkit/src/cmd/linuxkit/cache" + "github.com/linuxkit/linuxkit/src/cmd/linuxkit/util" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +func cachePullCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "pull", + Short: "pull images to the linuxkit cache from registry", + Long: `Pull named images from their registry to the linuxkit cache. Can provide short name, like linuxkit/kernel:6.6.13 + or nginx, or canonical name, like docker.io/library/nginx:latest. Will be saved into cache as canonical. + Will replace in cache if found. Blobs with the same content are not replaced.`, + Args: cobra.MinimumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + names := args + for _, name := range names { + fullname := util.ReferenceExpand(name, util.ReferenceWithTag()) + + p, err := cachepkg.NewProvider(cacheDir) + if err != nil { + log.Fatalf("unable to read a local cache: %v", err) + } + + if err := p.Pull(fullname, true); err != nil { + log.Fatalf("unable to push image named %s: %v", name, err) + } + } + return nil + }, + } + + return cmd +} diff --git a/src/cmd/linuxkit/cache_push.go b/src/cmd/linuxkit/cache_push.go new file mode 100644 index 000000000..533167761 --- /dev/null +++ b/src/cmd/linuxkit/cache_push.go @@ -0,0 +1,37 @@ +package main + +import ( + cachepkg "github.com/linuxkit/linuxkit/src/cmd/linuxkit/cache" + "github.com/linuxkit/linuxkit/src/cmd/linuxkit/util" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +func cachePushCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "push", + Short: "push images from the linuxkit cache", + Long: `Push named images from the linuxkit cache to registry. Can provide short name, like linuxkit/kernel:6.6.13 + or nginx, or canonical name, like docker.io/library/nginx:latest. + It is efficient, as blobs with the same content are not replaced.`, + Args: cobra.MinimumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + names := args + for _, name := range names { + fullname := util.ReferenceExpand(name) + + p, err := cachepkg.NewProvider(cacheDir) + if err != nil { + log.Fatalf("unable to read a local cache: %v", err) + } + + if err := p.Push(fullname, true); err != nil { + log.Fatalf("unable to push image named %s: %v", name, err) + } + } + return nil + }, + } + + return cmd +} diff --git a/src/cmd/linuxkit/util/reference.go b/src/cmd/linuxkit/util/reference.go index 42b42d4a4..b5aaedc31 100644 --- a/src/cmd/linuxkit/util/reference.go +++ b/src/cmd/linuxkit/util/reference.go @@ -2,15 +2,37 @@ package util import "strings" +type refOpts struct { + withTag bool +} +type ReferenceOption func(r *refOpts) + +// ReferenceWithTag returns a ReferenceOption that ensures a tag is filled. If the tag is not provided, +// the default is added +func ReferenceWithTag() ReferenceOption { + return func(r *refOpts) { + r.withTag = true + } +} + // ReferenceExpand expands "redis" to "docker.io/library/redis" so all images have a full domain -func ReferenceExpand(ref string) string { +func ReferenceExpand(ref string, options ...ReferenceOption) string { + var opts refOpts + for _, opt := range options { + opt(&opts) + } + var ret string parts := strings.Split(ref, "/") switch len(parts) { case 1: - return "docker.io/library/" + ref + ret = "docker.io/library/" + ref case 2: - return "docker.io/" + ref + ret = "docker.io/" + ref default: - return ref + ret = ref } + if opts.withTag && !strings.Contains(ret, ":") { + ret += ":latest" + } + return ret }