remote-tag enable copying across repositories and registries

Signed-off-by: Avi Deitcher <avi@deitcher.net>
This commit is contained in:
Avi Deitcher 2024-03-06 13:17:12 +02:00
parent 0d89422386
commit 51696d2905
30 changed files with 1661 additions and 358 deletions

View File

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/crane"
namepkg "github.com/google/go-containerregistry/pkg/name" namepkg "github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/v1/remote" "github.com/google/go-containerregistry/pkg/v1/remote"
"github.com/linuxkit/linuxkit/src/cmd/linuxkit/util" "github.com/linuxkit/linuxkit/src/cmd/linuxkit/util"
@ -19,9 +20,13 @@ func pkgRemoteTagCmd() *cobra.Command {
Long: `Tag a package in a remote registry with another tag, without downloading or pulling. Long: `Tag a package in a remote registry with another tag, without downloading or pulling.
Will simply tag using the identical descriptor. Will simply tag using the identical descriptor.
First argument is "from" tag, second is "to" tag. First argument is "from" tag, second is "to" tag.
If the "to" and "from" repositories are the same, then it is a simple tag operation.
If they are not, then the "from" image is pulled and pushed to the "to" repository.
`, `,
Args: cobra.ExactArgs(2), Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
var finalErr error
from := args[0] from := args[0]
to := args[1] to := args[1]
remoteOptions := []remote.Option{remote.WithAuthFromKeychain(authn.DefaultKeychain)} remoteOptions := []remote.Option{remote.WithAuthFromKeychain(authn.DefaultKeychain)}
@ -48,15 +53,19 @@ func pkgRemoteTagCmd() *cobra.Command {
} }
log.Infof("image %s already exists in the registry, but is different from %s, overwriting", toFullname, fromFullname) log.Infof("image %s already exists in the registry, but is different from %s, overwriting", toFullname, fromFullname)
} }
toTag, err := namepkg.NewTag(toFullname) // see if they are from the same sources
if err != nil { if fromRef.Context().String() == toRef.Context().String() {
return err toTag, err := namepkg.NewTag(toFullname)
} if err != nil {
if err := remote.Tag(toTag, fromDesc, remoteOptions...); err != nil { return err
return fmt.Errorf("error tagging image %s as %s: %v", fromFullname, toFullname, err) }
finalErr = remote.Tag(toTag, fromDesc, remoteOptions...)
} else {
// different, so need to copy
finalErr = crane.Copy(fromFullname, toFullname)
} }
return nil return finalErr
}, },
} }
cmd.Flags().StringVar(&release, "release", "", "Release the given version") cmd.Flags().StringVar(&release, "release", "", "Release the given version")

View File

@ -1,191 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
Copyright 2016 Antonio Murdaca <runcom@redhat.com>
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.

View File

@ -1,48 +0,0 @@
package util
import (
"fmt"
"strings"
"github.com/docker/distribution/reference"
)
const (
// DefaultHostname is the default built-in registry (DockerHub)
DefaultHostname = "docker.io"
// LegacyDefaultHostname is the old hostname used for DockerHub
LegacyDefaultHostname = "index.docker.io"
// DefaultRepoPrefix is the prefix used for official images in DockerHub
DefaultRepoPrefix = "library/"
)
func ParseName(name string) (reference.Named, error) {
distref, err := reference.ParseNormalizedNamed(name)
if err != nil {
return nil, err
}
hostname, remoteName := splitHostname(distref.String())
if hostname == "" {
return nil, fmt.Errorf("Please use a fully qualified repository name")
}
return reference.ParseNormalizedNamed(fmt.Sprintf("%s/%s", hostname, remoteName))
}
// splitHostname splits a repository name to hostname and remotename string.
// If no valid hostname is found, the default hostname is used. Repository name
// needs to be already validated before.
func splitHostname(name string) (hostname, remoteName string) {
i := strings.IndexRune(name, '/')
if i == -1 || (!strings.ContainsAny(name[:i], ".:") && name[:i] != "localhost") {
hostname, remoteName = DefaultHostname, name
} else {
hostname, remoteName = name[:i], name[i+1:]
}
if hostname == LegacyDefaultHostname {
hostname = DefaultHostname
}
if hostname == DefaultHostname && !strings.ContainsRune(remoteName, '/') {
remoteName = DefaultRepoPrefix + remoteName
}
return
}

