mirror of
https://github.com/GoogleContainerTools/kaniko
synced 2026-10-21 12:33:00 +02:00
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:
+2
-2
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
+21
@@ -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"
|
||||
)
|
||||
Generated
Vendored
+21
@@ -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"
|
||||
)
|
||||
+21
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
Generated
Vendored
+123
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
+5
@@ -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{
|
||||
|
||||
+39
-1
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
|
||||
+127
@@ -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.
|
||||
|
||||

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

|
||||
|
||||
> Well... what about [`containers/image`](https://godoc.org/github.com/containers/image/docker)?
|
||||
|
||||
That just uses the the `docker/distribution` client... and more!
|
||||
|
||||

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

|
||||
|
||||
*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)
|
||||
}
|
||||
}
|
||||
```
|
||||
+26
-11
@@ -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)
|
||||
|
||||
+123
-46
@@ -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)
|
||||
}
|
||||
|
||||
+44
@@ -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
|
||||
}
|
||||
+2
-2
@@ -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
|
||||
|
||||
+4
-5
@@ -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
|
||||
}
|
||||
|
||||
+44
@@ -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)
|
||||
}
|
||||
Generated
Vendored
+11
-5
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Generated
Vendored
+32
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
+5
-1
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user