```
* use go 1.17 semantics in go.mod
* pin tonistiigi/fsutil dep to match docker/docker's dep
go get -u ./...
go mod vendor
```
This commit is contained in:
Jason Hall
2022-01-21 13:59:16 -05:00
committed by GitHub
parent a59970f4ac
commit 6bbd8a331c
3595 changed files with 429699 additions and 120494 deletions
+136 -67
View File
@@ -19,103 +19,172 @@ import (
"context"
"io"
"io/ioutil"
"sync"
"github.com/docker/docker/client"
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/tarball"
"github.com/google/go-containerregistry/pkg/v1/types"
)
// image accesses an image from a docker daemon
type image struct {
v1.Image
}
ref name.Reference
opener *imageOpener
tarballImage v1.Image
var _ v1.Image = (*image)(nil)
once sync.Once
err error
}
type imageOpener struct {
ref name.Reference
ref name.Reference
ctx context.Context
buffered bool
client Client
once sync.Once
bytes []byte
err error
}
// ImageOption is a functional option for Image.
type ImageOption func(*imageOpener) error
func (i *imageOpener) Open() (v1.Image, error) {
var opener tarball.Opener
var err error
if i.buffered {
opener, err = i.bufferedOpener(i.ref)
} else {
opener, err = i.unbufferedOpener(i.ref)
}
if err != nil {
return nil, err
}
tb, err := tarball.Image(opener, nil)
if err != nil {
return nil, err
}
img := &image{
Image: tb,
}
return img, nil
func (i *imageOpener) saveImage() (io.ReadCloser, error) {
return i.client.ImageSave(i.ctx, []string{i.ref.Name()})
}
func (i *imageOpener) saveImage(ref name.Reference) (io.ReadCloser, error) {
return i.client.ImageSave(context.Background(), []string{ref.Name()})
}
func (i *imageOpener) bufferedOpener(ref name.Reference) (tarball.Opener, error) {
func (i *imageOpener) bufferedOpener() (io.ReadCloser, error) {
// Store the tarball in memory and return a new reader into the bytes each time we need to access something.
rc, err := i.saveImage(ref)
if err != nil {
return nil, err
}
defer rc.Close()
i.once.Do(func() {
i.bytes, i.err = func() ([]byte, error) {
rc, err := i.saveImage()
if err != nil {
return nil, err
}
defer rc.Close()
imageBytes, err := ioutil.ReadAll(rc)
if err != nil {
return nil, err
}
// The tarball interface takes a function that it can call to return an opened reader-like object.
// Daemon comes from a set of bytes, so wrap them in a ReadCloser so it looks like an opened file.
return func() (io.ReadCloser, error) {
return ioutil.NopCloser(bytes.NewReader(imageBytes)), nil
}, nil
return ioutil.ReadAll(rc)
}()
})
// Wrap the bytes in a ReadCloser so it looks like an opened file.
return ioutil.NopCloser(bytes.NewReader(i.bytes)), i.err
}
func (i *imageOpener) unbufferedOpener(ref name.Reference) (tarball.Opener, error) {
func (i *imageOpener) opener() tarball.Opener {
if i.buffered {
return i.bufferedOpener
}
// To avoid storing the tarball in memory, do a save every time we need to access something.
return func() (io.ReadCloser, error) {
return i.saveImage(ref)
}, nil
return i.saveImage
}
// Image provides access to an image reference from the Docker daemon,
// applying functional options to the underlying imageOpener before
// resolving the reference into a v1.Image.
func Image(ref name.Reference, options ...ImageOption) (v1.Image, error) {
func Image(ref name.Reference, options ...Option) (v1.Image, error) {
o, err := makeOptions(options...)
if err != nil {
return nil, err
}
i := &imageOpener{
ref: ref,
buffered: true, // buffer by default
}
for _, option := range options {
if err := option(i); err != nil {
return nil, err
}
buffered: o.buffered,
client: o.client,
ctx: o.ctx,
}
if i.client == nil {
var err error
i.client, err = client.NewClientWithOpts(client.FromEnv)
if err != nil {
return nil, err
}
return &image{
ref: ref,
opener: i,
}, nil
}
func (i *image) initialize() error {
// Don't re-initialize tarball if already initialized.
if i.tarballImage == nil {
i.once.Do(func() {
i.tarballImage, i.err = tarball.Image(i.opener.opener(), nil)
})
}
return i.err
}
func (i *image) Layers() ([]v1.Layer, error) {
if err := i.initialize(); err != nil {
return nil, err
}
return i.tarballImage.Layers()
}
func (i *image) MediaType() (types.MediaType, error) {
if err := i.initialize(); err != nil {
return "", err
}
return i.tarballImage.MediaType()
}
func (i *image) Size() (int64, error) {
if err := i.initialize(); err != nil {
return 0, err
}
return i.tarballImage.Size()
}
func (i *image) ConfigName() (v1.Hash, error) {
res, _, err := i.opener.client.ImageInspectWithRaw(i.opener.ctx, i.ref.String())
if err != nil {
return v1.Hash{}, err
}
return v1.NewHash(res.ID)
}
func (i *image) ConfigFile() (*v1.ConfigFile, error) {
if err := i.initialize(); err != nil {
return nil, err
}
i.client.NegotiateAPIVersion(context.Background())
return i.tarballImage.ConfigFile()
}
return i.Open()
func (i *image) RawConfigFile() ([]byte, error) {
if err := i.initialize(); err != nil {
return nil, err
}
return i.tarballImage.RawConfigFile()
}
func (i *image) Digest() (v1.Hash, error) {
if err := i.initialize(); err != nil {
return v1.Hash{}, err
}
return i.tarballImage.Digest()
}
func (i *image) Manifest() (*v1.Manifest, error) {
if err := i.initialize(); err != nil {
return nil, err
}
return i.tarballImage.Manifest()
}
func (i *image) RawManifest() ([]byte, error) {
if err := i.initialize(); err != nil {
return nil, err
}
return i.tarballImage.RawManifest()
}
func (i *image) LayerByDigest(h v1.Hash) (v1.Layer, error) {
if err := i.initialize(); err != nil {
return nil, err
}
return i.tarballImage.LayerByDigest(h)
}
func (i *image) LayerByDiffID(h v1.Hash) (v1.Layer, error) {
if err := i.initialize(); err != nil {
return nil, err
}
return i.tarballImage.LayerByDiffID(h)
}
+58 -15
View File
@@ -19,34 +19,76 @@ import (
"io"
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
)
// ImageOption is an alias for Option.
// Deprecated: Use Option instead.
type ImageOption Option
// Option is a functional option for daemon operations.
type Option func(*options)
type options struct {
ctx context.Context
client Client
buffered bool
}
var defaultClient = func() (Client, error) {
return client.NewClientWithOpts(client.FromEnv)
}
func makeOptions(opts ...Option) (*options, error) {
o := &options{
buffered: true,
ctx: context.Background(),
}
for _, opt := range opts {
opt(o)
}
if o.client == nil {
client, err := defaultClient()
if err != nil {
return nil, err
}
o.client = client
}
o.client.NegotiateAPIVersion(o.ctx)
return o, nil
}
// WithBufferedOpener buffers the image.
func WithBufferedOpener() ImageOption {
return func(i *imageOpener) error {
return i.setBuffered(true)
func WithBufferedOpener() Option {
return func(o *options) {
o.buffered = true
}
}
// WithUnbufferedOpener streams the image to avoid buffering.
func WithUnbufferedOpener() ImageOption {
return func(i *imageOpener) error {
return i.setBuffered(false)
func WithUnbufferedOpener() Option {
return func(o *options) {
o.buffered = false
}
}
func (i *imageOpener) setBuffered(buffer bool) error {
i.buffered = buffer
return nil
}
// WithClient is a functional option to allow injecting a docker client.
//
// By default, github.com/docker/docker/client.FromEnv is used.
func WithClient(client Client) ImageOption {
return func(i *imageOpener) error {
i.client = client
return nil
func WithClient(client Client) Option {
return func(o *options) {
o.client = client
}
}
// WithContext is a functional option to pass through a context.Context.
//
// By default, context.Background() is used.
func WithContext(ctx context.Context) Option {
return func(o *options) {
o.ctx = ctx
}
}
@@ -57,4 +99,5 @@ type Client interface {
ImageSave(context.Context, []string) (io.ReadCloser, error)
ImageLoad(context.Context, io.Reader, bool) (types.ImageLoadResponse, error)
ImageTag(context.Context, string, string) error
ImageInspectWithRaw(context.Context, string) (types.ImageInspect, []byte, error)
}
+10 -29
View File
@@ -15,47 +15,28 @@
package daemon
import (
"context"
"fmt"
"io"
"io/ioutil"
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/tarball"
)
// ImageLoader is an interface for testing.
type ImageLoader interface {
ImageLoad(context.Context, io.Reader, bool) (types.ImageLoadResponse, error)
ImageTag(context.Context, string, string) error
}
// GetImageLoader is a variable so we can override in tests.
var GetImageLoader = func() (ImageLoader, error) {
cli, err := client.NewClientWithOpts(client.FromEnv)
if err != nil {
return nil, err
}
cli.NegotiateAPIVersion(context.Background())
return cli, nil
}
// Tag adds a tag to an already existent image.
func Tag(src, dest name.Tag) error {
cli, err := GetImageLoader()
func Tag(src, dest name.Tag, options ...Option) error {
o, err := makeOptions(options...)
if err != nil {
return err
}
return cli.ImageTag(context.Background(), src.String(), dest.String())
return o.client.ImageTag(o.ctx, src.String(), dest.String())
}
// Write saves the image into the daemon as the given tag.
func Write(tag name.Tag, img v1.Image) (string, error) {
cli, err := GetImageLoader()
func Write(tag name.Tag, img v1.Image, options ...Option) (string, error) {
o, err := makeOptions(options...)
if err != nil {
return "", err
}
@@ -66,15 +47,15 @@ func Write(tag name.Tag, img v1.Image) (string, error) {
}()
// write the image in docker save format first, then load it
resp, err := cli.ImageLoad(context.Background(), pr, false)
resp, err := o.client.ImageLoad(o.ctx, pr, false)
if err != nil {
return "", fmt.Errorf("error loading image: %v", err)
return "", fmt.Errorf("error loading image: %w", err)
}
defer resp.Body.Close()
b, readErr := ioutil.ReadAll(resp.Body)
b, err := ioutil.ReadAll(resp.Body)
response := string(b)
if readErr != nil {
return response, fmt.Errorf("error reading load response body: %v", err)
if err != nil {
return response, fmt.Errorf("error reading load response body: %w", err)
}
return response, nil
}
+7 -7
View File
@@ -70,7 +70,7 @@ func NewGcloudAuthenticator() (authn.Authenticator, error) {
return authn.Anonymous, nil
}
ts := gcloudSource{GetGcloudCmd()}
ts := gcloudSource{GetGcloudCmd}
// Attempt to fetch a token to ensure gcloud is installed and we can run it.
token, err := ts.Token()
@@ -95,7 +95,7 @@ func NewJSONKeyAuthenticator(serviceAccountJSON string) authn.Authenticator {
// tokens by using the Google SDK to produce JWT tokens from a Service Account.
// More information: https://godoc.org/golang.org/x/oauth2/google#JWTAccessTokenSourceFromJSON
func NewTokenAuthenticator(serviceAccountJSON string, scope string) (authn.Authenticator, error) {
ts, err := googauth.JWTAccessTokenSourceFromJSON([]byte(serviceAccountJSON), string(scope))
ts, err := googauth.JWTAccessTokenSourceFromJSON([]byte(serviceAccountJSON), scope)
if err != nil {
return nil, err
}
@@ -145,12 +145,12 @@ type gcloudOutput struct {
type gcloudSource struct {
// This is passed in so that we mock out gcloud and test Token.
cmd *exec.Cmd
exec func() *exec.Cmd
}
// Token implements oauath2.TokenSource.
func (gs gcloudSource) Token() (*oauth2.Token, error) {
cmd := gs.cmd
cmd := gs.exec()
var out bytes.Buffer
cmd.Stdout = &out
@@ -158,17 +158,17 @@ func (gs gcloudSource) Token() (*oauth2.Token, error) {
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return nil, fmt.Errorf("error executing `gcloud config config-helper`: %v", err)
return nil, fmt.Errorf("error executing `gcloud config config-helper`: %w", err)
}
creds := gcloudOutput{}
if err := json.Unmarshal(out.Bytes(), &creds); err != nil {
return nil, fmt.Errorf("failed to parse `gcloud config config-helper` output: %v", err)
return nil, fmt.Errorf("failed to parse `gcloud config config-helper` output: %w", err)
}
expiry, err := time.Parse(time.RFC3339, creds.Credential.TokenExpiry)
if err != nil {
return nil, fmt.Errorf("failed to parse gcloud token expiry: %v", err)
return nil, fmt.Errorf("failed to parse gcloud token expiry: %w", err)
}
token := oauth2.Token{
+2 -2
View File
@@ -55,7 +55,7 @@ type googleKeychain struct {
func (gk *googleKeychain) Resolve(target authn.Resource) (authn.Authenticator, error) {
// Only authenticate GCR and AR so it works with authn.NewMultiKeychain to fallback.
host := target.RegistryStr()
if host != "gcr.io" && !strings.HasSuffix(host, ".gcr.io") && !strings.HasSuffix(host, ".pkg.dev") {
if host != "gcr.io" && !strings.HasSuffix(host, ".gcr.io") && !strings.HasSuffix(host, ".pkg.dev") && !strings.HasSuffix(host, ".google.com") {
return authn.Anonymous, nil
}
@@ -77,5 +77,5 @@ func resolve() (authn.Authenticator, error) {
return auth, nil
}
return nil, fmt.Errorf("failed to create token source from env: %v or gcloud: %v", envErr, gErr)
return nil, fmt.Errorf("failed to create token source from env: %v or gcloud: %v", envErr, gErr) //nolint: errorlint
}
+110 -38
View File
@@ -21,6 +21,7 @@ import (
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/google/go-containerregistry/pkg/authn"
@@ -29,9 +30,9 @@ import (
"github.com/google/go-containerregistry/pkg/v1/remote/transport"
)
// ListerOption is a functional option for List and Walk.
// Option is a functional option for List and Walk.
// TODO: Can we somehow reuse the remote options here?
type ListerOption func(*lister) error
type Option func(*lister) error
type lister struct {
auth authn.Authenticator
@@ -42,7 +43,7 @@ type lister struct {
userAgent string
}
func newLister(repo name.Repository, options ...ListerOption) (*lister, error) {
func newLister(repo name.Repository, options ...Option) (*lister, error) {
l := &lister{
auth: authn.Anonymous,
transport: http.DefaultTransport,
@@ -56,19 +57,23 @@ func newLister(repo name.Repository, options ...ListerOption) (*lister, error) {
}
}
// Wrap the transport in something that logs requests and responses.
// It's expensive to generate the dumps, so skip it if we're writing
// to nothing.
if logs.Enabled(logs.Debug) {
l.transport = transport.NewLogger(l.transport)
}
// transport.Wrapper is a signal that consumers are opt-ing into providing their own transport without any additional wrapping.
// This is to allow consumers full control over the transports logic, such as providing retry logic.
if _, ok := l.transport.(*transport.Wrapper); !ok {
// Wrap the transport in something that logs requests and responses.
// It's expensive to generate the dumps, so skip it if we're writing
// to nothing.
if logs.Enabled(logs.Debug) {
l.transport = transport.NewLogger(l.transport)
}
// Wrap the transport in something that can retry network flakes.
l.transport = transport.NewRetry(l.transport)
// Wrap the transport in something that can retry network flakes.
l.transport = transport.NewRetry(l.transport)
// Wrap this last to prevent transport.New from double-wrapping.
if l.userAgent != "" {
l.transport = transport.NewUserAgent(l.transport, l.userAgent)
// Wrap this last to prevent transport.New from double-wrapping.
if l.userAgent != "" {
l.transport = transport.NewUserAgent(l.transport, l.userAgent)
}
}
scopes := []string{repo.Scope(transport.PullScope)}
@@ -83,34 +88,101 @@ func newLister(repo name.Repository, options ...ListerOption) (*lister, error) {
}
func (l *lister) list(repo name.Repository) (*Tags, error) {
uri := url.URL{
uri := &url.URL{
Scheme: repo.Registry.Scheme(),
Host: repo.Registry.RegistryStr(),
Path: fmt.Sprintf("/v2/%s/tags/list", repo.RepositoryStr()),
}
req, err := http.NewRequestWithContext(l.ctx, http.MethodGet, uri.String(), nil)
if err != nil {
return nil, err
}
resp, err := l.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if err := transport.CheckError(resp, http.StatusOK); err != nil {
return nil, err
// ECR returns an error if n > 1000:
// https://github.com/google/go-containerregistry/issues/681
RawQuery: "n=1000",
}
tags := Tags{}
if err := json.NewDecoder(resp.Body).Decode(&tags); err != nil {
return nil, err
// get responses until there is no next page
for {
select {
case <-l.ctx.Done():
return nil, l.ctx.Err()
default:
}
req, err := http.NewRequest("GET", uri.String(), nil)
if err != nil {
return nil, err
}
req = req.WithContext(l.ctx)
resp, err := l.client.Do(req)
if err != nil {
return nil, err
}
if err := transport.CheckError(resp, http.StatusOK); err != nil {
return nil, err
}
parsed := Tags{}
if err := json.NewDecoder(resp.Body).Decode(&parsed); err != nil {
return nil, err
}
if err := resp.Body.Close(); err != nil {
return nil, err
}
if len(parsed.Manifests) != 0 || len(parsed.Children) != 0 {
// We're dealing with GCR, just return directly.
return &parsed, nil
}
// This isn't GCR, just append the tags and keep paginating.
tags.Tags = append(tags.Tags, parsed.Tags...)
uri, err = getNextPageURL(resp)
if err != nil {
return nil, err
}
// no next page
if uri == nil {
break
}
logs.Warn.Printf("saw non-google tag listing response, falling back to pagination")
}
return &tags, nil
}
// getNextPageURL checks if there is a Link header in a http.Response which
// contains a link to the next page. If yes it returns the url.URL of the next
// page otherwise it returns nil.
func getNextPageURL(resp *http.Response) (*url.URL, error) {
link := resp.Header.Get("Link")
if link == "" {
return nil, nil
}
if link[0] != '<' {
return nil, fmt.Errorf("failed to parse link header: missing '<' in: %s", link)
}
end := strings.Index(link, ">")
if end == -1 {
return nil, fmt.Errorf("failed to parse link header: missing '>' in: %s", link)
}
link = link[1:end]
linkURL, err := url.Parse(link)
if err != nil {
return nil, err
}
if resp.Request == nil || resp.Request.URL == nil {
return nil, nil
}
linkURL = resp.Request.URL.ResolveReference(linkURL)
return linkURL, nil
}
type rawManifestInfo struct {
Size string `json:"imageSizeBytes"`
MediaType string `json:"mediaType"`
@@ -157,7 +229,7 @@ func (m *ManifestInfo) UnmarshalJSON(data []byte) error {
}
if raw.Size != "" {
size, err := strconv.ParseUint(string(raw.Size), 10, 64)
size, err := strconv.ParseUint(raw.Size, 10, 64)
if err != nil {
return err
}
@@ -165,7 +237,7 @@ func (m *ManifestInfo) UnmarshalJSON(data []byte) error {
}
if raw.Created != "" {
created, err := strconv.ParseInt(string(raw.Created), 10, 64)
created, err := strconv.ParseInt(raw.Created, 10, 64)
if err != nil {
return err
}
@@ -173,7 +245,7 @@ func (m *ManifestInfo) UnmarshalJSON(data []byte) error {
}
if raw.Uploaded != "" {
uploaded, err := strconv.ParseInt(string(raw.Uploaded), 10, 64)
uploaded, err := strconv.ParseInt(raw.Uploaded, 10, 64)
if err != nil {
return err
}
@@ -195,7 +267,7 @@ type Tags struct {
}
// List calls /tags/list for the given repository.
func List(repo name.Repository, options ...ListerOption) (*Tags, error) {
func List(repo name.Repository, options ...Option) (*Tags, error) {
l, err := newLister(repo, options...)
if err != nil {
return nil, err
@@ -215,7 +287,7 @@ func List(repo name.Repository, options ...ListerOption) (*Tags, error) {
// TODO: Do we want a SkipDir error, as in filepath.WalkFunc?
type WalkFunc func(repo name.Repository, tags *Tags, err error) error
func walk(repo name.Repository, tags *Tags, walkFn WalkFunc, options ...ListerOption) error {
func walk(repo name.Repository, tags *Tags, walkFn WalkFunc, options ...Option) error {
if tags == nil {
// This shouldn't happen.
return fmt.Errorf("tags nil for %q", repo)
@@ -229,7 +301,7 @@ func walk(repo name.Repository, tags *Tags, walkFn WalkFunc, options ...ListerOp
child, err := name.NewRepository(fmt.Sprintf("%s/%s", repo, path), name.StrictValidation)
if err != nil {
// We don't expect this ever, so don't pass it through to walkFn.
return fmt.Errorf("unexpected path failure: %v", err)
return fmt.Errorf("unexpected path failure: %w", err)
}
childTags, err := List(child, options...)
@@ -249,7 +321,7 @@ func walk(repo name.Repository, tags *Tags, walkFn WalkFunc, options ...ListerOp
}
// Walk recursively descends repositories, calling walkFn.
func Walk(root name.Repository, walkFn WalkFunc, options ...ListerOption) error {
func Walk(root name.Repository, walkFn WalkFunc, options ...Option) error {
tags, err := List(root, options...)
if err != nil {
return walkFn(root, nil, err)
+5 -9
View File
@@ -19,12 +19,11 @@ import (
"net/http"
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/logs"
)
// WithTransport is a functional option for overriding the default transport
// on a remote image
func WithTransport(t http.RoundTripper) ListerOption {
func WithTransport(t http.RoundTripper) Option {
return func(l *lister) error {
l.transport = t
return nil
@@ -33,7 +32,7 @@ func WithTransport(t http.RoundTripper) ListerOption {
// WithAuth is a functional option for overriding the default authenticator
// on a remote image
func WithAuth(auth authn.Authenticator) ListerOption {
func WithAuth(auth authn.Authenticator) Option {
return func(l *lister) error {
l.auth = auth
return nil
@@ -42,15 +41,12 @@ func WithAuth(auth authn.Authenticator) ListerOption {
// WithAuthFromKeychain is a functional option for overriding the default
// authenticator on a remote image using an authn.Keychain
func WithAuthFromKeychain(keys authn.Keychain) ListerOption {
func WithAuthFromKeychain(keys authn.Keychain) Option {
return func(l *lister) error {
auth, err := keys.Resolve(l.repo.Registry)
if err != nil {
return err
}
if auth == authn.Anonymous {
logs.Warn.Printf("No matching credentials were found for %q, falling back on anonymous", l.repo.Registry)
}
l.auth = auth
return nil
}
@@ -58,7 +54,7 @@ func WithAuthFromKeychain(keys authn.Keychain) ListerOption {
// WithContext is a functional option for overriding the default
// context.Context for HTTP request to list remote images
func WithContext(ctx context.Context) ListerOption {
func WithContext(ctx context.Context) Option {
return func(l *lister) error {
l.ctx = ctx
return nil
@@ -69,7 +65,7 @@ func WithContext(ctx context.Context) ListerOption {
// requests. This header will also include "go-containerregistry/${version}".
//
// If you want to completely overwrite the User-Agent header, use WithTransport.
func WithUserAgent(ua string) ListerOption {
func WithUserAgent(ua string) Option {
return func(l *lister) error {
l.userAgent = ua
return nil
+1 -1
View File
@@ -87,7 +87,7 @@ func Hasher(name string) (hash.Hash, error) {
func (h *Hash) parse(unquoted string) error {
parts := strings.Split(unquoted, ":")
if len(parts) != 2 {
return fmt.Errorf("too many parts in hash: %s", unquoted)
return fmt.Errorf("cannot parse hash: %q", unquoted)
}
rest := strings.TrimLeft(parts[1], "0123456789abcdef")
@@ -1,47 +0,0 @@
// 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 and
import (
"io"
)
// ReadCloser implements io.ReadCloser by reading from a particular io.Reader
// and then calling the provided "Close()" method.
type ReadCloser struct {
io.Reader
CloseFunc func() error
}
var _ io.ReadCloser = (*ReadCloser)(nil)
// Close implements io.ReadCloser
func (rac *ReadCloser) Close() error {
return rac.CloseFunc()
}
// WriteCloser implements io.WriteCloser by reading from a particular io.Writer
// and then calling the provided "Close()" method.
type WriteCloser struct {
io.Writer
CloseFunc func() error
}
var _ io.WriteCloser = (*WriteCloser)(nil)
// Close implements io.WriteCloser
func (wac *WriteCloser) Close() error {
return wac.CloseFunc()
}
@@ -1,54 +0,0 @@
// 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 estargz
import (
"bytes"
"io"
"io/ioutil"
"github.com/containerd/stargz-snapshotter/estargz"
v1 "github.com/google/go-containerregistry/pkg/v1"
)
// Assert that what we're returning is an io.ReadCloser
var _ io.ReadCloser = (*estargz.Blob)(nil)
// ReadCloser reads uncompressed tarball input from the io.ReadCloser and
// returns:
// * An io.ReadCloser from which compressed data may be read, and
// * A v1.Hash with the hash of the estargz table of contents, or
// * An error if the estargz processing encountered a problem.
//
// Refer to estargz for the options:
// https://pkg.go.dev/github.com/containerd/stargz-snapshotter@v0.2.0/estargz#Option
func ReadCloser(r io.ReadCloser, opts ...estargz.Option) (*estargz.Blob, v1.Hash, error) {
defer r.Close()
// TODO(#876): Avoid buffering into memory.
bs, err := ioutil.ReadAll(r)
if err != nil {
return nil, v1.Hash{}, err
}
br := bytes.NewReader(bs)
rc, err := estargz.Build(io.NewSectionReader(br, 0, int64(len(bs))), opts...)
if err != nil {
return nil, v1.Hash{}, err
}
h, err := v1.NewHash(rc.TOCDigest().String())
return rc, h, err
}
@@ -1,116 +0,0 @@
// 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 gzip
import (
"bufio"
"bytes"
"compress/gzip"
"io"
"github.com/google/go-containerregistry/pkg/v1/internal/and"
)
var gzipMagicHeader = []byte{'\x1f', '\x8b'}
// ReadCloser reads uncompressed input data from the io.ReadCloser and
// returns an io.ReadCloser from which compressed data may be read.
// This uses gzip.BestSpeed for the compression level.
func ReadCloser(r io.ReadCloser) io.ReadCloser {
return ReadCloserLevel(r, gzip.BestSpeed)
}
// ReadCloserLevel reads uncompressed input data from the io.ReadCloser and
// returns an io.ReadCloser from which compressed data may be read.
// Refer to compress/gzip for the level:
// https://golang.org/pkg/compress/gzip/#pkg-constants
func ReadCloserLevel(r io.ReadCloser, level int) io.ReadCloser {
pr, pw := io.Pipe()
// For highly compressible layers, gzip.Writer will output a very small
// number of bytes per Write(). This is normally fine, but when pushing
// to a registry, we want to ensure that we're taking full advantage of
// the available bandwidth instead of sending tons of tiny writes over
// the wire.
// 64K ought to be small enough for anybody.
bw := bufio.NewWriterSize(pw, 2<<16)
// Returns err so we can pw.CloseWithError(err)
go func() error {
// TODO(go1.14): Just defer {pw,gw,r}.Close like you'd expect.
// Context: https://golang.org/issue/24283
gw, err := gzip.NewWriterLevel(bw, level)
if err != nil {
return pw.CloseWithError(err)
}
if _, err := io.Copy(gw, r); err != nil {
defer r.Close()
defer gw.Close()
return pw.CloseWithError(err)
}
// Close gzip writer to Flush it and write gzip trailers.
if err := gw.Close(); err != nil {
return pw.CloseWithError(err)
}
// Flush bufio writer to ensure we write out everything.
if err := bw.Flush(); err != nil {
return pw.CloseWithError(err)
}
// We dont' really care if these fail.
defer pw.Close()
defer r.Close()
return nil
}()
return pr
}
// UnzipReadCloser reads compressed input data from the io.ReadCloser and
// returns an io.ReadCloser from which uncompessed data may be read.
func UnzipReadCloser(r io.ReadCloser) (io.ReadCloser, error) {
gr, err := gzip.NewReader(r)
if err != nil {
return nil, err
}
return &and.ReadCloser{
Reader: gr,
CloseFunc: func() error {
// If the unzip fails, then this seems to return the same
// error as the read. We don't want this to interfere with
// us closing the main ReadCloser, since this could leave
// an open file descriptor (fails on Windows).
gr.Close()
return r.Close()
},
}, nil
}
// Is detects whether the input stream is compressed.
func Is(r io.Reader) (bool, error) {
magicHeader := make([]byte, 2)
n, err := r.Read(magicHeader)
if n == 0 && err == io.EOF {
return false, nil
}
if err != nil {
return false, err
}
return bytes.Equal(magicHeader, gzipMagicHeader), nil
}
@@ -1,62 +0,0 @@
// 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 verify
import (
"encoding/hex"
"fmt"
"hash"
"io"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/internal/and"
)
type verifyReader struct {
inner io.Reader
hasher hash.Hash
expected v1.Hash
}
// Read implements io.Reader
func (vc *verifyReader) Read(b []byte) (int, error) {
n, err := vc.inner.Read(b)
if err == io.EOF {
got := hex.EncodeToString(vc.hasher.Sum(make([]byte, 0, vc.hasher.Size())))
if want := vc.expected.Hex; got != want {
return n, fmt.Errorf("error verifying %s checksum; got %q, want %q",
vc.expected.Algorithm, got, want)
}
}
return n, err
}
// ReadCloser wraps the given io.ReadCloser to verify that its contents match
// the provided v1.Hash before io.EOF is returned.
func ReadCloser(r io.ReadCloser, h v1.Hash) (io.ReadCloser, error) {
w, err := v1.Hasher(h.Algorithm)
if err != nil {
return nil, err
}
r2 := io.TeeReader(r, w)
return &and.ReadCloser{
Reader: &verifyReader{
inner: r2,
hasher: w,
expected: h,
},
CloseFunc: r.Close,
}, nil
}
+21 -13
View File
@@ -17,6 +17,7 @@ package layout
import (
"fmt"
"io"
"os"
"sync"
v1 "github.com/google/go-containerregistry/pkg/v1"
@@ -84,25 +85,18 @@ func (li *layoutImage) LayerByDigest(h v1.Hash) (partial.CompressedLayer, error)
}
if h == manifest.Config.Digest {
return partial.CompressedLayer(&compressedBlob{
return &compressedBlob{
path: li.path,
desc: manifest.Config,
}), nil
}, nil
}
for _, desc := range manifest.Layers {
if h == desc.Digest {
switch desc.MediaType {
case types.OCILayer, types.DockerLayer:
return partial.CompressedToLayer(&compressedBlob{
path: li.path,
desc: desc,
})
default:
// TODO: We assume everything is a compressed blob, but that might not be true.
// TODO: Handle foreign layers.
return nil, fmt.Errorf("unexpected media type: %v for layer: %v", desc.MediaType, desc.Digest)
}
return &compressedBlob{
path: li.path,
desc: desc,
}, nil
}
}
@@ -129,3 +123,17 @@ func (b *compressedBlob) Size() (int64, error) {
func (b *compressedBlob) MediaType() (types.MediaType, error) {
return b.desc.MediaType, nil
}
// Descriptor implements partial.withDescriptor.
func (b *compressedBlob) Descriptor() (*v1.Descriptor, error) {
return &b.desc, nil
}
// See partial.Exists.
func (b *compressedBlob) Exists() (bool, error) {
_, err := os.Stat(b.path.blobPath(b.desc.Digest))
if os.IsNotExist(err) {
return false, nil
}
return err == nil, err
}
+37 -22
View File
@@ -3,40 +3,55 @@ package layout
import v1 "github.com/google/go-containerregistry/pkg/v1"
// Option is a functional option for Layout.
//
// TODO: We'll need to change this signature to support Sparse/Thin images.
// Or, alternatively, wrap it in a sparse.Image that returns an empty list for layers?
type Option func(*v1.Descriptor) error
type Option func(*options)
type options struct {
descOpts []descriptorOption
}
func makeOptions(opts ...Option) *options {
o := &options{
descOpts: []descriptorOption{},
}
for _, apply := range opts {
apply(o)
}
return o
}
type descriptorOption func(*v1.Descriptor)
// WithAnnotations adds annotations to the artifact descriptor.
func WithAnnotations(annotations map[string]string) Option {
return func(desc *v1.Descriptor) error {
if desc.Annotations == nil {
desc.Annotations = make(map[string]string)
}
for k, v := range annotations {
desc.Annotations[k] = v
}
return nil
return func(o *options) {
o.descOpts = append(o.descOpts, func(desc *v1.Descriptor) {
if desc.Annotations == nil {
desc.Annotations = make(map[string]string)
}
for k, v := range annotations {
desc.Annotations[k] = v
}
})
}
}
// WithURLs adds urls to the artifact descriptor.
func WithURLs(urls []string) Option {
return func(desc *v1.Descriptor) error {
if desc.URLs == nil {
desc.URLs = []string{}
}
desc.URLs = append(desc.URLs, urls...)
return nil
return func(o *options) {
o.descOpts = append(o.descOpts, func(desc *v1.Descriptor) {
if desc.URLs == nil {
desc.URLs = []string{}
}
desc.URLs = append(desc.URLs, urls...)
})
}
}
// WithPlatform sets the platform of the artifact descriptor.
func WithPlatform(platform v1.Platform) Option {
return func(desc *v1.Descriptor) error {
desc.Platform = &platform
return nil
return func(o *options) {
o.descOpts = append(o.descOpts, func(desc *v1.Descriptor) {
desc.Platform = &platform
})
}
}
+128 -26
View File
@@ -17,15 +17,19 @@ package layout
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"github.com/google/go-containerregistry/pkg/logs"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/match"
"github.com/google/go-containerregistry/pkg/v1/mutate"
"github.com/google/go-containerregistry/pkg/v1/partial"
"github.com/google/go-containerregistry/pkg/v1/stream"
"github.com/google/go-containerregistry/pkg/v1/types"
"golang.org/x/sync/errgroup"
)
@@ -62,10 +66,9 @@ func (l Path) AppendImage(img v1.Image, options ...Option) error {
Digest: d,
}
for _, opt := range options {
if err := opt(&desc); err != nil {
return err
}
o := makeOptions(options...)
for _, opt := range o.descOpts {
opt(&desc)
}
return l.AppendDescriptor(desc)
@@ -99,10 +102,9 @@ func (l Path) AppendIndex(ii v1.ImageIndex, options ...Option) error {
Digest: d,
}
for _, opt := range options {
if err := opt(&desc); err != nil {
return err
}
o := makeOptions(options...)
for _, opt := range o.descOpts {
opt(&desc)
}
return l.AppendDescriptor(desc)
@@ -163,10 +165,9 @@ func (l Path) replaceDescriptor(append mutate.Appendable, matcher match.Matcher,
return err
}
for _, opt := range options {
if err := opt(desc); err != nil {
return err
}
o := makeOptions(options...)
for _, opt := range o.descOpts {
opt(desc)
}
add := mutate.IndexAddendum{
@@ -219,40 +220,100 @@ func (l Path) WriteFile(name string, data []byte, perm os.FileMode) error {
}
return ioutil.WriteFile(l.path(name), data, perm)
}
// WriteBlob copies a file to the blobs/ directory in the Path from the given ReadCloser at
// blobs/{hash.Algorithm}/{hash.Hex}.
func (l Path) WriteBlob(hash v1.Hash, r io.ReadCloser) error {
return l.writeBlob(hash, -1, r, nil)
}
func (l Path) writeBlob(hash v1.Hash, size int64, r io.Reader, renamer func() (v1.Hash, error)) error {
if hash.Hex == "" && renamer == nil {
panic("writeBlob called an invalid hash and no renamer")
}
dir := l.path("blobs", hash.Algorithm)
if err := os.MkdirAll(dir, os.ModePerm); err != nil && !os.IsExist(err) {
return err
}
// Check if blob already exists and is the correct size
file := filepath.Join(dir, hash.Hex)
if _, err := os.Stat(file); err == nil {
// Blob already exists, that's fine.
if s, err := os.Stat(file); err == nil && !s.IsDir() && (s.Size() == size || size == -1) {
return nil
}
w, err := os.Create(file)
// If a renamer func was provided write to a temporary file
open := func() (*os.File, error) { return os.Create(file) }
if renamer != nil {
open = func() (*os.File, error) { return ioutil.TempFile(dir, hash.Hex) }
}
w, err := open()
if err != nil {
return err
}
if renamer != nil {
// Delete temp file if an error is encountered before renaming
defer func() {
if err := os.Remove(w.Name()); err != nil && !errors.Is(err, os.ErrNotExist) {
logs.Warn.Printf("error removing temporary file after encountering an error while writing blob: %v", err)
}
}()
}
defer w.Close()
_, err = io.Copy(w, r)
return err
// Write to file and exit if not renaming
if n, err := io.Copy(w, r); err != nil || renamer == nil {
return err
} else if size != -1 && n != size {
return fmt.Errorf("expected blob size %d, but only wrote %d", size, n)
}
// Always close file before renaming
if err := w.Close(); err != nil {
return err
}
// Rename file based on the final hash
finalHash, err := renamer()
if err != nil {
return fmt.Errorf("error getting final digest of layer: %w", err)
}
renamePath := l.path("blobs", finalHash.Algorithm, finalHash.Hex)
return os.Rename(w.Name(), renamePath)
}
// TODO: A streaming version of WriteBlob so we don't have to know the hash
// before we write it.
// TODO: For streaming layers we should write to a tmp file then Rename to the
// final digest.
// writeLayer writes the compressed layer to a blob. Unlike WriteBlob it will
// write to a temporary file (suffixed with .tmp) within the layout until the
// compressed reader is fully consumed and written to disk. Also unlike
// WriteBlob, it will not skip writing and exit without error when a blob file
// exists, but does not have the correct size. (The blob hash is not
// considered, because it may be expensive to compute.)
func (l Path) writeLayer(layer v1.Layer) error {
d, err := layer.Digest()
if err != nil {
if errors.Is(err, stream.ErrNotComputed) {
// Allow digest errors, since streams may not have calculated the hash
// yet. Instead, use an empty value, which will be transformed into a
// random file name with `ioutil.TempFile` and the final digest will be
// calculated after writing to a temp file and before renaming to the
// final path.
d = v1.Hash{Algorithm: "sha256", Hex: ""}
} else if err != nil {
return err
}
s, err := layer.Size()
if errors.Is(err, stream.ErrNotComputed) {
// Allow size errors, since streams may not have calculated the size
// yet. Instead, use zero as a sentinel value meaning that no size
// comparison can be done and any sized blob file should be considered
// valid and not overwritten.
//
// TODO: Provide an option to always overwrite blobs.
s = -1
} else if err != nil {
return err
}
@@ -261,7 +322,23 @@ func (l Path) writeLayer(layer v1.Layer) error {
return err
}
return l.WriteBlob(d, r)
if err := l.writeBlob(d, s, r, layer.Digest); err != nil {
return fmt.Errorf("error writing layer: %w", err)
}
return nil
}
// RemoveBlob removes a file from the blobs directory in the Path
// at blobs/{hash.Algorithm}/{hash.Hex}
// It does *not* remove any reference to it from other manifests or indexes, or
// from the root index.json.
func (l Path) RemoveBlob(hash v1.Hash) error {
dir := l.path("blobs", hash.Algorithm)
err := os.Remove(filepath.Join(dir, hash.Hex))
if err != nil && !os.IsNotExist(err) {
return err
}
return nil
}
// WriteImage writes an image, including its manifest, config and all of its
@@ -314,6 +391,14 @@ func (l Path) WriteImage(img v1.Image) error {
return l.WriteBlob(d, ioutil.NopCloser(bytes.NewReader(manifest)))
}
type withLayer interface {
Layer(v1.Hash) (v1.Layer, error)
}
type withBlob interface {
Blob(v1.Hash) (io.ReadCloser, error)
}
func (l Path) writeIndexToFile(indexFile string, ii v1.ImageIndex) error {
index, err := ii.IndexManifest()
if err != nil {
@@ -343,6 +428,24 @@ func (l Path) writeIndexToFile(indexFile string, ii v1.ImageIndex) error {
default:
// TODO: The layout could reference arbitrary things, which we should
// probably just pass through.
var blob io.ReadCloser
// Workaround for #819.
if wl, ok := ii.(withLayer); ok {
layer, lerr := wl.Layer(desc.Digest)
if lerr != nil {
return lerr
}
blob, err = layer.Compressed()
} else if wb, ok := ii.(withBlob); ok {
blob, err = wb.Blob(desc.Digest)
}
if err != nil {
return err
}
if err := l.WriteBlob(desc.Digest, blob); err != nil {
return err
}
}
}
@@ -374,7 +477,6 @@ func (l Path) WriteIndex(ii v1.ImageIndex) error {
indexFile := filepath.Join("blobs", h.Algorithm, h.Hex)
return l.writeIndexToFile(indexFile, ii)
}
// Write constructs a Path at path from an ImageIndex.
+3 -2
View File
@@ -23,8 +23,8 @@ import (
// Manifest represents the OCI image manifest in a structured way.
type Manifest struct {
SchemaVersion int64 `json:"schemaVersion,omitempty"`
MediaType types.MediaType `json:"mediaType"`
SchemaVersion int64 `json:"schemaVersion"`
MediaType types.MediaType `json:"mediaType,omitempty"`
Config Descriptor `json:"config"`
Layers []Descriptor `json:"layers"`
Annotations map[string]string `json:"annotations,omitempty"`
@@ -43,6 +43,7 @@ type Descriptor struct {
MediaType types.MediaType `json:"mediaType"`
Size int64 `json:"size"`
Digest Hash `json:"digest"`
Data []byte `json:"data,omitempty"`
URLs []string `json:"urls,omitempty"`
Annotations map[string]string `json:"annotations,omitempty"`
Platform *Platform `json:"platform,omitempty"`
+31 -16
View File
@@ -18,7 +18,6 @@ import (
"bytes"
"encoding/json"
"errors"
"strings"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/partial"
@@ -30,12 +29,14 @@ type image struct {
base v1.Image
adds []Addendum
computed bool
configFile *v1.ConfigFile
manifest *v1.Manifest
mediaType *types.MediaType
diffIDMap map[v1.Hash]v1.Layer
digestMap map[v1.Hash]v1.Layer
computed bool
configFile *v1.ConfigFile
manifest *v1.Manifest
annotations map[string]string
mediaType *types.MediaType
configMediaType *types.MediaType
diffIDMap map[v1.Hash]v1.Layer
digestMap map[v1.Hash]v1.Layer
}
var _ v1.Image = (*image)(nil)
@@ -129,13 +130,27 @@ func (i *image) compute() error {
manifest.Config.Digest = d
manifest.Config.Size = sz
// With OCI media types, this should not be set, see discussion:
// https://github.com/opencontainers/image-spec/pull/795
// If Data was set in the base image, we need to update it in the mutated image.
if m.Config.Data != nil {
manifest.Config.Data = rcfg
}
// If the user wants to mutate the media type of the config
if i.configMediaType != nil {
manifest.Config.MediaType = *i.configMediaType
}
if i.mediaType != nil {
if strings.Contains(string(*i.mediaType), types.OCIVendorPrefix) {
manifest.MediaType = ""
} else if strings.Contains(string(*i.mediaType), types.DockerVendorPrefix) {
manifest.MediaType = *i.mediaType
manifest.MediaType = *i.mediaType
}
if i.annotations != nil {
if manifest.Annotations == nil {
manifest.Annotations = map[string]string{}
}
for k, v := range i.annotations {
manifest.Annotations[k] = v
}
}
@@ -150,7 +165,7 @@ func (i *image) compute() error {
// Layers returns the ordered collection of filesystem layers that comprise this image.
// The order of the list is oldest/base layer first, and most-recent/top layer last.
func (i *image) Layers() ([]v1.Layer, error) {
if err := i.compute(); err == stream.ErrNotComputed {
if err := i.compute(); errors.Is(err, stream.ErrNotComputed) {
// Image contains a streamable layer which has not yet been
// consumed. Just return the layers we have in case the caller
// is going to consume the layers.
@@ -194,7 +209,7 @@ func (i *image) ConfigFile() (*v1.ConfigFile, error) {
if err := i.compute(); err != nil {
return nil, err
}
return i.configFile, nil
return i.configFile.DeepCopy(), nil
}
// RawConfigFile returns the serialized bytes of ConfigFile()
@@ -226,7 +241,7 @@ func (i *image) Manifest() (*v1.Manifest, error) {
if err := i.compute(); err != nil {
return nil, err
}
return i.manifest, nil
return i.manifest.DeepCopy(), nil
}
// RawManifest returns the serialized bytes of Manifest()
+39 -13
View File
@@ -16,7 +16,7 @@ package mutate
import (
"encoding/json"
"strings"
"fmt"
"github.com/google/go-containerregistry/pkg/logs"
v1 "github.com/google/go-containerregistry/pkg/v1"
@@ -50,6 +50,9 @@ func computeDescriptor(ia IndexAddendum) (*v1.Descriptor, error) {
if len(ia.Descriptor.Annotations) != 0 {
desc.Annotations = ia.Descriptor.Annotations
}
if ia.Descriptor.Data != nil {
desc.Data = ia.Descriptor.Data
}
return desc, nil
}
@@ -60,11 +63,13 @@ type index struct {
// remove is removed before adds
remove match.Matcher
computed bool
manifest *v1.IndexManifest
mediaType *types.MediaType
imageMap map[v1.Hash]v1.Image
indexMap map[v1.Hash]v1.ImageIndex
computed bool
manifest *v1.IndexManifest
annotations map[string]string
mediaType *types.MediaType
imageMap map[v1.Hash]v1.Image
indexMap map[v1.Hash]v1.ImageIndex
layerMap map[v1.Hash]v1.Layer
}
var _ v1.ImageIndex = (*index)(nil)
@@ -86,6 +91,7 @@ func (i *index) compute() error {
i.imageMap = make(map[v1.Hash]v1.Image)
i.indexMap = make(map[v1.Hash]v1.ImageIndex)
i.layerMap = make(map[v1.Hash]v1.Layer)
m, err := i.base.IndexManifest()
if err != nil {
@@ -115,6 +121,8 @@ func (i *index) compute() error {
i.indexMap[desc.Digest] = idx
} else if img, ok := add.Add.(v1.Image); ok {
i.imageMap[desc.Digest] = img
} else if l, ok := add.Add.(v1.Layer); ok {
i.layerMap[desc.Digest] = l
} else {
logs.Warn.Printf("Unexpected index addendum: %T", add.Add)
}
@@ -122,13 +130,16 @@ func (i *index) compute() error {
manifest.Manifests = manifests
// With OCI media types, this should not be set, see discussion:
// https://github.com/opencontainers/image-spec/pull/795
if i.mediaType != nil {
if strings.Contains(string(*i.mediaType), types.OCIVendorPrefix) {
manifest.MediaType = ""
} else if strings.Contains(string(*i.mediaType), types.DockerVendorPrefix) {
manifest.MediaType = *i.mediaType
manifest.MediaType = *i.mediaType
}
if i.annotations != nil {
if manifest.Annotations == nil {
manifest.Annotations = map[string]string{}
}
for k, v := range i.annotations {
manifest.Annotations[k] = v
}
}
@@ -151,6 +162,21 @@ func (i *index) ImageIndex(h v1.Hash) (v1.ImageIndex, error) {
return i.base.ImageIndex(h)
}
type withLayer interface {
Layer(v1.Hash) (v1.Layer, error)
}
// Workaround for #819.
func (i *index) Layer(h v1.Hash) (v1.Layer, error) {
if layer, ok := i.layerMap[h]; ok {
return layer, nil
}
if wl, ok := i.base.(withLayer); ok {
return wl.Layer(h)
}
return nil, fmt.Errorf("layer not found: %s", h)
}
// Digest returns the sha256 of this image's manifest.
func (i *index) Digest() (v1.Hash, error) {
if err := i.compute(); err != nil {
@@ -164,7 +190,7 @@ func (i *index) IndexManifest() (*v1.IndexManifest, error) {
if err := i.compute(); err != nil {
return nil, err
}
return i.manifest, nil
return i.manifest.DeepCopy(), nil
}
// RawManifest returns the serialized bytes of Manifest()
+109 -22
View File
@@ -17,6 +17,8 @@ package mutate
import (
"archive/tar"
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
@@ -24,10 +26,11 @@ import (
"strings"
"time"
"github.com/google/go-containerregistry/internal/gzip"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/empty"
"github.com/google/go-containerregistry/pkg/v1/internal/gzip"
"github.com/google/go-containerregistry/pkg/v1/match"
"github.com/google/go-containerregistry/pkg/v1/partial"
"github.com/google/go-containerregistry/pkg/v1/tarball"
"github.com/google/go-containerregistry/pkg/v1/types"
)
@@ -113,6 +116,72 @@ func Config(base v1.Image, cfg v1.Config) (v1.Image, error) {
return ConfigFile(base, cf)
}
// Annotatable represents a manifest that can carry annotations.
type Annotatable interface {
partial.WithRawManifest
}
// Annotations mutates the annotations on an annotatable image or index manifest.
//
// The annotatable input is expected to be a v1.Image or v1.ImageIndex, and
// returns the same type. You can type-assert the result like so:
//
// img := Annotations(empty.Image, map[string]string{
// "foo": "bar",
// }).(v1.Image)
//
// Or for an index:
//
// idx := Annotations(empty.Index, map[string]string{
// "foo": "bar",
// }).(v1.ImageIndex)
//
// If the input Annotatable is not an Image or ImageIndex, the result will
// attempt to lazily annotate the raw manifest.
func Annotations(f Annotatable, anns map[string]string) Annotatable {
if img, ok := f.(v1.Image); ok {
return &image{
base: img,
annotations: anns,
}
}
if idx, ok := f.(v1.ImageIndex); ok {
return &index{
base: idx,
annotations: anns,
}
}
return arbitraryRawManifest{f, anns}
}
type arbitraryRawManifest struct {
a Annotatable
anns map[string]string
}
func (a arbitraryRawManifest) RawManifest() ([]byte, error) {
b, err := a.a.RawManifest()
if err != nil {
return nil, err
}
var m map[string]interface{}
if err := json.Unmarshal(b, &m); err != nil {
return nil, err
}
if ann, ok := m["annotations"]; ok {
if annm, ok := ann.(map[string]string); ok {
for k, v := range a.anns {
annm[k] = v
}
} else {
return nil, fmt.Errorf(".annotations is not a map: %T", ann)
}
} else {
m["annotations"] = a.anns
}
return json.Marshal(m)
}
// ConfigFile mutates the provided v1.Image to have the provided v1.ConfigFile
func ConfigFile(base v1.Image, cfg *v1.ConfigFile) (v1.Image, error) {
m, err := base.Manifest()
@@ -173,7 +242,7 @@ func extract(img v1.Image, w io.Writer) error {
layers, err := img.Layers()
if err != nil {
return fmt.Errorf("retrieving image layers: %v", err)
return fmt.Errorf("retrieving image layers: %w", err)
}
// we iterate through the layers in reverse order because it makes handling
// whiteout layers more efficient, since we can just keep track of the removed
@@ -182,19 +251,23 @@ func extract(img v1.Image, w io.Writer) error {
layer := layers[i]
layerReader, err := layer.Uncompressed()
if err != nil {
return fmt.Errorf("reading layer contents: %v", err)
return fmt.Errorf("reading layer contents: %w", err)
}
defer layerReader.Close()
tarReader := tar.NewReader(layerReader)
for {
header, err := tarReader.Next()
if err == io.EOF {
if errors.Is(err, io.EOF) {
break
}
if err != nil {
return fmt.Errorf("reading tar: %v", err)
return fmt.Errorf("reading tar: %w", err)
}
// Some tools prepend everything with "./", so if we don't Clean the
// name, we may have duplicate entries, which angers tar-split.
header.Name = filepath.Clean(header.Name)
basename := filepath.Base(header.Name)
dirname := filepath.Dir(header.Name)
tombstone := strings.HasPrefix(basename, whiteoutPrefix)
@@ -226,7 +299,7 @@ func extract(img v1.Image, w io.Writer) error {
if !tombstone {
tarWriter.WriteHeader(header)
if header.Size > 0 {
if _, err := io.Copy(tarWriter, tarReader); err != nil {
if _, err := io.CopyN(tarWriter, tarReader, header.Size); err != nil {
return err
}
}
@@ -259,32 +332,32 @@ func Time(img v1.Image, t time.Time) (v1.Image, error) {
layers, err := img.Layers()
if err != nil {
return nil, fmt.Errorf("getting image layers: %v", err)
return nil, fmt.Errorf("getting image layers: %w", err)
}
// Strip away all timestamps from layers
var newLayers []v1.Layer
for _, layer := range layers {
newLayers := make([]v1.Layer, len(layers))
for idx, layer := range layers {
newLayer, err := layerTime(layer, t)
if err != nil {
return nil, fmt.Errorf("setting layer times: %v", err)
return nil, fmt.Errorf("setting layer times: %w", err)
}
newLayers = append(newLayers, newLayer)
newLayers[idx] = newLayer
}
newImage, err = AppendLayers(newImage, newLayers...)
if err != nil {
return nil, fmt.Errorf("appending layers: %v", err)
return nil, fmt.Errorf("appending layers: %w", err)
}
ocf, err := img.ConfigFile()
if err != nil {
return nil, fmt.Errorf("getting original config file: %v", err)
return nil, fmt.Errorf("getting original config file: %w", err)
}
cf, err := newImage.ConfigFile()
if err != nil {
return nil, fmt.Errorf("setting config file: %v", err)
return nil, fmt.Errorf("setting config file: %w", err)
}
cfg := cf.DeepCopy()
@@ -298,8 +371,13 @@ func Time(img v1.Image, t time.Time) (v1.Image, error) {
// Strip away timestamps from the config file
cfg.Created = v1.Time{Time: t}
for _, h := range cfg.History {
for i, h := range cfg.History {
h.Created = v1.Time{Time: t}
h.CreatedBy = ocf.History[i].CreatedBy
h.Comment = ocf.History[i].Comment
h.EmptyLayer = ocf.History[i].EmptyLayer
// Explicitly ignore Author field; which hinders reproducibility
cfg.History[i] = h
}
return ConfigFile(newImage, cfg)
@@ -308,7 +386,7 @@ func Time(img v1.Image, t time.Time) (v1.Image, error) {
func layerTime(layer v1.Layer, t time.Time) (v1.Layer, error) {
layerReader, err := layer.Uncompressed()
if err != nil {
return nil, fmt.Errorf("getting layer: %v", err)
return nil, fmt.Errorf("getting layer: %w", err)
}
defer layerReader.Close()
w := new(bytes.Buffer)
@@ -318,21 +396,22 @@ func layerTime(layer v1.Layer, t time.Time) (v1.Layer, error) {
tarReader := tar.NewReader(layerReader)
for {
header, err := tarReader.Next()
if err == io.EOF {
if errors.Is(err, io.EOF) {
break
}
if err != nil {
return nil, fmt.Errorf("reading layer: %v", err)
return nil, fmt.Errorf("reading layer: %w", err)
}
header.ModTime = t
if err := tarWriter.WriteHeader(header); err != nil {
return nil, fmt.Errorf("writing tar header: %v", err)
return nil, fmt.Errorf("writing tar header: %w", err)
}
if header.Typeflag == tar.TypeReg {
if _, err = io.Copy(tarWriter, tarReader); err != nil {
return nil, fmt.Errorf("writing layer file: %v", err)
// TODO(#1168): This should be lazy, and not buffer the entire layer contents.
if _, err = io.CopyN(tarWriter, tarReader, header.Size); err != nil {
return nil, fmt.Errorf("writing layer file: %w", err)
}
}
}
@@ -348,7 +427,7 @@ func layerTime(layer v1.Layer, t time.Time) (v1.Layer, error) {
}
layer, err = tarball.LayerFromOpener(opener)
if err != nil {
return nil, fmt.Errorf("creating layer: %v", err)
return nil, fmt.Errorf("creating layer: %w", err)
}
return layer, nil
@@ -387,6 +466,14 @@ func MediaType(img v1.Image, mt types.MediaType) v1.Image {
}
}
// ConfigMediaType modifies the MediaType() of the given image's Config.
func ConfigMediaType(img v1.Image, mt types.MediaType) v1.Image {
return &image{
base: img,
configMediaType: &mt,
}
}
// IndexMediaType modifies the MediaType() of the given index.
func IndexMediaType(idx v1.ImageIndex, mt types.MediaType) v1.ImageIndex {
return &index{
+12 -12
View File
@@ -27,7 +27,7 @@ func Rebase(orig, oldBase, newBase v1.Image) (v1.Image, error) {
// not based on oldBase at all.
origLayers, err := orig.Layers()
if err != nil {
return nil, fmt.Errorf("failed to get layers for original: %v", err)
return nil, fmt.Errorf("failed to get layers for original: %w", err)
}
oldBaseLayers, err := oldBase.Layers()
if err != nil {
@@ -39,11 +39,11 @@ func Rebase(orig, oldBase, newBase v1.Image) (v1.Image, error) {
for i, l := range oldBaseLayers {
oldLayerDigest, err := l.Digest()
if err != nil {
return nil, fmt.Errorf("failed to get digest of layer %d of %q: %v", i, oldBase, err)
return nil, fmt.Errorf("failed to get digest of layer %d of %q: %w", i, oldBase, err)
}
origLayerDigest, err := origLayers[i].Digest()
if err != nil {
return nil, fmt.Errorf("failed to get digest of layer %d of %q: %v", i, orig, err)
return nil, fmt.Errorf("failed to get digest of layer %d of %q: %w", i, orig, err)
}
if oldLayerDigest != origLayerDigest {
return nil, fmt.Errorf("image %q is not based on %q (layer %d mismatch)", orig, oldBase, i)
@@ -52,17 +52,17 @@ func Rebase(orig, oldBase, newBase v1.Image) (v1.Image, error) {
oldConfig, err := oldBase.ConfigFile()
if err != nil {
return nil, fmt.Errorf("failed to get config for old base: %v", err)
return nil, fmt.Errorf("failed to get config for old base: %w", err)
}
origConfig, err := orig.ConfigFile()
if err != nil {
return nil, fmt.Errorf("failed to get config for original: %v", err)
return nil, fmt.Errorf("failed to get config for original: %w", err)
}
newConfig, err := newBase.ConfigFile()
if err != nil {
return nil, fmt.Errorf("could not get config for new base: %v", err)
return nil, fmt.Errorf("could not get config for new base: %w", err)
}
// Stitch together an image that contains:
@@ -72,13 +72,13 @@ func Rebase(orig, oldBase, newBase v1.Image) (v1.Image, error) {
// - new base image's history + top of original image's history
rebasedImage, err := Config(empty.Image, *origConfig.Config.DeepCopy())
if err != nil {
return nil, fmt.Errorf("failed to create empty image with original config: %v", err)
return nil, fmt.Errorf("failed to create empty image with original config: %w", err)
}
// Add new config properties from existing images.
rebasedConfig, err := rebasedImage.ConfigFile()
if err != nil {
return nil, fmt.Errorf("could not get config for rebased image: %v", err)
return nil, fmt.Errorf("could not get config for rebased image: %w", err)
}
// OS/Arch properties from new base
rebasedConfig.Architecture = newConfig.Architecture
@@ -88,24 +88,24 @@ func Rebase(orig, oldBase, newBase v1.Image) (v1.Image, error) {
// Apply config properties to rebased.
rebasedImage, err = ConfigFile(rebasedImage, rebasedConfig)
if err != nil {
return nil, fmt.Errorf("failed to replace config for rebased image: %v", err)
return nil, fmt.Errorf("failed to replace config for rebased image: %w", err)
}
// Get new base layers and config for history.
newBaseLayers, err := newBase.Layers()
if err != nil {
return nil, fmt.Errorf("could not get new base layers for new base: %v", err)
return nil, fmt.Errorf("could not get new base layers for new base: %w", err)
}
// Add new base layers.
rebasedImage, err = Append(rebasedImage, createAddendums(0, 0, newConfig.History, newBaseLayers)...)
if err != nil {
return nil, fmt.Errorf("failed to append new base image: %v", err)
return nil, fmt.Errorf("failed to append new base image: %w", err)
}
// Add original layers above the old base.
rebasedImage, err = Append(rebasedImage, createAddendums(len(oldConfig.History), len(oldBaseLayers)+1, origConfig.History, origLayers)...)
if err != nil {
return nil, fmt.Errorf("failed to append original image: %v", err)
return nil, fmt.Errorf("failed to append original image: %w", err)
}
return rebasedImage, nil
+17 -1
View File
@@ -29,7 +29,7 @@ In a tarball, blobs are (often) uncompressed, so it's easiest to implement a `v1
of uncompressed layers. `tarball.uncompressedImage` does this by implementing `UncompressedImageCore`:
```go
type CompressedImageCore interface {
type UncompressedImageCore interface {
RawConfigFile() ([]byte, error)
MediaType() (types.MediaType, error)
LayerByDiffID(v1.Hash) (UncompressedLayer, error)
@@ -64,3 +64,19 @@ there are cases where it is very helpful to know the layer size, e.g. when
writing the uncompressed layer into a tarball.
See [`#655`](https://github.com/google/go-containerregistry/pull/655).
### [`partial.Exists`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/partial#Exists)
We generally don't care about the existence of something as granular as a
layer, and would rather ensure all the invariants of an image are upheld via
the `validate` package. However, there are situations where we want to do a
quick smoke test to ensure that the underlying storage engine hasn't been
corrupted by something e.g. deleting files or blobs. Thus, we've exposed an
optional `Exists` method that does an existence check without actually reading
any bytes.
The `remote` package implements this via `HEAD` requests.
The `layout` package implements this via `os.Stat`.
See [`#838`](https://github.com/google/go-containerregistry/pull/838).
+21 -3
View File
@@ -17,8 +17,9 @@ package partial
import (
"io"
"github.com/google/go-containerregistry/internal/and"
"github.com/google/go-containerregistry/internal/gzip"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/internal/gzip"
"github.com/google/go-containerregistry/pkg/v1/types"
)
@@ -45,11 +46,28 @@ type compressedLayerExtender struct {
// Uncompressed implements v1.Layer
func (cle *compressedLayerExtender) Uncompressed() (io.ReadCloser, error) {
r, err := cle.Compressed()
rc, err := cle.Compressed()
if err != nil {
return nil, err
}
return gzip.UnzipReadCloser(r)
// Often, the "compressed" bytes are not actually gzip-compressed.
// Peek at the first two bytes to determine whether or not it's correct to
// wrap this with gzip.UnzipReadCloser.
gzipped, pr, err := gzip.Peek(rc)
if err != nil {
return nil, err
}
prc := &and.ReadCloser{
Reader: pr,
CloseFunc: rc.Close,
}
if !gzipped {
return prc, nil
}
return gzip.UnzipReadCloser(prc)
}
// DiffID implements v1.Layer
+1 -1
View File
@@ -26,7 +26,7 @@ func FindManifests(index v1.ImageIndex, matcher match.Matcher) ([]v1.Descriptor,
// get the actual manifest list
indexManifest, err := index.IndexManifest()
if err != nil {
return nil, fmt.Errorf("unable to get raw index: %v", err)
return nil, fmt.Errorf("unable to get raw index: %w", err)
}
manifests := []v1.Descriptor{}
// try to get the root of our image
@@ -19,8 +19,8 @@ import (
"io"
"sync"
"github.com/google/go-containerregistry/internal/gzip"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/internal/gzip"
"github.com/google/go-containerregistry/pkg/v1/types"
)
+45 -38
View File
@@ -297,33 +297,10 @@ type Describable interface {
// UncompressedToImage.
func Descriptor(d Describable) (*v1.Descriptor, error) {
// If Describable implements Descriptor itself, return that.
if wd, ok := d.(withDescriptor); ok {
if wd, ok := unwrap(d).(withDescriptor); ok {
return wd.Descriptor()
}
// Otherwise, try to unwrap any partial implementations to see
// if the wrapped struct implements Descriptor.
if ule, ok := d.(*uncompressedLayerExtender); ok {
if wd, ok := ule.UncompressedLayer.(withDescriptor); ok {
return wd.Descriptor()
}
}
if cle, ok := d.(*compressedLayerExtender); ok {
if wd, ok := cle.CompressedLayer.(withDescriptor); ok {
return wd.Descriptor()
}
}
if uie, ok := d.(*uncompressedImageExtender); ok {
if wd, ok := uie.UncompressedImageCore.(withDescriptor); ok {
return wd.Descriptor()
}
}
if cie, ok := d.(*compressedImageExtender); ok {
if wd, ok := cie.CompressedImageCore.(withDescriptor); ok {
return wd.Descriptor()
}
}
// If all else fails, compute the descriptor from the individual methods.
var (
desc v1.Descriptor
@@ -354,23 +331,10 @@ type withUncompressedSize interface {
// for streaming layers.
func UncompressedSize(l v1.Layer) (int64, error) {
// If the layer implements UncompressedSize itself, return that.
if wus, ok := l.(withUncompressedSize); ok {
if wus, ok := unwrap(l).(withUncompressedSize); ok {
return wus.UncompressedSize()
}
// Otherwise, try to unwrap any partial implementations to see
// if the wrapped struct implements UncompressedSize.
if ule, ok := l.(*uncompressedLayerExtender); ok {
if wus, ok := ule.UncompressedLayer.(withUncompressedSize); ok {
return wus.UncompressedSize()
}
}
if cle, ok := l.(*compressedLayerExtender); ok {
if wus, ok := cle.CompressedLayer.(withUncompressedSize); ok {
return wus.UncompressedSize()
}
}
// The layer doesn't implement UncompressedSize, we need to compute it.
rc, err := l.Uncompressed()
if err != nil {
@@ -380,3 +344,46 @@ func UncompressedSize(l v1.Layer) (int64, error) {
return io.Copy(ioutil.Discard, rc)
}
type withExists interface {
Exists() (bool, error)
}
// Exists checks to see if a layer exists. This is a hack to work around the
// mistakes of the partial package. Don't use this.
func Exists(l v1.Layer) (bool, error) {
// If the layer implements Exists itself, return that.
if we, ok := unwrap(l).(withExists); ok {
return we.Exists()
}
// The layer doesn't implement Exists, so we hope that calling Compressed()
// is enough to trigger an error if the layer does not exist.
rc, err := l.Compressed()
if err != nil {
return false, err
}
defer rc.Close()
// We may want to try actually reading a single byte, but if we need to do
// that, we should just fix this hack.
return true, nil
}
// Recursively unwrap our wrappers so that we can check for the original implementation.
// We might want to expose this?
func unwrap(i interface{}) interface{} {
if ule, ok := i.(*uncompressedLayerExtender); ok {
return unwrap(ule.UncompressedLayer)
}
if cle, ok := i.(*compressedLayerExtender); ok {
return unwrap(cle.CompressedLayer)
}
if uie, ok := i.(*uncompressedImageExtender); ok {
return unwrap(uie.UncompressedImageCore)
}
if cie, ok := i.(*compressedImageExtender); ok {
return unwrap(cie.CompressedImageCore)
}
return i
}
+4 -5
View File
@@ -50,10 +50,9 @@ func stringSliceEqual(a, b []string) bool {
// stringSliceEqualIgnoreOrder compares 2 string slices and returns if their contents are identical, ignoring order
func stringSliceEqualIgnoreOrder(a, b []string) bool {
a1, b1 := a[:], b[:]
if a1 != nil && b1 != nil {
sort.Strings(a1)
sort.Strings(b1)
if a != nil && b != nil {
sort.Strings(a)
sort.Strings(b)
}
return stringSliceEqual(a1, b1)
return stringSliceEqual(a, b)
}
+1 -1
View File
@@ -81,7 +81,7 @@ func Image(byteSize, layers int64) (v1.Image, error) {
// Layer returns a layer with pseudo-randomly generated content.
func Layer(byteSize int64, mt types.MediaType) (v1.Layer, error) {
fileName := fmt.Sprintf("random_file_%d.txt", mrand.Int())
fileName := fmt.Sprintf("random_file_%d.txt", mrand.Int()) //nolint: gosec
// Hash the contents as we write it out to the buffer.
var b bytes.Buffer
+7 -4
View File
@@ -88,10 +88,13 @@ func Catalog(ctx context.Context, target name.Registry, options ...Option) ([]st
}
uri := &url.URL{
Scheme: target.Scheme(),
Host: target.RegistryStr(),
Path: "/v2/_catalog",
RawQuery: "n=10000",
Scheme: target.Scheme(),
Host: target.RegistryStr(),
Path: "/v2/_catalog",
}
if o.pageSize > 0 {
uri.RawQuery = fmt.Sprintf("n=%d", o.pageSize)
}
client := http.Client{Transport: tr}
+2 -2
View File
@@ -20,13 +20,13 @@ import (
func CheckPushPermission(ref name.Reference, kc authn.Keychain, t http.RoundTripper) error {
auth, err := kc.Resolve(ref.Context().Registry)
if err != nil {
return fmt.Errorf("resolving authorization for %v failed: %v", ref.Context().Registry, err)
return fmt.Errorf("resolving authorization for %v failed: %w", ref.Context().Registry, err)
}
scopes := []string{ref.Scope(transport.PushScope)}
tr, err := transport.New(ref.Context().Registry, auth, t, scopes)
if err != nil {
return fmt.Errorf("creating push check transport for %v failed: %v", ref.Context().Registry, err)
return fmt.Errorf("creating push check transport for %v failed: %w", ref.Context().Registry, err)
}
// TODO(jasonhall): Against GCR, just doing the token handshake is
// enough, but this doesn't extend to Dockerhub
+47 -9
View File
@@ -22,13 +22,12 @@ import (
"io/ioutil"
"net/http"
"net/url"
"strconv"
"strings"
"github.com/google/go-containerregistry/internal/verify"
"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/internal/verify"
"github.com/google/go-containerregistry/pkg/v1/partial"
"github.com/google/go-containerregistry/pkg/v1/remote/transport"
"github.com/google/go-containerregistry/pkg/v1/types"
@@ -324,14 +323,22 @@ func (f *fetcher) headManifest(ref name.Reference, acceptable []types.MediaType)
return nil, err
}
mediaType := types.MediaType(resp.Header.Get("Content-Type"))
mth := resp.Header.Get("Content-Type")
if mth == "" {
return nil, fmt.Errorf("HEAD %s: response did not include Content-Type header", u.String())
}
mediaType := types.MediaType(mth)
size, err := strconv.ParseInt(resp.Header.Get("Content-Length"), 10, 64)
if err != nil {
return nil, err
size := resp.ContentLength
if size == -1 {
return nil, fmt.Errorf("GET %s: response did not include Content-Length header", u.String())
}
digest, err := v1.NewHash(resp.Header.Get("Docker-Content-Digest"))
dh := resp.Header.Get("Docker-Content-Digest")
if dh == "" {
return nil, fmt.Errorf("HEAD %s: response did not include Docker-Content-Digest header", u.String())
}
digest, err := v1.NewHash(dh)
if err != nil {
return nil, err
}
@@ -351,7 +358,7 @@ func (f *fetcher) headManifest(ref name.Reference, acceptable []types.MediaType)
}, nil
}
func (f *fetcher) fetchBlob(ctx context.Context, h v1.Hash) (io.ReadCloser, error) {
func (f *fetcher) fetchBlob(ctx context.Context, size int64, h v1.Hash) (io.ReadCloser, error) {
u := f.url("blobs", h.String())
req, err := http.NewRequest(http.MethodGet, u.String(), nil)
if err != nil {
@@ -368,7 +375,18 @@ func (f *fetcher) fetchBlob(ctx context.Context, h v1.Hash) (io.ReadCloser, erro
return nil, err
}
return verify.ReadCloser(resp.Body, h)
// Do whatever we can.
// If we have an expected size and Content-Length doesn't match, return an error.
// If we don't have an expected size and we do have a Content-Length, use Content-Length.
if hsize := resp.ContentLength; hsize != -1 {
if size == verify.SizeUnknown {
size = hsize
} else if hsize != size {
return nil, fmt.Errorf("GET %s: Content-Length header %d does not match expected size %d", u.String(), hsize, size)
}
}
return verify.ReadCloser(resp.Body, size, h)
}
func (f *fetcher) headBlob(h v1.Hash) (*http.Response, error) {
@@ -390,3 +408,23 @@ func (f *fetcher) headBlob(h v1.Hash) (*http.Response, error) {
return resp, nil
}
func (f *fetcher) blobExists(h v1.Hash) (bool, error) {
u := f.url("blobs", h.String())
req, err := http.NewRequest(http.MethodHead, u.String(), nil)
if err != nil {
return false, err
}
resp, err := f.Client.Do(req.WithContext(f.context))
if err != nil {
return false, err
}
defer resp.Body.Close()
if err := transport.CheckError(resp, http.StatusOK, http.StatusNotFound); err != nil {
return false, err
}
return resp.StatusCode == http.StatusOK, nil
}
+22 -4
View File
@@ -15,16 +15,17 @@
package remote
import (
"bytes"
"io"
"io/ioutil"
"net/http"
"net/url"
"sync"
"github.com/google/go-containerregistry/pkg/internal/redact"
"github.com/google/go-containerregistry/internal/redact"
"github.com/google/go-containerregistry/internal/verify"
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/internal/verify"
"github.com/google/go-containerregistry/pkg/v1/partial"
"github.com/google/go-containerregistry/pkg/v1/remote/transport"
"github.com/google/go-containerregistry/pkg/v1/types"
@@ -100,7 +101,15 @@ func (r *remoteImage) RawConfigFile() ([]byte, error) {
return nil, err
}
body, err := r.fetchBlob(r.context, m.Config.Digest)
if m.Config.Data != nil {
if err := verify.Descriptor(m.Config); err != nil {
return nil, err
}
r.config = m.Config.Data
return r.config, nil
}
body, err := r.fetchBlob(r.context, m.Config.Size, m.Config.Digest)
if err != nil {
return nil, err
}
@@ -143,6 +152,10 @@ func (rl *remoteImageLayer) Compressed() (io.ReadCloser, error) {
return nil, err
}
if d.Data != nil {
return verify.ReadCloser(ioutil.NopCloser(bytes.NewReader(d.Data)), d.Size, d.Digest)
}
// We don't want to log binary layers -- this can break terminals.
ctx := redact.NewContext(rl.ri.context, "omitting binary blobs from logs")
@@ -177,7 +190,7 @@ func (rl *remoteImageLayer) Compressed() (io.ReadCloser, error) {
continue
}
return verify.ReadCloser(resp.Body, rl.digest)
return verify.ReadCloser(resp.Body, d.Size, rl.digest)
}
return nil, lastErr
@@ -221,6 +234,11 @@ func (rl *remoteImageLayer) Descriptor() (*v1.Descriptor, error) {
return partial.BlobDescriptor(rl, rl.digest)
}
// See partial.Exists.
func (rl *remoteImageLayer) Exists() (bool, error) {
return rl.ri.blobExists(rl.digest)
}
// LayerByDigest implements partial.CompressedLayer
func (r *remoteImage) LayerByDigest(h v1.Hash) (partial.CompressedLayer, error) {
return &remoteImageLayer{
+74 -4
View File
@@ -19,6 +19,7 @@ import (
"fmt"
"sync"
"github.com/google/go-containerregistry/internal/verify"
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/partial"
@@ -122,6 +123,64 @@ func (r *remoteIndex) ImageIndex(h v1.Hash) (v1.ImageIndex, error) {
return desc.ImageIndex()
}
// Workaround for #819.
func (r *remoteIndex) Layer(h v1.Hash) (v1.Layer, error) {
index, err := r.IndexManifest()
if err != nil {
return nil, err
}
for _, childDesc := range index.Manifests {
if h == childDesc.Digest {
l, err := partial.CompressedToLayer(&remoteLayer{
fetcher: r.fetcher,
digest: h,
})
if err != nil {
return nil, err
}
return &MountableLayer{
Layer: l,
Reference: r.Ref.Context().Digest(h.String()),
}, nil
}
}
return nil, fmt.Errorf("layer not found: %s", h)
}
// Experiment with a better API for v1.ImageIndex. We might want to move this
// to partial?
func (r *remoteIndex) Manifests() ([]partial.Describable, error) {
m, err := r.IndexManifest()
if err != nil {
return nil, err
}
manifests := []partial.Describable{}
for _, desc := range m.Manifests {
switch {
case desc.MediaType.IsImage():
img, err := r.Image(desc.Digest)
if err != nil {
return nil, err
}
manifests = append(manifests, img)
case desc.MediaType.IsIndex():
idx, err := r.ImageIndex(desc.Digest)
if err != nil {
return nil, err
}
manifests = append(manifests, idx)
default:
layer, err := r.Layer(desc.Digest)
if err != nil {
return nil, err
}
manifests = append(manifests, layer)
}
}
return manifests, nil
}
func (r *remoteIndex) imageByPlatform(platform v1.Platform) (v1.Image, error) {
desc, err := r.childByPlatform(platform)
if err != nil {
@@ -155,7 +214,7 @@ func (r *remoteIndex) childByPlatform(platform v1.Platform) (*Descriptor, error)
return r.childDescriptor(childDesc, platform)
}
}
return nil, fmt.Errorf("no child with platform %s/%s in index %s", platform.OS, platform.Architecture, r.Ref)
return nil, fmt.Errorf("no child with platform %+v in index %s", platform, r.Ref)
}
func (r *remoteIndex) childByHash(h v1.Hash) (*Descriptor, error) {
@@ -174,9 +233,20 @@ func (r *remoteIndex) childByHash(h v1.Hash) (*Descriptor, error) {
// Convert one of this index's child's v1.Descriptor into a remote.Descriptor, with the given platform option.
func (r *remoteIndex) childDescriptor(child v1.Descriptor, platform v1.Platform) (*Descriptor, error) {
ref := r.Ref.Context().Digest(child.Digest.String())
manifest, _, err := r.fetchManifest(ref, []types.MediaType{child.MediaType})
if err != nil {
return nil, err
var (
manifest []byte
err error
)
if child.Data != nil {
if err := verify.Descriptor(child); err != nil {
return nil, err
}
manifest = child.Data
} else {
manifest, _, err = r.fetchManifest(ref, []types.MediaType{child.MediaType})
if err != nil {
return nil, err
}
}
return &Descriptor{
fetcher: fetcher{
+9 -2
View File
@@ -17,7 +17,8 @@ package remote
import (
"io"
"github.com/google/go-containerregistry/pkg/internal/redact"
"github.com/google/go-containerregistry/internal/redact"
"github.com/google/go-containerregistry/internal/verify"
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/partial"
@@ -34,7 +35,7 @@ type remoteLayer struct {
func (rl *remoteLayer) Compressed() (io.ReadCloser, error) {
// We don't want to log binary layers -- this can break terminals.
ctx := redact.NewContext(rl.context, "omitting binary blobs from logs")
return rl.fetchBlob(ctx, rl.digest)
return rl.fetchBlob(ctx, verify.SizeUnknown, rl.digest)
}
// Compressed implements partial.CompressedLayer
@@ -43,6 +44,7 @@ func (rl *remoteLayer) Size() (int64, error) {
if err != nil {
return -1, err
}
defer resp.Body.Close()
return resp.ContentLength, nil
}
@@ -56,6 +58,11 @@ func (rl *remoteLayer) MediaType() (types.MediaType, error) {
return types.DockerLayer, nil
}
// See partial.Exists.
func (rl *remoteLayer) Exists() (bool, error) {
return rl.blobExists(rl.digest)
}
// Layer reads the given blob reference from a registry as a Layer. A blob
// reference here is just a punned name.Digest where the digest portion is the
// digest of the blob to be read and the repository portion is the repo where
+12 -17
View File
@@ -31,14 +31,16 @@ type tags struct {
Tags []string `json:"tags"`
}
// List wraps ListWithContext using the background context.
func List(repo name.Repository, options ...Option) ([]string, error) {
return ListWithContext(context.Background(), repo, options...)
// ListWithContext calls List with the given context.
//
// Deprecated: Use List and WithContext. This will be removed in a future release.
func ListWithContext(ctx context.Context, repo name.Repository, options ...Option) ([]string, error) {
return List(repo, append(options, WithContext(ctx))...)
}
// ListWithContext calls /tags/list for the given repository, returning the list of tags
// List calls /tags/list for the given repository, returning the list of tags
// in the "tags" property.
func ListWithContext(ctx context.Context, repo name.Repository, options ...Option) ([]string, error) {
func List(repo name.Repository, options ...Option) ([]string, error) {
o, err := makeOptions(repo, options...)
if err != nil {
return nil, err
@@ -53,16 +55,10 @@ func ListWithContext(ctx context.Context, repo name.Repository, options ...Optio
Scheme: repo.Registry.Scheme(),
Host: repo.Registry.RegistryStr(),
Path: fmt.Sprintf("/v2/%s/tags/list", repo.RepositoryStr()),
// ECR returns an error if n > 1000:
// https://github.com/google/go-containerregistry/issues/681
RawQuery: "n=1000",
}
// This is lazy, but I want to make sure List(..., WithContext(ctx)) works
// without calling makeOptions() twice (which can have side effects).
// This means ListWithContext(ctx, ..., WithContext(ctx2)) prefers ctx2.
if o.context != context.Background() {
ctx = o.context
if o.pageSize > 0 {
uri.RawQuery = fmt.Sprintf("n=%d", o.pageSize)
}
client := http.Client{Transport: tr}
@@ -72,16 +68,15 @@ func ListWithContext(ctx context.Context, repo name.Repository, options ...Optio
// get responses until there is no next page
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-o.context.Done():
return nil, o.context.Err()
default:
}
req, err := http.NewRequest("GET", uri.String(), nil)
req, err := http.NewRequestWithContext(o.context, "GET", uri.String(), nil)
if err != nil {
return nil, err
}
req = req.WithContext(ctx)
resp, err := client.Do(req)
if err != nil {
+5
View File
@@ -34,6 +34,11 @@ func (ml *MountableLayer) Descriptor() (*v1.Descriptor, error) {
return partial.Descriptor(ml.Layer)
}
// Exists is a hack. See partial.Exists.
func (ml *MountableLayer) Exists() (bool, error) {
return partial.Exists(ml.Layer)
}
// mountableImage wraps the v1.Layer references returned by the embedded v1.Image
// in MountableLayer's so that remote.Write might attempt to mount them from their
// source repository.
+109 -47
View File
@@ -15,6 +15,7 @@
package remote
import (
"context"
"fmt"
"net/http"
@@ -33,7 +34,7 @@ import (
// Current limitations:
// - All refs must share the same repository.
// - Images cannot consist of stream.Layers.
func MultiWrite(m map[name.Reference]Taggable, options ...Option) error {
func MultiWrite(m map[name.Reference]Taggable, options ...Option) (rerr error) {
// Determine the repository being pushed to; if asked to push to
// multiple repositories, give up.
var repo, zero name.Repository
@@ -45,23 +46,27 @@ func MultiWrite(m map[name.Reference]Taggable, options ...Option) error {
}
}
o, err := makeOptions(repo, options...)
if err != nil {
return err
}
// Collect unique blobs (layers and config blobs).
blobs := map[v1.Hash]v1.Layer{}
newManifests := []map[name.Reference]Taggable{}
// Separate originally requested images and indexes, so we can push images first.
images, indexes := map[name.Reference]Taggable{}, map[name.Reference]Taggable{}
var err error
for ref, i := range m {
if img, ok := i.(v1.Image); ok {
images[ref] = i
if err := addImageBlobs(img, blobs); err != nil {
if err := addImageBlobs(img, blobs, o.allowNondistributableArtifacts); err != nil {
return err
}
continue
}
if idx, ok := i.(v1.ImageIndex); ok {
indexes[ref] = i
newManifests, err = addIndexBlobs(idx, blobs, repo, newManifests, 0)
newManifests, err = addIndexBlobs(idx, blobs, repo, newManifests, 0, o.allowNondistributableArtifacts)
if err != nil {
return err
}
@@ -70,10 +75,6 @@ func MultiWrite(m map[name.Reference]Taggable, options ...Option) error {
return fmt.Errorf("pushable resource was not Image or ImageIndex: %T", i)
}
o, err := makeOptions(repo, options...)
if err != nil {
return err
}
// Determine if any of the layers are Mountable, because if so we need
// to request Pull scope too.
ls := []v1.Layer{}
@@ -86,36 +87,85 @@ func MultiWrite(m map[name.Reference]Taggable, options ...Option) error {
return err
}
w := writer{
repo: repo,
client: &http.Client{Transport: tr},
context: o.context,
repo: repo,
client: &http.Client{Transport: tr},
context: o.context,
updates: o.updates,
lastUpdate: &v1.Update{},
backoff: o.retryBackoff,
predicate: o.retryPredicate,
}
// Collect the total size of blobs and manifests we're about to write.
if o.updates != nil {
defer close(o.updates)
defer func() { _ = sendError(o.updates, rerr) }()
for _, b := range blobs {
size, err := b.Size()
if err != nil {
return err
}
w.lastUpdate.Total += size
}
countManifest := func(t Taggable) error {
b, err := t.RawManifest()
if err != nil {
return err
}
w.lastUpdate.Total += int64(len(b))
return nil
}
for _, i := range images {
if err := countManifest(i); err != nil {
return err
}
}
for _, nm := range newManifests {
for _, i := range nm {
if err := countManifest(i); err != nil {
return err
}
}
}
for _, i := range indexes {
if err := countManifest(i); err != nil {
return err
}
}
}
// Upload individual blobs and collect any errors.
blobChan := make(chan v1.Layer, 2*o.jobs)
var g errgroup.Group
ctx := o.context
g, gctx := errgroup.WithContext(o.context)
for i := 0; i < o.jobs; i++ {
// Start N workers consuming blobs to upload.
g.Go(func() error {
for b := range blobChan {
if err := w.uploadOne(b); err != nil {
if err := w.uploadOne(gctx, b); err != nil {
return err
}
}
return nil
})
}
go func() {
g.Go(func() error {
defer close(blobChan)
for _, b := range blobs {
blobChan <- b
select {
case blobChan <- b:
case <-gctx.Done():
return gctx.Err()
}
}
close(blobChan)
}()
return nil
})
if err := g.Wait(); err != nil {
return err
}
commitMany := func(m map[name.Reference]Taggable) error {
commitMany := func(ctx context.Context, m map[name.Reference]Taggable) error {
g, ctx := errgroup.WithContext(ctx)
// With all of the constituent elements uploaded, upload the manifests
// to commit the images and indexes, and collect any errors.
type task struct {
@@ -127,7 +177,7 @@ func MultiWrite(m map[name.Reference]Taggable, options ...Option) error {
// Start N workers consuming tasks to upload manifests.
g.Go(func() error {
for t := range taskChan {
if err := w.commitManifest(t.i, t.ref); err != nil {
if err := w.commitManifest(ctx, t.i, t.ref); err != nil {
return err
}
}
@@ -144,24 +194,24 @@ func MultiWrite(m map[name.Reference]Taggable, options ...Option) error {
}
// Push originally requested image manifests. These have no
// dependencies.
if err := commitMany(images); err != nil {
if err := commitMany(ctx, images); err != nil {
return err
}
// Push new manifests from lowest levels up.
for i := len(newManifests) - 1; i >= 0; i-- {
if err := commitMany(newManifests[i]); err != nil {
if err := commitMany(ctx, newManifests[i]); err != nil {
return err
}
}
// Push originally requested index manifests, which might depend on
// newly discovered manifests.
return commitMany(indexes)
return commitMany(ctx, indexes)
}
// addIndexBlobs adds blobs to the set of blobs we intend to upload, and
// returns the latest copy of the ordered collection of manifests to upload.
func addIndexBlobs(idx v1.ImageIndex, blobs map[v1.Hash]v1.Layer, repo name.Repository, newManifests []map[name.Reference]Taggable, lvl int) ([]map[name.Reference]Taggable, error) {
func addIndexBlobs(idx v1.ImageIndex, blobs map[v1.Hash]v1.Layer, repo name.Repository, newManifests []map[name.Reference]Taggable, lvl int, allowNondistributableArtifacts bool) ([]map[name.Reference]Taggable, error) {
if lvl > len(newManifests)-1 {
newManifests = append(newManifests, map[name.Reference]Taggable{})
}
@@ -177,7 +227,7 @@ func addIndexBlobs(idx v1.ImageIndex, blobs map[v1.Hash]v1.Layer, repo name.Repo
if err != nil {
return nil, err
}
newManifests, err = addIndexBlobs(idx, blobs, repo, newManifests, lvl+1)
newManifests, err = addIndexBlobs(idx, blobs, repo, newManifests, lvl+1, allowNondistributableArtifacts)
if err != nil {
return nil, err
}
@@ -189,42 +239,59 @@ func addIndexBlobs(idx v1.ImageIndex, blobs map[v1.Hash]v1.Layer, repo name.Repo
if err != nil {
return nil, err
}
if err := addImageBlobs(img, blobs); err != nil {
if err := addImageBlobs(img, blobs, allowNondistributableArtifacts); err != nil {
return nil, err
}
// Also track the sub-image manifest to upload later by digest.
newManifests[lvl][repo.Digest(desc.Digest.String())] = img
default:
return nil, fmt.Errorf("unknown media type: %v", desc.MediaType)
// Workaround for #819.
if wl, ok := idx.(withLayer); ok {
layer, err := wl.Layer(desc.Digest)
if err != nil {
return nil, err
}
if err := addLayerBlob(layer, blobs, allowNondistributableArtifacts); err != nil {
return nil, err
}
} else {
return nil, fmt.Errorf("unknown media type: %v", desc.MediaType)
}
}
}
return newManifests, nil
}
func addImageBlobs(img v1.Image, blobs map[v1.Hash]v1.Layer) error {
func addLayerBlob(l v1.Layer, blobs map[v1.Hash]v1.Layer, allowNondistributableArtifacts bool) error {
// Ignore foreign layers.
mt, err := l.MediaType()
if err != nil {
return err
}
if mt.IsDistributable() || allowNondistributableArtifacts {
d, err := l.Digest()
if err != nil {
return err
}
blobs[d] = l
}
return nil
}
func addImageBlobs(img v1.Image, blobs map[v1.Hash]v1.Layer, allowNondistributableArtifacts bool) error {
ls, err := img.Layers()
if err != nil {
return err
}
// Collect all layers.
for _, l := range ls {
d, err := l.Digest()
if err != nil {
if err := addLayerBlob(l, blobs, allowNondistributableArtifacts); err != nil {
return err
}
// Ignore foreign layers.
mt, err := l.MediaType()
if err != nil {
return err
}
if !mt.IsDistributable() {
// TODO(jonjohnsonjr): Add "allow-nondistributable-artifacts" option.
continue
}
blobs[d] = l
}
// Collect config blob.
@@ -232,10 +299,5 @@ func addImageBlobs(img v1.Image, blobs map[v1.Hash]v1.Layer) error {
if err != nil {
return err
}
cld, err := cl.Digest()
if err != nil {
return err
}
blobs[cld] = cl
return nil
return addLayerBlob(cl, blobs, allowNondistributableArtifacts)
}
+137 -28
View File
@@ -17,8 +17,13 @@ package remote
import (
"context"
"errors"
"io"
"net"
"net/http"
"syscall"
"time"
"github.com/google/go-containerregistry/internal/retry"
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/logs"
v1 "github.com/google/go-containerregistry/pkg/v1"
@@ -29,13 +34,18 @@ import (
type Option func(*options) error
type options struct {
auth authn.Authenticator
keychain authn.Keychain
transport http.RoundTripper
platform v1.Platform
context context.Context
jobs int
userAgent string
auth authn.Authenticator
keychain authn.Keychain
transport http.RoundTripper
platform v1.Platform
context context.Context
jobs int
userAgent string
allowNondistributableArtifacts bool
updates chan<- v1.Update
pageSize int
retryBackoff Backoff
retryPredicate retry.Predicate
}
var defaultPlatform = v1.Platform{
@@ -43,15 +53,63 @@ var defaultPlatform = v1.Platform{
OS: "linux",
}
const defaultJobs = 4
// Backoff is an alias of retry.Backoff to expose this configuration option to consumers of this lib
type Backoff = retry.Backoff
var defaultRetryPredicate retry.Predicate = func(err error) bool {
// Various failure modes here, as we're often reading from and writing to
// the network.
if retry.IsTemporary(err) || errors.Is(err, io.ErrUnexpectedEOF) || errors.Is(err, syscall.EPIPE) {
logs.Warn.Printf("retrying %v", err)
return true
}
return false
}
// Try this three times, waiting 1s after first failure, 3s after second.
var defaultRetryBackoff = Backoff{
Duration: 1.0 * time.Second,
Factor: 3.0,
Jitter: 0.1,
Steps: 3,
}
const (
defaultJobs = 4
// ECR returns an error if n > 1000:
// https://github.com/google/go-containerregistry/issues/1091
defaultPageSize = 1000
)
// DefaultTransport is based on http.DefaultTransport with modifications
// documented inline below.
var DefaultTransport = &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
// By default we wrap the transport in retries, so reduce the
// default dial timeout to 5s to avoid 5x 30s of connection
// timeouts when doing the "ping" on certain http registries.
Timeout: 5 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
func makeOptions(target authn.Resource, opts ...Option) (*options, error) {
o := &options{
auth: authn.Anonymous,
transport: http.DefaultTransport,
platform: defaultPlatform,
context: context.Background(),
jobs: defaultJobs,
auth: authn.Anonymous,
transport: DefaultTransport,
platform: defaultPlatform,
context: context.Background(),
jobs: defaultJobs,
pageSize: defaultPageSize,
retryPredicate: defaultRetryPredicate,
retryBackoff: defaultRetryBackoff,
}
for _, option := range opts {
@@ -65,25 +123,26 @@ func makeOptions(target authn.Resource, opts ...Option) (*options, error) {
if err != nil {
return nil, err
}
if auth == authn.Anonymous {
logs.Warn.Printf("No matching credentials were found for %q, falling back on anonymous", target)
}
o.auth = auth
}
// Wrap the transport in something that logs requests and responses.
// It's expensive to generate the dumps, so skip it if we're writing
// to nothing.
if logs.Enabled(logs.Debug) {
o.transport = transport.NewLogger(o.transport)
}
// transport.Wrapper is a signal that consumers are opt-ing into providing their own transport without any additional wrapping.
// This is to allow consumers full control over the transports logic, such as providing retry logic.
if _, ok := o.transport.(*transport.Wrapper); !ok {
// Wrap the transport in something that logs requests and responses.
// It's expensive to generate the dumps, so skip it if we're writing
// to nothing.
if logs.Enabled(logs.Debug) {
o.transport = transport.NewLogger(o.transport)
}
// Wrap the transport in something that can retry network flakes.
o.transport = transport.NewRetry(o.transport)
// Wrap the transport in something that can retry network flakes.
o.transport = transport.NewRetry(o.transport)
// Wrap this last to prevent transport.New from double-wrapping.
if o.userAgent != "" {
o.transport = transport.NewUserAgent(o.transport, o.userAgent)
// Wrap this last to prevent transport.New from double-wrapping.
if o.userAgent != "" {
o.transport = transport.NewUserAgent(o.transport, o.userAgent)
}
}
return o, nil
@@ -91,8 +150,10 @@ func makeOptions(target authn.Resource, opts ...Option) (*options, error) {
// WithTransport is a functional option for overriding the default transport
// for remote operations.
// If transport.Wrapper is provided, this signals that the consumer does *not* want any further wrapping to occur.
// i.e. logging, retry and useragent
//
// The default transport its http.DefaultTransport.
// The default transport is DefaultTransport.
func WithTransport(t http.RoundTripper) Option {
return func(o *options) error {
o.transport = t
@@ -173,3 +234,51 @@ func WithUserAgent(ua string) Option {
return nil
}
}
// WithNondistributable includes non-distributable (foreign) layers
// when writing images, see:
// https://github.com/opencontainers/image-spec/blob/master/layer.md#non-distributable-layers
//
// The default behaviour is to skip these layers
func WithNondistributable(o *options) error {
o.allowNondistributableArtifacts = true
return nil
}
// WithProgress takes a channel that will receive progress updates as bytes are written.
//
// Sending updates to an unbuffered channel will block writes, so callers
// should provide a buffered channel to avoid potential deadlocks.
func WithProgress(updates chan<- v1.Update) Option {
return func(o *options) error {
o.updates = updates
return nil
}
}
// WithPageSize sets the given size as the value of parameter 'n' in the request.
//
// To omit the `n` parameter entirely, use WithPageSize(0).
// The default value is 1000.
func WithPageSize(size int) Option {
return func(o *options) error {
o.pageSize = size
return nil
}
}
// WithRetryBackoff sets the httpBackoff for retry HTTP operations.
func WithRetryBackoff(backoff Backoff) Option {
return func(o *options) error {
o.retryBackoff = backoff
return nil
}
}
// WithRetryPredicate sets the predicate for retry HTTP operations.
func WithRetryPredicate(predicate retry.Predicate) Option {
return func(o *options) error {
o.retryPredicate = predicate
return nil
}
}
@@ -17,6 +17,7 @@ package transport
import (
"context"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net"
@@ -25,8 +26,9 @@ import (
"strings"
authchallenge "github.com/docker/distribution/registry/client/auth/challenge"
"github.com/google/go-containerregistry/internal/redact"
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/internal/redact"
"github.com/google/go-containerregistry/pkg/logs"
"github.com/google/go-containerregistry/pkg/name"
)
@@ -138,7 +140,8 @@ func (bt *bearerTransport) refresh(ctx context.Context) error {
// the Username should be set to <token>, which indicates
// we are using an oauth flow.
content, err = bt.refreshOauth(ctx)
if terr, ok := err.(*Error); ok && terr.StatusCode == http.StatusNotFound {
var terr *Error
if errors.As(err, &terr) && terr.StatusCode == http.StatusNotFound {
// Note: Not all token servers implement oauth2.
// If the request to the endpoint returns 404 using the HTTP POST method,
// refer to Token Documentation for using the HTTP GET method supported by all token servers.
@@ -262,6 +265,7 @@ func (bt *bearerTransport) refreshOauth(ctx context.Context) ([]byte, error) {
defer resp.Body.Close()
if err := CheckError(resp, http.StatusOK); err != nil {
logs.Warn.Printf("No matching credentials were found for %q", bt.registry)
return nil, err
}
@@ -301,6 +305,7 @@ func (bt *bearerTransport) refreshBasic(ctx context.Context) ([]byte, error) {
defer resp.Body.Close()
if err := CheckError(resp, http.StatusOK); err != nil {
logs.Warn.Printf("No matching credentials were found for %q", bt.registry)
return nil, err
}
@@ -46,10 +46,10 @@ type Error struct {
Errors []Diagnostic `json:"errors,omitempty"`
// The http status code returned.
StatusCode int
// The request that failed.
Request *http.Request
// The raw body if we couldn't understand it.
rawBody string
// The request that failed.
request *http.Request
}
// Check that Error implements error
@@ -58,8 +58,8 @@ var _ error = (*Error)(nil)
// Error implements error
func (e *Error) Error() string {
prefix := ""
if e.request != nil {
prefix = fmt.Sprintf("%s %s: ", e.request.Method, redactURL(e.request.URL))
if e.Request != nil {
prefix = fmt.Sprintf("%s %s: ", e.Request.Method, redactURL(e.Request.URL))
}
return prefix + e.responseErr()
}
@@ -68,7 +68,7 @@ func (e *Error) responseErr() string {
switch len(e.Errors) {
case 0:
if len(e.rawBody) == 0 {
if e.request != nil && e.request.Method == http.MethodHead {
if e.Request != nil && e.Request.Method == http.MethodHead {
return fmt.Sprintf("unexpected status code %d %s (HEAD responses have no body, use GET for details)", e.StatusCode, http.StatusText(e.StatusCode))
}
return fmt.Sprintf("unexpected status code %d %s", e.StatusCode, http.StatusText(e.StatusCode))
@@ -100,7 +100,7 @@ func (e *Error) Temporary() bool {
return true
}
// TODO(jonjohnsonjr): Consider moving to pkg/internal/redact.
// TODO(jonjohnsonjr): Consider moving to internal/redact.
func redactURL(original *url.URL) *url.URL {
qs := original.Query()
for k, v := range qs {
@@ -154,18 +154,27 @@ const (
DeniedErrorCode ErrorCode = "DENIED"
UnsupportedErrorCode ErrorCode = "UNSUPPORTED"
TooManyRequestsErrorCode ErrorCode = "TOOMANYREQUESTS"
UnknownErrorCode ErrorCode = "UNKNOWN"
// This isn't defined by either docker or OCI spec, but is defined by docker/distribution:
// https://github.com/distribution/distribution/blob/6a977a5a754baa213041443f841705888107362a/registry/api/errcode/register.go#L60
UnavailableErrorCode ErrorCode = "UNAVAILABLE"
)
// TODO: Include other error types.
var temporaryErrorCodes = map[ErrorCode]struct{}{
BlobUploadInvalidErrorCode: {},
TooManyRequestsErrorCode: {},
UnknownErrorCode: {},
UnavailableErrorCode: {},
}
var temporaryStatusCodes = map[int]struct{}{
http.StatusRequestTimeout: {},
http.StatusInternalServerError: {},
http.StatusBadGateway: {},
http.StatusServiceUnavailable: {},
http.StatusGatewayTimeout: {},
}
// CheckError returns a structured error if the response status is not in codes.
@@ -190,7 +199,7 @@ func CheckError(resp *http.Response, codes ...int) error {
structuredError.rawBody = string(b)
structuredError.StatusCode = resp.StatusCode
structuredError.request = resp.Request
structuredError.Request = resp.Request
return structuredError
}
@@ -20,7 +20,7 @@ import (
"net/http/httputil"
"time"
"github.com/google/go-containerregistry/pkg/internal/redact"
"github.com/google/go-containerregistry/internal/redact"
"github.com/google/go-containerregistry/pkg/logs"
)
@@ -108,8 +108,8 @@ func ping(ctx context.Context, reg name.Registry, t http.RoundTripper) (*pingRes
}, nil
case http.StatusUnauthorized:
if challenges := authchallenge.ResponseChallenges(resp); len(challenges) != 0 {
// If we hit more than one, I'm not even sure what to do.
wac := challenges[0]
// If we hit more than one, let's try to find one that we know how to handle.
wac := pickFromMultipleChallenges(challenges)
return &pingResp{
challenge: challenge(wac.Scheme).Canonical(),
parameters: wac.Parameters,
@@ -127,3 +127,21 @@ func ping(ctx context.Context, reg name.Registry, t http.RoundTripper) (*pingRes
}
return nil, errors.New(strings.Join(errs, "; "))
}
func pickFromMultipleChallenges(challenges []authchallenge.Challenge) authchallenge.Challenge {
// It might happen there are multiple www-authenticate headers, e.g. `Negotiate` and `Basic`.
// Picking simply the first one could result eventually in `unrecognized challenge` error,
// that's why we're looping through the challenges in search for one that can be handled.
allowedSchemes := []string{"basic", "bearer"}
for _, wac := range challenges {
currentScheme := strings.ToLower(wac.Scheme)
for _, allowed := range allowedSchemes {
if allowed == currentScheme {
return wac
}
}
}
return challenges[0]
}
@@ -18,7 +18,7 @@ import (
"net/http"
"time"
"github.com/google/go-containerregistry/pkg/internal/retry"
"github.com/google/go-containerregistry/internal/retry"
)
// Sleep for 0.1, 0.3, 0.9, 2.7 seconds. This should cover networking blips.
@@ -46,8 +46,11 @@ type options struct {
predicate retry.Predicate
}
// Backoff is an alias of retry.Backoff to expose this configuration option to consumers of this lib
type Backoff = retry.Backoff
// WithRetryBackoff sets the backoff for retry operations.
func WithRetryBackoff(backoff retry.Backoff) Option {
func WithRetryBackoff(backoff Backoff) Option {
return func(o *options) {
o.backoff = backoff
}
@@ -68,10 +68,8 @@ func NewWithContext(ctx context.Context, reg name.Registry, auth authn.Authentic
}
switch pr.challenge.Canonical() {
case anonymous:
return t, nil
case basic:
return &basicTransport{inner: t, auth: auth, target: reg.RegistryStr()}, nil
case anonymous, basic:
return &Wrapper{&basicTransport{inner: t, auth: auth, target: reg.RegistryStr()}}, nil
case bearer:
// We require the realm, which tells us where to send our Basic auth to turn it into Bearer auth.
realm, ok := pr.parameters["realm"]
@@ -96,8 +94,19 @@ func NewWithContext(ctx context.Context, reg name.Registry, auth authn.Authentic
if err := bt.refresh(ctx); err != nil {
return nil, err
}
return bt, nil
return &Wrapper{bt}, nil
default:
return nil, fmt.Errorf("unrecognized challenge: %s", pr.challenge)
}
}
// Wrapper results in *not* wrapping supplied transport with additional logic such as retries, useragent and debug logging
// Consumers are opt-ing into providing their own transport without any additional wrapping.
type Wrapper struct {
inner http.RoundTripper
}
// RoundTrip delegates to the inner RoundTripper
func (w *Wrapper) RoundTrip(in *http.Request) (*http.Response, error) {
return w.inner.RoundTrip(in)
}
+385 -108
View File
@@ -23,15 +23,16 @@ import (
"net/http"
"net/url"
"strings"
"time"
"sync/atomic"
"github.com/google/go-containerregistry/pkg/internal/redact"
"github.com/google/go-containerregistry/pkg/internal/retry"
"github.com/google/go-containerregistry/internal/redact"
"github.com/google/go-containerregistry/internal/retry"
"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/partial"
"github.com/google/go-containerregistry/pkg/v1/remote/transport"
"github.com/google/go-containerregistry/pkg/v1/stream"
"github.com/google/go-containerregistry/pkg/v1/types"
"golang.org/x/sync/errgroup"
)
@@ -42,63 +43,97 @@ type Taggable interface {
}
// Write pushes the provided img to the specified image reference.
func Write(ref name.Reference, img v1.Image, options ...Option) error {
ls, err := img.Layers()
if err != nil {
return err
}
func Write(ref name.Reference, img v1.Image, options ...Option) (rerr error) {
o, err := makeOptions(ref.Context(), options...)
if err != nil {
return err
}
var lastUpdate *v1.Update
if o.updates != nil {
lastUpdate = &v1.Update{}
lastUpdate.Total, err = countImage(img, o.allowNondistributableArtifacts)
if err != nil {
return err
}
defer close(o.updates)
defer func() { _ = sendError(o.updates, rerr) }()
}
return writeImage(o.context, ref, img, o, lastUpdate)
}
func writeImage(ctx context.Context, ref name.Reference, img v1.Image, o *options, lastUpdate *v1.Update) error {
ls, err := img.Layers()
if err != nil {
return err
}
scopes := scopesForUploadingImage(ref.Context(), ls)
tr, err := transport.NewWithContext(o.context, ref.Context().Registry, o.auth, o.transport, scopes)
if err != nil {
return err
}
w := writer{
repo: ref.Context(),
client: &http.Client{Transport: tr},
context: o.context,
repo: ref.Context(),
client: &http.Client{Transport: tr},
context: ctx,
updates: o.updates,
lastUpdate: lastUpdate,
backoff: o.retryBackoff,
predicate: o.retryPredicate,
}
// Upload individual blobs and collect any errors.
blobChan := make(chan v1.Layer, 2*o.jobs)
g, gctx := errgroup.WithContext(ctx)
for i := 0; i < o.jobs; i++ {
// Start N workers consuming blobs to upload.
g.Go(func() error {
for b := range blobChan {
if err := w.uploadOne(gctx, b); err != nil {
return err
}
}
return nil
})
}
// Upload individual layers in goroutines and collect any errors.
// If we can dedupe by the layer digest, try to do so. If we can't determine
// the digest for whatever reason, we can't dedupe and might re-upload.
var g errgroup.Group
uploaded := map[v1.Hash]bool{}
for _, l := range ls {
l := l
g.Go(func() error {
defer close(blobChan)
uploaded := map[v1.Hash]bool{}
for _, l := range ls {
l := l
// Handle foreign layers.
mt, err := l.MediaType()
if err != nil {
return err
}
if !mt.IsDistributable() {
// TODO(jonjohnsonjr): Add "allow-nondistributable-artifacts" option.
continue
}
// Streaming layers calculate their digests while uploading them. Assume
// an error here indicates we need to upload the layer.
h, err := l.Digest()
if err == nil {
// If we can determine the layer's digest ahead of
// time, use it to dedupe uploads.
if uploaded[h] {
continue // Already uploading.
// Handle foreign layers.
mt, err := l.MediaType()
if err != nil {
return err
}
if !mt.IsDistributable() && !o.allowNondistributableArtifacts {
continue
}
uploaded[h] = true
}
// TODO(#803): Pipe through remote.WithJobs and upload these in parallel.
g.Go(func() error {
return w.uploadOne(l)
})
}
// Streaming layers calculate their digests while uploading them. Assume
// an error here indicates we need to upload the layer.
h, err := l.Digest()
if err == nil {
// If we can determine the layer's digest ahead of
// time, use it to dedupe uploads.
if uploaded[h] {
continue // Already uploading.
}
uploaded[h] = true
}
select {
case blobChan <- l:
case <-gctx.Done():
return gctx.Err()
}
}
return nil
})
if l, err := partial.ConfigLayer(img); err != nil {
// We can't read the ConfigLayer, possibly because of streaming layers,
@@ -113,13 +148,13 @@ func Write(ref name.Reference, img v1.Image, options ...Option) error {
if err != nil {
return err
}
if err := w.uploadOne(l); err != nil {
if err := w.uploadOne(ctx, l); err != nil {
return err
}
} else {
// We *can* read the ConfigLayer, so upload it concurrently with the layers.
g.Go(func() error {
return w.uploadOne(l)
return w.uploadOne(gctx, l)
})
// Wait for the layers + config.
@@ -130,7 +165,7 @@ func Write(ref name.Reference, img v1.Image, options ...Option) error {
// With all of the constituent elements uploaded, upload the manifest
// to commit the image.
return w.commitManifest(img, ref)
return w.commitManifest(ctx, img, ref)
}
// writer writes the elements of an image to a remote image reference.
@@ -138,6 +173,18 @@ type writer struct {
repo name.Repository
client *http.Client
context context.Context
updates chan<- v1.Update
lastUpdate *v1.Update
backoff Backoff
predicate retry.Predicate
}
func sendError(ch chan<- v1.Update, err error) error {
if err != nil && ch != nil {
ch <- v1.Update{Error: err}
}
return err
}
// url returns a url.Url for the specified path in the context of this remote image reference.
@@ -260,14 +307,54 @@ func (w *writer) initiateUpload(from, mount string) (location string, mounted bo
}
}
type progressReader struct {
rc io.ReadCloser
count *int64 // number of bytes this reader has read, to support resetting on retry.
updates chan<- v1.Update
lastUpdate *v1.Update
}
func (r *progressReader) Read(b []byte) (int, error) {
n, err := r.rc.Read(b)
if err != nil {
return n, err
}
atomic.AddInt64(r.count, int64(n))
// TODO: warn/debug log if sending takes too long, or if sending is blocked while context is cancelled.
r.updates <- v1.Update{
Total: r.lastUpdate.Total,
Complete: atomic.AddInt64(&r.lastUpdate.Complete, int64(n)),
}
return n, nil
}
func (r *progressReader) Close() error { return r.rc.Close() }
// streamBlob streams the contents of the blob to the specified location.
// On failure, this will return an error. On success, this will return the location
// header indicating how to commit the streamed blob.
func (w *writer) streamBlob(ctx context.Context, blob io.ReadCloser, streamLocation string) (commitLocation string, err error) {
func (w *writer) streamBlob(ctx context.Context, blob io.ReadCloser, streamLocation string) (commitLocation string, rerr error) {
reset := func() {}
defer func() {
if rerr != nil {
reset()
}
}()
if w.updates != nil {
var count int64
blob = &progressReader{rc: blob, updates: w.updates, lastUpdate: w.lastUpdate, count: &count}
reset = func() {
atomic.AddInt64(&w.lastUpdate.Complete, -count)
w.updates <- *w.lastUpdate
}
}
req, err := http.NewRequest(http.MethodPatch, streamLocation, blob)
if err != nil {
return "", err
}
req.Header.Set("Content-Type", "application/octet-stream")
resp, err := w.client.Do(req.WithContext(ctx))
if err != nil {
@@ -299,6 +386,7 @@ func (w *writer) commitBlob(location, digest string) error {
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/octet-stream")
resp, err := w.client.Do(req.WithContext(w.context))
if err != nil {
@@ -309,8 +397,19 @@ func (w *writer) commitBlob(location, digest string) error {
return transport.CheckError(resp, http.StatusCreated)
}
// incrProgress increments and sends a progress update, if WithProgress is used.
func (w *writer) incrProgress(written int64) {
if w.updates == nil {
return
}
w.updates <- v1.Update{
Total: w.lastUpdate.Total,
Complete: atomic.AddInt64(&w.lastUpdate.Complete, written),
}
}
// uploadOne performs a complete upload of a single layer.
func (w *writer) uploadOne(l v1.Layer) error {
func (w *writer) uploadOne(ctx context.Context, l v1.Layer) error {
var from, mount string
if h, err := l.Digest(); err == nil {
// If we know the digest, this isn't a streaming layer. Do an existence
@@ -320,6 +419,11 @@ func (w *writer) uploadOne(l v1.Layer) error {
return err
}
if existing {
size, err := l.Size()
if err != nil {
return err
}
w.incrProgress(size)
logs.Progress.Printf("existing blob: %v", h)
return nil
}
@@ -332,13 +436,16 @@ func (w *writer) uploadOne(l v1.Layer) error {
}
}
ctx := w.context
tryUpload := func() error {
location, mounted, err := w.initiateUpload(from, mount)
if err != nil {
return err
} else if mounted {
size, err := l.Size()
if err != nil {
return err
}
w.incrProgress(size)
h, err := l.Digest()
if err != nil {
return err
@@ -380,23 +487,24 @@ func (w *writer) uploadOne(l v1.Layer) error {
return nil
}
// Try this three times, waiting 1s after first failure, 3s after second.
backoff := retry.Backoff{
Duration: 1.0 * time.Second,
Factor: 3.0,
Jitter: 0.1,
Steps: 3,
}
return retry.Retry(tryUpload, retry.IsTemporary, backoff)
return retry.Retry(tryUpload, w.predicate, w.backoff)
}
func (w *writer) writeIndex(ref name.Reference, ii v1.ImageIndex, options ...Option) error {
type withLayer interface {
Layer(v1.Hash) (v1.Layer, error)
}
func (w *writer) writeIndex(ctx context.Context, ref name.Reference, ii v1.ImageIndex, options ...Option) error {
index, err := ii.IndexManifest()
if err != nil {
return err
}
o, err := makeOptions(ref.Context(), options...)
if err != nil {
return err
}
// TODO(#803): Pipe through remote.WithJobs and upload these in parallel.
for _, desc := range index.Manifests {
ref := ref.Context().Digest(desc.Digest.String())
@@ -415,8 +523,7 @@ func (w *writer) writeIndex(ref name.Reference, ii v1.ImageIndex, options ...Opt
if err != nil {
return err
}
if err := w.writeIndex(ref, ii); err != nil {
if err := w.writeIndex(ctx, ref, ii, options...); err != nil {
return err
}
case types.OCIManifestSchema1, types.DockerManifestSchema2:
@@ -424,18 +531,26 @@ func (w *writer) writeIndex(ref name.Reference, ii v1.ImageIndex, options ...Opt
if err != nil {
return err
}
// TODO: Ideally we could reuse this writer, but we need to know
// scopes before we do the token exchange. To be lazy here, just
// re-do the token exchange. MultiWrite fixes this.
if err := Write(ref, img, options...); err != nil {
if err := writeImage(ctx, ref, img, o, w.lastUpdate); err != nil {
return err
}
default:
// Workaround for #819.
if wl, ok := ii.(withLayer); ok {
layer, err := wl.Layer(desc.Digest)
if err != nil {
return err
}
if err := w.uploadOne(ctx, layer); err != nil {
return err
}
}
}
}
// With all of the constituent elements uploaded, upload the manifest
// to commit the image.
return w.commitManifest(ii, ref)
return w.commitManifest(ctx, ii, ref)
}
type withMediaType interface {
@@ -481,34 +596,39 @@ func unpackTaggable(t Taggable) ([]byte, *v1.Descriptor, error) {
}
// commitManifest does a PUT of the image's manifest.
func (w *writer) commitManifest(t Taggable, ref name.Reference) error {
raw, desc, err := unpackTaggable(t)
if err != nil {
return err
func (w *writer) commitManifest(ctx context.Context, t Taggable, ref name.Reference) error {
tryUpload := func() error {
raw, desc, err := unpackTaggable(t)
if err != nil {
return err
}
u := w.url(fmt.Sprintf("/v2/%s/manifests/%s", w.repo.RepositoryStr(), ref.Identifier()))
// Make the request to PUT the serialized manifest
req, err := http.NewRequest(http.MethodPut, u.String(), bytes.NewBuffer(raw))
if err != nil {
return err
}
req.Header.Set("Content-Type", string(desc.MediaType))
resp, err := w.client.Do(req.WithContext(ctx))
if err != nil {
return err
}
defer resp.Body.Close()
if err := transport.CheckError(resp, http.StatusOK, http.StatusCreated, http.StatusAccepted); err != nil {
return err
}
// The image was successfully pushed!
logs.Progress.Printf("%v: digest: %v size: %d", ref, desc.Digest, desc.Size)
w.incrProgress(int64(len(raw)))
return nil
}
u := w.url(fmt.Sprintf("/v2/%s/manifests/%s", w.repo.RepositoryStr(), ref.Identifier()))
// Make the request to PUT the serialized manifest
req, err := http.NewRequest(http.MethodPut, u.String(), bytes.NewBuffer(raw))
if err != nil {
return err
}
req.Header.Set("Content-Type", string(desc.MediaType))
resp, err := w.client.Do(req.WithContext(w.context))
if err != nil {
return err
}
defer resp.Body.Close()
if err := transport.CheckError(resp, http.StatusOK, http.StatusCreated, http.StatusAccepted); err != nil {
return err
}
// The image was successfully pushed!
logs.Progress.Printf("%v: digest: %v size: %d", ref, desc.Digest, desc.Size)
return nil
return retry.Retry(tryUpload, w.predicate, w.backoff)
}
func scopesForUploadingImage(repo name.Repository, layers []v1.Layer) []string {
@@ -539,26 +659,149 @@ func scopesForUploadingImage(repo name.Repository, layers []v1.Layer) []string {
// WriteIndex pushes the provided ImageIndex to the specified image reference.
// WriteIndex will attempt to push all of the referenced manifests before
// attempting to push the ImageIndex, to retain referential integrity.
func WriteIndex(ref name.Reference, ii v1.ImageIndex, options ...Option) error {
func WriteIndex(ref name.Reference, ii v1.ImageIndex, options ...Option) (rerr error) {
o, err := makeOptions(ref.Context(), options...)
if err != nil {
return err
}
scopes := []string{ref.Scope(transport.PushScope)}
tr, err := transport.NewWithContext(o.context, ref.Context().Registry, o.auth, o.transport, scopes)
if err != nil {
return err
}
w := writer{
repo: ref.Context(),
client: &http.Client{Transport: tr},
context: o.context,
repo: ref.Context(),
client: &http.Client{Transport: tr},
context: o.context,
updates: o.updates,
backoff: o.retryBackoff,
predicate: o.retryPredicate,
}
return w.writeIndex(ref, ii, options...)
if o.updates != nil {
w.lastUpdate = &v1.Update{}
w.lastUpdate.Total, err = countIndex(ii, o.allowNondistributableArtifacts)
if err != nil {
return err
}
defer close(o.updates)
defer func() { sendError(o.updates, rerr) }()
}
return w.writeIndex(o.context, ref, ii, options...)
}
// countImage counts the total size of all layers + config blob + manifest for
// an image. It de-dupes duplicate layers.
func countImage(img v1.Image, allowNondistributableArtifacts bool) (int64, error) {
var total int64
ls, err := img.Layers()
if err != nil {
return 0, err
}
seen := map[v1.Hash]bool{}
for _, l := range ls {
// Handle foreign layers.
mt, err := l.MediaType()
if err != nil {
return 0, err
}
if !mt.IsDistributable() && !allowNondistributableArtifacts {
continue
}
// TODO: support streaming layers which update the total count as they write.
if _, ok := l.(*stream.Layer); ok {
return 0, errors.New("cannot use stream.Layer and WithProgress")
}
// Dedupe layers.
d, err := l.Digest()
if err != nil {
return 0, err
}
if seen[d] {
continue
}
seen[d] = true
size, err := l.Size()
if err != nil {
return 0, err
}
total += size
}
b, err := img.RawConfigFile()
if err != nil {
return 0, err
}
total += int64(len(b))
size, err := img.Size()
if err != nil {
return 0, err
}
total += size
return total, nil
}
// countIndex counts the total size of all images + sub-indexes for an index.
// It does not attempt to de-dupe duplicate images, etc.
func countIndex(idx v1.ImageIndex, allowNondistributableArtifacts bool) (int64, error) {
var total int64
mf, err := idx.IndexManifest()
if err != nil {
return 0, err
}
for _, desc := range mf.Manifests {
switch desc.MediaType {
case types.OCIImageIndex, types.DockerManifestList:
sidx, err := idx.ImageIndex(desc.Digest)
if err != nil {
return 0, err
}
size, err := countIndex(sidx, allowNondistributableArtifacts)
if err != nil {
return 0, err
}
total += size
case types.OCIManifestSchema1, types.DockerManifestSchema2:
simg, err := idx.Image(desc.Digest)
if err != nil {
return 0, err
}
size, err := countImage(simg, allowNondistributableArtifacts)
if err != nil {
return 0, err
}
total += size
default:
// Workaround for #819.
if wl, ok := idx.(withLayer); ok {
layer, err := wl.Layer(desc.Digest)
if err != nil {
return 0, err
}
size, err := layer.Size()
if err != nil {
return 0, err
}
total += size
}
}
}
size, err := idx.Size()
if err != nil {
return 0, err
}
total += size
return total, nil
}
// WriteLayer uploads the provided Layer to the specified repo.
func WriteLayer(repo name.Repository, layer v1.Layer, options ...Option) error {
func WriteLayer(repo name.Repository, layer v1.Layer, options ...Option) (rerr error) {
o, err := makeOptions(repo, options...)
if err != nil {
return err
@@ -569,12 +812,29 @@ func WriteLayer(repo name.Repository, layer v1.Layer, options ...Option) error {
return err
}
w := writer{
repo: repo,
client: &http.Client{Transport: tr},
context: o.context,
repo: repo,
client: &http.Client{Transport: tr},
context: o.context,
updates: o.updates,
backoff: o.retryBackoff,
predicate: o.retryPredicate,
}
return w.uploadOne(layer)
if o.updates != nil {
defer close(o.updates)
defer func() { sendError(o.updates, rerr) }()
// TODO: support streaming layers which update the total count as they write.
if _, ok := layer.(*stream.Layer); ok {
return errors.New("cannot use stream.Layer and WithProgress")
}
size, err := layer.Size()
if err != nil {
return err
}
w.lastUpdate = &v1.Update{Total: size}
}
return w.uploadOne(o.context, layer)
}
// Tag adds a tag to the given Taggable via PUT /v2/.../manifests/<tag>
@@ -589,11 +849,26 @@ func WriteLayer(repo name.Repository, layer v1.Layer, options ...Option) error {
// should ensure that all blobs or manifests that are referenced by t exist
// in the target registry.
func Tag(tag name.Tag, t Taggable, options ...Option) error {
o, err := makeOptions(tag.Context(), options...)
return Put(tag, t, options...)
}
// Put adds a manifest from the given Taggable via PUT /v1/.../manifest/<ref>
//
// Notable implementations of Taggable are v1.Image, v1.ImageIndex, and
// remote.Descriptor.
//
// If t implements MediaType, we will use that for the Content-Type, otherwise
// we will default to types.DockerManifestSchema2.
//
// Put does not attempt to write anything other than the manifest, so callers
// should ensure that all blobs or manifests that are referenced by t exist
// in the target registry.
func Put(ref name.Reference, t Taggable, options ...Option) error {
o, err := makeOptions(ref.Context(), options...)
if err != nil {
return err
}
scopes := []string{tag.Scope(transport.PushScope)}
scopes := []string{ref.Scope(transport.PushScope)}
// TODO: This *always* does a token exchange. For some registries,
// that's pretty slow. Some ideas;
@@ -601,15 +876,17 @@ func Tag(tag name.Tag, t Taggable, options ...Option) error {
// * Allow callers to pass in a transport.Transport, typecheck
// it to allow them to reuse the transport across multiple calls.
// * WithTag option to do multiple manifest PUTs in commitManifest.
tr, err := transport.NewWithContext(o.context, tag.Context().Registry, o.auth, o.transport, scopes)
tr, err := transport.NewWithContext(o.context, ref.Context().Registry, o.auth, o.transport, scopes)
if err != nil {
return err
}
w := writer{
repo: tag.Context(),
client: &http.Client{Transport: tr},
context: o.context,
repo: ref.Context(),
client: &http.Client{Transport: tr},
context: o.context,
backoff: o.retryBackoff,
predicate: o.retryPredicate,
}
return w.commitManifest(t, tag)
return w.commitManifest(o.context, t, ref)
}
+89 -68
View File
@@ -126,18 +126,36 @@ func (l *Layer) Compressed() (io.ReadCloser, error) {
return newCompressedReader(l)
}
// finalize sets the layer to consumed and computes all hash and size values.
func (l *Layer) finalize(uncompressed, compressed hash.Hash, size int64) error {
l.mu.Lock()
defer l.mu.Unlock()
diffID, err := v1.NewHash("sha256:" + hex.EncodeToString(uncompressed.Sum(nil)))
if err != nil {
return err
}
l.diffID = &diffID
digest, err := v1.NewHash("sha256:" + hex.EncodeToString(compressed.Sum(nil)))
if err != nil {
return err
}
l.digest = &digest
l.size = size
l.consumed = true
return nil
}
type compressedReader struct {
closer io.Closer // original blob's Closer.
h, zh hash.Hash // collects digests of compressed and uncompressed stream.
pr io.Reader
bw *bufio.Writer
count *countWriter
l *Layer // stream.Layer to update upon Close.
pr io.Reader
closer func() error
}
func newCompressedReader(l *Layer) (*compressedReader, error) {
// Collect digests of compressed and uncompressed stream and size of
// compressed stream.
h := sha256.New()
zh := sha256.New()
count := &countWriter{}
@@ -158,24 +176,74 @@ func newCompressedReader(l *Layer) (*compressedReader, error) {
return nil, err
}
doneDigesting := make(chan struct{})
cr := &compressedReader{
closer: newMultiCloser(zw, l.blob),
pr: pr,
bw: bw,
h: h,
zh: zh,
count: count,
l: l,
pr: pr,
closer: func() error {
// Immediately close pw without error. There are three ways to get
// here.
//
// 1. There was a copy error due from the underlying reader, in which
// case the error will not be overwritten.
// 2. Copying from the underlying reader completed successfully.
// 3. Close has been called before the underlying reader has been
// fully consumed. In this case pw must be closed in order to
// keep the flush of bw from blocking indefinitely.
//
// NOTE: pw.Close never returns an error. The signature is only to
// implement io.Closer.
_ = pw.Close()
// Close the inner ReadCloser.
//
// NOTE: net/http will call close on success, so if we've already
// closed the inner rc, it's not an error.
if err := l.blob.Close(); err != nil && !errors.Is(err, os.ErrClosed) {
return err
}
// Finalize layer with its digest and size values.
<-doneDigesting
return l.finalize(h, zh, count.n)
},
}
go func() {
if _, err := io.Copy(io.MultiWriter(h, zw), l.blob); err != nil {
// Copy blob into the gzip writer, which also hashes and counts the
// size of the compressed output, and hasher of the raw contents.
_, copyErr := io.Copy(io.MultiWriter(h, zw), l.blob)
// Close the gzip writer once copying is done. If this is done in the
// Close method of compressedReader instead, then it can cause a panic
// when the compressedReader is closed before the blob is fully
// consumed and io.Copy in this goroutine is still blocking.
closeErr := zw.Close()
// Check errors from writing and closing streams.
if copyErr != nil {
close(doneDigesting)
pw.CloseWithError(copyErr)
return
}
if closeErr != nil {
close(doneDigesting)
pw.CloseWithError(closeErr)
return
}
// Flush the buffer once all writes are complete to the gzip writer.
if err := bw.Flush(); err != nil {
close(doneDigesting)
pw.CloseWithError(err)
return
}
// Now close the compressed reader, to flush the gzip stream
// and calculate digest/diffID/size. This will cause pr to
// return EOF which will cause readers of the Compressed stream
// to finish reading.
// Notify closer that digests are done being written.
close(doneDigesting)
// Close the compressed reader to calculate digest/diffID/size. This
// will cause pr to return EOF which will cause readers of the
// Compressed stream to finish reading.
pw.CloseWithError(cr.Close())
}()
@@ -184,36 +252,7 @@ func newCompressedReader(l *Layer) (*compressedReader, error) {
func (cr *compressedReader) Read(b []byte) (int, error) { return cr.pr.Read(b) }
func (cr *compressedReader) Close() error {
cr.l.mu.Lock()
defer cr.l.mu.Unlock()
// Close the inner ReadCloser.
if err := cr.closer.Close(); err != nil {
return err
}
// Flush the buffer.
if err := cr.bw.Flush(); err != nil {
return err
}
diffID, err := v1.NewHash("sha256:" + hex.EncodeToString(cr.h.Sum(nil)))
if err != nil {
return err
}
cr.l.diffID = &diffID
digest, err := v1.NewHash("sha256:" + hex.EncodeToString(cr.zh.Sum(nil)))
if err != nil {
return err
}
cr.l.digest = &digest
cr.l.size = cr.count.n
cr.l.consumed = true
return nil
}
func (cr *compressedReader) Close() error { return cr.closer() }
// countWriter counts bytes written to it.
type countWriter struct{ n int64 }
@@ -222,21 +261,3 @@ func (c *countWriter) Write(p []byte) (int, error) {
c.n += int64(len(p))
return len(p), nil
}
// multiCloser is a Closer that collects multiple Closers and Closes them in order.
type multiCloser []io.Closer
var _ io.Closer = (multiCloser)(nil)
func newMultiCloser(c ...io.Closer) multiCloser { return multiCloser(c) }
func (m multiCloser) Close() error {
for _, c := range m {
// NOTE: net/http will call close on success, so if we've already
// closed the inner rc, it's not an error.
if err := c.Close(); err != nil && !errors.Is(err, os.ErrClosed) {
return err
}
}
return nil
}
+24 -2
View File
@@ -23,11 +23,13 @@ import (
"io"
"io/ioutil"
"os"
"path"
"path/filepath"
"sync"
"github.com/google/go-containerregistry/internal/gzip"
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/internal/gzip"
"github.com/google/go-containerregistry/pkg/v1/partial"
"github.com/google/go-containerregistry/pkg/v1/types"
)
@@ -68,6 +70,22 @@ func ImageFromPath(path string, tag *name.Tag) (v1.Image, error) {
return Image(pathOpener(path), tag)
}
// LoadManifest load manifest
func LoadManifest(opener Opener) (Manifest, error) {
m, err := extractFileFromTar(opener, "manifest.json")
if err != nil {
return nil, err
}
defer m.Close()
var manifest Manifest
if err := json.NewDecoder(m).Decode(&manifest); err != nil {
return nil, err
}
return manifest, nil
}
// Image exposes an image from the tarball at the provided path.
func Image(opener Opener, tag *name.Tag) (v1.Image, error) {
img := &image{
@@ -209,13 +227,17 @@ func extractFileFromTar(opener Opener, filePath string) (io.ReadCloser, error) {
tf := tar.NewReader(f)
for {
hdr, err := tf.Next()
if err == io.EOF {
if errors.Is(err, io.EOF) {
break
}
if err != nil {
return nil, err
}
if hdr.Name == filePath {
if hdr.Typeflag == tar.TypeSymlink || hdr.Typeflag == tar.TypeLink {
currentDir := filepath.Dir(filePath)
return extractFileFromTar(opener, path.Join(currentDir, path.Clean(hdr.Linkname)))
}
close = false
return tarFile{
Reader: tf,
+3 -3
View File
@@ -23,10 +23,10 @@ import (
"sync"
"github.com/containerd/stargz-snapshotter/estargz"
"github.com/google/go-containerregistry/internal/and"
gestargz "github.com/google/go-containerregistry/internal/estargz"
ggzip "github.com/google/go-containerregistry/internal/gzip"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/internal/and"
gestargz "github.com/google/go-containerregistry/pkg/v1/internal/estargz"
ggzip "github.com/google/go-containerregistry/pkg/v1/internal/gzip"
"github.com/google/go-containerregistry/pkg/v1/types"
)
+13 -15
View File
@@ -98,7 +98,7 @@ func MultiRefWrite(refToImage map[name.Reference]v1.Image, w io.Writer, opts ...
}
}
size, _, mBytes, err := getSizeAndManifest(refToImage)
size, mBytes, err := getSizeAndManifest(refToImage)
if err != nil {
return sendUpdateReturn(o, err)
}
@@ -290,25 +290,25 @@ func calculateManifest(refToImage map[name.Reference]v1.Image) (m Manifest, err
// CalculateSize calculates the expected complete size of the output tar file
func CalculateSize(refToImage map[name.Reference]v1.Image) (size int64, err error) {
size, _, _, err = getSizeAndManifest(refToImage)
size, _, err = getSizeAndManifest(refToImage)
return size, err
}
func getSizeAndManifest(refToImage map[name.Reference]v1.Image) (size int64, m Manifest, mBytes []byte, err error) {
m, err = calculateManifest(refToImage)
func getSizeAndManifest(refToImage map[name.Reference]v1.Image) (int64, []byte, error) {
m, err := calculateManifest(refToImage)
if err != nil {
return 0, nil, nil, fmt.Errorf("unable to calculate manifest: %v", err)
return 0, nil, fmt.Errorf("unable to calculate manifest: %w", err)
}
mBytes, err = json.Marshal(m)
mBytes, err := json.Marshal(m)
if err != nil {
return 0, nil, nil, fmt.Errorf("could not marshall manifest to bytes: %v", err)
return 0, nil, fmt.Errorf("could not marshall manifest to bytes: %w", err)
}
size, err = calculateTarballSize(refToImage, mBytes)
size, err := calculateTarballSize(refToImage, mBytes)
if err != nil {
return 0, nil, nil, fmt.Errorf("error calculating tarball size: %v", err)
return 0, nil, fmt.Errorf("error calculating tarball size: %w", err)
}
return size, m, mBytes, nil
return size, mBytes, nil
}
// calculateTarballSize calculates the size of the tar file
@@ -318,7 +318,7 @@ func calculateTarballSize(refToImage map[name.Reference]v1.Image, mBytes []byte)
for img, name := range imageToTags {
manifest, err := img.Manifest()
if err != nil {
return size, fmt.Errorf("unable to get manifest for img %s: %v", name, err)
return size, fmt.Errorf("unable to get manifest for img %s: %w", name, err)
}
size += calculateSingleFileInTarSize(manifest.Config.Size)
for _, l := range manifest.Layers {
@@ -354,10 +354,8 @@ func dedupRefToImage(refToImage map[name.Reference]v1.Image) map[v1.Image][]stri
ts = fmt.Sprintf("%s:%s", ts, name.DefaultTag)
}
imageToTags[img] = append(imageToTags[img], ts)
} else {
if _, ok := imageToTags[img]; !ok {
imageToTags[img] = nil
}
} else if _, ok := imageToTags[img]; !ok {
imageToTags[img] = nil
}
}
+41 -4
View File
@@ -18,16 +18,18 @@ import (
"bytes"
"errors"
"fmt"
"io"
"strings"
"github.com/google/go-cmp/cmp"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/partial"
)
// Image validates that img does not violate any invariants of the image format.
func Image(img v1.Image) error {
func Image(img v1.Image, opt ...Option) error {
errs := []string{}
if err := validateLayers(img); err != nil {
if err := validateLayers(img, opt...); err != nil {
errs = append(errs, fmt.Sprintf("validating layers: %v", err))
}
@@ -100,18 +102,34 @@ func validateConfig(img v1.Image) error {
return nil
}
func validateLayers(img v1.Image) error {
func validateLayers(img v1.Image, opt ...Option) error {
o := makeOptions(opt...)
layers, err := img.Layers()
if err != nil {
return err
}
if o.fast {
return layersExist(layers)
}
digests := []v1.Hash{}
diffids := []v1.Hash{}
udiffids := []v1.Hash{}
sizes := []int64{}
for _, layer := range layers {
for i, layer := range layers {
cl, err := computeLayer(layer)
if errors.Is(err, io.ErrUnexpectedEOF) {
// Errored while reading tar content of layer because a header or
// content section was not the correct length. This is most likely
// due to an incomplete download or otherwise interrupted process.
m, err := img.Manifest()
if err != nil {
return fmt.Errorf("undersized layer[%d] content", i)
}
return fmt.Errorf("undersized layer[%d] content: Manifest.Layers[%d].Size=%d", i, i, m.Layers[i].Size)
}
if err != nil {
return err
}
@@ -249,3 +267,22 @@ func validateManifest(img v1.Image) error {
return nil
}
func layersExist(layers []v1.Layer) error {
errs := []string{}
for _, layer := range layers {
ok, err := partial.Exists(layer)
if err != nil {
errs = append(errs, err.Error())
}
if !ok {
errs = append(errs, "layer does not exist")
}
}
if len(errs) != 0 {
return errors.New(strings.Join(errs, "\n"))
}
return nil
}
+26 -6
View File
@@ -27,10 +27,10 @@ import (
)
// Index validates that idx does not violate any invariants of the index format.
func Index(idx v1.ImageIndex) error {
func Index(idx v1.ImageIndex, opt ...Option) error {
errs := []string{}
if err := validateChildren(idx); err != nil {
if err := validateChildren(idx, opt...); err != nil {
errs = append(errs, fmt.Sprintf("validating children: %v", err))
}
@@ -44,7 +44,11 @@ func Index(idx v1.ImageIndex) error {
return nil
}
func validateChildren(idx v1.ImageIndex) error {
type withLayer interface {
Layer(v1.Hash) (v1.Layer, error)
}
func validateChildren(idx v1.ImageIndex, opt ...Option) error {
manifest, err := idx.IndexManifest()
if err != nil {
return err
@@ -58,7 +62,7 @@ func validateChildren(idx v1.ImageIndex) error {
if err != nil {
return err
}
if err := Index(idx); err != nil {
if err := Index(idx, opt...); err != nil {
errs = append(errs, fmt.Sprintf("failed to validate index Manifests[%d](%s): %v", i, desc.Digest, err))
}
if err := validateMediaType(idx, desc.MediaType); err != nil {
@@ -69,14 +73,30 @@ func validateChildren(idx v1.ImageIndex) error {
if err != nil {
return err
}
if err := Image(img); err != nil {
if err := Image(img, opt...); err != nil {
errs = append(errs, fmt.Sprintf("failed to validate image Manifests[%d](%s): %v", i, desc.Digest, err))
}
if err := validateMediaType(img, desc.MediaType); err != nil {
errs = append(errs, fmt.Sprintf("failed to validate image MediaType[%d](%s): %v", i, desc.Digest, err))
}
default:
logs.Warn.Printf("Unexpected manifest: %s", desc.MediaType)
// Workaround for #819.
if wl, ok := idx.(withLayer); ok {
layer, err := wl.Layer(desc.Digest)
if err != nil {
return fmt.Errorf("failed to get layer Manifests[%d]: %w", i, err)
}
if err := Layer(layer, opt...); err != nil {
lerr := fmt.Sprintf("failed to validate layer Manifests[%d](%s): %v", i, desc.Digest, err)
if desc.MediaType.IsDistributable() {
errs = append(errs, lerr)
} else {
logs.Warn.Printf("nondistributable layer failure: %v", lerr)
}
}
} else {
logs.Warn.Printf("Unexpected manifest: %s", desc.MediaType)
}
}
}
+15 -2
View File
@@ -26,11 +26,24 @@ import (
"strings"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/partial"
)
// Layer validates that the values return by its methods are consistent with the
// contents returned by Compressed and Uncompressed.
func Layer(layer v1.Layer) error {
func Layer(layer v1.Layer, opt ...Option) error {
o := makeOptions(opt...)
if o.fast {
ok, err := partial.Exists(layer)
if err != nil {
return err
}
if !ok {
return fmt.Errorf("layer does not exist")
}
return nil
}
cl, err := computeLayer(layer)
if err != nil {
return err
@@ -128,7 +141,7 @@ func computeLayer(layer v1.Layer) (*computedLayer, error) {
files := make(map[string]struct{})
for {
hdr, err := tarReader.Next()
if err == io.EOF {
if errors.Is(err, io.EOF) {
break
}
if err != nil {
@@ -0,0 +1,37 @@
// Copyright 2021 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 validate
// Option is a functional option for validate.
type Option func(*options)
type options struct {
fast bool
}
func makeOptions(opts ...Option) options {
opt := options{
fast: false,
}
for _, o := range opts {
o(&opt)
}
return opt
}
// Fast causes validate to skip reading and digesting layer bytes.
func Fast(o *options) {
o.fast = true
}
@@ -115,6 +115,11 @@ func (in *ConfigFile) DeepCopy() *ConfigFile {
func (in *Descriptor) DeepCopyInto(out *Descriptor) {
*out = *in
out.Digest = in.Digest
if in.Data != nil {
in, out := &in.Data, &out.Data
*out = make([]byte, len(*in))
copy(*out, *in)
}
if in.URLs != nil {
in, out := &in.URLs, &out.URLs
*out = make([]string, len(*in))