add support for pushing and pulling images

Signed-off-by: Avi Deitcher <avi@deitcher.net>
This commit is contained in:
Avi Deitcher 2024-02-28 13:08:47 +02:00
parent 8b9b3f673b
commit 54d9db8650
5 changed files with 184 additions and 4 deletions

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}