working commit
This commit is contained in:
+129
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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...)
|
||||
}
|
||||
Reference in New Issue
Block a user