View File

@ -1,42 +0,0 @@
package util
import "fmt"
//go:generate go run osgen.go
var (
armVariants = map[string]bool{
"v5": true,
"v6": true,
"v7": true,
}
)
// IsValidOSArch checks against the generated list of os/arch combinations
// from Go as well as checking for valid variants for ARM (the only architecture that uses variants)
func IsValidOSArch(os string, arch string, variant string) bool {
osarch := fmt.Sprintf("%s/%s", os, arch)
if _, ok := validOS[os]; !ok {
return false
}
if _, ok := validArch[arch]; !ok {
return false
}
if variant == "" {
return true
}
// only arm/arm64 can use variant
switch osarch {
case "linux/arm":
_, ok := armVariants[variant]
return ok
case "linux/arm64":
if variant == "v8" {
return true
}
default:
return false
}
return false
}

View File

@ -1,36 +0,0 @@
// Code generated by go generate; DO NOT EDIT.
// This file was generated by robots at
// 2021-02-09 13:11:28.537236306 -0500 EST m=+0.034330659
// using data from 'go tool dist list'
package util
var validOS = map[string]bool{
"darwin": true,
"dragonfly": true,
"illumos": true,
"js": true,
"netbsd": true,
"plan9": true,
"aix": true,
"android": true,
"windows": true,
"openbsd": true,
"solaris": true,
"freebsd": true,
"linux": true,
}
var validArch = map[string]bool{
"wasm": true,
"mips": true,
"mips64le": true,
"mipsle": true,
"ppc64le": true,
"amd64": true,
"arm64": true,
"arm": true,
"mips64": true,
"riscv64": true,
"s390x": true,
"ppc64": true,
"386": true,
}

View File

@ -1,33 +0,0 @@
package util
import (
"crypto/tls"
"net/http"
"github.com/containerd/containerd/remotes"
"github.com/containerd/containerd/remotes/docker"
)
func NewResolver(username, password string, insecure, plainHTTP bool, configs ...string) remotes.Resolver {
opts := docker.ResolverOptions{
PlainHTTP: plainHTTP,
}
client := http.DefaultClient
if insecure {
client.Transport = &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
}
opts.Client = client
if username != "" || password != "" {
opts.Credentials = func(hostName string) (string, string, error) {
return username, password, nil
}
return docker.NewResolver(opts)
}
return docker.NewResolver(opts)
}

View File

@ -0,0 +1,57 @@
// 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 legacy provides methods for interacting with legacy image formats.
package legacy
import (
"bytes"
"encoding/json"
"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/v1/remote"
)
// CopySchema1 allows `[g]crane cp` to work with old images without adding
// full support for schema 1 images to this package.
func CopySchema1(desc *remote.Descriptor, srcRef, dstRef name.Reference, opts ...remote.Option) error {
m := schema1{}
if err := json.NewDecoder(bytes.NewReader(desc.Manifest)).Decode(&m); err != nil {
return err
}
for _, layer := range m.FSLayers {
src := srcRef.Context().Digest(layer.BlobSum)
dst := dstRef.Context().Digest(layer.BlobSum)
blob, err := remote.Layer(src, opts...)
if err != nil {
return err
}
if err := remote.WriteLayer(dst.Context(), blob, opts...); err != nil {
return err
}
}
return remote.Put(dstRef, desc, opts...)
}
type fslayer struct {
BlobSum string `json:"blobSum"`
}
type schema1 struct {
FSLayers []fslayer `json:"fsLayers"`
}

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 (
"fmt"
"os"
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"
)
// Append reads a layer from path and appends it the the v1.Image base.
func Append(base v1.Image, paths ...string) (v1.Image, error) {
layers := make([]v1.Layer, 0, len(paths))
for _, path := range paths {
layer, err := getLayer(path)
if err != nil {
return nil, fmt.Errorf("reading layer %q: %w", path, err)
}
layers = append(layers, layer)
}
return mutate.AppendLayers(base, layers...)
}
func getLayer(path string) (v1.Layer, error) {
f, err := streamFile(path)
if err != nil {
return nil, err
}
if f != nil {
return stream.NewLayer(f), nil
}
return tarball.LayerFromFile(path)
}
// 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
}

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...)
}

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()
}

