working commit

This commit is contained in:
2026-02-21 13:16:56 +02:00
parent d650d58a6d
commit 7be3cf8de7
1136 changed files with 722443 additions and 0 deletions
+129
View File
@@ -0,0 +1,129 @@
// Copyright 2018 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 (
"fmt"
"os"
comp "github.com/google/go-containerregistry/internal/compression"
"github.com/google/go-containerregistry/internal/windows"
"github.com/google/go-containerregistry/pkg/compression"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/mutate"
"github.com/google/go-containerregistry/pkg/v1/stream"
"github.com/google/go-containerregistry/pkg/v1/tarball"
"github.com/google/go-containerregistry/pkg/v1/types"
)
func isWindows(img v1.Image) (bool, error) {
cfg, err := img.ConfigFile()
if err != nil {
return false, err
}
return cfg != nil && cfg.OS == "windows", nil
}
// Append reads a layer from path and appends it the the v1.Image base.
//
// If the base image is a Windows base image (i.e., its config.OS is
// "windows"), the contents of the tarballs will be modified to be suitable for
// a Windows container image.`,
func Append(base v1.Image, paths ...string) (v1.Image, error) {
if base == nil {
return nil, fmt.Errorf("invalid argument: base")
}
win, err := isWindows(base)
if err != nil {
return nil, fmt.Errorf("getting base image: %w", err)
}
baseMediaType, err := base.MediaType()
if err != nil {
return nil, fmt.Errorf("getting base image media type: %w", err)
}
layerType := types.DockerLayer
if baseMediaType == types.OCIManifestSchema1 {
layerType = types.OCILayer
}
layers := make([]v1.Layer, 0, len(paths))
for _, path := range paths {
layer, err := getLayer(path, layerType)
if err != nil {
return nil, fmt.Errorf("reading layer %q: %w", path, err)
}
if win {
layer, err = windows.Windows(layer)
if err != nil {
return nil, fmt.Errorf("converting %q for Windows: %w", path, err)
}
}
layers = append(layers, layer)
}
return mutate.AppendLayers(base, layers...)
}
func getLayer(path string, layerType types.MediaType) (v1.Layer, error) {
f, err := streamFile(path)
if err != nil {
return nil, err
}
if f != nil {
return stream.NewLayer(f, stream.WithMediaType(layerType)), nil
}
// This is dumb but the tarball package assumes things about mediaTypes that aren't true
// and doesn't have enough context to know what the right default is.
f, err = os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
z, _, err := comp.PeekCompression(f)
if err != nil {
return nil, err
}
if z == compression.ZStd {
layerType = types.OCILayerZStd
}
return tarball.LayerFromFile(path, tarball.WithMediaType(layerType))
}
// If we're dealing with a named pipe, trying to open it multiple times will
// fail, so we need to do a streaming upload.
//
// returns nil, nil for non-streaming files
func streamFile(path string) (*os.File, error) {
if path == "-" {
return os.Stdin, nil
}
fi, err := os.Stat(path)
if err != nil {
return nil, err
}
if !fi.Mode().IsRegular() {
return os.Open(path)
}
return nil, nil
}
+35
View File
@@ -0,0 +1,35 @@
// Copyright 2019 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 (
"context"
"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/v1/remote"
)
// Catalog returns the repositories in a registry's catalog.
func Catalog(src string, opt ...Option) (res []string, err error) {
o := makeOptions(opt...)
reg, err := name.NewRegistry(src, o.Name...)
if err != nil {
return nil, err
}
// This context gets overridden by remote.WithContext, which is set by
// crane.WithContext.
return remote.Catalog(context.Background(), reg, o.Remote...)
}
+24
View File
@@ -0,0 +1,24 @@
// Copyright 2018 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
// Config returns the config file for the remote image ref.
func Config(ref string, opt ...Option) ([]byte, error) {
i, _, err := getImage(ref, opt...)
if err != nil {
return nil, err
}
return i.RawConfigFile()
}
+185
View File
@@ -0,0 +1,185 @@
// Copyright 2018 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"
"net/http"
"github.com/google/go-containerregistry/pkg/logs"
"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/v1/remote"
"github.com/google/go-containerregistry/pkg/v1/remote/transport"
"golang.org/x/sync/errgroup"
)
// ErrRefusingToClobberExistingTag is returned when NoClobber is true and the
// tag already exists in the target registry/repo.
var ErrRefusingToClobberExistingTag = errors.New("refusing to clobber existing tag")
// Copy copies a remote image or index from src to dst.
func Copy(src, dst string, opt ...Option) error {
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)
}
puller, err := remote.NewPuller(o.Remote...)
if err != nil {
return err
}
if tag, ok := dstRef.(name.Tag); ok {
if o.noclobber {
logs.Progress.Printf("Checking existing tag %v", tag)
head, err := puller.Head(o.ctx, tag)
var terr *transport.Error
if errors.As(err, &terr) {
if terr.StatusCode != http.StatusNotFound && terr.StatusCode != http.StatusForbidden {
return err
}
} else if err != nil {
return err
}
if head != nil {
return fmt.Errorf("%w %s@%s", ErrRefusingToClobberExistingTag, tag, head.Digest)
}
}
}
pusher, err := remote.NewPusher(o.Remote...)
if err != nil {
return err
}
logs.Progress.Printf("Copying from %v to %v", srcRef, dstRef)
desc, err := puller.Get(o.ctx, srcRef)
if err != nil {
return fmt.Errorf("fetching %q: %w", src, err)
}
if o.Platform == nil {
return pusher.Push(o.ctx, dstRef, desc)
}
// If platform is explicitly set, don't copy the whole index, just the appropriate image.
img, err := desc.Image()
if err != nil {
return err
}
return pusher.Push(o.ctx, dstRef, img)
}
// CopyRepository copies every tag from src to dst.
func CopyRepository(src, dst string, opt ...Option) error {
o := makeOptions(opt...)
srcRepo, err := name.NewRepository(src, o.Name...)
if err != nil {
return err
}
dstRepo, err := name.NewRepository(dst, o.Name...)
if err != nil {
return fmt.Errorf("parsing reference for %q: %w", dst, err)
}
puller, err := remote.NewPuller(o.Remote...)
if err != nil {
return err
}
ignoredTags := map[string]struct{}{}
if o.noclobber {
// TODO: It would be good to propagate noclobber down into remote so we can use Etags.
have, err := puller.List(o.ctx, dstRepo)
if err != nil {
var terr *transport.Error
if errors.As(err, &terr) {
// Some registries create repository on first push, so listing tags will fail.
// If we see 404 or 403, assume we failed because the repository hasn't been created yet.
if terr.StatusCode != http.StatusNotFound && terr.StatusCode != http.StatusForbidden {
return err
}
} else {
return err
}
}
for _, tag := range have {
ignoredTags[tag] = struct{}{}
}
}
pusher, err := remote.NewPusher(o.Remote...)
if err != nil {
return err
}
lister, err := puller.Lister(o.ctx, srcRepo)
if err != nil {
return err
}
g, ctx := errgroup.WithContext(o.ctx)
g.SetLimit(o.jobs)
for lister.HasNext() {
tags, err := lister.Next(ctx)
if err != nil {
return err
}
for _, tag := range tags.Tags {
tag := tag
if o.noclobber {
if _, ok := ignoredTags[tag]; ok {
logs.Progress.Printf("Skipping %s due to no-clobber", tag)
continue
}
}
g.Go(func() error {
srcTag, err := name.ParseReference(src+":"+tag, o.Name...)
if err != nil {
return fmt.Errorf("failed to parse tag: %w", err)
}
dstTag, err := name.ParseReference(dst+":"+tag, o.Name...)
if err != nil {
return fmt.Errorf("failed to parse tag: %w", err)
}
logs.Progress.Printf("Fetching %s", srcTag)
desc, err := puller.Get(ctx, srcTag)
if err != nil {
return err
}
logs.Progress.Printf("Pushing %s", dstTag)
return pusher.Push(ctx, dstTag, desc)
})
}
}
return g.Wait()
}
+33
View File
@@ -0,0 +1,33 @@
// Copyright 2018 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 (
"fmt"
"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/v1/remote"
)
// Delete deletes the remote reference at src.
func Delete(src string, opt ...Option) error {
o := makeOptions(opt...)
ref, err := name.ParseReference(src, o.Name...)
if err != nil {
return fmt.Errorf("parsing reference %q: %w", src, err)
}
return remote.Delete(ref, o.Remote...)
}
+52
View File
@@ -0,0 +1,52 @@
// Copyright 2018 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 "github.com/google/go-containerregistry/pkg/logs"
// Digest returns the sha256 hash of the remote image at ref.
func Digest(ref string, opt ...Option) (string, error) {
o := makeOptions(opt...)
if o.Platform != nil {
desc, err := getManifest(ref, opt...)
if err != nil {
return "", err
}
if !desc.MediaType.IsIndex() {
return desc.Digest.String(), nil
}
// TODO: does not work for indexes which contain schema v1 manifests
img, err := desc.Image()
if err != nil {
return "", err
}
digest, err := img.Digest()
if err != nil {
return "", err
}
return digest.String(), nil
}
desc, err := Head(ref, opt...)
if err != nil {
logs.Warn.Printf("HEAD request failed, falling back on GET: %v", err)
rdesc, err := getManifest(ref, opt...)
if err != nil {
return "", err
}
return rdesc.Digest.String(), nil
}
return desc.Digest.String(), nil
}
+16
View File
@@ -0,0 +1,16 @@
// Copyright 2019 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 holds libraries used to implement the crane CLI.
package crane
+54
View File
@@ -0,0 +1,54 @@
// Copyright 2018 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 (
"io"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/mutate"
)
// Export writes the filesystem contents (as a tarball) of img to w.
// If img has a single layer, just write the (uncompressed) contents to w so
// that this "just works" for images that just wrap a single blob.
func Export(img v1.Image, w io.Writer) error {
layers, err := img.Layers()
if err != nil {
return err
}
if len(layers) == 1 {
// If it's a single layer...
l := layers[0]
mt, err := l.MediaType()
if err != nil {
return err
}
if !mt.IsLayer() {
// ...and isn't an OCI mediaType, we don't have to flatten it.
// This lets export work for single layer, non-tarball images.
rc, err := l.Uncompressed()
if err != nil {
return err
}
_, err = io.Copy(w, rc)
return err
}
}
fs := mutate.Extract(img)
_, err = io.Copy(w, fs)
return err
}
+72
View File
@@ -0,0 +1,72 @@
// Copyright 2018 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 (
"archive/tar"
"bytes"
"io"
"sort"
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/tarball"
)
// Layer creates a layer from a single file map. These layers are reproducible and consistent.
// A filemap is a path -> file content map representing a file system.
func Layer(filemap map[string][]byte) (v1.Layer, error) {
b := &bytes.Buffer{}
w := tar.NewWriter(b)
fn := make([]string, 0, len(filemap))
for f := range filemap {
fn = append(fn, f)
}
sort.Strings(fn)
for _, f := range fn {
c := filemap[f]
if err := w.WriteHeader(&tar.Header{
Name: f,
Size: int64(len(c)),
}); err != nil {
return nil, err
}
if _, err := w.Write(c); err != nil {
return nil, err
}
}
if err := w.Close(); err != nil {
return nil, err
}
// Return a new copy of the buffer each time it's opened.
return tarball.LayerFromOpener(func() (io.ReadCloser, error) {
return io.NopCloser(bytes.NewBuffer(b.Bytes())), nil
})
}
// Image creates a image with the given filemaps as its contents. These images are reproducible and consistent.
// A filemap is a path -> file content map representing a file system.
func Image(filemap map[string][]byte) (v1.Image, error) {
y, err := Layer(filemap)
if err != nil {
return nil, err
}
return mutate.AppendLayers(empty.Image, y)
}
+61
View File
@@ -0,0 +1,61 @@
// Copyright 2018 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 (
"fmt"
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/remote"
)
func getImage(r string, opt ...Option) (v1.Image, name.Reference, error) {
o := makeOptions(opt...)
ref, err := name.ParseReference(r, o.Name...)
if err != nil {
return nil, nil, fmt.Errorf("parsing reference %q: %w", r, err)
}
img, err := remote.Image(ref, o.Remote...)
if err != nil {
return nil, nil, fmt.Errorf("reading image %q: %w", ref, err)
}
return img, ref, nil
}
func getManifest(r string, opt ...Option) (*remote.Descriptor, error) {
o := makeOptions(opt...)
ref, err := name.ParseReference(r, o.Name...)
if err != nil {
return nil, fmt.Errorf("parsing reference %q: %w", r, err)
}
return remote.Get(ref, o.Remote...)
}
// Get calls remote.Get and returns an uninterpreted response.
func Get(r string, opt ...Option) (*remote.Descriptor, error) {
return getManifest(r, opt...)
}
// Head performs a HEAD request for a manifest and returns a content descriptor
// based on the registry's response.
func Head(r string, opt ...Option) (*v1.Descriptor, error) {
o := makeOptions(opt...)
ref, err := name.ParseReference(r, o.Name...)
if err != nil {
return nil, err
}
return remote.Head(ref, o.Remote...)
}
+33
View File
@@ -0,0 +1,33 @@
// Copyright 2018 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 (
"fmt"
"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/v1/remote"
)
// ListTags returns the tags in repository src.
func ListTags(src string, opt ...Option) ([]string, error) {
o := makeOptions(opt...)
repo, err := name.NewRepository(src, o.Name...)
if err != nil {
return nil, fmt.Errorf("parsing repo %q: %w", src, err)
}
return remote.List(repo, o.Remote...)
}
+32
View File
@@ -0,0 +1,32 @@
// Copyright 2018 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
// Manifest returns the manifest for the remote image or index ref.
func Manifest(ref string, opt ...Option) ([]byte, error) {
desc, err := getManifest(ref, opt...)
if err != nil {
return nil, err
}
o := makeOptions(opt...)
if o.Platform != nil {
img, err := desc.Image()
if err != nil {
return nil, err
}
return img.RawManifest()
}
return desc.Manifest, nil
}
+178
View File
@@ -0,0 +1,178 @@
// Copyright 2019 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 (
"context"
"crypto/tls"
"net/http"
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/remote"
)
// Options hold the options that crane uses when calling other packages.
type Options struct {
Name []name.Option
Remote []remote.Option
Platform *v1.Platform
Keychain authn.Keychain
Transport http.RoundTripper
auth authn.Authenticator
insecure bool
jobs int
noclobber bool
ctx context.Context
}
// GetOptions exposes the underlying []remote.Option, []name.Option, and
// platform, based on the passed Option. Generally, you shouldn't need to use
// this unless you've painted yourself into a dependency corner as we have
// with the crane and gcrane cli packages.
func GetOptions(opts ...Option) Options {
return makeOptions(opts...)
}
func makeOptions(opts ...Option) Options {
opt := Options{
Remote: []remote.Option{
remote.WithAuthFromKeychain(authn.DefaultKeychain),
},
Keychain: authn.DefaultKeychain,
jobs: 4,
ctx: context.Background(),
}
for _, o := range opts {
o(&opt)
}
// Allow for untrusted certificates if the user
// passed Insecure but no custom transport.
if opt.insecure && opt.Transport == nil {
transport := remote.DefaultTransport.(*http.Transport).Clone()
transport.TLSClientConfig = &tls.Config{
InsecureSkipVerify: true, //nolint: gosec
}
WithTransport(transport)(&opt)
} else if opt.Transport == nil {
opt.Transport = remote.DefaultTransport
}
return opt
}
// Option is a functional option for crane.
type Option func(*Options)
// WithTransport is a functional option for overriding the default transport
// for remote operations. Setting a transport will override the Insecure option's
// configuration allowing for image registries to use untrusted certificates.
func WithTransport(t http.RoundTripper) Option {
return func(o *Options) {
o.Remote = append(o.Remote, remote.WithTransport(t))
o.Transport = t
}
}
// Insecure is an Option that allows image references to be fetched without TLS.
// This will also allow for untrusted (e.g. self-signed) certificates in cases where
// the default transport is used (i.e. when WithTransport is not used).
func Insecure(o *Options) {
o.Name = append(o.Name, name.Insecure)
o.insecure = true
}
// WithPlatform is an Option to specify the platform.
func WithPlatform(platform *v1.Platform) Option {
return func(o *Options) {
if platform != nil {
o.Remote = append(o.Remote, remote.WithPlatform(*platform))
}
o.Platform = platform
}
}
// WithAuthFromKeychain is a functional option for overriding the default
// authenticator for remote operations, using an authn.Keychain to find
// credentials.
//
// By default, crane will use authn.DefaultKeychain.
func WithAuthFromKeychain(keys authn.Keychain) Option {
return func(o *Options) {
// Replace the default keychain at position 0.
o.Remote[0] = remote.WithAuthFromKeychain(keys)
o.Keychain = keys
}
}
// WithAuth is a functional option for overriding the default authenticator
// for remote operations.
//
// By default, crane will use authn.DefaultKeychain.
func WithAuth(auth authn.Authenticator) Option {
return func(o *Options) {
// Replace the default keychain at position 0.
o.Remote[0] = remote.WithAuth(auth)
o.auth = auth
}
}
// WithUserAgent adds the given string to the User-Agent header for any HTTP
// requests.
func WithUserAgent(ua string) Option {
return func(o *Options) {
o.Remote = append(o.Remote, remote.WithUserAgent(ua))
}
}
// WithNondistributable is an option that allows pushing non-distributable
// layers.
func WithNondistributable() Option {
return func(o *Options) {
o.Remote = append(o.Remote, remote.WithNondistributable)
}
}
// WithContext is a functional option for setting the context.
func WithContext(ctx context.Context) Option {
return func(o *Options) {
o.ctx = ctx
o.Remote = append(o.Remote, remote.WithContext(ctx))
}
}
// WithJobs sets the number of concurrent jobs to run.
//
// The default number of jobs is GOMAXPROCS.
func WithJobs(jobs int) Option {
return func(o *Options) {
if jobs > 0 {
o.jobs = jobs
}
o.Remote = append(o.Remote, remote.WithJobs(o.jobs))
}
}
// WithNoClobber modifies behavior to avoid overwriting existing tags, if possible.
func WithNoClobber(noclobber bool) Option {
return func(o *Options) {
o.noclobber = noclobber
}
}
+142
View File
@@ -0,0 +1,142 @@
// Copyright 2018 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 (
"fmt"
"os"
legacy "github.com/google/go-containerregistry/pkg/legacy/tarball"
"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/layout"
"github.com/google/go-containerregistry/pkg/v1/remote"
"github.com/google/go-containerregistry/pkg/v1/tarball"
)
// Tag applied to images that were pulled by digest. This denotes that the
// image was (probably) never tagged with this, but lets us avoid applying the
// ":latest" tag which might be misleading.
const iWasADigestTag = "i-was-a-digest"
// Pull returns a v1.Image of the remote image src.
func Pull(src string, opt ...Option) (v1.Image, error) {
o := makeOptions(opt...)
ref, err := name.ParseReference(src, o.Name...)
if err != nil {
return nil, fmt.Errorf("parsing reference %q: %w", src, err)
}
return remote.Image(ref, o.Remote...)
}
// Save writes the v1.Image img as a tarball at path with tag src.
func Save(img v1.Image, src, path string) error {
imgMap := map[string]v1.Image{src: img}
return MultiSave(imgMap, path)
}
// MultiSave writes collection of v1.Image img with tag as a tarball.
func MultiSave(imgMap map[string]v1.Image, path string, opt ...Option) error {
o := makeOptions(opt...)
tagToImage := map[name.Tag]v1.Image{}
for src, img := range imgMap {
ref, err := name.ParseReference(src, o.Name...)
if err != nil {
return fmt.Errorf("parsing ref %q: %w", src, err)
}
// WriteToFile wants a tag to write to the tarball, but we might have
// been given a digest.
// If the original ref was a tag, use that. Otherwise, if it was a
// digest, tag the image with :i-was-a-digest instead.
tag, ok := ref.(name.Tag)
if !ok {
d, ok := ref.(name.Digest)
if !ok {
return fmt.Errorf("ref wasn't a tag or digest")
}
tag = d.Tag(iWasADigestTag)
}
tagToImage[tag] = img
}
// no progress channel (for now)
return tarball.MultiWriteToFile(path, tagToImage)
}
// PullLayer returns the given layer from a registry.
func PullLayer(ref string, opt ...Option) (v1.Layer, error) {
o := makeOptions(opt...)
digest, err := name.NewDigest(ref, o.Name...)
if err != nil {
return nil, err
}
return remote.Layer(digest, o.Remote...)
}
// SaveLegacy writes the v1.Image img as a legacy tarball at path with tag src.
func SaveLegacy(img v1.Image, src, path string) error {
imgMap := map[string]v1.Image{src: img}
return MultiSave(imgMap, path)
}
// MultiSaveLegacy writes collection of v1.Image img with tag as a legacy tarball.
func MultiSaveLegacy(imgMap map[string]v1.Image, path string) error {
refToImage := map[name.Reference]v1.Image{}
for src, img := range imgMap {
ref, err := name.ParseReference(src)
if err != nil {
return fmt.Errorf("parsing ref %q: %w", src, err)
}
refToImage[ref] = img
}
w, err := os.Create(path)
if err != nil {
return err
}
defer w.Close()
return legacy.MultiWrite(refToImage, w)
}
// SaveOCI writes the v1.Image img as an OCI Image Layout at path. If a layout
// already exists at that path, it will add the image to the index.
func SaveOCI(img v1.Image, path string) error {
imgMap := map[string]v1.Image{"": img}
return MultiSaveOCI(imgMap, path)
}
// MultiSaveOCI writes collection of v1.Image img as an OCI Image Layout at path. If a layout
// already exists at that path, it will add the image to the index.
func MultiSaveOCI(imgMap map[string]v1.Image, path string) error {
p, err := layout.FromPath(path)
if err != nil {
p, err = layout.Write(path, empty.Index)
if err != nil {
return err
}
}
for _, img := range imgMap {
if err = p.AppendImage(img); err != nil {
return err
}
}
return nil
}
+65
View File
@@ -0,0 +1,65 @@
// Copyright 2018 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 (
"fmt"
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/remote"
"github.com/google/go-containerregistry/pkg/v1/tarball"
)
// Load reads the tarball at path as a v1.Image.
func Load(path string, opt ...Option) (v1.Image, error) {
return LoadTag(path, "", opt...)
}
// LoadTag reads a tag from the tarball at path as a v1.Image.
// If tag is "", will attempt to read the tarball as a single image.
func LoadTag(path, tag string, opt ...Option) (v1.Image, error) {
if tag == "" {
return tarball.ImageFromPath(path, nil)
}
o := makeOptions(opt...)
t, err := name.NewTag(tag, o.Name...)
if err != nil {
return nil, fmt.Errorf("parsing tag %q: %w", tag, err)
}
return tarball.ImageFromPath(path, &t)
}
// Push pushes the v1.Image img to a registry as dst.
func Push(img v1.Image, dst string, opt ...Option) error {
o := makeOptions(opt...)
tag, err := name.ParseReference(dst, o.Name...)
if err != nil {
return fmt.Errorf("parsing reference %q: %w", dst, err)
}
return remote.Write(tag, img, 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)
}
return remote.WriteLayer(ref, layer, o.Remote...)
}
+39
View File
@@ -0,0 +1,39 @@
// Copyright 2019 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 (
"fmt"
"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/v1/remote"
)
// Tag adds tag to the remote img.
func Tag(img, tag string, opt ...Option) error {
o := makeOptions(opt...)
ref, err := name.ParseReference(img, o.Name...)
if err != nil {
return fmt.Errorf("parsing reference %q: %w", img, err)
}
desc, err := remote.Get(ref, o.Remote...)
if err != nil {
return fmt.Errorf("fetching %q: %w", img, err)
}
dst := ref.Context().Tag(tag)
return remote.Tag(dst, desc, o.Remote...)
}