Upgrade dependency to go-containerregistry third-party library

By doing so it will fix issues met when mixing source/remote registry

Close #808
This commit is contained in:
Ben Einaudi
2020-01-26 18:55:52 +01:00
parent e8fffda7cb
commit 03b8922ede
1160 changed files with 107253 additions and 38471 deletions
+2 -2
View File
@@ -18,8 +18,8 @@ package authn
type anonymous struct{}
// Authorization implements Authenticator.
func (a *anonymous) Authorization() (string, error) {
return "", nil
func (a *anonymous) Authorization() (*AuthConfig, error) {
return &AuthConfig{}, nil
}
// Anonymous is a singleton Authenticator for providing anonymous auth.
+9 -8
View File
@@ -14,16 +14,17 @@
package authn
import (
"fmt"
)
// auth implements Authenticator for an "auth" entry of the docker config.
// auth is an Authenticator that simply returns the wrapped AuthConfig.
type auth struct {
token string
config AuthConfig
}
// FromConfig returns an Authenticator that just returns the given AuthConfig.
func FromConfig(cfg AuthConfig) Authenticator {
return &auth{cfg}
}
// Authorization implements Authenticator.
func (a *auth) Authorization() (string, error) {
return fmt.Sprintf("Basic %s", a.token), nil
func (a *auth) Authorization() (*AuthConfig, error) {
return &a.config, nil
}
+16 -1
View File
@@ -14,8 +14,23 @@
package authn
// AuthConfig contains authorization information for connecting to a Registry
// Inlined what we use from github.com/cli/cli/config/types
type AuthConfig struct {
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
Auth string `json:"auth,omitempty"`
// IdentityToken is used to authenticate the user and get
// an access token for the registry.
IdentityToken string `json:"identitytoken,omitempty"`
// RegistryToken is a bearer token to be sent to a registry
RegistryToken string `json:"registrytoken,omitempty"`
}
// Authenticator is used to authenticate Docker transports.
type Authenticator interface {
// Authorization returns the value to use in an http transport's Authorization header.
Authorization() (string, error)
Authorization() (*AuthConfig, error)
}
+5 -9
View File
@@ -14,11 +14,6 @@
package authn
import (
"encoding/base64"
"fmt"
)
// Basic implements Authenticator for basic authentication.
type Basic struct {
Username string
@@ -26,8 +21,9 @@ type Basic struct {
}
// Authorization implements Authenticator.
func (b *Basic) Authorization() (string, error) {
delimited := fmt.Sprintf("%s:%s", b.Username, b.Password)
encoded := base64.StdEncoding.EncodeToString([]byte(delimited))
return fmt.Sprintf("Basic %s", encoded), nil
func (b *Basic) Authorization() (*AuthConfig, error) {
return &AuthConfig{
Username: b.Username,
Password: b.Password,
}, nil
}
+4 -6
View File
@@ -14,16 +14,14 @@
package authn
import (
"fmt"
)
// Bearer implements Authenticator for bearer authentication.
type Bearer struct {
Token string `json:"token"`
}
// Authorization implements Authenticator.
func (b *Bearer) Authorization() (string, error) {
return fmt.Sprintf("Bearer %s", b.Token), nil
func (b *Bearer) Authorization() (*AuthConfig, error) {
return &AuthConfig{
RegistryToken: b.Token,
}, nil
}
-102
View File
@@ -1,102 +0,0 @@
// Copyright 2018 Google LLC All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package authn
import (
"bytes"
"encoding/json"
"fmt"
"os/exec"
"strings"
"github.com/google/go-containerregistry/pkg/name"
)
// magicNotFoundMessage is the string that the CLI special cases to mean
// that a given registry domain wasn't found.
const (
magicNotFoundMessage = "credentials not found in native keychain"
)
// runner allows us to swap out how we "Run" os/exec commands.
type runner interface {
Run(*exec.Cmd) error
}
// defaultRunner implements runner by just calling Run().
type defaultRunner struct{}
// Run implements runner.
func (dr *defaultRunner) Run(cmd *exec.Cmd) error {
return cmd.Run()
}
// helper executes the named credential helper against the given domain.
type helper struct {
name string
domain name.Registry
// We add this layer of indirection to facilitate unit testing.
r runner
}
// helperOutput is the expected JSON output form of a credential helper
// (or at least these are the fields that we care about).
type helperOutput struct {
Username string
Secret string
}
// Authorization implements Authenticator.
func (h *helper) Authorization() (string, error) {
helperName := fmt.Sprintf("docker-credential-%s", h.name)
// We want to execute:
// echo -n {domain} | docker-credential-{name} get
cmd := exec.Command(helperName, "get")
// Some keychains expect a scheme:
// https://github.com/bazelbuild/rules_docker/issues/111
cmd.Stdin = strings.NewReader(fmt.Sprintf("https://%v", h.domain))
var out bytes.Buffer
cmd.Stdout = &out
cmdErr := h.r.Run(cmd)
// If we see this specific message, it means the domain wasn't found
// and we should fall back on anonymous auth.
output := strings.TrimSpace(out.String())
if output == magicNotFoundMessage {
return Anonymous.Authorization()
}
// Any other output should be parsed as JSON and the Username / Secret
// fields used for Basic authentication.
ho := helperOutput{}
if err := json.Unmarshal([]byte(output), &ho); err != nil {
if cmdErr != nil {
// If we failed to parse output, it won't contain Secret, so returning it
// in an error should be fine.
return "", fmt.Errorf("invoking %s: %v; output: %s", helperName, cmdErr, output)
}
return "", err
}
if cmdErr != nil {
return "", fmt.Errorf("invoking %s: %v", helperName, cmdErr)
}
b := Basic{Username: ho.Username, Password: ho.Secret}
return b.Authorization()
}
+19 -24
View File
@@ -23,14 +23,6 @@ import (
"k8s.io/client-go/rest"
"k8s.io/kubernetes/pkg/credentialprovider"
credentialprovidersecrets "k8s.io/kubernetes/pkg/credentialprovider/secrets"
// Credential providers
_ "k8s.io/kubernetes/pkg/credentialprovider/aws"
_ "k8s.io/kubernetes/pkg/credentialprovider/azure"
_ "k8s.io/kubernetes/pkg/credentialprovider/gcp"
// TODO(mattmoor): This doesn't seem to build, figure out why `dep ensure`
// is not working and add constraints.
// _ "k8s.io/kubernetes/pkg/credentialprovider/rancher"
)
// Options holds configuration data for guiding credential resolution.
@@ -133,19 +125,15 @@ func NewNoClient() (authn.Keychain, error) {
type lazyProvider credentialprovider.LazyAuthConfiguration
// Authorization implements Authenticator.
func (lp lazyProvider) Authorization() (string, error) {
func (lp lazyProvider) Authorization() (*authn.AuthConfig, error) {
authConfig := credentialprovider.LazyProvide(credentialprovider.LazyAuthConfiguration(lp))
if authConfig.Auth != "" {
return "Basic " + authConfig.Auth, nil
}
if authConfig.Username != "" {
basic := authn.Basic{
Username: authConfig.Username,
Password: authConfig.Password,
}
return basic.Authorization()
}
return authn.Anonymous.Authorization()
return &authn.AuthConfig{
Username: authConfig.Username,
Password: authConfig.Password,
Auth: authConfig.Auth,
IdentityToken: authConfig.IdentityToken,
RegistryToken: authConfig.RegistryToken,
}, nil
}
type keychain struct {
@@ -153,10 +141,17 @@ type keychain struct {
}
// Resolve implements authn.Keychain
func (kc *keychain) Resolve(reg name.Registry) (authn.Authenticator, error) {
// TODO(mattmoor): Lookup expects an image reference and we only have a registry,
// find something better than this.
creds, found := kc.keyring.Lookup(reg.String() + "/foo/bar")
func (kc *keychain) Resolve(target authn.Resource) (authn.Authenticator, error) {
var (
creds []credentialprovider.LazyAuthConfiguration
found bool
)
if repo, ok := target.(name.Repository); ok {
creds, found = kc.keyring.Lookup(repo.String())
} else {
// Lookup expects an image reference and we only have a registry.
creds, found = kc.keyring.Lookup(target.RegistryStr() + "/foo/bar")
}
if !found || len(creds) < 1 {
return authn.Anonymous, nil
}
@@ -0,0 +1,21 @@
// +build !disable_aws
// Copyright 2019 Google LLC All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package k8schain
import (
// aws credential provider
_ "k8s.io/kubernetes/pkg/credentialprovider/aws"
)
@@ -0,0 +1,21 @@
// +build !disable_azure
// Copyright 2019 Google LLC All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package k8schain
import (
// azure credential provider
_ "k8s.io/kubernetes/pkg/credentialprovider/azure"
)
@@ -0,0 +1,21 @@
// +build !disable_gcp
// Copyright 2019 Google LLC All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package k8schain
import (
// gcp credential provider
_ "k8s.io/kubernetes/pkg/credentialprovider/gcp"
)
+48 -105
View File
@@ -16,137 +16,80 @@ package authn
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"runtime"
"github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/config/types"
"github.com/google/go-containerregistry/pkg/logs"
"github.com/google/go-containerregistry/pkg/name"
)
// Resource represents a registry or repository that can be authenticated against.
type Resource interface {
// String returns the full string representation of the target, e.g.
// gcr.io/my-project or just gcr.io.
String() string
// RegistryStr returns just the registry portion of the target, e.g. for
// gcr.io/my-project, this should just return gcr.io. This is needed to
// pull out an appropriate hostname.
RegistryStr() string
}
// Keychain is an interface for resolving an image reference to a credential.
type Keychain interface {
// Resolve looks up the most appropriate credential for the specified registry.
Resolve(name.Registry) (Authenticator, error)
// Resolve looks up the most appropriate credential for the specified target.
Resolve(Resource) (Authenticator, error)
}
// defaultKeychain implements Keychain with the semantics of the standard Docker
// credential keychain.
type defaultKeychain struct{}
// configDir returns the directory containing Docker's config.json
func configDir() (string, error) {
if dc := os.Getenv("DOCKER_CONFIG"); dc != "" {
return dc, nil
}
if h := dockerUserHomeDir(); h != "" {
return filepath.Join(dockerUserHomeDir(), ".docker"), nil
}
return "", errNoHomeDir
}
var errNoHomeDir = errors.New("could not determine home directory")
// dockerUserHomeDir returns the current user's home directory, as interpreted by Docker.
func dockerUserHomeDir() string {
if runtime.GOOS == "windows" {
// Docker specifically expands "%USERPROFILE%" on Windows,
return os.Getenv("USERPROFILE")
}
// Docker defaults to "$HOME" Linux and OSX.
return os.Getenv("HOME")
}
// authEntry is a helper for JSON parsing an "auth" entry of config.json
// This is not meant for direct consumption.
type authEntry struct {
Auth string `json:"auth"`
Username string `json:"username"`
Password string `json:"password"`
}
// cfg is a helper for JSON parsing Docker's config.json
// This is not meant for direct consumption.
type cfg struct {
CredHelper map[string]string `json:"credHelpers,omitempty"`
CredStore string `json:"credsStore,omitempty"`
Auths map[string]authEntry `json:"auths,omitempty"`
}
// There are a variety of ways a domain may get qualified within the Docker credential file.
// We enumerate them here as format strings.
var (
domainForms = []string{
// Allow naked domains
"%s",
// Allow scheme-prefixed.
"https://%s",
"http://%s",
// Allow scheme-prefixes with version in url path.
"https://%s/v1/",
"http://%s/v1/",
"https://%s/v2/",
"http://%s/v2/",
}
// Export an instance of the default keychain.
// DefaultKeychain implements Keychain by interpreting the docker config file.
DefaultKeychain Keychain = &defaultKeychain{}
)
const (
defaultAuthKey = "https://" + name.DefaultRegistry + "/v1/"
)
// Resolve implements Keychain.
func (dk *defaultKeychain) Resolve(reg name.Registry) (Authenticator, error) {
dir, err := configDir()
func (dk *defaultKeychain) Resolve(target Resource) (Authenticator, error) {
cf, err := config.Load(os.Getenv("DOCKER_CONFIG"))
if err != nil {
logs.Warn.Printf("Unable to determine config dir: %v", err)
return Anonymous, nil
}
file := filepath.Join(dir, "config.json")
content, err := ioutil.ReadFile(file)
if err != nil {
logs.Warn.Printf("Unable to read %q: %v", file, err)
return Anonymous, nil
return nil, err
}
var cf cfg
if err := json.Unmarshal(content, &cf); err != nil {
logs.Warn.Printf("Unable to parse %q: %v", file, err)
return Anonymous, nil
// See:
// https://github.com/google/ko/issues/90
// https://github.com/moby/moby/blob/fc01c2b481097a6057bec3cd1ab2d7b4488c50c4/registry/config.go#L397-L404
key := target.RegistryStr()
if key == name.DefaultRegistry {
key = defaultAuthKey
}
// Per-registry credential helpers take precedence.
if cf.CredHelper != nil {
for _, form := range domainForms {
if entry, ok := cf.CredHelper[fmt.Sprintf(form, reg.Name())]; ok {
return &helper{name: entry, domain: reg, r: &defaultRunner{}}, nil
}
cfg, err := cf.GetAuthConfig(key)
if err != nil {
return nil, err
}
if logs.Enabled(logs.Debug) {
b, err := json.Marshal(cfg)
if err == nil {
logs.Debug.Printf("defaultKeychain.Resolve(%q) = %s", key, string(b))
}
}
// A global credential helper is next in precedence.
if cf.CredStore != "" {
return &helper{name: cf.CredStore, domain: reg, r: &defaultRunner{}}, nil
empty := types.AuthConfig{}
if cfg == empty {
return Anonymous, nil
}
// Lastly, the 'auths' section directly contains basic auth entries.
if cf.Auths != nil {
for _, form := range domainForms {
if entry, ok := cf.Auths[fmt.Sprintf(form, reg.Name())]; ok {
if entry.Auth != "" {
return &auth{entry.Auth}, nil
} else if entry.Username != "" {
return &Basic{Username: entry.Username, Password: entry.Password}, nil
} else {
// TODO(mattmoor): Support identitytoken
// TODO(mattmoor): Support registrytoken
return nil, fmt.Errorf("Unsupported entry in \"auths\" section of %q", file)
}
}
}
}
// Fallback on anonymous.
return Anonymous, nil
return FromConfig(AuthConfig{
Username: cfg.Username,
Password: cfg.Password,
Auth: cfg.Auth,
IdentityToken: cfg.IdentityToken,
RegistryToken: cfg.RegistryToken,
}), nil
}
+2 -6
View File
@@ -14,10 +14,6 @@
package authn
import (
"github.com/google/go-containerregistry/pkg/name"
)
type multiKeychain struct {
keychains []Keychain
}
@@ -31,9 +27,9 @@ func NewMultiKeychain(kcs ...Keychain) Keychain {
}
// Resolve implements Keychain.
func (mk *multiKeychain) Resolve(reg name.Registry) (Authenticator, error) {
func (mk *multiKeychain) Resolve(target Resource) (Authenticator, error) {
for _, kc := range mk.keychains {
auth, err := kc.Resolve(reg)
auth, err := kc.Resolve(target)
if err != nil {
return nil, err
}
+10 -1
View File
@@ -17,11 +17,17 @@
package retry
import (
"context"
"fmt"
"k8s.io/apimachinery/pkg/util/wait"
"github.com/google/go-containerregistry/pkg/internal/retry/wait"
)
// Backoff is an alias of our own wait.Backoff to avoid name conflicts with
// the kubernetes wait package. Typing retry.Backoff is aesier than fixing
// the wrong import every time you use wait.Backoff.
type Backoff = wait.Backoff
// This is implemented by several errors in the net package as well as our
// transport.Error.
type temporary interface {
@@ -30,6 +36,9 @@ type temporary interface {
// IsTemporary returns true if err implements Temporary() and it returns true.
func IsTemporary(err error) bool {
if err == context.DeadlineExceeded {
return false
}
if te, ok := err.(temporary); ok && te.Temporary() {
return true
}
@@ -0,0 +1,123 @@
/*
Copyright 2014 The Kubernetes Authors.
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 wait is a subset of k8s.io/apimachinery to avoid conflicts
// in dependencies (specifically, logging).
package wait
import (
"errors"
"math/rand"
"time"
)
// Jitter returns a time.Duration between duration and duration + maxFactor *
// duration.
//
// This allows clients to avoid converging on periodic behavior. If maxFactor
// is 0.0, a suggested default value will be chosen.
func Jitter(duration time.Duration, maxFactor float64) time.Duration {
if maxFactor <= 0.0 {
maxFactor = 1.0
}
wait := duration + time.Duration(rand.Float64()*maxFactor*float64(duration))
return wait
}
// ErrWaitTimeout is returned when the condition exited without success.
var ErrWaitTimeout = errors.New("timed out waiting for the condition")
// ConditionFunc returns true if the condition is satisfied, or an error
// if the loop should be aborted.
type ConditionFunc func() (done bool, err error)
// Backoff holds parameters applied to a Backoff function.
type Backoff struct {
// The initial duration.
Duration time.Duration
// Duration is multiplied by factor each iteration, if factor is not zero
// and the limits imposed by Steps and Cap have not been reached.
// Should not be negative.
// The jitter does not contribute to the updates to the duration parameter.
Factor float64
// The sleep at each iteration is the duration plus an additional
// amount chosen uniformly at random from the interval between
// zero and `jitter*duration`.
Jitter float64
// The remaining number of iterations in which the duration
// parameter may change (but progress can be stopped earlier by
// hitting the cap). If not positive, the duration is not
// changed. Used for exponential backoff in combination with
// Factor and Cap.
Steps int
// A limit on revised values of the duration parameter. If a
// multiplication by the factor parameter would make the duration
// exceed the cap then the duration is set to the cap and the
// steps parameter is set to zero.
Cap time.Duration
}
// Step (1) returns an amount of time to sleep determined by the
// original Duration and Jitter and (2) mutates the provided Backoff
// to update its Steps and Duration.
func (b *Backoff) Step() time.Duration {
if b.Steps < 1 {
if b.Jitter > 0 {
return Jitter(b.Duration, b.Jitter)
}
return b.Duration
}
b.Steps--
duration := b.Duration
// calculate the next step
if b.Factor != 0 {
b.Duration = time.Duration(float64(b.Duration) * b.Factor)
if b.Cap > 0 && b.Duration > b.Cap {
b.Duration = b.Cap
b.Steps = 0
}
}
if b.Jitter > 0 {
duration = Jitter(duration, b.Jitter)
}
return duration
}
// ExponentialBackoff repeats a condition check with exponential backoff.
//
// It repeatedly checks the condition and then sleeps, using `backoff.Step()`
// to determine the length of the sleep and adjust Duration and Steps.
// Stops and returns as soon as:
// 1. the condition check returns true or an error,
// 2. `backoff.Steps` checks of the condition have been done, or
// 3. a sleep truncated by the cap on duration has been completed.
// In case (1) the returned error is what the condition function returned.
// In all other cases, ErrWaitTimeout is returned.
func ExponentialBackoff(backoff Backoff, condition ConditionFunc) error {
for backoff.Steps > 0 {
if ok, err := condition(); err != nil || ok {
return err
}
if backoff.Steps == 1 {
break
}
time.Sleep(backoff.Step())
}
return ErrWaitTimeout
}
+10
View File
@@ -26,4 +26,14 @@ var (
// Progress is used to log notable, successful events.
Progress = log.New(ioutil.Discard, "", log.LstdFlags)
// Debug is used to log information that is useful for debugging.
Debug = log.New(ioutil.Discard, "", log.LstdFlags)
)
// Enabled checks to see if the logger's writer is set to something other
// than ioutil.Discard. This allows callers to avoid expensive operations
// that will end up in /dev/null anyway.
func Enabled(l *log.Logger) bool {
return l.Writer() != ioutil.Discard
}
+9 -3
View File
@@ -28,7 +28,8 @@ const (
// Digest stores a digest name in a structured form.
type Digest struct {
Repository
digest string
digest string
original string
}
// Ensure Digest implements Reference
@@ -54,8 +55,9 @@ func (d Digest) Name() string {
return d.Repository.Name() + digestDelim + d.DigestStr()
}
// String returns the original input string.
func (d Digest) String() string {
return d.Name()
return d.original
}
func checkDigest(name string) error {
@@ -86,5 +88,9 @@ func NewDigest(name string, opts ...Option) (Digest, error) {
if err != nil {
return Digest{}, err
}
return Digest{repo, digest}, nil
return Digest{
Repository: repo,
digest: digest,
original: name,
}, nil
}
+2 -3
View File
@@ -15,7 +15,6 @@
package name
import (
"errors"
"fmt"
)
@@ -45,6 +44,6 @@ func ParseReference(s string, opts ...Option) (Reference, error) {
if d, err := NewDigest(s, opts...); err == nil {
return d, nil
}
// TODO: Combine above errors into something more useful?
return nil, errors.New("could not parse reference")
return nil, NewErrBadName("could not parse reference: " + s)
}
+21
View File
@@ -50,6 +50,7 @@ func (r Repository) Name() string {
if regName != "" {
return regName + regRepoDelimiter + r.RepositoryStr()
}
// TODO: As far as I can tell, this is unreachable.
return r.RepositoryStr()
}
@@ -98,3 +99,23 @@ func NewRepository(name string, opts ...Option) (Repository, error) {
}
return Repository{reg, repo}, nil
}
// Tag returns a Tag in this Repository.
func (r Repository) Tag(identifier string) Tag {
t := Tag{
tag: identifier,
Repository: r,
}
t.original = t.Name()
return t
}
// Digest returns a Digest in this Repository.
func (r Repository) Digest(identifier string) Digest {
d := Digest{
digest: identifier,
Repository: r,
}
d.original = d.Name()
return d
}
+9 -3
View File
@@ -28,7 +28,8 @@ const (
// Tag stores a docker tag name in a structured form.
type Tag struct {
Repository
tag string
tag string
original string
}
// Ensure Tag implements Reference
@@ -57,8 +58,9 @@ func (t Tag) Name() string {
return t.Repository.Name() + tagDelim + t.TagStr()
}
// String returns the original input string.
func (t Tag) String() string {
return t.Name()
return t.original
}
// Scope returns the scope required to perform the given action on the tag.
@@ -98,5 +100,9 @@ func NewTag(name string, opts ...Option) (Tag, error) {
if err != nil {
return Tag{}, err
}
return Tag{repo, tag}, nil
return Tag{
Repository: repo,
tag: tag,
original: name,
}, nil
}
+34 -35
View File
@@ -24,17 +24,16 @@ import (
// how to launch a container. See:
// https://github.com/opencontainers/image-spec/blob/master/config.md
type ConfigFile struct {
Architecture string `json:"architecture"`
Author string `json:"author,omitempty"`
Container string `json:"container,omitempty"`
Created Time `json:"created,omitempty"`
DockerVersion string `json:"docker_version,omitempty"`
History []History `json:"history,omitempty"`
OS string `json:"os"`
RootFS RootFS `json:"rootfs"`
Config Config `json:"config"`
ContainerConfig Config `json:"container_config,omitempty"`
OSVersion string `json:"osversion,omitempty"`
Architecture string `json:"architecture"`
Author string `json:"author,omitempty"`
Container string `json:"container,omitempty"`
Created Time `json:"created,omitempty"`
DockerVersion string `json:"docker_version,omitempty"`
History []History `json:"history,omitempty"`
OS string `json:"os"`
RootFS RootFS `json:"rootfs"`
Config Config `json:"config"`
OSVersion string `json:"osversion,omitempty"`
}
// History is one entry of a list recording how this container image was built.
@@ -95,30 +94,30 @@ type HealthConfig struct {
// and
// https://github.com/opencontainers/image-spec/blob/master/config.md
type Config struct {
AttachStderr bool
AttachStdin bool
AttachStdout bool
Cmd []string
Healthcheck *HealthConfig
Domainname string
Entrypoint []string
Env []string
Hostname string
Image string
Labels map[string]string
OnBuild []string
OpenStdin bool
StdinOnce bool
Tty bool
User string
Volumes map[string]struct{}
WorkingDir string
ExposedPorts map[string]struct{}
ArgsEscaped bool
NetworkDisabled bool
MacAddress string
StopSignal string
Shell []string
AttachStderr bool `json:"AttachStderr,omitempty"`
AttachStdin bool `json:"AttachStdin,omitempty"`
AttachStdout bool `json:"AttachStdout,omitempty"`
Cmd []string `json:"Cmd,omitempty"`
Healthcheck *HealthConfig `json:"Healthcheck,omitempty"`
Domainname string `json:"Domainname,omitempty"`
Entrypoint []string `json:"Entrypoint,omitempty"`
Env []string `json:"Env,omitempty"`
Hostname string `json:"Hostname,omitempty"`
Image string `json:"Image,omitempty"`
Labels map[string]string `json:"Labels,omitempty"`
OnBuild []string `json:"OnBuild,omitempty"`
OpenStdin bool `json:"OpenStdin,omitempty"`
StdinOnce bool `json:"StdinOnce,omitempty"`
Tty bool `json:"Tty,omitempty"`
User string `json:"User,omitempty"`
Volumes map[string]struct{} `json:"Volumes,omitempty"`
WorkingDir string `json:"WorkingDir,omitempty"`
ExposedPorts map[string]struct{} `json:"ExposedPorts,omitempty"`
ArgsEscaped bool `json:"ArgsEscaped,omitempty"`
NetworkDisabled bool `json:"NetworkDisabled,omitempty"`
MacAddress string `json:"MacAddress,omitempty"`
StopSignal string `json:"StopSignal,omitempty"`
Shell []string `json:"Shell,omitempty"`
}
// ParseConfigFile parses the io.Reader's contents into a ConfigFile.
+1 -4
View File
@@ -29,9 +29,6 @@ import (
// image accesses an image from a docker daemon
type image struct {
v1.Image
opener tarball.Opener
ref name.Reference
}
var _ v1.Image = (*image)(nil)
@@ -73,7 +70,7 @@ type ImageSaver interface {
// This is a variable so we can override in tests.
var getImageSaver = func() (ImageSaver, error) {
cli, err := client.NewEnvClient()
cli, err := client.NewClientWithOpts(client.FromEnv)
if err != nil {
return nil, err
}
+4 -4
View File
@@ -16,6 +16,7 @@ package daemon
import (
"context"
"fmt"
"io"
"io/ioutil"
@@ -24,7 +25,6 @@ import (
"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/pkg/errors"
)
// ImageLoader is an interface for testing.
@@ -35,7 +35,7 @@ type ImageLoader interface {
// GetImageLoader is a variable so we can override in tests.
var GetImageLoader = func() (ImageLoader, error) {
cli, err := client.NewEnvClient()
cli, err := client.NewClientWithOpts(client.FromEnv)
if err != nil {
return nil, err
}
@@ -68,13 +68,13 @@ 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)
if err != nil {
return "", errors.Wrapf(err, "error loading image")
return "", fmt.Errorf("error loading image: %v", err)
}
defer resp.Body.Close()
b, readErr := ioutil.ReadAll(resp.Body)
response := string(b)
if readErr != nil {
return response, errors.Wrapf(err, "error reading load response body")
return response, fmt.Errorf("error reading load response body: %v", err)
}
return response, nil
}
+32 -2
View File
@@ -15,8 +15,38 @@
package empty
import (
"github.com/google/go-containerregistry/pkg/v1/random"
"fmt"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/partial"
"github.com/google/go-containerregistry/pkg/v1/types"
)
// Image is a singleton empty image, think: FROM scratch.
var Image, _ = random.Image(0, 0)
var Image, _ = partial.UncompressedToImage(emptyImage{})
type emptyImage struct{}
// MediaType implements partial.UncompressedImageCore.
func (i emptyImage) MediaType() (types.MediaType, error) {
return types.DockerManifestSchema2, nil
}
// RawConfigFile implements partial.UncompressedImageCore.
func (i emptyImage) RawConfigFile() ([]byte, error) {
return partial.RawConfigFile(i)
}
// ConfigFile implements v1.Image.
func (i emptyImage) ConfigFile() (*v1.ConfigFile, error) {
return &v1.ConfigFile{
RootFS: v1.RootFS{
// Some clients check this.
Type: "layers",
},
}, nil
}
func (i emptyImage) LayerByDiffID(h v1.Hash) (partial.UncompressedLayer, error) {
return nil, fmt.Errorf("LayerByDiffID(%s): empty image", h)
}
+12 -8
View File
@@ -36,18 +36,16 @@ func (i emptyIndex) Digest() (v1.Hash, error) {
return partial.Digest(i)
}
func (i emptyIndex) Size() (int64, error) {
return partial.Size(i)
}
func (i emptyIndex) IndexManifest() (*v1.IndexManifest, error) {
return &v1.IndexManifest{
SchemaVersion: 2,
}, nil
return base(), nil
}
func (i emptyIndex) RawManifest() ([]byte, error) {
im, err := i.IndexManifest()
if err != nil {
return nil, err
}
return json.Marshal(im)
return json.Marshal(base())
}
func (i emptyIndex) Image(v1.Hash) (v1.Image, error) {
@@ -57,3 +55,9 @@ func (i emptyIndex) Image(v1.Hash) (v1.Image, error) {
func (i emptyIndex) ImageIndex(v1.Hash) (v1.ImageIndex, error) {
return nil, errors.New("empty index")
}
func base() *v1.IndexManifest {
return &v1.IndexManifest{
SchemaVersion: 2,
}
}
+12
View File
@@ -62,6 +62,18 @@ func (h *Hash) UnmarshalJSON(data []byte) error {
return h.parse(s)
}
// MarshalText implements encoding.TextMarshaler. This is required to use
// v1.Hash as a key in a map when marshalling JSON.
func (h Hash) MarshalText() (text []byte, err error) {
return []byte(h.String()), nil
}
// UnmarshalText implements encoding.TextUnmarshaler. This is required to use
// v1.Hash as a key in a map when unmarshalling JSON.
func (h *Hash) UnmarshalText(text []byte) error {
return h.parse(string(text))
}
// Hasher returns a hash.Hash for the named algorithm (e.g. "sha256")
func Hasher(name string) (hash.Hash, error) {
switch name {
+3
View File
@@ -28,6 +28,9 @@ type Image interface {
// MediaType of this image's manifest.
MediaType() (types.MediaType, error)
// Size returns the size of the manifest.
Size() (int64, error)
// ConfigName returns the hash of the image's config file.
ConfigName() (Hash, error)
+3
View File
@@ -27,6 +27,9 @@ type ImageIndex interface {
// Digest returns the sha256 of this index's manifest.
Digest() (Hash, error)
// Size returns the size of the manifest.
Size() (int64, error)
// IndexManifest returns this image index's manifest object.
IndexManifest() (*IndexManifest, error)
+14 -7
View File
@@ -28,8 +28,9 @@ import (
var _ v1.ImageIndex = (*layoutIndex)(nil)
type layoutIndex struct {
path Path
rawIndex []byte
mediaType types.MediaType
path Path
rawIndex []byte
}
// ImageIndexFromPath is a convenience function which constructs a Path and returns its v1.ImageIndex.
@@ -49,21 +50,26 @@ func (l Path) ImageIndex() (v1.ImageIndex, error) {
}
idx := &layoutIndex{
path: l,
rawIndex: rawIndex,
mediaType: types.OCIImageIndex,
path: l,
rawIndex: rawIndex,
}
return idx, nil
}
func (i *layoutIndex) MediaType() (types.MediaType, error) {
return types.OCIImageIndex, nil
return i.mediaType, nil
}
func (i *layoutIndex) Digest() (v1.Hash, error) {
return partial.Digest(i)
}
func (i *layoutIndex) Size() (int64, error) {
return partial.Size(i)
}
func (i *layoutIndex) IndexManifest() (*v1.IndexManifest, error) {
var index v1.IndexManifest
err := json.Unmarshal(i.rawIndex, &index)
@@ -109,8 +115,9 @@ func (i *layoutIndex) ImageIndex(h v1.Hash) (v1.ImageIndex, error) {
}
return &layoutIndex{
path: i.path,
rawIndex: rawIndex,
mediaType: desc.MediaType,
path: i.path,
rawIndex: rawIndex,
}, nil
}
+260
View File
@@ -0,0 +1,260 @@
// Copyright 2019 Google LLC All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package mutate
import (
"bytes"
"encoding/json"
"errors"
"strings"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/partial"
"github.com/google/go-containerregistry/pkg/v1/stream"
"github.com/google/go-containerregistry/pkg/v1/types"
)
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
}
var _ v1.Image = (*image)(nil)
func (i *image) MediaType() (types.MediaType, error) {
if i.mediaType != nil {
return *i.mediaType, nil
}
return i.base.MediaType()
}
func (i *image) compute() error {
// Don't re-compute if already computed.
if i.computed {
return nil
}
var configFile *v1.ConfigFile
if i.configFile != nil {
configFile = i.configFile
} else {
cf, err := i.base.ConfigFile()
if err != nil {
return err
}
configFile = cf.DeepCopy()
}
diffIDs := configFile.RootFS.DiffIDs
history := configFile.History
diffIDMap := make(map[v1.Hash]v1.Layer)
digestMap := make(map[v1.Hash]v1.Layer)
for _, add := range i.adds {
diffID, err := add.Layer.DiffID()
if err != nil {
return err
}
diffIDs = append(diffIDs, diffID)
history = append(history, add.History)
diffIDMap[diffID] = add.Layer
}
m, err := i.base.Manifest()
if err != nil {
return err
}
manifest := m.DeepCopy()
manifestLayers := manifest.Layers
for _, add := range i.adds {
d := v1.Descriptor{}
var err error
if d.Size, err = add.Layer.Size(); err != nil {
return err
}
if d.Digest, err = add.Layer.Digest(); err != nil {
return err
}
if d.MediaType, err = add.Layer.MediaType(); err != nil {
return err
}
d.Annotations = add.Annotations
d.URLs = add.URLs
manifestLayers = append(manifestLayers, d)
digestMap[d.Digest] = add.Layer
}
configFile.RootFS.DiffIDs = diffIDs
configFile.History = history
manifest.Layers = manifestLayers
rcfg, err := json.Marshal(configFile)
if err != nil {
return err
}
d, sz, err := v1.SHA256(bytes.NewBuffer(rcfg))
if err != nil {
return err
}
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 i.mediaType != nil && strings.Contains(string(*i.mediaType), types.OCIVendorPrefix) {
manifest.MediaType = ""
}
i.configFile = configFile
i.manifest = manifest
i.diffIDMap = diffIDMap
i.digestMap = digestMap
i.computed = true
return nil
}
// 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 {
// 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.
layers, err := i.base.Layers()
if err != nil {
return nil, err
}
for _, add := range i.adds {
layers = append(layers, add.Layer)
}
return layers, nil
} else if err != nil {
return nil, err
}
diffIDs, err := partial.DiffIDs(i)
if err != nil {
return nil, err
}
ls := make([]v1.Layer, 0, len(diffIDs))
for _, h := range diffIDs {
l, err := i.LayerByDiffID(h)
if err != nil {
return nil, err
}
ls = append(ls, l)
}
return ls, nil
}
// ConfigName returns the hash of the image's config file.
func (i *image) ConfigName() (v1.Hash, error) {
if err := i.compute(); err != nil {
return v1.Hash{}, err
}
return partial.ConfigName(i)
}
// ConfigFile returns this image's config file.
func (i *image) ConfigFile() (*v1.ConfigFile, error) {
if err := i.compute(); err != nil {
return nil, err
}
return i.configFile, nil
}
// RawConfigFile returns the serialized bytes of ConfigFile()
func (i *image) RawConfigFile() ([]byte, error) {
if err := i.compute(); err != nil {
return nil, err
}
return json.Marshal(i.configFile)
}
// Digest returns the sha256 of this image's manifest.
func (i *image) Digest() (v1.Hash, error) {
if err := i.compute(); err != nil {
return v1.Hash{}, err
}
return partial.Digest(i)
}
// Size implements v1.Image.
func (i *image) Size() (int64, error) {
if err := i.compute(); err != nil {
return -1, err
}
return partial.Size(i)
}
// Manifest returns this image's Manifest object.
func (i *image) Manifest() (*v1.Manifest, error) {
if err := i.compute(); err != nil {
return nil, err
}
return i.manifest, nil
}
// RawManifest returns the serialized bytes of Manifest()
func (i *image) RawManifest() ([]byte, error) {
if err := i.compute(); err != nil {
return nil, err
}
return json.Marshal(i.manifest)
}
// LayerByDigest returns a Layer for interacting with a particular layer of
// the image, looking it up by "digest" (the compressed hash).
func (i *image) LayerByDigest(h v1.Hash) (v1.Layer, error) {
if cn, err := i.ConfigName(); err != nil {
return nil, err
} else if h == cn {
return partial.ConfigLayer(i)
}
if layer, ok := i.digestMap[h]; ok {
return layer, nil
}
return i.base.LayerByDigest(h)
}
// LayerByDiffID is an analog to LayerByDigest, looking up by "diff id"
// (the uncompressed hash).
func (i *image) LayerByDiffID(h v1.Hash) (v1.Layer, error) {
if layer, ok := i.diffIDMap[h]; ok {
return layer, nil
}
return i.base.LayerByDiffID(h)
}
func validate(adds []Addendum) error {
for _, add := range adds {
if add.Layer == nil {
return errors.New("unable to add a nil layer to the image")
}
}
return nil
}
+155
View File
@@ -0,0 +1,155 @@
// Copyright 2019 Google LLC All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package mutate
import (
"encoding/json"
"strings"
"github.com/google/go-containerregistry/pkg/logs"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/partial"
"github.com/google/go-containerregistry/pkg/v1/types"
)
func computeDescriptor(desc v1.Descriptor, add Appendable) (*v1.Descriptor, error) {
d, err := add.Digest()
if err != nil {
return nil, err
}
mt, err := add.MediaType()
if err != nil {
return nil, err
}
sz, err := add.Size()
if err != nil {
return nil, err
}
// The IndexAddendum allows overriding These values.
if desc.Size == 0 {
desc.Size = sz
}
if string(desc.MediaType) == "" {
desc.MediaType = mt
}
if desc.Digest == (v1.Hash{}) {
desc.Digest = d
}
return &desc, nil
}
type index struct {
base v1.ImageIndex
adds []IndexAddendum
computed bool
manifest *v1.IndexManifest
mediaType *types.MediaType
imageMap map[v1.Hash]v1.Image
indexMap map[v1.Hash]v1.ImageIndex
}
var _ v1.ImageIndex = (*index)(nil)
func (i *index) MediaType() (types.MediaType, error) {
if i.mediaType != nil {
return *i.mediaType, nil
}
return i.base.MediaType()
}
func (i *index) Size() (int64, error) { return partial.Size(i) }
func (i *index) compute() error {
// Don't re-compute if already computed.
if i.computed {
return nil
}
i.imageMap = make(map[v1.Hash]v1.Image)
i.indexMap = make(map[v1.Hash]v1.ImageIndex)
m, err := i.base.IndexManifest()
if err != nil {
return err
}
manifest := m.DeepCopy()
manifests := manifest.Manifests
for _, add := range i.adds {
desc, err := computeDescriptor(add.Descriptor, add.Add)
if err != nil {
return err
}
manifests = append(manifests, *desc)
if idx, ok := add.Add.(v1.ImageIndex); ok {
i.indexMap[desc.Digest] = idx
} else if img, ok := add.Add.(v1.Image); ok {
i.imageMap[desc.Digest] = img
} else {
logs.Warn.Printf("Unexpected index addendum: %T", add.Add)
}
}
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 && strings.Contains(string(*i.mediaType), types.OCIVendorPrefix) {
manifest.MediaType = ""
}
i.manifest = manifest
i.computed = true
return nil
}
func (i *index) Image(h v1.Hash) (v1.Image, error) {
if img, ok := i.imageMap[h]; ok {
return img, nil
}
return i.base.Image(h)
}
func (i *index) ImageIndex(h v1.Hash) (v1.ImageIndex, error) {
if idx, ok := i.indexMap[h]; ok {
return idx, nil
}
return i.base.ImageIndex(h)
}
// Digest returns the sha256 of this image's manifest.
func (i *index) Digest() (v1.Hash, error) {
if err := i.compute(); err != nil {
return v1.Hash{}, err
}
return partial.Digest(i)
}
// Manifest returns this image's Manifest object.
func (i *index) IndexManifest() (*v1.IndexManifest, error) {
if err := i.compute(); err != nil {
return nil, err
}
return i.manifest, nil
}
// RawManifest returns the serialized bytes of Manifest()
func (i *index) RawManifest() ([]byte, error) {
if err := i.compute(); err != nil {
return nil, err
}
return json.Marshal(i.manifest)
}
+56 -238
View File
@@ -17,8 +17,6 @@ package mutate
import (
"archive/tar"
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
@@ -28,8 +26,6 @@ import (
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/empty"
"github.com/google/go-containerregistry/pkg/v1/partial"
"github.com/google/go-containerregistry/pkg/v1/stream"
"github.com/google/go-containerregistry/pkg/v1/tarball"
"github.com/google/go-containerregistry/pkg/v1/types"
"github.com/google/go-containerregistry/pkg/v1/v1util"
@@ -40,11 +36,13 @@ const whiteoutPrefix = ".wh."
// Addendum contains layers and history to be appended
// to a base image
type Addendum struct {
Layer v1.Layer
History v1.History
Layer v1.Layer
History v1.History
URLs []string
Annotations map[string]string
}
// AppendLayers applies layers to a base image
// AppendLayers applies layers to a base image.
func AppendLayers(base v1.Image, layers ...v1.Layer) (v1.Image, error) {
additions := make([]Addendum, 0, len(layers))
for _, layer := range layers {
@@ -69,6 +67,30 @@ func Append(base v1.Image, adds ...Addendum) (v1.Image, error) {
}, nil
}
// Appendable is an interface that represents something that can be appended
// to an ImageIndex. We need to be able to construct a v1.Descriptor in order
// to append something, and this is the minimum required information for that.
type Appendable interface {
MediaType() (types.MediaType, error)
Digest() (v1.Hash, error)
Size() (int64, error)
}
// IndexAddendum represents an appendable thing and all the properties that
// we may want to override in the resulting v1.Descriptor.
type IndexAddendum struct {
Add Appendable
v1.Descriptor
}
// AppendManifests appends a manifest to the ImageIndex.
func AppendManifests(base v1.ImageIndex, adds ...IndexAddendum) v1.ImageIndex {
return &index{
base: base,
adds: adds,
}
}
// Config mutates the provided v1.Image to have the provided v1.Config
func Config(base v1.Image, cfg v1.Config) (v1.Image, error) {
cf, err := base.ConfigFile()
@@ -77,8 +99,6 @@ func Config(base v1.Image, cfg v1.Config) (v1.Image, error) {
}
cf.Config = cfg
// Downstream tooling expects these to match.
cf.ContainerConfig = cfg
return ConfigFile(base, cf)
}
@@ -112,216 +132,6 @@ func CreatedAt(base v1.Image, created v1.Time) (v1.Image, error) {
return ConfigFile(base, cfg)
}
type image struct {
base v1.Image
adds []Addendum
computed bool
configFile *v1.ConfigFile
manifest *v1.Manifest
diffIDMap map[v1.Hash]v1.Layer
digestMap map[v1.Hash]v1.Layer
}
var _ v1.Image = (*image)(nil)
func (i *image) MediaType() (types.MediaType, error) { return i.base.MediaType() }
func (i *image) compute() error {
// Don't re-compute if already computed.
if i.computed {
return nil
}
var configFile *v1.ConfigFile
if i.configFile != nil {
configFile = i.configFile
} else {
cf, err := i.base.ConfigFile()
if err != nil {
return err
}
configFile = cf.DeepCopy()
}
diffIDs := configFile.RootFS.DiffIDs
history := configFile.History
diffIDMap := make(map[v1.Hash]v1.Layer)
digestMap := make(map[v1.Hash]v1.Layer)
for _, add := range i.adds {
diffID, err := add.Layer.DiffID()
if err != nil {
return err
}
diffIDs = append(diffIDs, diffID)
history = append(history, add.History)
diffIDMap[diffID] = add.Layer
}
m, err := i.base.Manifest()
if err != nil {
return err
}
manifest := m.DeepCopy()
manifestLayers := manifest.Layers
for _, add := range i.adds {
d := v1.Descriptor{}
var err error
if d.Size, err = add.Layer.Size(); err != nil {
return err
}
if d.Digest, err = add.Layer.Digest(); err != nil {
return err
}
if d.MediaType, err = add.Layer.MediaType(); err != nil {
return err
}
manifestLayers = append(manifestLayers, d)
digestMap[d.Digest] = add.Layer
}
configFile.RootFS.DiffIDs = diffIDs
configFile.History = history
manifest.Layers = manifestLayers
rcfg, err := json.Marshal(configFile)
if err != nil {
return err
}
d, sz, err := v1.SHA256(bytes.NewBuffer(rcfg))
if err != nil {
return err
}
manifest.Config.Digest = d
manifest.Config.Size = sz
i.configFile = configFile
i.manifest = manifest
i.diffIDMap = diffIDMap
i.digestMap = digestMap
i.computed = true
return nil
}
// 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 {
// 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.
layers, err := i.base.Layers()
if err != nil {
return nil, err
}
for _, add := range i.adds {
layers = append(layers, add.Layer)
}
return layers, nil
} else if err != nil {
return nil, err
}
diffIDs, err := partial.DiffIDs(i)
if err != nil {
return nil, err
}
ls := make([]v1.Layer, 0, len(diffIDs))
for _, h := range diffIDs {
l, err := i.LayerByDiffID(h)
if err != nil {
return nil, err
}
ls = append(ls, l)
}
return ls, nil
}
// ConfigName returns the hash of the image's config file.
func (i *image) ConfigName() (v1.Hash, error) {
if err := i.compute(); err != nil {
return v1.Hash{}, err
}
return partial.ConfigName(i)
}
// ConfigFile returns this image's config file.
func (i *image) ConfigFile() (*v1.ConfigFile, error) {
if err := i.compute(); err != nil {
return nil, err
}
return i.configFile, nil
}
// RawConfigFile returns the serialized bytes of ConfigFile()
func (i *image) RawConfigFile() ([]byte, error) {
if err := i.compute(); err != nil {
return nil, err
}
return json.Marshal(i.configFile)
}
// Digest returns the sha256 of this image's manifest.
func (i *image) Digest() (v1.Hash, error) {
if err := i.compute(); err != nil {
return v1.Hash{}, err
}
return partial.Digest(i)
}
// Manifest returns this image's Manifest object.
func (i *image) Manifest() (*v1.Manifest, error) {
if err := i.compute(); err != nil {
return nil, err
}
return i.manifest, nil
}
// RawManifest returns the serialized bytes of Manifest()
func (i *image) RawManifest() ([]byte, error) {
if err := i.compute(); err != nil {
return nil, err
}
return json.Marshal(i.manifest)
}
// LayerByDigest returns a Layer for interacting with a particular layer of
// the image, looking it up by "digest" (the compressed hash).
func (i *image) LayerByDigest(h v1.Hash) (v1.Layer, error) {
if cn, err := i.ConfigName(); err != nil {
return nil, err
} else if h == cn {
return partial.ConfigLayer(i)
}
if layer, ok := i.digestMap[h]; ok {
return layer, nil
}
return i.base.LayerByDigest(h)
}
// LayerByDiffID is an analog to LayerByDigest, looking up by "diff id"
// (the uncompressed hash).
func (i *image) LayerByDiffID(h v1.Hash) (v1.Layer, error) {
if layer, ok := i.diffIDMap[h]; ok {
return layer, nil
}
return i.base.LayerByDiffID(h)
}
func validate(adds []Addendum) error {
for _, add := range adds {
if add.Layer == nil {
return errors.New("Unable to add a nil layer to the image")
}
}
return nil
}
// Extract takes an image and returns an io.ReadCloser containing the image's
// flattened filesystem.
//
@@ -439,8 +249,7 @@ func Time(img v1.Image, t time.Time) (v1.Image, error) {
layers, err := img.Layers()
if err != nil {
return nil, fmt.Errorf("Error getting image layers: %v", err)
return nil, fmt.Errorf("getting image layers: %v", err)
}
// Strip away all timestamps from layers
@@ -448,31 +257,30 @@ func Time(img v1.Image, t time.Time) (v1.Image, error) {
for _, layer := range layers {
newLayer, err := layerTime(layer, t)
if err != nil {
return nil, fmt.Errorf("Error setting layer times: %v", err)
return nil, fmt.Errorf("setting layer times: %v", err)
}
newLayers = append(newLayers, newLayer)
}
newImage, err = AppendLayers(newImage, newLayers...)
if err != nil {
return nil, fmt.Errorf("Error appending layers: %v", err)
return nil, fmt.Errorf("appending layers: %v", err)
}
ocf, err := img.ConfigFile()
if err != nil {
return nil, fmt.Errorf("Error getting original config file: %v", err)
return nil, fmt.Errorf("getting original config file: %v", err)
}
cf, err := newImage.ConfigFile()
if err != nil {
return nil, fmt.Errorf("Error setting config file: %v", err)
return nil, fmt.Errorf("setting config file: %v", err)
}
cfg := cf.DeepCopy()
// Copy basic config over
cfg.Config = ocf.Config
cfg.ContainerConfig = ocf.Config // Downstream tooling expects these to match.
// Strip away timestamps from the config file
cfg.Created = v1.Time{Time: t}
@@ -487,7 +295,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("Error getting layer: %v", err)
return nil, fmt.Errorf("getting layer: %v", err)
}
w := new(bytes.Buffer)
tarWriter := tar.NewWriter(w)
@@ -500,17 +308,17 @@ func layerTime(layer v1.Layer, t time.Time) (v1.Layer, error) {
break
}
if err != nil {
return nil, fmt.Errorf("Error reading layer: %v", err)
return nil, fmt.Errorf("reading layer: %v", err)
}
header.ModTime = t
if err := tarWriter.WriteHeader(header); err != nil {
return nil, fmt.Errorf("Error writing tar header: %v", err)
return nil, fmt.Errorf("writing tar header: %v", err)
}
if header.Typeflag == tar.TypeReg {
if _, err = io.Copy(tarWriter, tarReader); err != nil {
return nil, fmt.Errorf("Error writing layer file: %v", err)
return nil, fmt.Errorf("writing layer file: %v", err)
}
}
}
@@ -522,16 +330,11 @@ func layerTime(layer v1.Layer, t time.Time) (v1.Layer, error) {
b := w.Bytes()
// gzip the contents, then create the layer
opener := func() (io.ReadCloser, error) {
g, err := v1util.GzipReadCloser(ioutil.NopCloser(bytes.NewReader(b)))
if err != nil {
return nil, fmt.Errorf("Error compressing layer: %v", err)
}
return g, nil
return v1util.GzipReadCloser(ioutil.NopCloser(bytes.NewReader(b))), nil
}
layer, err = tarball.LayerFromOpener(opener)
if err != nil {
return nil, fmt.Errorf("Error creating layer: %v", err)
return nil, fmt.Errorf("creating layer: %v", err)
}
return layer, nil
@@ -557,8 +360,23 @@ func Canonical(img v1.Image) (v1.Image, error) {
cfg.Container = ""
cfg.Config.Hostname = ""
cfg.ContainerConfig.Hostname = ""
cfg.DockerVersion = ""
return ConfigFile(img, cfg)
}
// MediaType modifies the MediaType() of the given image.
func MediaType(img v1.Image, mt types.MediaType) v1.Image {
return &image{
base: img,
mediaType: &mt,
}
}
// IndexMediaType modifies the MediaType() of the given index.
func IndexMediaType(idx v1.ImageIndex, mt types.MediaType) v1.ImageIndex {
return &index{
base: idx,
mediaType: &mt,
}
}
@@ -150,6 +150,11 @@ func (i *compressedImageExtender) Manifest() (*v1.Manifest, error) {
return Manifest(i)
}
// Size implements v1.Image
func (i *compressedImageExtender) Size() (int64, error) {
return Size(i)
}
// CompressedToImage fills in the missing methods from a CompressedImageCore so that it implements v1.Image
func CompressedToImage(cic CompressedImageCore) (v1.Image, error) {
return &compressedImageExtender{
@@ -54,7 +54,7 @@ func (ule *uncompressedLayerExtender) Compressed() (io.ReadCloser, error) {
if err != nil {
return nil, err
}
return v1util.GzipReadCloser(u)
return v1util.GzipReadCloser(u), nil
}
// Digest implements v1.Layer
@@ -81,6 +81,30 @@ func (ule *uncompressedLayerExtender) calcSizeHash() {
})
}
// Descriptor returns the layer manifest descriptor for this uncompressed layer.
// The embedded UncompressedLayer is checked to see if it has a Descriptor
// function which is returned if it exists. The underlying descriptor provides
// foreign layer data like URLs. Otherwise, a new descriptor is
// generated.
func (ule *uncompressedLayerExtender) Descriptor() (*v1.Descriptor, error) {
if withDesc, ok := ule.UncompressedLayer.(withDescriptor); ok {
return withDesc.Descriptor()
}
ule.calcSizeHash()
if ule.hashSizeError != nil {
return nil, ule.hashSizeError
}
mt, err := ule.MediaType()
if err != nil {
return nil, err
}
return &v1.Descriptor{
MediaType: mt,
Size: ule.size,
Digest: ule.hash,
}, nil
}
// UncompressedToLayer fills in the missing methods from an UncompressedLayer so that it implements v1.Layer
func UncompressedToLayer(ul UncompressedLayer) (v1.Layer, error) {
return &uncompressedLayerExtender{UncompressedLayer: ul}, nil
@@ -155,6 +179,15 @@ func (i *uncompressedImageExtender) Manifest() (*v1.Manifest, error) {
m.Layers = make([]v1.Descriptor, len(ls))
for i, l := range ls {
// Check if the layer implementation provides the descriptor directly.
if withDesc, ok := l.(withDescriptor); ok {
desc, err := withDesc.Descriptor()
if err != nil {
return nil, err
}
m.Layers[i] = *desc
continue
}
sz, err := l.Size()
if err != nil {
return nil, err
@@ -180,6 +213,11 @@ func (i *uncompressedImageExtender) RawManifest() ([]byte, error) {
return RawManifest(i)
}
// Size implements v1.Image
func (i *uncompressedImageExtender) Size() (int64, error) {
return Size(i)
}
// ConfigName implements v1.Image
func (i *uncompressedImageExtender) ConfigName() (v1.Hash, error) {
return ConfigName(i)
+34 -29
View File
@@ -23,7 +23,6 @@ import (
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/types"
"github.com/google/go-containerregistry/pkg/v1/v1util"
)
// WithRawConfigFile defines the subset of v1.Image used by these helper methods
@@ -136,15 +135,6 @@ type WithUncompressedLayer interface {
UncompressedLayer(v1.Hash) (io.ReadCloser, error)
}
// Layer is the same as Blob, but takes the "diff id".
func Layer(wul WithUncompressedLayer, h v1.Hash) (io.ReadCloser, error) {
rc, err := wul.UncompressedLayer(h)
if err != nil {
return nil, err
}
return v1util.GzipReadCloser(rc)
}
// WithRawManifest defines the subset of v1.Image used by these helper methods
type WithRawManifest interface {
// RawManifest returns the serialized bytes of this image's config file.
@@ -185,6 +175,15 @@ func RawManifest(i WithManifest) ([]byte, error) {
return json.Marshal(m)
}
// Size is a helper for implementing v1.Image
func Size(i WithRawManifest) (int64, error) {
b, err := i.RawManifest()
if err != nil {
return -1, err
}
return int64(len(b)), nil
}
// FSLayers is a helper for implementing v1.Image
func FSLayers(i WithManifest) ([]v1.Hash, error) {
m, err := i.Manifest()
@@ -200,16 +199,30 @@ func FSLayers(i WithManifest) ([]v1.Hash, error) {
// BlobSize is a helper for implementing v1.Image
func BlobSize(i WithManifest, h v1.Hash) (int64, error) {
m, err := i.Manifest()
d, err := BlobDescriptor(i, h)
if err != nil {
return -1, err
}
return d.Size, nil
}
// BlobDescriptor is a helper for implementing v1.Image
func BlobDescriptor(i WithManifest, h v1.Hash) (v1.Descriptor, error) {
m, err := i.Manifest()
if err != nil {
return v1.Descriptor{}, err
}
if m.Config.Digest == h {
return m.Config, nil
}
for _, l := range m.Layers {
if l.Digest == h {
return l.Size, nil
return l, nil
}
}
return -1, fmt.Errorf("blob %v not found", h)
return v1.Descriptor{}, fmt.Errorf("blob %v not found", h)
}
// WithManifestAndConfigFile defines the subset of v1.Image used by these helper methods
@@ -262,25 +275,17 @@ func DiffIDToBlob(wm WithManifestAndConfigFile, h v1.Hash) (v1.Hash, error) {
}
}
return v1.Hash{}, fmt.Errorf("unknown diffID %v", h)
}
// WithBlob defines the subset of v1.Image used by these helper methods
type WithBlob interface {
// Blob returns a ReadCloser for streaming the blob's content.
Blob(v1.Hash) (io.ReadCloser, error)
}
// UncompressedBlob returns a ReadCloser for streaming the blob's content uncompressed.
func UncompressedBlob(b WithBlob, h v1.Hash) (io.ReadCloser, error) {
rc, err := b.Blob(h)
if err != nil {
return nil, err
}
return v1util.GunzipReadCloser(rc)
}
// WithDiffID defines the subset of v1.Layer for exposing the DiffID method.
type WithDiffID interface {
DiffID() (v1.Hash, error)
}
// withDescriptor allows partial layer implementations to provide a layer
// descriptor to the partial image manifest builder. This allows partial
// uncompressed layers to provide foreign layer metadata like URLs to the
// uncompressed image manifest.
type withDescriptor interface {
Descriptor() (*v1.Descriptor, error)
}
+56 -82
View File
@@ -18,21 +18,26 @@ import (
"archive/tar"
"bytes"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"io/ioutil"
mrand "math/rand"
"time"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/empty"
"github.com/google/go-containerregistry/pkg/v1/mutate"
"github.com/google/go-containerregistry/pkg/v1/partial"
"github.com/google/go-containerregistry/pkg/v1/types"
)
// uncompressedLayer implements partial.UncompressedLayer from raw bytes.
// TODO(mattmoor): Consider moving this into a library.
type uncompressedLayer struct {
diffID v1.Hash
content []byte
diffID v1.Hash
mediaType types.MediaType
content []byte
}
// DiffID implements partial.UncompressedLayer
@@ -47,97 +52,66 @@ func (ul *uncompressedLayer) Uncompressed() (io.ReadCloser, error) {
// MediaType returns the media type of the layer
func (ul *uncompressedLayer) MediaType() (types.MediaType, error) {
// Technically the media type should be 'application/tar' but given that our
// v1.Layer doesn't force consumers to care about whether the layer is compressed
// we should be fine returning the DockerLayer media type
return types.DockerLayer, nil
return ul.mediaType, nil
}
var _ partial.UncompressedLayer = (*uncompressedLayer)(nil)
// Image returns a pseudo-randomly generated Image.
func Image(byteSize, layers int64) (v1.Image, error) {
layerz := make(map[v1.Hash]partial.UncompressedLayer)
adds := make([]mutate.Addendum, 0, 5)
for i := int64(0); i < layers; i++ {
var b bytes.Buffer
tw := tar.NewWriter(&b)
if err := tw.WriteHeader(&tar.Header{
Name: fmt.Sprintf("random_file_%d.txt", i),
Size: byteSize,
Typeflag: tar.TypeRegA,
}); err != nil {
return nil, err
}
if _, err := io.CopyN(tw, rand.Reader, byteSize); err != nil {
return nil, err
}
if err := tw.Close(); err != nil {
return nil, err
}
bts := b.Bytes()
h, _, err := v1.SHA256(bytes.NewReader(bts))
layer, err := Layer(byteSize, types.DockerLayer)
if err != nil {
return nil, err
}
layerz[h] = &uncompressedLayer{
diffID: h,
content: bts,
}
}
cfg := &v1.ConfigFile{}
// Some clients check this.
cfg.RootFS.Type = "layers"
// It is ok that iteration order is random in Go, because this is the random image anyways.
for k := range layerz {
cfg.RootFS.DiffIDs = append(cfg.RootFS.DiffIDs, k)
}
for i := int64(0); i < layers; i++ {
cfg.History = append(cfg.History, v1.History{
Author: "random.Image",
Comment: fmt.Sprintf("this is a random history %d", i),
CreatedBy: "random",
Created: v1.Time{time.Now()},
adds = append(adds, mutate.Addendum{
Layer: layer,
History: v1.History{
Author: "random.Image",
Comment: fmt.Sprintf("this is a random history %d", i),
CreatedBy: "random",
Created: v1.Time{time.Now()},
},
})
}
return partial.UncompressedToImage(&image{
config: cfg,
layers: layerz,
return mutate.Append(empty.Image, adds...)
}
// 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())
// Hash the contents as we write it out to the buffer.
var b bytes.Buffer
hasher := sha256.New()
mw := io.MultiWriter(&b, hasher)
// Write a single file with a random name and random contents.
tw := tar.NewWriter(mw)
if err := tw.WriteHeader(&tar.Header{
Name: fileName,
Size: byteSize,
Typeflag: tar.TypeRegA,
}); err != nil {
return nil, err
}
if _, err := io.CopyN(tw, rand.Reader, byteSize); err != nil {
return nil, err
}
if err := tw.Close(); err != nil {
return nil, err
}
h := v1.Hash{
Algorithm: "sha256",
Hex: hex.EncodeToString(hasher.Sum(make([]byte, 0, hasher.Size()))),
}
return partial.UncompressedToLayer(&uncompressedLayer{
diffID: h,
mediaType: mt,
content: b.Bytes(),
})
}
// image is pseudo-randomly generated.
type image struct {
config *v1.ConfigFile
layers map[v1.Hash]partial.UncompressedLayer
}
var _ partial.UncompressedImageCore = (*image)(nil)
// RawConfigFile implements partial.UncompressedImageCore
func (i *image) RawConfigFile() ([]byte, error) {
return partial.RawConfigFile(i)
}
// ConfigFile implements v1.Image
func (i *image) ConfigFile() (*v1.ConfigFile, error) {
return i.config, nil
}
// MediaType implements partial.UncompressedImageCore
func (i *image) MediaType() (types.MediaType, error) {
return types.DockerManifestSchema2, nil
}
// LayerByDiffID implements partial.UncompressedImageCore
func (i *image) LayerByDiffID(diffID v1.Hash) (partial.UncompressedLayer, error) {
l, ok := i.layers[diffID]
if !ok {
return nil, fmt.Errorf("unknown diff_id: %v", diffID)
}
return l, nil
}
+4
View File
@@ -80,6 +80,10 @@ func (i *randomIndex) Digest() (v1.Hash, error) {
return partial.Digest(i)
}
func (i *randomIndex) Size() (int64, error) {
return partial.Size(i)
}
func (i *randomIndex) IndexManifest() (*v1.IndexManifest, error) {
return i.manifest, nil
}
+142
View File
@@ -0,0 +1,142 @@
// Copyright 2019 Google LLC All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package remote
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/v1/remote/transport"
)
type catalog struct {
Repos []string `json:"repositories"`
}
// CatalogPage calls /_catalog, returning the list of repositories on the registry.
func CatalogPage(target name.Registry, last string, n int, options ...Option) ([]string, error) {
o, err := makeOptions(target, options...)
if err != nil {
return nil, err
}
scopes := []string{target.Scope(transport.PullScope)}
tr, err := transport.New(target, o.auth, o.transport, scopes)
if err != nil {
return nil, err
}
query := fmt.Sprintf("last=%s&n=%d", url.QueryEscape(last), n)
uri := url.URL{
Scheme: target.Scheme(),
Host: target.RegistryStr(),
Path: "/v2/_catalog",
RawQuery: query,
}
client := http.Client{Transport: tr}
resp, err := client.Get(uri.String())
if err != nil {
return nil, err
}
defer resp.Body.Close()
if err := transport.CheckError(resp, http.StatusOK); err != nil {
return nil, err
}
var parsed catalog
if err := json.NewDecoder(resp.Body).Decode(&parsed); err != nil {
return nil, err
}
return parsed.Repos, nil
}
// Catalog calls /_catalog, returning the list of repositories on the registry.
func Catalog(ctx context.Context, target name.Registry, options ...Option) ([]string, error) {
o, err := makeOptions(target, options...)
if err != nil {
return nil, err
}
scopes := []string{target.Scope(transport.PullScope)}
tr, err := transport.New(target, o.auth, o.transport, scopes)
if err != nil {
return nil, err
}
uri := &url.URL{
Scheme: target.Scheme(),
Host: target.RegistryStr(),
Path: "/v2/_catalog",
RawQuery: "n=10000",
}
client := http.Client{Transport: tr}
var (
parsed catalog
repoList []string
)
// get responses until there is no next page
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
req, err := http.NewRequest("GET", uri.String(), nil)
if err != nil {
return nil, err
}
req = req.WithContext(ctx)
resp, err := client.Do(req)
if err != nil {
return nil, err
}
if err := transport.CheckError(resp, http.StatusOK); err != nil {
return nil, err
}
if err := json.NewDecoder(resp.Body).Decode(&parsed); err != nil {
return nil, err
}
if err := resp.Body.Close(); err != nil {
return nil, err
}
repoList = append(repoList, parsed.Repos...)
uri, err = getNextPageURL(resp)
if err != nil {
return nil, err
}
// no next page
if uri == nil {
break
}
}
return repoList, nil
}
+2 -12
View File
@@ -16,7 +16,6 @@ package remote
import (
"fmt"
"io/ioutil"
"net/http"
"net/url"
@@ -26,7 +25,7 @@ import (
// Delete removes the specified image reference from the remote registry.
func Delete(ref name.Reference, options ...Option) error {
o, err := makeOptions(ref.Context().Registry, options...)
o, err := makeOptions(ref.Context(), options...)
if err != nil {
return err
}
@@ -54,14 +53,5 @@ func Delete(ref name.Reference, options ...Option) error {
}
defer resp.Body.Close()
switch resp.StatusCode {
case http.StatusOK, http.StatusAccepted:
return nil
default:
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
return fmt.Errorf("unrecognized status code during DELETE: %v; %v", resp.Status, string(b))
}
return transport.CheckError(resp, http.StatusOK, http.StatusAccepted)
}
+65 -25
View File
@@ -17,16 +17,19 @@ package remote
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"strings"
"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/types"
"github.com/google/go-containerregistry/pkg/v1/v1util"
)
var defaultPlatform = v1.Platform{
@@ -41,7 +44,8 @@ type ErrSchema1 struct {
schema string
}
func NewErrSchema1(schema types.MediaType) error {
// newErrSchema1 returns an ErrSchema1 with the unexpected MediaType.
func newErrSchema1(schema types.MediaType) error {
return &ErrSchema1{
schema: string(schema),
}
@@ -63,6 +67,11 @@ type Descriptor struct {
platform v1.Platform
}
// RawManifest exists to satisfy the Taggable interface.
func (d *Descriptor) RawManifest() ([]byte, error) {
return d.Manifest, nil
}
// Get returns a remote.Descriptor for the given reference. The response from
// the registry is left un-interpreted, for the most part. This is useful for
// querying what kind of artifact a reference represents.
@@ -82,28 +91,20 @@ func Get(ref name.Reference, options ...Option) (*Descriptor, error) {
// Handle options and fetch the manifest with the acceptable MediaTypes in the
// Accept header.
func get(ref name.Reference, acceptable []types.MediaType, options ...Option) (*Descriptor, error) {
o, err := makeOptions(ref.Context().Registry, options...)
o, err := makeOptions(ref.Context(), options...)
if err != nil {
return nil, err
}
tr, err := transport.New(ref.Context().Registry, o.auth, o.transport, []string{ref.Scope(transport.PullScope)})
f, err := makeFetcher(ref, o)
if err != nil {
return nil, err
}
f := fetcher{
Ref: ref,
Client: &http.Client{Transport: tr},
}
b, desc, err := f.fetchManifest(ref, acceptable)
if err != nil {
return nil, err
}
return &Descriptor{
fetcher: f,
fetcher: *f,
Manifest: b,
Descriptor: *desc,
platform: o.platform,
@@ -123,7 +124,7 @@ func (d *Descriptor) Image() (v1.Image, error) {
case types.DockerManifestSchema1, types.DockerManifestSchema1Signed:
// We don't care to support schema 1 images:
// https://github.com/google/go-containerregistry/issues/377
return nil, NewErrSchema1(d.MediaType)
return nil, newErrSchema1(d.MediaType)
case types.OCIImageIndex, types.DockerManifestList:
// We want an image but the registry has an index, resolve it to an image.
return d.remoteIndex().imageByPlatform(d.platform)
@@ -132,7 +133,7 @@ func (d *Descriptor) Image() (v1.Image, error) {
default:
// We could just return an error here, but some registries (e.g. static
// registries) don't set the Content-Type headers correctly, so instead...
// TODO(#390): Log a warning.
logs.Warn.Printf("Unexpected media type for Image(): %s", d.MediaType)
}
// Wrap the v1.Layers returned by this v1.Image in a hint for downstream
@@ -153,7 +154,7 @@ func (d *Descriptor) ImageIndex() (v1.ImageIndex, error) {
case types.DockerManifestSchema1, types.DockerManifestSchema1Signed:
// We don't care to support schema 1 images:
// https://github.com/google/go-containerregistry/issues/377
return nil, NewErrSchema1(d.MediaType)
return nil, newErrSchema1(d.MediaType)
case types.OCIManifestSchema1, types.DockerManifestSchema2:
// We want an index but the registry has an image, nothing we can do.
return nil, fmt.Errorf("unexpected media type for ImageIndex(): %s; call Image() instead", d.MediaType)
@@ -162,7 +163,7 @@ func (d *Descriptor) ImageIndex() (v1.ImageIndex, error) {
default:
// We could just return an error here, but some registries (e.g. static
// registries) don't set the Content-Type headers correctly, so instead...
// TODO(#390): Log a warning.
logs.Warn.Printf("Unexpected media type for ImageIndex(): %s", d.MediaType)
}
return d.remoteIndex(), nil
}
@@ -195,6 +196,17 @@ type fetcher struct {
Client *http.Client
}
func makeFetcher(ref name.Reference, o *options) (*fetcher, error) {
tr, err := transport.New(ref.Context().Registry, o.auth, o.transport, []string{ref.Scope(transport.PullScope)})
if err != nil {
return nil, err
}
return &fetcher{
Ref: ref,
Client: &http.Client{Transport: tr},
}, nil
}
// url returns a url.Url for the specified path in the context of this remote image reference.
func (f *fetcher) url(resource, identifier string) url.URL {
return url.URL{
@@ -249,16 +261,14 @@ func (f *fetcher) fetchManifest(ref name.Reference, acceptable []types.MediaType
if digest.String() != dgst.DigestStr() {
return nil, nil, fmt.Errorf("manifest digest: %q does not match requested digest: %q for %q", digest, dgst.DigestStr(), f.Ref)
}
} else {
// Do nothing for tags; I give up.
//
// We'd like to validate that the "Docker-Content-Digest" header matches what is returned by the registry,
// but so many registries implement this incorrectly that it's not worth checking.
//
// For reference:
// https://github.com/docker/distribution/issues/2395
// https://github.com/GoogleContainerTools/kaniko/issues/298
}
// Do nothing for tags; I give up.
//
// We'd like to validate that the "Docker-Content-Digest" header matches what is returned by the registry,
// but so many registries implement this incorrectly that it's not worth checking.
//
// For reference:
// https://github.com/GoogleContainerTools/kaniko/issues/298
// Return all this info since we have to calculate it anyway.
desc := v1.Descriptor{
@@ -269,3 +279,33 @@ func (f *fetcher) fetchManifest(ref name.Reference, acceptable []types.MediaType
return manifest, &desc, nil
}
func (f *fetcher) fetchBlob(h v1.Hash) (io.ReadCloser, error) {
u := f.url("blobs", h.String())
resp, err := f.Client.Get(u.String())
if err != nil {
return nil, err
}
if err := transport.CheckError(resp, http.StatusOK); err != nil {
resp.Body.Close()
return nil, err
}
return v1util.VerifyReadCloser(resp.Body, h)
}
func (f *fetcher) headBlob(h v1.Hash) (*http.Response, error) {
u := f.url("blobs", h.String())
resp, err := f.Client.Head(u.String())
if err != nil {
return nil, err
}
if err := transport.CheckError(resp, http.StatusOK); err != nil {
resp.Body.Close()
return nil, err
}
return resp, nil
}
+47 -39
View File
@@ -15,10 +15,10 @@
package remote
import (
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"sync"
"github.com/google/go-containerregistry/pkg/name"
@@ -43,15 +43,7 @@ var _ partial.CompressedImageCore = (*remoteImage)(nil)
// Image provides access to a remote image reference.
func Image(ref name.Reference, options ...Option) (v1.Image, error) {
acceptable := []types.MediaType{
types.DockerManifestSchema2,
types.OCIManifestSchema1,
// We resolve these to images later.
types.DockerManifestList,
types.OCIImageIndex,
}
desc, err := get(ref, acceptable, options...)
desc, err := Get(ref, options...)
if err != nil {
return nil, err
}
@@ -102,11 +94,7 @@ func (r *remoteImage) RawConfigFile() ([]byte, error) {
return nil, err
}
cl, err := r.LayerByDigest(m.Config.Digest)
if err != nil {
return nil, err
}
body, err := cl.Compressed()
body, err := r.fetchBlob(m.Config.Digest)
if err != nil {
return nil, err
}
@@ -119,74 +107,94 @@ func (r *remoteImage) RawConfigFile() ([]byte, error) {
return r.config, nil
}
// remoteLayer implements partial.CompressedLayer
type remoteLayer struct {
// remoteImageLayer implements partial.CompressedLayer
type remoteImageLayer struct {
ri *remoteImage
digest v1.Hash
}
// Digest implements partial.CompressedLayer
func (rl *remoteLayer) Digest() (v1.Hash, error) {
func (rl *remoteImageLayer) Digest() (v1.Hash, error) {
return rl.digest, nil
}
// Compressed implements partial.CompressedLayer
func (rl *remoteLayer) Compressed() (io.ReadCloser, error) {
u := rl.ri.url("blobs", rl.digest.String())
resp, err := rl.ri.Client.Get(u.String())
func (rl *remoteImageLayer) Compressed() (io.ReadCloser, error) {
urls := []url.URL{rl.ri.url("blobs", rl.digest.String())}
// Add alternative layer sources from URLs (usually none).
d, err := partial.BlobDescriptor(rl, rl.digest)
if err != nil {
return nil, err
}
if err := transport.CheckError(resp, http.StatusOK); err != nil {
resp.Body.Close()
return nil, err
for _, s := range d.URLs {
u, err := url.Parse(s)
if err != nil {
return nil, err
}
urls = append(urls, *u)
}
return v1util.VerifyReadCloser(resp.Body, rl.digest)
// The lastErr for most pulls will be the same (the first error), but for
// foreign layers we'll want to surface the last one, since we try to pull
// from the registry first, which would often fail.
// TODO: Maybe we don't want to try pulling from the registry first?
var lastErr error
for _, u := range urls {
resp, err := rl.ri.Client.Get(u.String())
if err != nil {
lastErr = err
continue
}
if err := transport.CheckError(resp, http.StatusOK); err != nil {
resp.Body.Close()
lastErr = err
continue
}
return v1util.VerifyReadCloser(resp.Body, rl.digest)
}
return nil, lastErr
}
// Manifest implements partial.WithManifest so that we can use partial.BlobSize below.
func (rl *remoteLayer) Manifest() (*v1.Manifest, error) {
func (rl *remoteImageLayer) Manifest() (*v1.Manifest, error) {
return partial.Manifest(rl.ri)
}
// MediaType implements v1.Layer
func (rl *remoteLayer) MediaType() (types.MediaType, error) {
m, err := rl.Manifest()
func (rl *remoteImageLayer) MediaType() (types.MediaType, error) {
bd, err := partial.BlobDescriptor(rl, rl.digest)
if err != nil {
return "", err
}
for _, layer := range m.Layers {
if layer.Digest == rl.digest {
return layer.MediaType, nil
}
}
return "", fmt.Errorf("unable to find layer with digest: %v", rl.digest)
return bd.MediaType, nil
}
// Size implements partial.CompressedLayer
func (rl *remoteLayer) Size() (int64, error) {
func (rl *remoteImageLayer) Size() (int64, error) {
// Look up the size of this digest in the manifest to avoid a request.
return partial.BlobSize(rl, rl.digest)
}
// ConfigFile implements partial.WithManifestAndConfigFile so that we can use partial.BlobToDiffID below.
func (rl *remoteLayer) ConfigFile() (*v1.ConfigFile, error) {
func (rl *remoteImageLayer) ConfigFile() (*v1.ConfigFile, error) {
return partial.ConfigFile(rl.ri)
}
// DiffID implements partial.WithDiffID so that we don't recompute a DiffID that we already have
// available in our ConfigFile.
func (rl *remoteLayer) DiffID() (v1.Hash, error) {
func (rl *remoteImageLayer) DiffID() (v1.Hash, error) {
return partial.BlobToDiffID(rl, rl.digest)
}
// LayerByDigest implements partial.CompressedLayer
func (r *remoteImage) LayerByDigest(h v1.Hash) (partial.CompressedLayer, error) {
return &remoteLayer{
return &remoteImageLayer{
ri: r,
digest: h,
}, nil
+5 -8
View File
@@ -59,6 +59,10 @@ func (r *remoteIndex) Digest() (v1.Hash, error) {
return partial.Digest(r)
}
func (r *remoteIndex) Size() (int64, error) {
return partial.Size(r)
}
func (r *remoteIndex) RawManifest() ([]byte, error) {
r.manifestLock.Lock()
defer r.manifestLock.Unlock()
@@ -158,16 +162,9 @@ func (r *remoteIndex) childByHash(h v1.Hash) (*Descriptor, error) {
return nil, fmt.Errorf("no child with digest %s in index %s", h, r.Ref)
}
func (r *remoteIndex) childRef(h v1.Hash) (name.Reference, error) {
return name.ParseReference(fmt.Sprintf("%s@%s", r.Ref.Context(), h), name.StrictValidation)
}
// 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, err := r.childRef(child.Digest)
if err != nil {
return nil, err
}
ref := r.Ref.Context().Digest(child.Digest.String())
manifest, desc, err := r.fetchManifest(ref, []types.MediaType{child.MediaType})
if err != nil {
return nil, err
+84
View File
@@ -0,0 +1,84 @@
// Copyright 2019 Google LLC All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package remote
import (
"io"
"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/types"
)
// remoteImagelayer implements partial.CompressedLayer
type remoteLayer struct {
fetcher
digest v1.Hash
}
// Compressed implements partial.CompressedLayer
func (rl *remoteLayer) Compressed() (io.ReadCloser, error) {
return rl.fetchBlob(rl.digest)
}
// Compressed implements partial.CompressedLayer
func (rl *remoteLayer) Size() (int64, error) {
resp, err := rl.headBlob(rl.digest)
if err != nil {
return -1, err
}
return resp.ContentLength, nil
}
// Digest implements partial.CompressedLayer
func (rl *remoteLayer) Digest() (v1.Hash, error) {
return rl.digest, nil
}
// MediaType implements v1.Layer
func (rl *remoteLayer) MediaType() (types.MediaType, error) {
return types.DockerLayer, nil
}
// 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
// that blob lives.
func Layer(ref name.Digest, options ...Option) (v1.Layer, error) {
o, err := makeOptions(ref.Context(), options...)
if err != nil {
return nil, err
}
f, err := makeFetcher(ref, o)
if err != nil {
return nil, err
}
h, err := v1.NewHash(ref.Identifier())
if err != nil {
return nil, err
}
l, err := partial.CompressedToLayer(&remoteLayer{
fetcher: *f,
digest: h,
})
if err != nil {
return nil, err
}
return &MountableLayer{
Layer: l,
Reference: ref,
}, nil
}
+88 -19
View File
@@ -15,10 +15,12 @@
package remote
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/v1/remote/transport"
@@ -29,10 +31,15 @@ type tags struct {
Tags []string `json:"tags"`
}
// List calls /tags/list for the given repository, returning the list of tags
// in the "tags" property.
// List wraps ListWithContext using the background context.
func List(repo name.Repository, options ...Option) ([]string, error) {
o, err := makeOptions(repo.Registry, options...)
return ListWithContext(context.Background(), repo, options...)
}
// ListWithContext 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) {
o, err := makeOptions(repo, options...)
if err != nil {
return nil, err
}
@@ -42,27 +49,89 @@ func List(repo name.Repository, options ...Option) ([]string, error) {
return nil, err
}
uri := url.URL{
Scheme: repo.Registry.Scheme(),
Host: repo.Registry.RegistryStr(),
Path: fmt.Sprintf("/v2/%s/tags/list", repo.RepositoryStr()),
uri := &url.URL{
Scheme: repo.Registry.Scheme(),
Host: repo.Registry.RegistryStr(),
Path: fmt.Sprintf("/v2/%s/tags/list", repo.RepositoryStr()),
RawQuery: "n=10000",
}
client := http.Client{Transport: tr}
resp, err := client.Get(uri.String())
tagList := []string{}
parsed := tags{}
// get responses until there is no next page
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
req, err := http.NewRequest("GET", uri.String(), nil)
if err != nil {
return nil, err
}
req = req.WithContext(ctx)
resp, err := client.Do(req)
if err != nil {
return nil, err
}
if err := transport.CheckError(resp, http.StatusOK); err != nil {
return nil, err
}
if err := json.NewDecoder(resp.Body).Decode(&parsed); err != nil {
return nil, err
}
if err := resp.Body.Close(); err != nil {
return nil, err
}
tagList = append(tagList, parsed.Tags...)
uri, err = getNextPageURL(resp)
if err != nil {
return nil, err
}
// no next page
if uri == nil {
break
}
}
return tagList, 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
}
defer resp.Body.Close()
if err := transport.CheckError(resp, http.StatusOK); err != nil {
return nil, err
if resp.Request == nil || resp.Request.URL == nil {
return nil, nil
}
parsed := tags{}
if err := json.NewDecoder(resp.Body).Decode(&parsed); err != nil {
return nil, err
}
return parsed.Tags, nil
linkURL = resp.Request.URL.ResolveReference(linkURL)
return linkURL, nil
}
+9 -3
View File
@@ -19,7 +19,6 @@ import (
"github.com/google/go-containerregistry/pkg/authn"
"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/remote/transport"
)
@@ -34,7 +33,7 @@ type options struct {
platform v1.Platform
}
func makeOptions(reg name.Registry, opts ...Option) (*options, error) {
func makeOptions(target authn.Resource, opts ...Option) (*options, error) {
o := &options{
auth: authn.Anonymous,
transport: http.DefaultTransport,
@@ -48,7 +47,7 @@ func makeOptions(reg name.Registry, opts ...Option) (*options, error) {
}
if o.keychain != nil {
auth, err := o.keychain.Resolve(reg)
auth, err := o.keychain.Resolve(target)
if err != nil {
return nil, err
}
@@ -58,6 +57,13 @@ func makeOptions(reg name.Registry, opts ...Option) (*options, error) {
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)
}
// Wrap the transport in something that can retry network flakes.
o.transport = transport.NewRetry(o.transport)
@@ -0,0 +1,127 @@
# `transport`
The [distribution protocol](https://github.com/opencontainers/distribution-spec) is fairly simple, but correctly implementing authentication is **hard**.
This package [implements](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/remote/transport#New) an [`http.RoundTripper`](https://godoc.org/net/http#RoundTripper)
that transparently performs:
* [Token
Authentication](https://docs.docker.com/registry/spec/auth/token/) and
* [OAuth2
Authentication](https://docs.docker.com/registry/spec/auth/oauth/)
for registry clients.
## Raison d'être
> Why not just use the [`docker/distribution`](https://godoc.org/github.com/docker/distribution/registry/client/auth) client?
Great question! Mostly, because I don't want to depend on [`prometheus/client_golang`](https://github.com/prometheus/client_golang).
As a performance optimization, that client uses [a cache](https://github.com/docker/distribution/blob/a8371794149d1d95f1e846744b05c87f2f825e5a/registry/client/repository.go#L173) to keep track of a mapping between blob digests and their [descriptors](https://github.com/docker/distribution/blob/a8371794149d1d95f1e846744b05c87f2f825e5a/blobs.go#L57-L86). Unfortunately, the cache [uses prometheus](https://github.com/docker/distribution/blob/a8371794149d1d95f1e846744b05c87f2f825e5a/registry/storage/cache/cachedblobdescriptorstore.go#L44) to track hits and misses, so if you want to use that client you have to pull in all of prometheus, which is pretty large.
![docker/distribution](../../../../images/docker.dot.svg)
> Why does it matter if you depend on prometheus? Who cares?
It's generally polite to your downstream to reduce the number of dependencies your package requires:
* Downloading your package is faster, which helps our Australian friends and people on airplanes.
* There is less code to compile, which speeds up builds and saves the planet from global warming.
* You reduce the likelihood of inflicting dependency hell upon your consumers.
* [Tim Hockin](https://twitter.com/thockin/status/958606077456654336) prefers it based on his experience working on Kubernetes, and he's a pretty smart guy.
> Okay, what about [`containerd/containerd`](https://godoc.org/github.com/containerd/containerd/remotes/docker)?
Similar reasons! That ends up pulling in grpc, protobuf, and logrus.
![containerd/containerd](../../../../images/containerd.dot.svg)
> Well... what about [`containers/image`](https://godoc.org/github.com/containers/image/docker)?
That just uses the the `docker/distribution` client... and more!
![containerd/containerd](../../../../images/containers.dot.svg)
> Wow, what about this package?
Of course, this package isn't perfect either. `transport` depends on `authn`,
which in turn depends on docker's config file parsing and handling package,
which you don't strictly need but almost certainly want if you're going to be
interacting with a registry.
![google/go-containerregistry](../../../../images/ggcr.dot.svg)
*These graphs were generated by
[`kisielk/godepgraph`](https://github.com/kisielk/godepgraph).*
## Usage
This is heavily used by the
[`remote`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/remote)
package, which implements higher level image-centric functionality, but this
package is useful if you want to interact directly with the registry to do
something that `remote` doesn't support, e.g. [to handle with schema 1
images](https://github.com/google/go-containerregistry/pull/509).
This package also includes some [error
handling](https://github.com/opencontainers/distribution-spec/blob/60be706c34ee7805bdd1d3d11affec53b0dfb8fb/spec.md#errors)
facilities in the form of
[`CheckError`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/remote/transport#CheckError),
which will parse the response body into a structured error for unexpected http
status codes.
Here's a "simple" program that writes the result of
[listing tags](https://github.com/opencontainers/distribution-spec/blob/60be706c34ee7805bdd1d3d11affec53b0dfb8fb/spec.md#tags)
for [`gcr.io/google-containers/pause`](https://gcr.io/google-containers/pause)
to stdout.
```go
package main
import (
"io"
"net/http"
"os"
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/v1/remote/transport"
)
func main() {
repo, err := name.NewRepository("gcr.io/google-containers/pause")
if err != nil {
panic(err)
}
// Fetch credentials based on your docker config file, which is $HOME/.docker/config.json or $DOCKER_CONFIG.
auth, err := authn.DefaultKeychain.Resolve(repo.Registry)
if err != nil {
panic(err)
}
// Construct an http.Client that is authorized to pull from gcr.io/google-containers/pause.
scopes := []string{repo.Scope(transport.PullScope)}
t, err := transport.New(repo.Registry, auth, http.DefaultTransport, scopes)
if err != nil {
panic(err)
}
client := &http.Client{Transport: t}
// Make the actual request.
resp, err := client.Get("https://gcr.io/v2/google-containers/pause/tags/list")
if err != nil {
panic(err)
}
// Assert that we get a 200, otherwise attempt to parse body as a structured error.
if err := transport.CheckError(resp, http.StatusOK); err != nil {
panic(err)
}
// Write the response to stdout.
if _, err := io.Copy(os.Stdout, resp.Body); err != nil {
panic(err)
}
}
```
@@ -15,6 +15,8 @@
package transport
import (
"encoding/base64"
"fmt"
"net/http"
"github.com/google/go-containerregistry/pkg/authn"
@@ -30,18 +32,31 @@ var _ http.RoundTripper = (*basicTransport)(nil)
// RoundTrip implements http.RoundTripper
func (bt *basicTransport) RoundTrip(in *http.Request) (*http.Response, error) {
hdr, err := bt.auth.Authorization()
if err != nil {
return nil, err
}
if bt.auth != authn.Anonymous {
auth, err := bt.auth.Authorization()
if err != nil {
return nil, err
}
// http.Client handles redirects at a layer above the http.RoundTripper
// abstraction, so to avoid forwarding Authorization headers to places
// we are redirected, only set it when the authorization header matches
// the host with which we are interacting.
// In case of redirect http.Client can use an empty Host, check URL too.
if hdr != "" && (in.Host == bt.target || in.URL.Host == bt.target) {
in.Header.Set("Authorization", hdr)
// http.Client handles redirects at a layer above the http.RoundTripper
// abstraction, so to avoid forwarding Authorization headers to places
// we are redirected, only set it when the authorization header matches
// the host with which we are interacting.
// In case of redirect http.Client can use an empty Host, check URL too.
if in.Host == bt.target || in.URL.Host == bt.target {
if bearer := auth.RegistryToken; bearer != "" {
hdr := fmt.Sprintf("Bearer %s", bearer)
in.Header.Set("Authorization", hdr)
} else if user, pass := auth.Username, auth.Password; user != "" && pass != "" {
delimited := fmt.Sprintf("%s:%s", user, pass)
encoded := base64.StdEncoding.EncodeToString([]byte(delimited))
hdr := fmt.Sprintf("Basic %s", encoded)
in.Header.Set("Authorization", hdr)
} else if token := auth.Auth; token != "" {
hdr := fmt.Sprintf("Basic %s", token)
in.Header.Set("Authorization", hdr)
}
}
}
in.Header.Set("User-Agent", transportName)
return bt.inner.RoundTrip(in)
@@ -55,29 +55,19 @@ var portMap = map[string]string{
// RoundTrip implements http.RoundTripper
func (bt *bearerTransport) RoundTrip(in *http.Request) (*http.Response, error) {
sendRequest := func() (*http.Response, error) {
hdr, err := bt.bearer.Authorization()
if err != nil {
return nil, err
}
// http.Client handles redirects at a layer above the http.RoundTripper
// abstraction, so to avoid forwarding Authorization headers to places
// we are redirected, only set it when the authorization header matches
// the registry with which we are interacting.
// In case of redirect http.Client can use an empty Host, check URL too.
canonicalHeaderHost := bt.canonicalAddress(in.Host)
canonicalURLHost := bt.canonicalAddress(in.URL.Host)
canonicalRegistryHost := bt.canonicalAddress(bt.registry.RegistryStr())
if canonicalHeaderHost == canonicalRegistryHost || canonicalURLHost == canonicalRegistryHost {
if matchesHost(bt.registry, in, bt.scheme) {
auth, err := bt.bearer.Authorization()
if err != nil {
return nil, err
}
hdr := fmt.Sprintf("Bearer %s", auth.RegistryToken)
in.Header.Set("Authorization", hdr)
// When we ping() the registry, we determine whether to use http or https
// based on which scheme was successful. That is only valid for the
// registry server and not e.g. a separate token server or blob storage,
// so we should only override the scheme if the host is the registry.
in.URL.Scheme = bt.scheme
}
in.Header.Set("User-Agent", transportName)
return bt.inner.RoundTrip(in)
}
@@ -97,42 +87,40 @@ func (bt *bearerTransport) RoundTrip(in *http.Request) (*http.Response, error) {
return res, err
}
// It's unclear which authentication flow to use based purely on the protocol,
// so we rely on heuristics and fallbacks to support as many registries as possible.
// The basic token exchange is attempted first, falling back to the oauth flow.
// If the IdentityToken is set, this indicates that we should start with the oauth flow.
func (bt *bearerTransport) refresh() error {
u, err := url.Parse(bt.realm)
auth, err := bt.basic.Authorization()
if err != nil {
return err
}
b := &basicTransport{
inner: bt.inner,
auth: bt.basic,
target: u.Host,
var content []byte
if auth.IdentityToken != "" {
// If the secret being stored is an identity token,
// the Username should be set to <token>, which indicates
// we are using an oauth flow.
content, err = bt.refreshOauth()
if terr, ok := err.(*Error); ok && 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.
content, err = bt.refreshBasic()
}
} else {
content, err = bt.refreshBasic()
}
client := http.Client{Transport: b}
u.RawQuery = url.Values{
"scope": bt.scopes,
"service": []string{bt.service},
}.Encode()
resp, err := client.Get(u.String())
if err != nil {
return err
}
defer resp.Body.Close()
if err := CheckError(resp, http.StatusOK); err != nil {
return err
}
content, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
// Some registries don't have "token" in the response. See #54.
type tokenResponse struct {
Token string `json:"token"`
AccessToken string `json:"access_token"`
Token string `json:"token"`
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
// TODO: handle expiry?
}
var response tokenResponse
@@ -140,22 +128,39 @@ func (bt *bearerTransport) refresh() error {
return err
}
// Some registries set access_token instead of token.
if response.AccessToken != "" {
response.Token = response.AccessToken
}
// Find a token to turn into a Bearer authenticator
var bearer authn.Bearer
if response.Token != "" {
bearer = authn.Bearer{Token: response.Token}
} else if response.AccessToken != "" {
bearer = authn.Bearer{Token: response.AccessToken}
} else {
return fmt.Errorf("no token in bearer response:\n%s", content)
}
// If we obtained a refresh token from the oauth flow, use that for refresh() now.
if response.RefreshToken != "" {
bt.basic = authn.FromConfig(authn.AuthConfig{
IdentityToken: response.RefreshToken,
})
}
// Replace our old bearer authenticator (if we had one) with our newly refreshed authenticator.
bt.bearer = &bearer
return nil
}
func (bt *bearerTransport) canonicalAddress(host string) (address string) {
func matchesHost(reg name.Registry, in *http.Request, scheme string) bool {
canonicalHeaderHost := canonicalAddress(in.Host, scheme)
canonicalURLHost := canonicalAddress(in.URL.Host, scheme)
canonicalRegistryHost := canonicalAddress(reg.RegistryStr(), scheme)
return canonicalHeaderHost == canonicalRegistryHost || canonicalURLHost == canonicalRegistryHost
}
func canonicalAddress(host, scheme string) (address string) {
// The host may be any one of:
// - hostname
// - hostname:port
@@ -171,11 +176,83 @@ func (bt *bearerTransport) canonicalAddress(host string) (address string) {
return host
}
if port == "" {
port = portMap[bt.scheme]
port = portMap[scheme]
}
return net.JoinHostPort(hostname, port)
}
return net.JoinHostPort(host, portMap[bt.scheme])
return net.JoinHostPort(host, portMap[scheme])
}
// https://docs.docker.com/registry/spec/auth/oauth/
func (bt *bearerTransport) refreshOauth() ([]byte, error) {
auth, err := bt.basic.Authorization()
if err != nil {
return nil, err
}
u, err := url.Parse(bt.realm)
if err != nil {
return nil, err
}
v := url.Values{}
v.Set("scope", strings.Join(bt.scopes, " "))
v.Set("service", bt.service)
v.Set("client_id", transportName)
if auth.IdentityToken != "" {
v.Set("grant_type", "refresh_token")
v.Set("refresh_token", auth.IdentityToken)
} else if auth.Username != "" && auth.Password != "" {
// TODO(#629): This is unreachable.
v.Set("grant_type", "password")
v.Set("username", auth.Username)
v.Set("password", auth.Password)
v.Set("access_type", "offline")
}
client := http.Client{Transport: bt.inner}
resp, err := client.PostForm(u.String(), v)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if err := CheckError(resp, http.StatusOK); err != nil {
return nil, err
}
return ioutil.ReadAll(resp.Body)
}
// https://docs.docker.com/registry/spec/auth/token/
func (bt *bearerTransport) refreshBasic() ([]byte, error) {
u, err := url.Parse(bt.realm)
if err != nil {
return nil, err
}
b := &basicTransport{
inner: bt.inner,
auth: bt.basic,
target: u.Host,
}
client := http.Client{Transport: b}
u.RawQuery = url.Values{
"scope": bt.scopes,
"service": []string{bt.service},
}.Encode()
resp, err := client.Get(u.String())
if err != nil {
return nil, err
}
defer resp.Body.Close()
if err := CheckError(resp, http.StatusOK); err != nil {
return nil, err
}
return ioutil.ReadAll(resp.Body)
}
@@ -0,0 +1,44 @@
package transport
import (
"fmt"
"net/http"
"net/http/httputil"
"github.com/google/go-containerregistry/pkg/logs"
)
type logTransport struct {
inner http.RoundTripper
}
// NewLogger returns a transport that logs requests and responses to
// github.com/google/go-containerregistry/pkg/logs.Debug.
func NewLogger(inner http.RoundTripper) http.RoundTripper {
return &logTransport{inner}
}
func (t *logTransport) RoundTrip(in *http.Request) (out *http.Response, err error) {
// Inspired by: github.com/motemen/go-loghttp
logs.Debug.Printf("--> %s %s", in.Method, in.URL)
b, err := httputil.DumpRequestOut(in, true)
if err == nil {
logs.Debug.Println(string(b))
}
out, err = t.inner.RoundTrip(in)
if err != nil {
logs.Debug.Printf("<-- %v %s", err, in.URL)
}
if out != nil {
msg := fmt.Sprintf("<-- %d", out.StatusCode)
if out.Request != nil {
msg = fmt.Sprintf("%s %s", msg, out.Request.URL)
}
logs.Debug.Print(msg)
b, err := httputil.DumpResponse(out, true)
if err == nil {
logs.Debug.Println(string(b))
}
}
return
}
@@ -93,7 +93,7 @@ func ping(reg name.Registry, t http.RoundTripper) (*pingResp, error) {
scheme: scheme,
}, nil
case http.StatusUnauthorized:
wac := resp.Header.Get(http.CanonicalHeaderKey("WWW-Authenticate"))
wac := resp.Header.Get("WWW-Authenticate")
if parts := strings.SplitN(wac, " ", 2); len(parts) == 2 {
// If there are two parts, then parse the challenge parameters.
return &pingResp{
@@ -108,7 +108,7 @@ func ping(reg name.Registry, t http.RoundTripper) (*pingResp, error) {
scheme: scheme,
}, nil
default:
return nil, fmt.Errorf("unrecognized HTTP status: %v", resp.Status)
return nil, CheckError(resp, http.StatusOK, http.StatusUnauthorized)
}
}
return nil, connErr
@@ -19,11 +19,10 @@ import (
"time"
"github.com/google/go-containerregistry/pkg/internal/retry"
"k8s.io/apimachinery/pkg/util/wait"
)
// Sleep for 0.1, 0.3, 0.9, 2.7 seconds. This should cover networking blips.
var defaultBackoff = wait.Backoff{
var defaultBackoff = retry.Backoff{
Duration: 100 * time.Millisecond,
Factor: 3.0,
Jitter: 0.1,
@@ -35,7 +34,7 @@ var _ http.RoundTripper = (*retryTransport)(nil)
// retryTransport wraps a RoundTripper and retries temporary network errors.
type retryTransport struct {
inner http.RoundTripper
backoff wait.Backoff
backoff retry.Backoff
predicate retry.Predicate
}
@@ -43,12 +42,12 @@ type retryTransport struct {
type Option func(*options)
type options struct {
backoff wait.Backoff
backoff retry.Backoff
predicate retry.Predicate
}
// WithRetryBackoff sets the backoff for retry operations.
func WithRetryBackoff(backoff wait.Backoff) Option {
func WithRetryBackoff(backoff retry.Backoff) Option {
return func(o *options) {
o.backoff = backoff
}
@@ -0,0 +1,44 @@
// Copyright 2019 Google LLC All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package transport
import (
"net/http"
"github.com/google/go-containerregistry/pkg/name"
)
type schemeTransport struct {
// Scheme we should use, determined by ping response.
scheme string
// Registry we're talking to.
registry name.Registry
// Wrapped by schemeTransport.
inner http.RoundTripper
}
// RoundTrip implements http.RoundTripper
func (st *schemeTransport) RoundTrip(in *http.Request) (*http.Response, error) {
// When we ping() the registry, we determine whether to use http or https
// based on which scheme was successful. That is only valid for the
// registry server and not e.g. a separate token server or blob storage,
// so we should only override the scheme if the host is the registry.
if matchesHost(st.registry, in, st.scheme) {
in.URL.Scheme = st.scheme
}
return st.inner.RoundTrip(in)
}
@@ -22,10 +22,6 @@ import (
"github.com/google/go-containerregistry/pkg/name"
)
const (
transportName = "go-containerregistry"
)
// New returns a new RoundTripper based on the provided RoundTripper that has been
// setup to authenticate with the remote registry "reg", in the capacity
// laid out by the specified scopes.
@@ -49,6 +45,16 @@ func New(reg name.Registry, auth authn.Authenticator, t http.RoundTripper, scope
return nil, err
}
// Wrap the given transport in transports that use an appropriate scheme,
// (based on the ping response) and set the user agent.
t = &useragentTransport{
inner: &schemeTransport{
scheme: pr.scheme,
registry: reg,
inner: t,
},
}
switch pr.challenge.Canonical() {
case anonymous:
return t, nil
@@ -80,6 +86,6 @@ func New(reg name.Registry, auth authn.Authenticator, t http.RoundTripper, scope
}
return bt, nil
default:
return nil, fmt.Errorf("Unrecognized challenge: %s", pr.challenge)
return nil, fmt.Errorf("unrecognized challenge: %s", pr.challenge)
}
}
@@ -0,0 +1,32 @@
// Copyright 2019 Google LLC All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package transport
import "net/http"
const (
transportName = "go-containerregistry"
)
type useragentTransport struct {
// Wrapped by useragentTransport.
inner http.RoundTripper
}
// RoundTrip implements http.RoundTripper
func (ut *useragentTransport) RoundTrip(in *http.Request) (*http.Response, error) {
in.Header.Set("User-Agent", transportName)
return ut.inner.RoundTrip(in)
}
+112 -24
View File
@@ -31,13 +31,11 @@ import (
"github.com/google/go-containerregistry/pkg/v1/remote/transport"
"github.com/google/go-containerregistry/pkg/v1/types"
"golang.org/x/sync/errgroup"
"k8s.io/apimachinery/pkg/util/wait"
)
type manifest interface {
// Taggable is an interface that enables a manifest PUT (e.g. for tagging).
type Taggable interface {
RawManifest() ([]byte, error)
MediaType() (types.MediaType, error)
Digest() (v1.Hash, error)
}
// Write pushes the provided img to the specified image reference.
@@ -47,7 +45,7 @@ func Write(ref name.Reference, img v1.Image, options ...Option) error {
return err
}
o, err := makeOptions(ref.Context().Registry, options...)
o, err := makeOptions(ref.Context(), options...)
if err != nil {
return err
}
@@ -70,6 +68,16 @@ func Write(ref name.Reference, img v1.Image, options ...Option) error {
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()
@@ -346,7 +354,7 @@ func (w *writer) uploadOne(l v1.Layer) error {
}
// Try this three times, waiting 1s after first failure, 3s after second.
backoff := wait.Backoff{
backoff := retry.Backoff{
Duration: 1.0 * time.Second,
Factor: 3.0,
Jitter: 0.1,
@@ -356,13 +364,55 @@ func (w *writer) uploadOne(l v1.Layer) error {
return retry.Retry(tryUpload, retry.IsTemporary, backoff)
}
type withMediaType interface {
MediaType() (types.MediaType, error)
}
// This is really silly, but go interfaces don't let me satisfy remote.Taggable
// with remote.Descriptor because of name collisions between method names and
// struct fields.
//
// Use reflection to either pull the v1.Descriptor out of remote.Descriptor or
// create a descriptor based on the RawManifest and (optionally) MediaType.
func unpackTaggable(t Taggable) (*v1.Descriptor, error) {
if d, ok := t.(*Descriptor); ok {
return &d.Descriptor, nil
}
b, err := t.RawManifest()
if err != nil {
return nil, err
}
// A reasonable default if Taggable doesn't implement MediaType.
mt := types.DockerManifestSchema2
if wmt, ok := t.(withMediaType); ok {
m, err := wmt.MediaType()
if err != nil {
return nil, err
}
mt = m
}
h, sz, err := v1.SHA256(bytes.NewReader(b))
if err != nil {
return nil, err
}
return &v1.Descriptor{
MediaType: mt,
Size: sz,
Digest: h,
}, nil
}
// commitImage does a PUT of the image's manifest.
func (w *writer) commitImage(man manifest) error {
raw, err := man.RawManifest()
func (w *writer) commitImage(t Taggable) error {
raw, err := t.RawManifest()
if err != nil {
return err
}
mt, err := man.MediaType()
desc, err := unpackTaggable(t)
if err != nil {
return err
}
@@ -374,7 +424,7 @@ func (w *writer) commitImage(man manifest) error {
if err != nil {
return err
}
req.Header.Set("Content-Type", string(mt))
req.Header.Set("Content-Type", string(desc.MediaType))
resp, err := w.client.Do(req)
if err != nil {
@@ -386,13 +436,8 @@ func (w *writer) commitImage(man manifest) error {
return err
}
digest, err := man.Digest()
if err != nil {
return err
}
// The image was successfully pushed!
logs.Progress.Printf("%v: digest: %v size: %d", w.ref, digest, len(raw))
logs.Progress.Printf("%v: digest: %v size: %d", w.ref, desc.Digest, len(raw))
return nil
}
@@ -402,9 +447,10 @@ func scopesForUploadingImage(ref name.Reference, layers []v1.Layer) []string {
for _, l := range layers {
if ml, ok := l.(*MountableLayer); ok {
// we add push scope for ref.Context() after the loop
if ml.Reference.Context() != ref.Context() {
scopeSet[ml.Reference.Context().Scope(transport.PullScope)] = struct{}{}
// we will add push scope for ref.Context() after the loop.
// for now we ask pull scope for references of the same registry
if ml.Reference.Context() != ref.Context() && ml.Reference.Context().Registry == ref.Context().Registry {
scopeSet[ml.Reference.Scope(transport.PullScope)] = struct{}{}
}
}
}
@@ -429,7 +475,7 @@ func WriteIndex(ref name.Reference, ii v1.ImageIndex, options ...Option) error {
return err
}
o, err := makeOptions(ref.Context().Registry, options...)
o, err := makeOptions(ref.Context(), options...)
if err != nil {
return err
}
@@ -444,10 +490,7 @@ func WriteIndex(ref name.Reference, ii v1.ImageIndex, options ...Option) error {
}
for _, desc := range index.Manifests {
ref, err := name.ParseReference(fmt.Sprintf("%s@%s", ref.Context(), desc.Digest), name.StrictValidation)
if err != nil {
return err
}
ref := ref.Context().Digest(desc.Digest.String())
exists, err := w.checkExistingManifest(desc.Digest, desc.MediaType)
if err != nil {
return err
@@ -482,3 +525,48 @@ func WriteIndex(ref name.Reference, ii v1.ImageIndex, options ...Option) error {
// to commit the image.
return w.commitImage(ii)
}
// WriteLayer uploads the provided Layer to the specified name.Digest.
func WriteLayer(ref name.Digest, layer v1.Layer, options ...Option) error {
o, err := makeOptions(ref.Context(), options...)
if err != nil {
return err
}
scopes := scopesForUploadingImage(ref, []v1.Layer{layer})
tr, err := transport.New(ref.Context().Registry, o.auth, o.transport, scopes)
if err != nil {
return err
}
w := writer{
ref: ref,
client: &http.Client{Transport: tr},
}
return w.uploadOne(layer)
}
// Tag adds a tag to the given Taggable.
func Tag(tag name.Tag, t Taggable, options ...Option) error {
o, err := makeOptions(tag.Context(), options...)
if err != nil {
return err
}
scopes := []string{tag.Scope(transport.PushScope)}
// TODO: This *always* does a token exchange. For some registries,
// that's pretty slow. Some ideas;
// * Tag could take a list of tags.
// * 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 commitImage.
tr, err := transport.New(tag.Context().Registry, o.auth, o.transport, scopes)
if err != nil {
return err
}
w := writer{
ref: tag,
client: &http.Client{Transport: tr},
}
return w.commitImage(t)
}
+26 -4
View File
@@ -39,8 +39,9 @@ var (
// Layer is a streaming implementation of v1.Layer.
type Layer struct {
blob io.ReadCloser
consumed bool
blob io.ReadCloser
consumed bool
compression int
mu sync.Mutex
digest, diffID *v1.Hash
@@ -49,8 +50,29 @@ type Layer struct {
var _ v1.Layer = (*Layer)(nil)
// LayerOption applies options to layer
type LayerOption func(*Layer)
// WithCompressionLevel sets the gzip compression. See `gzip.NewWriterLevel` for possible values.
func WithCompressionLevel(level int) LayerOption {
return func(l *Layer) {
l.compression = level
}
}
// NewLayer creates a Layer from an io.ReadCloser.
func NewLayer(rc io.ReadCloser) *Layer { return &Layer{blob: rc} }
func NewLayer(rc io.ReadCloser, opts ...LayerOption) *Layer {
layer := &Layer{
blob: rc,
compression: gzip.BestSpeed,
}
for _, opt := range opts {
opt(layer)
}
return layer
}
// Digest implements v1.Layer.
func (l *Layer) Digest() (v1.Hash, error) {
@@ -121,7 +143,7 @@ func newCompressedReader(l *Layer) (*compressedReader, error) {
// capture compressed digest, and a countWriter to capture compressed
// size.
pr, pw := io.Pipe()
zw, err := gzip.NewWriterLevel(io.MultiWriter(pw, zh, count), gzip.BestSpeed)
zw, err := gzip.NewWriterLevel(io.MultiWriter(pw, zh, count), l.compression)
if err != nil {
return nil, err
}
+81 -46
View File
@@ -34,9 +34,9 @@ import (
type image struct {
opener Opener
td *tarDescriptor
manifest *Manifest
config []byte
imgDescriptor *singleImageTarDescriptor
imgDescriptor *Descriptor
tag *name.Tag
}
@@ -100,24 +100,27 @@ func (i *image) MediaType() (types.MediaType, error) {
return types.DockerManifestSchema2, nil
}
// singleImageTarDescriptor is the struct used to represent a single image inside a `docker save` tarball.
type singleImageTarDescriptor struct {
// Descriptor stores the manifest data for a single image inside a `docker save` tarball.
type Descriptor struct {
Config string
RepoTags []string
Layers []string
// Tracks foreign layer info. Key is DiffID.
LayerSources map[v1.Hash]v1.Descriptor `json:",omitempty"`
}
// tarDescriptor is the struct used inside the `manifest.json` file of a `docker save` tarball.
type tarDescriptor []singleImageTarDescriptor
// Manifest represents the manifests of all images as the `manifest.json` file in a `docker save` tarball.
type Manifest []Descriptor
func (td tarDescriptor) findSpecifiedImageDescriptor(tag *name.Tag) (*singleImageTarDescriptor, error) {
func (m Manifest) findDescriptor(tag *name.Tag) (*Descriptor, error) {
if tag == nil {
if len(td) != 1 {
if len(m) != 1 {
return nil, errors.New("tarball must contain only a single image to be used with tarball.Image")
}
return &(td)[0], nil
return &(m)[0], nil
}
for _, img := range td {
for _, img := range m {
for _, tagStr := range img.RepoTags {
repoTag, err := name.NewTag(tagStr)
if err != nil {
@@ -147,17 +150,17 @@ func (i *image) areLayersCompressed() (bool, error) {
}
func (i *image) loadTarDescriptorAndConfig() error {
td, err := extractFileFromTar(i.opener, "manifest.json")
m, err := extractFileFromTar(i.opener, "manifest.json")
if err != nil {
return err
}
defer td.Close()
defer m.Close()
if err := json.NewDecoder(td).Decode(&i.td); err != nil {
if err := json.NewDecoder(m).Decode(&i.manifest); err != nil {
return err
}
i.imgDescriptor, err = i.td.findSpecifiedImageDescriptor(i.tag)
i.imgDescriptor, err = i.manifest.findDescriptor(i.tag)
if err != nil {
return err
}
@@ -211,9 +214,22 @@ func extractFileFromTar(opener Opener, filePath string) (io.ReadCloser, error) {
// uncompressedLayerFromTarball implements partial.UncompressedLayer
type uncompressedLayerFromTarball struct {
diffID v1.Hash
opener Opener
filePath string
diffID v1.Hash
mediaType types.MediaType
opener Opener
filePath string
}
// foreignUncompressedLayer implements partial.UncompressedLayer but returns
// a custom descriptor. This allows the foreign layer URLs to be included in
// the generated image manifest for uncompressed layers.
type foreignUncompressedLayer struct {
uncompressedLayerFromTarball
desc v1.Descriptor
}
func (fl *foreignUncompressedLayer) Descriptor() (*v1.Descriptor, error) {
return &fl.desc, nil
}
// DiffID implements partial.UncompressedLayer
@@ -227,10 +243,7 @@ func (ulft *uncompressedLayerFromTarball) Uncompressed() (io.ReadCloser, error)
}
func (ulft *uncompressedLayerFromTarball) MediaType() (types.MediaType, error) {
// Technically the media type should be 'application/tar' but given that our
// v1.Layer doesn't force consumers to care about whether the layer is compressed
// we should be fine returning the DockerLayer media type
return types.DockerLayer, nil
return ulft.mediaType, nil
}
func (i *uncompressedImage) LayerByDiffID(h v1.Hash) (partial.UncompressedLayer, error) {
@@ -240,10 +253,27 @@ func (i *uncompressedImage) LayerByDiffID(h v1.Hash) (partial.UncompressedLayer,
}
for idx, diffID := range cfg.RootFS.DiffIDs {
if diffID == h {
// Technically the media type should be 'application/tar' but given that our
// v1.Layer doesn't force consumers to care about whether the layer is compressed
// we should be fine returning the DockerLayer media type
mt := types.DockerLayer
if bd, ok := i.imgDescriptor.LayerSources[h]; ok {
// Overwrite the mediaType for foreign layers.
return &foreignUncompressedLayer{
uncompressedLayerFromTarball: uncompressedLayerFromTarball{
diffID: diffID,
mediaType: bd.MediaType,
opener: i.opener,
filePath: i.imgDescriptor.Layers[idx],
},
desc: bd,
}, nil
}
return &uncompressedLayerFromTarball{
diffID: diffID,
opener: i.opener,
filePath: i.imgDescriptor.Layers[idx],
diffID: diffID,
mediaType: mt,
opener: i.opener,
filePath: i.imgDescriptor.Layers[idx],
}, nil
}
}
@@ -277,21 +307,32 @@ func (c *compressedImage) Manifest() (*v1.Manifest, error) {
},
}
for _, p := range c.imgDescriptor.Layers {
l, err := extractFileFromTar(c.opener, p)
for i, p := range c.imgDescriptor.Layers {
cfg, err := partial.ConfigFile(c)
if err != nil {
return nil, err
}
defer l.Close()
sha, size, err := v1.SHA256(l)
if err != nil {
return nil, err
diffid := cfg.RootFS.DiffIDs[i]
if d, ok := c.imgDescriptor.LayerSources[diffid]; ok {
// If it's a foreign layer, just append the descriptor so we can avoid
// reading the entire file.
c.manifest.Layers = append(c.manifest.Layers, d)
} else {
l, err := extractFileFromTar(c.opener, p)
if err != nil {
return nil, err
}
defer l.Close()
sha, size, err := v1.SHA256(l)
if err != nil {
return nil, err
}
c.manifest.Layers = append(c.manifest.Layers, v1.Descriptor{
MediaType: types.DockerLayer,
Size: size,
Digest: sha,
})
}
c.manifest.Layers = append(c.manifest.Layers, v1.Descriptor{
MediaType: types.DockerLayer,
Size: size,
Digest: sha,
})
}
return c.manifest, nil
}
@@ -302,14 +343,14 @@ func (c *compressedImage) RawManifest() ([]byte, error) {
// compressedLayerFromTarball implements partial.CompressedLayer
type compressedLayerFromTarball struct {
digest v1.Hash
desc v1.Descriptor
opener Opener
filePath string
}
// Digest implements partial.CompressedLayer
func (clft *compressedLayerFromTarball) Digest() (v1.Hash, error) {
return clft.digest, nil
return clft.desc.Digest, nil
}
// Compressed implements partial.CompressedLayer
@@ -319,18 +360,12 @@ func (clft *compressedLayerFromTarball) Compressed() (io.ReadCloser, error) {
// MediaType implements partial.CompressedLayer
func (clft *compressedLayerFromTarball) MediaType() (types.MediaType, error) {
return types.DockerLayer, nil
return clft.desc.MediaType, nil
}
// Size implements partial.CompressedLayer
func (clft *compressedLayerFromTarball) Size() (int64, error) {
r, err := clft.Compressed()
if err != nil {
return -1, err
}
defer r.Close()
_, i, err := v1.SHA256(r)
return i, err
return clft.desc.Size, nil
}
func (c *compressedImage) LayerByDigest(h v1.Hash) (partial.CompressedLayer, error) {
@@ -342,7 +377,7 @@ func (c *compressedImage) LayerByDigest(h v1.Hash) (partial.CompressedLayer, err
if l.Digest == h {
fp := c.imgDescriptor.Layers[i]
return &compressedLayerFromTarball{
digest: h,
desc: l,
opener: c.opener,
filePath: fp,
}, nil
+35 -28
View File
@@ -27,11 +27,12 @@ import (
)
type layer struct {
digest v1.Hash
diffID v1.Hash
size int64
opener Opener
compressed bool
digest v1.Hash
diffID v1.Hash
size int64
opener Opener
compressed bool
compression int
}
func (l *layer) Digest() (v1.Hash, error) {
@@ -45,7 +46,7 @@ func (l *layer) DiffID() (v1.Hash, error) {
func (l *layer) Compressed() (io.ReadCloser, error) {
rc, err := l.opener()
if err == nil && !l.compressed {
return v1util.GzipReadCloser(rc)
return v1util.GzipReadCloserLevel(rc, l.compression), nil
}
return rc, err
@@ -68,16 +69,26 @@ func (l *layer) MediaType() (types.MediaType, error) {
return types.DockerLayer, nil
}
// LayerOption applies options to layer
type LayerOption func(*layer)
// WithCompressionLevel sets the gzip compression. See `gzip.NewWriterLevel` for possible values.
func WithCompressionLevel(level int) LayerOption {
return func(l *layer) {
l.compression = level
}
}
// LayerFromFile returns a v1.Layer given a tarball
func LayerFromFile(path string) (v1.Layer, error) {
func LayerFromFile(path string, opts ...LayerOption) (v1.Layer, error) {
opener := func() (io.ReadCloser, error) {
return os.Open(path)
}
return LayerFromOpener(opener)
return LayerFromOpener(opener, opts...)
}
// LayerFromOpener returns a v1.Layer given an Opener function
func LayerFromOpener(opener Opener) (v1.Layer, error) {
func LayerFromOpener(opener Opener, opts ...LayerOption) (v1.Layer, error) {
rc, err := opener()
if err != nil {
return nil, err
@@ -89,24 +100,25 @@ func LayerFromOpener(opener Opener) (v1.Layer, error) {
return nil, err
}
var digest v1.Hash
var size int64
if digest, size, err = computeDigest(opener, compressed); err != nil {
layer := &layer{
compressed: compressed,
compression: gzip.BestSpeed,
opener: opener,
}
for _, opt := range opts {
opt(layer)
}
if layer.digest, layer.size, err = computeDigest(opener, compressed, layer.compression); err != nil {
return nil, err
}
diffID, err := computeDiffID(opener, compressed)
if err != nil {
if layer.diffID, err = computeDiffID(opener, compressed); err != nil {
return nil, err
}
return &layer{
digest: digest,
diffID: diffID,
size: size,
compressed: compressed,
opener: opener,
}, nil
return layer, nil
}
// LayerFromReader returns a v1.Layer given a io.Reader.
@@ -121,7 +133,7 @@ func LayerFromReader(reader io.Reader) (v1.Layer, error) {
})
}
func computeDigest(opener Opener, compressed bool) (v1.Hash, int64, error) {
func computeDigest(opener Opener, compressed bool, compression int) (v1.Hash, int64, error) {
rc, err := opener()
if err != nil {
return v1.Hash{}, 0, err
@@ -132,12 +144,7 @@ func computeDigest(opener Opener, compressed bool) (v1.Hash, int64, error) {
return v1.SHA256(rc)
}
reader, err := v1util.GzipReadCloser(ioutil.NopCloser(rc))
if err != nil {
return v1.Hash{}, 0, err
}
return v1.SHA256(reader)
return v1.SHA256(v1util.GzipReadCloserLevel(ioutil.NopCloser(rc), compression))
}
func computeDiffID(opener Opener, compressed bool) (v1.Hash, error) {
+35 -14
View File
@@ -24,6 +24,7 @@ import (
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/partial"
)
// WriteToFile writes in the compressed format to a tarball, on disk.
@@ -41,7 +42,7 @@ func WriteToFile(p string, ref name.Reference, img v1.Image) error {
// MultiWriteToFile writes in the compressed format to a tarball, on disk.
// This is just syntactic sugar wrapping tarball.MultiWrite with a new file.
func MultiWriteToFile(p string, tagToImage map[name.Tag]v1.Image) error {
var refToImage map[name.Reference]v1.Image = make(map[name.Reference]v1.Image, len(tagToImage))
refToImage := make(map[name.Reference]v1.Image, len(tagToImage))
for i, d := range tagToImage {
refToImage[i] = d
}
@@ -71,7 +72,7 @@ func Write(ref name.Reference, img v1.Image, w io.Writer) error {
// One file for each layer, named after the layer's SHA.
// One file for the config blob, named after its SHA.
func MultiWrite(tagToImage map[name.Tag]v1.Image, w io.Writer) error {
var refToImage map[name.Reference]v1.Image = make(map[name.Reference]v1.Image, len(tagToImage))
refToImage := make(map[name.Reference]v1.Image, len(tagToImage))
for i, d := range tagToImage {
refToImage[i] = d
}
@@ -88,8 +89,9 @@ func MultiRefWrite(refToImage map[name.Reference]v1.Image, w io.Writer) error {
defer tf.Close()
imageToTags := dedupRefToImage(refToImage)
var td tarDescriptor
var m Manifest
seenLayerDigests := make(map[string]struct{})
for img, tags := range imageToTags {
// Write the config.
cfgName, err := img.ConfigName()
@@ -104,6 +106,9 @@ func MultiRefWrite(refToImage map[name.Reference]v1.Image, w io.Writer) error {
return err
}
// Store foreign layer info.
layerSources := make(map[v1.Hash]v1.Descriptor)
// Write the layers.
layers, err := img.Layers()
if err != nil {
@@ -115,7 +120,6 @@ func MultiRefWrite(refToImage map[name.Reference]v1.Image, w io.Writer) error {
if err != nil {
return err
}
// Munge the file name to appease ancient technology.
//
// tar assumes anything with a colon is a remote tape drive:
@@ -127,6 +131,24 @@ func MultiRefWrite(refToImage map[name.Reference]v1.Image, w io.Writer) error {
// https://www.gnu.org/software/gzip/manual/html_node/Overview.html
layerFiles[i] = fmt.Sprintf("%s.tar.gz", hex)
if _, ok := seenLayerDigests[hex]; ok {
continue
}
seenLayerDigests[hex] = struct{}{}
// Add to LayerSources if it's a foreign layer.
desc, err := partial.BlobDescriptor(img, d)
if err != nil {
return err
}
if !desc.MediaType.IsDistributable() {
diffid, err := partial.BlobToDiffID(img, d)
if err != nil {
return err
}
layerSources[diffid] = desc
}
r, err := l.Compressed()
if err != nil {
return err
@@ -142,20 +164,19 @@ func MultiRefWrite(refToImage map[name.Reference]v1.Image, w io.Writer) error {
}
// Generate the tar descriptor and write it.
sitd := singleImageTarDescriptor{
Config: cfgName.String(),
RepoTags: tags,
Layers: layerFiles,
}
td = append(td, sitd)
m = append(m, Descriptor{
Config: cfgName.String(),
RepoTags: tags,
Layers: layerFiles,
LayerSources: layerSources,
})
}
tdBytes, err := json.Marshal(td)
mBytes, err := json.Marshal(m)
if err != nil {
return err
}
return writeTarEntry(tf, "manifest.json", bytes.NewReader(tdBytes), int64(len(tdBytes)))
return writeTarEntry(tf, "manifest.json", bytes.NewReader(mBytes), int64(len(mBytes)))
}
func dedupRefToImage(refToImage map[name.Reference]v1.Image) map[v1.Image][]string {
@@ -178,7 +199,7 @@ func dedupRefToImage(refToImage map[name.Reference]v1.Image) map[v1.Image][]stri
return imageToTags
}
// write a file to the provided writer with a corresponding tar header
// Writes a file to the provided writer with a corresponding tar header
func writeTarEntry(tf *tar.Writer, path string, r io.Reader, size int64) error {
hdr := &tar.Header{
Mode: 0644,
+13
View File
@@ -37,4 +37,17 @@ const (
DockerPluginConfig MediaType = "application/vnd.docker.plugin.v1+json"
DockerForeignLayer MediaType = "application/vnd.docker.image.rootfs.foreign.diff.tar.gzip"
DockerUncompressedLayer MediaType = "application/vnd.docker.image.rootfs.diff.tar"
OCIVendorPrefix = "vnd.oci"
DockerVendorPrefix = "vnd.docker"
)
// IsDistributable returns true if a layer is distributable, see:
// https://github.com/opencontainers/image-spec/blob/master/layer.md#non-distributable-layers
func (m MediaType) IsDistributable() bool {
switch m {
case DockerForeignLayer, OCIRestrictedLayer, OCIUncompressedRestrictedLayer:
return false
}
return true
}
+18 -10
View File
@@ -25,7 +25,7 @@ var gzipMagicHeader = []byte{'\x1f', '\x8b'}
// GzipReadCloser 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 GzipReadCloser(r io.ReadCloser) (io.ReadCloser, error) {
func GzipReadCloser(r io.ReadCloser) io.ReadCloser {
return GzipReadCloserLevel(r, gzip.BestSpeed)
}
@@ -33,23 +33,31 @@ func GzipReadCloser(r io.ReadCloser) (io.ReadCloser, error) {
// 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 GzipReadCloserLevel(r io.ReadCloser, level int) (io.ReadCloser, error) {
func GzipReadCloserLevel(r io.ReadCloser, level int) io.ReadCloser {
pr, pw := io.Pipe()
go func() {
// 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(pw, 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)
}
defer pw.Close()
defer r.Close()
gw, _ := gzip.NewWriterLevel(pw, level)
defer gw.Close()
_, err := io.Copy(gw, r)
if err != nil {
pr.CloseWithError(err)
}
return nil
}()
return pr, nil
return pr
}
// GunzipReadCloser reads compressed input data from the io.ReadCloser and
+34 -80
View File
@@ -15,15 +15,9 @@
package validate
import (
"archive/tar"
"bytes"
"compress/gzip"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"io"
"io/ioutil"
"strings"
"github.com/google/go-cmp/cmp"
@@ -114,87 +108,19 @@ func validateLayers(img v1.Image) error {
digests := []v1.Hash{}
diffids := []v1.Hash{}
udiffids := []v1.Hash{}
sizes := []int64{}
for _, layer := range layers {
// TODO: Test layer.Uncompressed.
compressed, err := layer.Compressed()
cl, err := computeLayer(layer)
if err != nil {
return err
}
// Keep track of compressed digest.
digester := sha256.New()
// Everything read from compressed is written to digester to compute digest.
hashCompressed := io.TeeReader(compressed, digester)
// Call io.Copy to write from the layer Reader through to the tarReader on
// the other side of the pipe.
pr, pw := io.Pipe()
var size int64
go func() {
n, err := io.Copy(pw, hashCompressed)
if err != nil {
pw.CloseWithError(err)
return
}
size = n
// 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.
pw.CloseWithError(compressed.Close())
}()
// Read the bytes through gzip.Reader to compute the DiffID.
uncompressed, err := gzip.NewReader(pr)
if err != nil {
return err
}
diffider := sha256.New()
hashUncompressed := io.TeeReader(uncompressed, diffider)
// Ensure there aren't duplicate file paths.
tarReader := tar.NewReader(hashUncompressed)
files := make(map[string]struct{})
for {
hdr, err := tarReader.Next()
if err == io.EOF {
break
}
if err != nil {
return err
}
if _, ok := files[hdr.Name]; ok {
return fmt.Errorf("duplicate file path: %s", hdr.Name)
}
files[hdr.Name] = struct{}{}
}
// Discard any trailing padding that the tar.Reader doesn't consume.
if _, err := io.Copy(ioutil.Discard, hashUncompressed); err != nil {
return err
}
if err := uncompressed.Close(); err != nil {
return err
}
digest := v1.Hash{
Algorithm: "sha256",
Hex: hex.EncodeToString(digester.Sum(make([]byte, 0, digester.Size()))),
}
diffid := v1.Hash{
Algorithm: "sha256",
Hex: hex.EncodeToString(diffider.Sum(make([]byte, 0, diffider.Size()))),
}
// Compute all of these first before we call Config() and Manifest() to allow
// for lazy access e.g. for stream.Layer.
digests = append(digests, digest)
diffids = append(diffids, diffid)
sizes = append(sizes, size)
digests = append(digests, cl.digest)
diffids = append(diffids, cl.diffid)
udiffids = append(udiffids, cl.uncompressedDiffid)
sizes = append(sizes, cl.size)
}
cf, err := img.ConfigFile()
@@ -221,6 +147,18 @@ func validateLayers(img v1.Image) error {
if err != nil {
return err
}
mediaType, err := layer.MediaType()
if err != nil {
return err
}
if _, err := img.LayerByDigest(digest); err != nil {
return err
}
if _, err := img.LayerByDiffID(diffid); err != nil {
return err
}
if digest != digests[i] {
errs = append(errs, fmt.Sprintf("mismatched layer[%d] digest: Digest()=%s, SHA256(Compressed())=%s", i, digest, digests[i]))
@@ -234,6 +172,10 @@ func validateLayers(img v1.Image) error {
errs = append(errs, fmt.Sprintf("mismatched layer[%d] diffid: DiffID()=%s, SHA256(Gunzip(Compressed()))=%s", i, diffid, diffids[i]))
}
if diffid != udiffids[i] {
errs = append(errs, fmt.Sprintf("mismatched layer[%d] diffid: DiffID()=%s, SHA256(Uncompressed())=%s", i, diffid, udiffids[i]))
}
if cf.RootFS.DiffIDs[i] != diffids[i] {
errs = append(errs, fmt.Sprintf("mismatched layer[%d] diffid: ConfigFile.RootFS.DiffIDs[%d]=%s, SHA256(Gunzip(Compressed()))=%s", i, i, cf.RootFS.DiffIDs[i], diffids[i]))
}
@@ -246,6 +188,9 @@ func validateLayers(img v1.Image) error {
errs = append(errs, fmt.Sprintf("mismatched layer[%d] size: Manifest.Layers[%d].Size=%d, len(Compressed())=%d", i, i, m.Layers[i].Size, sizes[i]))
}
if m.Layers[i].MediaType != mediaType {
errs = append(errs, fmt.Sprintf("mismatched layer[%d] mediaType: Manifest.Layers[%d].MediaType=%s, layer.MediaType()=%s", i, i, m.Layers[i].MediaType, mediaType))
}
}
if len(errs) != 0 {
return errors.New(strings.Join(errs, "\n"))
@@ -260,6 +205,11 @@ func validateManifest(img v1.Image) error {
return err
}
size, err := img.Size()
if err != nil {
return err
}
rm, err := img.RawManifest()
if err != nil {
return err
@@ -289,6 +239,10 @@ func validateManifest(img v1.Image) error {
errs = append(errs, fmt.Sprintf("mismatched manifest content: (-ParseManifest(RawManifest()) +Manifest()) %s", diff))
}
if size != int64(len(rm)) {
errs = append(errs, fmt.Sprintf("mismatched manifest size: Size()=%d, len(RawManifest())=%d", size, len(rm)))
}
if len(errs) != 0 {
return errors.New(strings.Join(errs, "\n"))
}
+33 -1
View File
@@ -21,6 +21,7 @@ import (
"strings"
"github.com/google/go-cmp/cmp"
"github.com/google/go-containerregistry/pkg/logs"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/types"
)
@@ -60,6 +61,9 @@ func validateChildren(idx v1.ImageIndex) error {
if err := Index(idx); 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 {
errs = append(errs, fmt.Sprintf("failed to validate index MediaType[%d](%s): %v", i, desc.Digest, err))
}
case types.OCIManifestSchema1, types.DockerManifestSchema2:
img, err := idx.Image(desc.Digest)
if err != nil {
@@ -68,8 +72,11 @@ func validateChildren(idx v1.ImageIndex) error {
if err := Image(img); 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:
return fmt.Errorf("todo: validate index Blob()")
logs.Warn.Printf("Unexpected manifest: %s", desc.MediaType)
}
}
@@ -80,12 +87,33 @@ func validateChildren(idx v1.ImageIndex) error {
return nil
}
type withMediaType interface {
MediaType() (types.MediaType, error)
}
func validateMediaType(i withMediaType, want types.MediaType) error {
got, err := i.MediaType()
if err != nil {
return err
}
if want != got {
return fmt.Errorf("mismatched mediaType: MediaType() = %v != %v", got, want)
}
return nil
}
func validateIndexManifest(idx v1.ImageIndex) error {
digest, err := idx.Digest()
if err != nil {
return err
}
size, err := idx.Size()
if err != nil {
return err
}
rm, err := idx.RawManifest()
if err != nil {
return err
@@ -115,6 +143,10 @@ func validateIndexManifest(idx v1.ImageIndex) error {
errs = append(errs, fmt.Sprintf("mismatched manifest content: (-ParseIndexManifest(RawManifest()) +Manifest()) %s", diff))
}
if size != int64(len(rm)) {
errs = append(errs, fmt.Sprintf("mismatched manifest size: Size()=%d, len(RawManifest())=%d", size, len(rm)))
}
if len(errs) != 0 {
return errors.New(strings.Join(errs, "\n"))
}
+178
View File
@@ -0,0 +1,178 @@
// Copyright 2019 Google LLC All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package validate
import (
"archive/tar"
"compress/gzip"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"io"
"io/ioutil"
"strings"
v1 "github.com/google/go-containerregistry/pkg/v1"
)
// 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 {
cl, err := computeLayer(layer)
if err != nil {
return err
}
errs := []string{}
digest, err := layer.Digest()
if err != nil {
return err
}
diffid, err := layer.DiffID()
if err != nil {
return err
}
size, err := layer.Size()
if err != nil {
return err
}
if digest != cl.digest {
errs = append(errs, fmt.Sprintf("mismatched digest: Digest()=%s, SHA256(Compressed())=%s", digest, cl.digest))
}
if diffid != cl.diffid {
errs = append(errs, fmt.Sprintf("mismatched diffid: DiffID()=%s, SHA256(Gunzip(Compressed()))=%s", diffid, cl.diffid))
}
if diffid != cl.uncompressedDiffid {
errs = append(errs, fmt.Sprintf("mismatched diffid: DiffID()=%s, SHA256(Uncompressed())=%s", diffid, cl.uncompressedDiffid))
}
if size != cl.size {
errs = append(errs, fmt.Sprintf("mismatched size: Size()=%d, len(Compressed())=%d", size, cl.size))
}
if len(errs) != 0 {
return errors.New(strings.Join(errs, "\n"))
}
return nil
}
type computedLayer struct {
// Calculated from Compressed stream.
digest v1.Hash
size int64
diffid v1.Hash
// Calculated from Uncompressed stream.
uncompressedDiffid v1.Hash
uncompressedSize int64
}
func computeLayer(layer v1.Layer) (*computedLayer, error) {
compressed, err := layer.Compressed()
if err != nil {
return nil, err
}
// Keep track of compressed digest.
digester := sha256.New()
// Everything read from compressed is written to digester to compute digest.
hashCompressed := io.TeeReader(compressed, digester)
// Call io.Copy to write from the layer Reader through to the tarReader on
// the other side of the pipe.
pr, pw := io.Pipe()
var size int64
go func() {
n, err := io.Copy(pw, hashCompressed)
if err != nil {
pw.CloseWithError(err)
return
}
size = n
// 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.
pw.CloseWithError(compressed.Close())
}()
// Read the bytes through gzip.Reader to compute the DiffID.
uncompressed, err := gzip.NewReader(pr)
if err != nil {
return nil, err
}
diffider := sha256.New()
hashUncompressed := io.TeeReader(uncompressed, diffider)
// Ensure there aren't duplicate file paths.
tarReader := tar.NewReader(hashUncompressed)
files := make(map[string]struct{})
for {
hdr, err := tarReader.Next()
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
if _, ok := files[hdr.Name]; ok {
return nil, fmt.Errorf("duplicate file path: %s", hdr.Name)
}
files[hdr.Name] = struct{}{}
}
// Discard any trailing padding that the tar.Reader doesn't consume.
if _, err := io.Copy(ioutil.Discard, hashUncompressed); err != nil {
return nil, err
}
if err := uncompressed.Close(); err != nil {
return nil, err
}
digest := v1.Hash{
Algorithm: "sha256",
Hex: hex.EncodeToString(digester.Sum(make([]byte, 0, digester.Size()))),
}
diffid := v1.Hash{
Algorithm: "sha256",
Hex: hex.EncodeToString(diffider.Sum(make([]byte, 0, diffider.Size()))),
}
ur, err := layer.Uncompressed()
if err != nil {
return nil, err
}
udiffid, usize, err := v1.SHA256(ur)
if err != nil {
return nil, err
}
return &computedLayer{
digest: digest,
diffid: diffid,
size: size,
uncompressedDiffid: udiffid,
uncompressedSize: usize,
}, nil
}
@@ -98,7 +98,6 @@ func (in *ConfigFile) DeepCopyInto(out *ConfigFile) {
}
in.RootFS.DeepCopyInto(&out.RootFS)
in.Config.DeepCopyInto(&out.Config)
in.ContainerConfig.DeepCopyInto(&out.ContainerConfig)
return
}
@@ -269,6 +268,11 @@ func (in *Platform) DeepCopyInto(out *Platform) {
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.Features != nil {
in, out := &in.Features, &out.Features
*out = make([]string, len(*in))
copy(*out, *in)
}
return
}