View File

@ -0,0 +1,88 @@
// 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/internal/legacy"
"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/types"
)
// 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)
}
logs.Progress.Printf("Copying from %v to %v", srcRef, dstRef)
desc, err := remote.Get(srcRef, o.Remote...)
if err != nil {
return fmt.Errorf("fetching %q: %w", src, err)
}
switch desc.MediaType {
case types.OCIImageIndex, types.DockerManifestList:
// Handle indexes separately.
if o.Platform != nil {
// If platform is explicitly set, don't copy the whole index, just the appropriate image.
if err := copyImage(desc, dstRef, o); err != nil {
return fmt.Errorf("failed to copy image: %w", err)
}
} else {
if err := copyIndex(desc, dstRef, o); err != nil {
return fmt.Errorf("failed to copy index: %w", err)
}
}
case types.DockerManifestSchema1, types.DockerManifestSchema1Signed:
// Handle schema 1 images separately.
if err := legacy.CopySchema1(desc, srcRef, dstRef, o.Remote...); err != nil {
return fmt.Errorf("failed to copy schema 1 image: %w", err)
}
default:
// Assume anything else is an image, since some registries don't set mediaTypes properly.
if err := copyImage(desc, dstRef, o); err != nil {
return fmt.Errorf("failed to copy image: %w", err)
}
}
return nil
}
func copyImage(desc *remote.Descriptor, dstRef name.Reference, o Options) error {
img, err := desc.Image()
if err != nil {
return err
}
return remote.Write(dstRef, img, o.Remote...)
}
func copyIndex(desc *remote.Descriptor, dstRef name.Reference, o Options) error {
idx, err := desc.ImageIndex()
if err != nil {
return err
}
return remote.WriteIndex(dstRef, idx, o.Remote...)
}

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...)
}

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
}

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

View File

@ -0,0 +1,29 @@
// 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.
func Export(img v1.Image, w io.Writer) error {
fs := mutate.Extract(img)
_, err := io.Copy(w, fs)
return err
}

View File

@ -0,0 +1,67 @@
// 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"
"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 := []string{}
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 tarball.LayerFromReader(b)
}
// 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)
}

View File

@ -0,0 +1,56 @@
// 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...)
}
// 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...)
}

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...)
}

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
}

View File

