From d02e5d13ee8dfcecf5d3e5a50b5a572dd24fd400 Mon Sep 17 00:00:00 2001 From: thesayyn Date: Tue, 23 Jan 2024 15:10:52 -0800 Subject: [PATCH] feat: mutate on oci-layout --- cmd/crane/cmd/append.go | 1 - cmd/crane/cmd/root.go | 8 ++++- internal/cmd/edit.go | 9 ++--- pkg/crane/config.go | 12 +++++++ pkg/crane/get.go | 4 +++ pkg/crane/local/options.go | 43 ++++++++++++++++++++++++ pkg/crane/local/read.go | 61 +++++++++++++++++++++++++++++++++ pkg/crane/local/write.go | 69 ++++++++++++++++++++++++++++++++++++++ pkg/crane/options.go | 11 ++++++ pkg/crane/pull.go | 5 ++- pkg/crane/push.go | 17 +++++++++- 11 files changed, 229 insertions(+), 11 deletions(-) create mode 100644 pkg/crane/local/options.go create mode 100644 pkg/crane/local/read.go create mode 100644 pkg/crane/local/write.go diff --git a/cmd/crane/cmd/append.go b/cmd/crane/cmd/append.go index 3555a7864..6642e58a1 100644 --- a/cmd/crane/cmd/append.go +++ b/cmd/crane/cmd/append.go @@ -117,6 +117,5 @@ container image.`, appendCmd.MarkFlagsMutuallyExclusive("oci-empty-base", "base") appendCmd.MarkFlagRequired("new_tag") - appendCmd.MarkFlagRequired("new_layer") return appendCmd } diff --git a/cmd/crane/cmd/root.go b/cmd/crane/cmd/root.go index 8fc8ccefa..f809e0286 100644 --- a/cmd/crane/cmd/root.go +++ b/cmd/crane/cmd/root.go @@ -47,6 +47,7 @@ func New(use, short string, options []crane.Option) *cobra.Command { insecure := false ndlayers := false platform := &platformValue{} + local := "" wt := &warnTransport{} @@ -68,6 +69,10 @@ func New(use, short string, options []crane.Option) *cobra.Command { if ndlayers { options = append(options, crane.WithNondistributable()) } + if local != "" { + options = append(options, crane.WithLocalPath(local)) + } + if Version != "" { binary := "crane" if len(os.Args[0]) != 0 { @@ -137,7 +142,8 @@ func New(use, short string, options []crane.Option) *cobra.Command { root.PersistentFlags().BoolVar(&insecure, "insecure", false, "Allow image references to be fetched without TLS") root.PersistentFlags().BoolVar(&ndlayers, "allow-nondistributable-artifacts", false, "Allow pushing non-distributable (foreign) layers") root.PersistentFlags().Var(platform, "platform", "Specifies the platform in the form os/arch[/variant][:osversion] (e.g. linux/amd64).") - + root.PersistentFlags().StringVar(&local, "local", "", "Use a local oci-layout as remote registry") + root.PersistentFlags().MarkHidden("local") return root } diff --git a/internal/cmd/edit.go b/internal/cmd/edit.go index 907a8371d..ef4fffca7 100644 --- a/internal/cmd/edit.go +++ b/internal/cmd/edit.go @@ -256,16 +256,11 @@ func editConfig(ctx context.Context, in io.Reader, out io.Writer, src, dst strin return nil, err } - pusher, err := remote.NewPusher(o.Remote...) - if err != nil { - return nil, err - } - - if err := pusher.Upload(ctx, dstRef.Context(), l); err != nil { + if err := crane.Upload(l, dstRef.Context().String(), options...); err != nil { return nil, err } - if err := pusher.Push(ctx, dstRef, rm); err != nil { + if err := crane.Put(rm, dstRef, options...); err != nil { return nil, err } diff --git a/pkg/crane/config.go b/pkg/crane/config.go index 3e55cc93a..ca85b568c 100644 --- a/pkg/crane/config.go +++ b/pkg/crane/config.go @@ -14,8 +14,20 @@ package crane +import ( + "github.com/google/go-containerregistry/pkg/crane/local" +) + // Config returns the config file for the remote image ref. func Config(ref string, opt ...Option) ([]byte, error) { + opts := makeOptions(opt...) + if opts.local { + i, err := local.Pull(ref, opts.Local...) + if err != nil { + return nil, err + } + return i.RawConfigFile() + } i, _, err := getImage(ref, opt...) if err != nil { return nil, err diff --git a/pkg/crane/get.go b/pkg/crane/get.go index 98a2e8933..949b18a5d 100644 --- a/pkg/crane/get.go +++ b/pkg/crane/get.go @@ -17,6 +17,7 @@ package crane import ( "fmt" + "github.com/google/go-containerregistry/pkg/crane/local" "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/remote" @@ -57,5 +58,8 @@ func Head(r string, opt ...Option) (*v1.Descriptor, error) { if err != nil { return nil, err } + if o.local { + return local.Head(ref, o.Local...) + } return remote.Head(ref, o.Remote...) } diff --git a/pkg/crane/local/options.go b/pkg/crane/local/options.go new file mode 100644 index 000000000..5721df6a8 --- /dev/null +++ b/pkg/crane/local/options.go @@ -0,0 +1,43 @@ +package local + +import ( + "errors" + + "github.com/google/go-containerregistry/pkg/v1/layout" +) + +// Option is a functional option for remote operations. +type Option func(*options) error + +type options struct { + path *layout.Path +} + +func makeOptions(opts ...Option) (*options, error) { + o := &options{ + path: nil, + } + + for _, option := range opts { + if err := option(o); err != nil { + return nil, err + } + } + + if o.path == nil { + return nil, errors.New("provide an option for local storage") + } + + return o, nil +} + +func WithPath(p string) Option { + return func(o *options) error { + path, err := layout.FromPath(p) + if err != nil { + return err + } + o.path = &path + return nil + } +} diff --git a/pkg/crane/local/read.go b/pkg/crane/local/read.go new file mode 100644 index 000000000..d79f742f7 --- /dev/null +++ b/pkg/crane/local/read.go @@ -0,0 +1,61 @@ +package local + +import ( + "errors" + "fmt" + + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + specsv1 "github.com/opencontainers/image-spec/specs-go/v1" +) + +func refEqDescriptor(ref name.Reference, descriptor v1.Descriptor) bool { + if _, ok := descriptor.Annotations[specsv1.AnnotationRefName]; ok { + return true + } + return false +} + +func Image(ref name.Reference, options ...Option) (v1.Image, error) { + o, err := makeOptions(options...) + if err != nil { + return nil, err + } + desc, err := Head(ref, options...) + if err != nil { + return nil, err + } + return o.path.Image(desc.Digest) +} + +// Pull returns a v1.Image of the remote image src. +func Pull(src string, options ...Option) (v1.Image, error) { + ref, err := name.ParseReference(src) + if err != nil { + return nil, fmt.Errorf("parsing reference %q: %w", src, err) + } + return Image(ref, options...) +} + +// Head returns a v1.Descriptor for the given reference +func Head(ref name.Reference, options ...Option) (*v1.Descriptor, error) { + o, err := makeOptions(options...) + if err != nil { + return nil, err + } + + idx, err := o.path.ImageIndex() + if err != nil { + return nil, err + } + im, err := idx.IndexManifest() + if err != nil { + return nil, err + } + for _, manifest := range im.Manifests { + if refEqDescriptor(ref, manifest) { + return &manifest, nil + } + } + return nil, errors.New("could not find the image in oci-layout") +} diff --git a/pkg/crane/local/write.go b/pkg/crane/local/write.go new file mode 100644 index 000000000..edf271dd0 --- /dev/null +++ b/pkg/crane/local/write.go @@ -0,0 +1,69 @@ +package local + +import ( + "bytes" + "io" + + "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/remote" + specsv1 "github.com/opencontainers/image-spec/specs-go/v1" +) + +func Write(ref name.Reference, img v1.Image, options ...Option) (rerr error) { + o, err := makeOptions(options...) + if err != nil { + return err + } + return o.path.AppendImage(img, layout.WithAnnotations(map[string]string{ + specsv1.AnnotationRefName: ref.String(), + })) +} + +func WriteLayer(layer v1.Layer, options ...Option) (rerr error) { + o, err := makeOptions(options...) + if err != nil { + return err + } + digest, err := layer.Digest() + if err != nil { + return err + } + rc, err := layer.Compressed() + if err != nil { + return err + } + return o.path.WriteBlob(digest, rc) +} + +func Put(ref name.Reference, t remote.Taggable, options ...Option) error { + o, err := makeOptions(options...) + if err != nil { + return err + } + rmf, err := t.RawManifest() + if err != nil { + return err + } + digest, _, err := v1.SHA256(bytes.NewReader(rmf)) + if err != nil { + return err + } + err = o.path.WriteBlob(digest, io.NopCloser(bytes.NewReader(rmf))) + if err != nil { + return err + } + mf, err := v1.ParseManifest(bytes.NewReader(rmf)) + if err != nil { + return err + } + return o.path.AppendDescriptor(v1.Descriptor{ + Digest: digest, + MediaType: mf.MediaType, + Size: int64(len(rmf)), + Annotations: map[string]string{ + specsv1.AnnotationRefName: ref.String(), + }, + }) +} diff --git a/pkg/crane/options.go b/pkg/crane/options.go index d9d441761..037318a7e 100644 --- a/pkg/crane/options.go +++ b/pkg/crane/options.go @@ -20,6 +20,7 @@ import ( "net/http" "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/crane/local" "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/remote" @@ -29,11 +30,13 @@ import ( type Options struct { Name []name.Option Remote []remote.Option + Local []local.Option Platform *v1.Platform Keychain authn.Keychain Transport http.RoundTripper auth authn.Authenticator + local bool insecure bool jobs int noclobber bool @@ -158,6 +161,14 @@ func WithContext(ctx context.Context) Option { } } +// WithLocalPath is a functional option for setting the context. +func WithLocalPath(path string) Option { + return func(o *Options) { + o.local = true + o.Local = []local.Option{local.WithPath(path)} + } +} + // WithJobs sets the number of concurrent jobs to run. // // The default number of jobs is GOMAXPROCS. diff --git a/pkg/crane/pull.go b/pkg/crane/pull.go index 7e6e5b7b6..a209a6b92 100644 --- a/pkg/crane/pull.go +++ b/pkg/crane/pull.go @@ -18,6 +18,7 @@ import ( "fmt" "os" + "github.com/google/go-containerregistry/pkg/crane/local" legacy "github.com/google/go-containerregistry/pkg/legacy/tarball" "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" @@ -39,7 +40,9 @@ func Pull(src string, opt ...Option) (v1.Image, error) { if err != nil { return nil, fmt.Errorf("parsing reference %q: %w", src, err) } - + if o.local { + return local.Image(ref, o.Local...) + } return remote.Image(ref, o.Remote...) } diff --git a/pkg/crane/push.go b/pkg/crane/push.go index 90a058502..13bd1c95d 100644 --- a/pkg/crane/push.go +++ b/pkg/crane/push.go @@ -17,6 +17,7 @@ package crane import ( "fmt" + "github.com/google/go-containerregistry/pkg/crane/local" "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/remote" @@ -50,9 +51,21 @@ func Push(img v1.Image, dst string, opt ...Option) error { if err != nil { return fmt.Errorf("parsing reference %q: %w", dst, err) } + if o.local { + return local.Write(tag, img, o.Local...) + } return remote.Write(tag, img, o.Remote...) } +// Put puts the remote.Taggable to a registry as dst. +func Put(t remote.Taggable, ref name.Reference, opt ...Option) error { + o := makeOptions(opt...) + if o.local { + return local.Put(ref, t, o.Local...) + } + return remote.Put(ref, t, o.Remote...) +} + // Upload pushes the v1.Layer to a given repo. func Upload(layer v1.Layer, repo string, opt ...Option) error { o := makeOptions(opt...) @@ -60,6 +73,8 @@ func Upload(layer v1.Layer, repo string, opt ...Option) error { if err != nil { return fmt.Errorf("parsing repo %q: %w", repo, err) } - + if o.local { + return local.WriteLayer(layer, o.Local...) + } return remote.WriteLayer(ref, layer, o.Remote...) }