Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: mutate on oci-layout #1869

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion cmd/crane/cmd/append.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,5 @@ container image.`,

appendCmd.MarkFlagsMutuallyExclusive("oci-empty-base", "base")
appendCmd.MarkFlagRequired("new_tag")
appendCmd.MarkFlagRequired("new_layer")
return appendCmd
}
8 changes: 7 additions & 1 deletion cmd/crane/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ func New(use, short string, options []crane.Option) *cobra.Command {
insecure := false
ndlayers := false
platform := &platformValue{}
local := ""

wt := &warnTransport{}

Expand All @@ -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 {
Expand Down Expand Up @@ -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")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels like a big change, since it affects all commands. Would it be worth having some e2e tests that set up a local layout and crane pull --local=<that>, crane mutate, etc.?

root.PersistentFlags().MarkHidden("local")
return root
}

Expand Down
9 changes: 2 additions & 7 deletions internal/cmd/edit.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
12 changes: 12 additions & 0 deletions pkg/crane/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions pkg/crane/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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...)
}
43 changes: 43 additions & 0 deletions pkg/crane/local/options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package local

Check failure on line 1 in pkg/crane/local/options.go

View workflow job for this annotation

GitHub Actions / Go headers

[Go headers] pkg/crane/local/options.go#L1

missing boilerplate:
Raw output
pkg/crane/local/options.go:1: missing boilerplate:
// Copyright 2024 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.

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
}
}
61 changes: 61 additions & 0 deletions pkg/crane/local/read.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package local

Check failure on line 1 in pkg/crane/local/read.go

View workflow job for this annotation

GitHub Actions / Go headers

[Go headers] pkg/crane/local/read.go#L1

missing boilerplate:
Raw output
pkg/crane/local/read.go:1: missing boilerplate:
// Copyright 2024 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.

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 {

Check failure on line 12 in pkg/crane/local/read.go

View workflow job for this annotation

GitHub Actions / Lint

`refEqDescriptor` - `ref` is unused (unparam)
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")
}
69 changes: 69 additions & 0 deletions pkg/crane/local/write.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package local

Check failure on line 1 in pkg/crane/local/write.go

View workflow job for this annotation

GitHub Actions / Go headers

[Go headers] pkg/crane/local/write.go#L1

missing boilerplate:
Raw output
pkg/crane/local/write.go:1: missing boilerplate:
// Copyright 2024 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.

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(),
},
})
}
11 changes: 11 additions & 0 deletions pkg/crane/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand Down
5 changes: 4 additions & 1 deletion pkg/crane/pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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...)
}

Expand Down
17 changes: 16 additions & 1 deletion pkg/crane/push.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -50,16 +51,30 @@ 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...)
ref, err := name.NewRepository(repo, o.Name...)
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...)
}
Loading