@ -0,0 +1,237 @@
// Copyright 2020 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"
"github.com/containerd/stargz-snapshotter/estargz"
"github.com/google/go-containerregistry/pkg/logs"
"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/mutate"
"github.com/google/go-containerregistry/pkg/v1/remote"
"github.com/google/go-containerregistry/pkg/v1/tarball"
"github.com/google/go-containerregistry/pkg/v1/types"
)
// Optimize optimizes a remote image or index from src to dst.
// THIS API IS EXPERIMENTAL AND SUBJECT TO CHANGE WITHOUT WARNING.
func Optimize(src, dst string, prioritize []string, opt ...Option) error {
pset := newStringSet(prioritize)
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)
}
logs.Progress.Printf("Optimizing from %v to %v", srcRef, dstRef)
desc, err := remote.Get(srcRef, o.Remote...)
if err != nil {
return fmt.Errorf("fetching %q: %w", src, err)
}
switch desc.MediaType {
case types.OCIImageIndex, types.DockerManifestList:
// Handle indexes separately.
if o.Platform != nil {
// If platform is explicitly set, don't optimize the whole index, just the appropriate image.
if err := optimizeAndPushImage(desc, dstRef, pset, o); err != nil {
return fmt.Errorf("failed to optimize image: %w", err)
}
} else {
if err := optimizeAndPushIndex(desc, dstRef, pset, o); err != nil {
return fmt.Errorf("failed to optimize index: %w", err)
}
}
case types.DockerManifestSchema1, types.DockerManifestSchema1Signed:
return errors.New("docker schema 1 images are not supported")
default:
// Assume anything else is an image, since some registries don't set mediaTypes properly.
if err := optimizeAndPushImage(desc, dstRef, pset, o); err != nil {
return fmt.Errorf("failed to optimize image: %w", err)
}
}
return nil
}
func optimizeAndPushImage(desc *remote.Descriptor, dstRef name.Reference, prioritize stringSet, o Options) error {
img, err := desc.Image()
if err != nil {
return err
}
missing, oimg, err := optimizeImage(img, prioritize)
if err != nil {
return err
}
if len(missing) > 0 {
return fmt.Errorf("the following prioritized files were missing from image: %v", missing.List())
}
return remote.Write(dstRef, oimg, o.Remote...)
}
func optimizeImage(img v1.Image, prioritize stringSet) (stringSet, v1.Image, error) {
cfg, err := img.ConfigFile()
if err != nil {
return nil, nil, err
}
ocfg := cfg.DeepCopy()
ocfg.History = nil
ocfg.RootFS.DiffIDs = nil
oimg, err := mutate.ConfigFile(empty.Image, ocfg)
if err != nil {
return nil, nil, err
}
layers, err := img.Layers()
if err != nil {
return nil, nil, err
}
missingFromImage := newStringSet(prioritize.List())
olayers := make([]mutate.Addendum, 0, len(layers))
for _, layer := range layers {
missingFromLayer := []string{}
olayer, err := tarball.LayerFromOpener(layer.Uncompressed,
tarball.WithEstargz,
tarball.WithEstargzOptions(
estargz.WithPrioritizedFiles(prioritize.List()),
estargz.WithAllowPrioritizeNotFound(&missingFromLayer),
))
if err != nil {
return nil, nil, err
}
missingFromImage = missingFromImage.Intersection(newStringSet(missingFromLayer))
olayers = append(olayers, mutate.Addendum{
Layer: olayer,
MediaType: types.DockerLayer,
})
}
oimg, err = mutate.Append(oimg, olayers...)
if err != nil {
return nil, nil, err
}
return missingFromImage, oimg, nil
}
func optimizeAndPushIndex(desc *remote.Descriptor, dstRef name.Reference, prioritize stringSet, o Options) error {
idx, err := desc.ImageIndex()
if err != nil {
return err
}
missing, oidx, err := optimizeIndex(idx, prioritize)
if err != nil {
return err
}
if len(missing) > 0 {
return fmt.Errorf("the following prioritized files were missing from all images: %v", missing.List())
}
return remote.WriteIndex(dstRef, oidx, o.Remote...)
}
func optimizeIndex(idx v1.ImageIndex, prioritize stringSet) (stringSet, v1.ImageIndex, error) {
im, err := idx.IndexManifest()
if err != nil {
return nil, nil, err
}
missingFromIndex := newStringSet(prioritize.List())
// Build an image for each child from the base and append it to a new index to produce the result.
adds := make([]mutate.IndexAddendum, 0, len(im.Manifests))
for _, desc := range im.Manifests {
img, err := idx.Image(desc.Digest)
if err != nil {
return nil, nil, err
}
missingFromImage, oimg, err := optimizeImage(img, prioritize)
if err != nil {
return nil, nil, err
}
missingFromIndex = missingFromIndex.Intersection(missingFromImage)
adds = append(adds, mutate.IndexAddendum{
Add: oimg,
Descriptor: v1.Descriptor{
URLs: desc.URLs,
MediaType: desc.MediaType,
Annotations: desc.Annotations,
Platform: desc.Platform,
},
})
}
idxType, err := idx.MediaType()
if err != nil {
return nil, nil, err
}
return missingFromIndex, mutate.IndexMediaType(mutate.AppendManifests(empty.Index, adds...), idxType), nil
}
type stringSet map[string]struct{}
func newStringSet(in []string) stringSet {
ss := stringSet{}
for _, s := range in {
ss[s] = struct{}{}
}
return ss
}
func (s stringSet) List() []string {
result := make([]string, 0, len(s))
for k := range s {
result = append(result, k)
}
return result
}
func (s stringSet) Intersection(rhs stringSet) stringSet {
// To appease ST1016
lhs := s
// Make sure len(lhs) >= len(rhs)
if len(lhs) < len(rhs) {
return rhs.Intersection(lhs)
}
result := stringSet{}
for k := range lhs {
if _, ok := rhs[k]; ok {
result[k] = struct{}{}
}
}
return result
}

View File

@ -0,0 +1,116 @@
// 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"
"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
}
// 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),
},
}
for _, o := range opts {
o(&opt)
}
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.
func WithTransport(t http.RoundTripper) Option {
return func(o *Options) {
o.Remote = append(o.Remote, remote.WithTransport(t))
}
}
// Insecure is an Option that allows image references to be fetched without TLS.
func Insecure(o *Options) {
o.Name = append(o.Name, name.Insecure)
}
// 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)
}
}
// 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)
}
}
// 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))
}
}
// WithContext is a functional option for setting the context.
func WithContext(ctx context.Context) Option {
return func(o *Options) {
o.Remote = append(o.Remote, remote.WithContext(ctx))
}
}

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.Repository.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
}

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, "")
}
// 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...)
}

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...)
}

View File

@ -0,0 +1,33 @@
// 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 legacy
import (
v1 "github.com/google/go-containerregistry/pkg/v1"
)
// LayerConfigFile is the configuration file that holds the metadata describing
// a v1 layer. See:
// https://github.com/moby/moby/blob/master/image/spec/v1.md
type LayerConfigFile struct {
v1.ConfigFile
ContainerConfig v1.Config `json:"container_config,omitempty"`
ID string `json:"id,omitempty"`
Parent string `json:"parent,omitempty"`
Throwaway bool `json:"throwaway,omitempty"`
Comment string `json:"comment,omitempty"`
}

View File

@ -0,0 +1,18 @@
// 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 legacy provides functionality to work with docker images in the v1
// format.
// See: https://github.com/moby/moby/blob/master/image/spec/v1.md
package legacy

View File

@ -0,0 +1,6 @@
# `legacy/tarball`
[![GoDoc](https://godoc.org/github.com/google/go-containerregistry/pkg/legacy/tarball?status.svg)](https://godoc.org/github.com/google/go-containerregistry/pkg/legacy/tarball)
This package implements support for writing legacy tarballs, as described
[here](https://github.com/moby/moby/blob/749d90e10f989802638ae542daf54257f3bf71f2/image/spec/v1.2.md#combined-image-json--filesystem-changeset-format).

View File

@ -0,0 +1,18 @@
// 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 tarball provides facilities for writing v1 docker images
// (https://github.com/moby/moby/blob/master/image/spec/v1.md) from/to a tarball
// on-disk.
package tarball

View File

@ -0,0 +1,373 @@
// 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 tarball
import (
"archive/tar"
"bytes"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"sort"
"strings"
"github.com/google/go-containerregistry/pkg/legacy"
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/partial"
"github.com/google/go-containerregistry/pkg/v1/tarball"
)
// repositoriesTarDescriptor represents the repositories file inside a `docker save` tarball.
type repositoriesTarDescriptor map[string]map[string]string
// v1Layer represents a layer with metadata needed by the v1 image spec https://github.com/moby/moby/blob/master/image/spec/v1.md.
type v1Layer struct {
// config is the layer metadata.
config *legacy.LayerConfigFile
// layer is the v1.Layer object this v1Layer represents.
layer v1.Layer
}
// json returns the raw bytes of the json metadata of the given v1Layer.
func (l *v1Layer) json() ([]byte, error) {
return json.Marshal(l.config)
}
// version returns the raw bytes of the "VERSION" file of the given v1Layer.
func (l *v1Layer) version() []byte {
return []byte("1.0")
}
// v1LayerID computes the v1 image format layer id for the given v1.Layer with the given v1 parent ID and raw image config.
func v1LayerID(layer v1.Layer, parentID string, rawConfig []byte) (string, error) {
d, err := layer.Digest()
if err != nil {
return "", fmt.Errorf("unable to get layer digest to generate v1 layer ID: %w", err)
}
s := fmt.Sprintf("%s %s", d.Hex, parentID)
if len(rawConfig) != 0 {
s = fmt.Sprintf("%s %s", s, string(rawConfig))
}
rawDigest := sha256.Sum256([]byte(s))
return hex.EncodeToString(rawDigest[:]), nil
}
// newTopV1Layer creates a new v1Layer for a layer other than the top layer in a v1 image tarball.
func newV1Layer(layer v1.Layer, parent *v1Layer, history v1.History) (*v1Layer, error) {
parentID := ""
if parent != nil {
parentID = parent.config.ID
}
id, err := v1LayerID(layer, parentID, nil)
if err != nil {
return nil, fmt.Errorf("unable to generate v1 layer ID: %w", err)
}
result := &v1Layer{
layer: layer,
config: &legacy.LayerConfigFile{
ConfigFile: v1.ConfigFile{
Created: history.Created,
Author: history.Author,
},
ContainerConfig: v1.Config{
Cmd: []string{history.CreatedBy},
},
ID: id,
Parent: parentID,
Throwaway: history.EmptyLayer,
Comment: history.Comment,
},
}
return result, nil
}
// newTopV1Layer creates a new v1Layer for the top layer in a v1 image tarball.
func newTopV1Layer(layer v1.Layer, parent *v1Layer, history v1.History, imgConfig *v1.ConfigFile, rawConfig []byte) (*v1Layer, error) {
result, err := newV1Layer(layer, parent, history)
if err != nil {
return nil, err
}
id, err := v1LayerID(layer, result.config.Parent, rawConfig)
if err != nil {
return nil, fmt.Errorf("unable to generate v1 layer ID for top layer: %w", err)
}
result.config.ID = id
result.config.Architecture = imgConfig.Architecture
result.config.Container = imgConfig.Container
result.config.DockerVersion = imgConfig.DockerVersion
result.config.OS = imgConfig.OS
result.config.Config = imgConfig.Config
result.config.Created = imgConfig.Created
return result, nil
}
// splitTag splits the given tagged image name <registry>/<repository>:<tag>
// into <registry>/<repository> and <tag>.
func splitTag(name string) (string, string) {
// Split on ":"
parts := strings.Split(name, ":")
// Verify that we aren't confusing a tag for a hostname w/ port for the purposes of weak validation.
if len(parts) > 1 && !strings.Contains(parts[len(parts)-1], "/") {
base := strings.Join(parts[:len(parts)-1], ":")
tag := parts[len(parts)-1]
return base, tag
}
return name, ""
}
// addTags adds the given image tags to the given "repositories" file descriptor in a v1 image tarball.
func addTags(repos repositoriesTarDescriptor, tags []string, topLayerID string) {
for _, t := range tags {
base, tag := splitTag(t)
tagToID, ok := repos[base]
if !ok {
tagToID = make(map[string]string)
repos[base] = tagToID
}
tagToID[tag] = topLayerID
}
}
// updateLayerSources updates the given layer digest to descriptor map with the descriptor of the given layer in the given image if it's an undistributable layer.
func updateLayerSources(layerSources map[v1.Hash]v1.Descriptor, layer v1.Layer, img v1.Image) error {
d, err := layer.Digest()
if err != nil {
return err
}
// Add to LayerSources if it's a foreign layer.
desc, err := partial.BlobDescriptor(img, d)
if err != nil {
return err
}
if !desc.MediaType.IsDistributable() {
diffid, err := partial.BlobToDiffID(img, d)
if err != nil {
return err
}
layerSources[diffid] = *desc
}
return nil
}
// Write is a wrapper to write a single image in V1 format and tag to a tarball.
func Write(ref name.Reference, img v1.Image, w io.Writer) error {
return MultiWrite(map[name.Reference]v1.Image{ref: img}, w)
}
// filterEmpty filters out the history corresponding to empty layers from the
// given history.
func filterEmpty(h []v1.History) []v1.History {
result := []v1.History{}
for _, i := range h {
if i.EmptyLayer {
continue
}
result = append(result, i)
}
return result
}
// MultiWrite writes the contents of each image to the provided reader, in the V1 image tarball format.
// The contents are written in the following format:
// One manifest.json file at the top level containing information about several images.
// One repositories file mapping from the image <registry>/<repo name> to <tag> to the id of the top most layer.
// For every layer, a directory named with the layer ID is created with the following contents:
// layer.tar - The uncompressed layer tarball.
// <layer id>.json- Layer metadata json.
// VERSION- Schema version string. Always set to "1.0".
// One file for the config blob, named after its SHA.
func MultiWrite(refToImage map[name.Reference]v1.Image, w io.Writer) error {
tf := tar.NewWriter(w)
defer tf.Close()
sortedImages, imageToTags := dedupRefToImage(refToImage)
var m tarball.Manifest
repos := make(repositoriesTarDescriptor)
seenLayerIDs := make(map[string]struct{})
for _, img := range sortedImages {
tags := imageToTags[img]
// Write the config.
cfgName, err := img.ConfigName()
if err != nil {
return err
}
cfgFileName := fmt.Sprintf("%s.json", cfgName.Hex)
cfgBlob, err := img.RawConfigFile()
if err != nil {
return err
}
if err := writeTarEntry(tf, cfgFileName, bytes.NewReader(cfgBlob), int64(len(cfgBlob))); err != nil {
return err
}
cfg, err := img.ConfigFile()
if err != nil {
return err
}
// Store foreign layer info.
layerSources := make(map[v1.Hash]v1.Descriptor)
// Write the layers.
layers, err := img.Layers()
if err != nil {
return err
}
history := filterEmpty(cfg.History)
// Create a blank config history if the config didn't have a history.
if len(history) == 0 && len(layers) != 0 {
history = make([]v1.History, len(layers))
} else if len(layers) != len(history) {
return fmt.Errorf("image config had layer history which did not match the number of layers, got len(history)=%d, len(layers)=%d, want len(history)=len(layers)", len(history), len(layers))
}
layerFiles := make([]string, len(layers))
var prev *v1Layer
for i, l := range layers {
if err := updateLayerSources(layerSources, l, img); err != nil {
return fmt.Errorf("unable to update image metadata to include undistributable layer source information: %w", err)
}
var cur *v1Layer
if i < (len(layers) - 1) {
cur, err = newV1Layer(l, prev, history[i])
} else {
cur, err = newTopV1Layer(l, prev, history[i], cfg, cfgBlob)
}
if err != nil {
return err
}
layerFiles[i] = fmt.Sprintf("%s/layer.tar", cur.config.ID)
if _, ok := seenLayerIDs[cur.config.ID]; ok {
prev = cur
continue
}
seenLayerIDs[cur.config.ID] = struct{}{}
// If the v1.Layer implements UncompressedSize efficiently, use that
// for the tar header. Otherwise, this iterates over Uncompressed().
// NOTE: If using a streaming layer, this may consume the layer.
size, err := partial.UncompressedSize(l)
if err != nil {
return err
}
u, err := l.Uncompressed()
if err != nil {
return err
}
defer u.Close()
if err := writeTarEntry(tf, layerFiles[i], u, size); err != nil {
return err
}
j, err := cur.json()
if err != nil {
return err
}
if err := writeTarEntry(tf, fmt.Sprintf("%s/json", cur.config.ID), bytes.NewReader(j), int64(len(j))); err != nil {
return err
}
v := cur.version()
if err := writeTarEntry(tf, fmt.Sprintf("%s/VERSION", cur.config.ID), bytes.NewReader(v), int64(len(v))); err != nil {
return err
}
prev = cur
}
// Generate the tar descriptor and write it.
m = append(m, tarball.Descriptor{
Config: cfgFileName,
RepoTags: tags,
Layers: layerFiles,
LayerSources: layerSources,
})
// prev should be the top layer here. Use it to add the image tags
// to the tarball repositories file.
addTags(repos, tags, prev.config.ID)
}
mBytes, err := json.Marshal(m)
if err != nil {
return err
}
if err := writeTarEntry(tf, "manifest.json", bytes.NewReader(mBytes), int64(len(mBytes))); err != nil {
return err
}
reposBytes, err := json.Marshal(&repos)
if err != nil {
return err
}
if err := writeTarEntry(tf, "repositories", bytes.NewReader(reposBytes), int64(len(reposBytes))); err != nil {
return err
}
return nil
}
func dedupRefToImage(refToImage map[name.Reference]v1.Image) ([]v1.Image, map[v1.Image][]string) {
imageToTags := make(map[v1.Image][]string)
for ref, img := range refToImage {
if tag, ok := ref.(name.Tag); ok {
if tags, ok := imageToTags[img]; ok && tags != nil {
imageToTags[img] = append(tags, tag.String())
} else {
imageToTags[img] = []string{tag.String()}
}
} else {
if _, ok := imageToTags[img]; !ok {
imageToTags[img] = nil
}
}
}
// Force specific order on tags
imgs := []v1.Image{}
for img, tags := range imageToTags {
sort.Strings(tags)
imgs = append(imgs, img)
}
sort.Slice(imgs, func(i, j int) bool {
cfI, err := imgs[i].ConfigName()
if err != nil {
return false
}
cfJ, err := imgs[j].ConfigName()
if err != nil {
return false
}
return cfI.Hex < cfJ.Hex
})
return imgs, imageToTags
}
// Writes a file to the provided writer with a corresponding tar header
func writeTarEntry(tf *tar.Writer, path string, r io.Reader, size int64) error {
hdr := &tar.Header{
Mode: 0644,
Typeflag: tar.TypeReg,
Size: size,
Name: path,
}
if err := tf.WriteHeader(hdr); err != nil {
return err
}
_, err := io.Copy(tf, r)
return err
}

View File

@ -261,7 +261,6 @@ github.com/docker/go-connections/tlsconfig
github.com/docker/go-units github.com/docker/go-units
# github.com/estesp/manifest-tool/v2 v2.0.7-0.20230216152337-24a86fc0b513 # github.com/estesp/manifest-tool/v2 v2.0.7-0.20230216152337-24a86fc0b513
## explicit; go 1.19 ## explicit; go 1.19
github.com/estesp/manifest-tool/v2/pkg/util
# github.com/felixge/httpsnoop v1.0.2 # github.com/felixge/httpsnoop v1.0.2
## explicit; go 1.13 ## explicit; go 1.13
github.com/felixge/httpsnoop github.com/felixge/httpsnoop
@ -314,11 +313,15 @@ github.com/google/go-cmp/cmp/internal/value
github.com/google/go-containerregistry/internal/and github.com/google/go-containerregistry/internal/and
github.com/google/go-containerregistry/internal/estargz github.com/google/go-containerregistry/internal/estargz
github.com/google/go-containerregistry/internal/gzip github.com/google/go-containerregistry/internal/gzip
github.com/google/go-containerregistry/internal/legacy
github.com/google/go-containerregistry/internal/redact github.com/google/go-containerregistry/internal/redact
github.com/google/go-containerregistry/internal/retry github.com/google/go-containerregistry/internal/retry
github.com/google/go-containerregistry/internal/retry/wait github.com/google/go-containerregistry/internal/retry/wait
github.com/google/go-containerregistry/internal/verify github.com/google/go-containerregistry/internal/verify
github.com/google/go-containerregistry/pkg/authn github.com/google/go-containerregistry/pkg/authn
github.com/google/go-containerregistry/pkg/crane
github.com/google/go-containerregistry/pkg/legacy
github.com/google/go-containerregistry/pkg/legacy/tarball
github.com/google/go-containerregistry/pkg/logs github.com/google/go-containerregistry/pkg/logs
github.com/google/go-containerregistry/pkg/name github.com/google/go-containerregistry/pkg/name
github.com/google/go-containerregistry/pkg/v1 github.com/google/go-containerregistry/pkg/v1