mirror of
https://github.com/GoogleContainerTools/kaniko
synced 2026-10-18 10:39:18 +02:00
Revert "Update go-containerregistry to get https://github.com/google/go-containerregistry/pull/693"
This reverts commit f55c34ab92.
This commit is contained in:
-242
@@ -1,242 +0,0 @@
|
||||
# `authn`
|
||||
|
||||
[](https://godoc.org/github.com/google/go-containerregistry/pkg/authn)
|
||||
|
||||
This README outlines how we acquire and use credentials when interacting with a registry.
|
||||
|
||||
As much as possible, we attempt to emulate docker's authentication behavior and configuration so that this library "just works" if you've already configured credentials that work with docker; however, when things don't work, a basic understanding of what's going on can help with debugging.
|
||||
|
||||
The official documentation for how docker authentication works is (reasonably) scattered across several different sites and GitHub repositories, so we've tried to summarize the relevant bits here.
|
||||
|
||||
## tl;dr for consumers of this package
|
||||
|
||||
By default, [`pkg/v1/remote`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/remote) uses [`Anonymous`](https://godoc.org/github.com/google/go-containerregistry/pkg/authn#Anonymous) credentials (i.e. _none_), which for most registries will only allow read access to public images.
|
||||
|
||||
To use the credentials found in your docker config file, you can use the [`DefaultKeychain`](https://godoc.org/github.com/google/go-containerregistry/pkg/authn#DefaultKeychain), e.g.:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/google/go-containerregistry/pkg/authn"
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ref, err := name.ParseReference("registry.example.com/private/repo")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Fetch the manifest using default credentials.
|
||||
img, err := remote.Get(ref, remote.WithAuthFromKeychain(authn.DefaultKeychain))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Prints the digest of registry.example.com/private/repo
|
||||
fmt.Println(img.Digest)
|
||||
}
|
||||
```
|
||||
|
||||
(If you're only using [gcr.io](https://gcr.io), see the [`pkg/v1/google.Keychain`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/google#Keychain), which emulates [`docker-credential-gcr`](https://github.com/GoogleCloudPlatform/docker-credential-gcr).)
|
||||
|
||||
## The Config File
|
||||
|
||||
This file contains various configuration options for docker and is (by default) located at:
|
||||
* `$HOME/.docker/config.json` (on linux and darwin), or
|
||||
* `%USERPROFILE%\.docker\config.json` (on windows).
|
||||
|
||||
You can override this location with the `DOCKER_CONFIG` environment variable.
|
||||
|
||||
### Plaintext
|
||||
|
||||
The config file is where your credentials are stored when you invoke `docker login`, e.g. the contents may look something like this:
|
||||
|
||||
```json
|
||||
{
|
||||
"auths": {
|
||||
"registry.example.com": {
|
||||
"auth": "QXp1cmVEaWFtb25kOmh1bnRlcjI="
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `auths` map has an entry per registry, and the `auth` field contains your username and password encoded as [HTTP 'Basic' Auth](https://tools.ietf.org/html/rfc7617).
|
||||
|
||||
**NOTE**: This means that your credentials are stored _in plaintext_:
|
||||
|
||||
```bash
|
||||
$ echo "QXp1cmVEaWFtb25kOmh1bnRlcjI=" | base64 -d
|
||||
AzureDiamond:hunter2
|
||||
```
|
||||
|
||||
For what it's worth, this config file is equivalent to:
|
||||
|
||||
```json
|
||||
{
|
||||
"auths": {
|
||||
"registry.example.com": {
|
||||
"username": "AzureDiamond",
|
||||
"password": "hunter2"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
... which is useful to know if e.g. your CI system provides you a registry username and password via environment variables and you want to populate this file manually without invoking `docker login`.
|
||||
|
||||
### Helpers
|
||||
|
||||
If you log in like this, docker will warn you that you should use a [credential helper](https://docs.docker.com/engine/reference/commandline/login/#credentials-store), and you should!
|
||||
|
||||
To configure a global credential helper:
|
||||
```json
|
||||
{
|
||||
"credsStore": "osxkeychain"
|
||||
}
|
||||
```
|
||||
|
||||
To configure a per-registry credential helper:
|
||||
```json
|
||||
{
|
||||
"credHelpers": {
|
||||
"gcr.io": "gcr"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
We use [`github.com/docker/cli/cli/config.Load`](https://godoc.org/github.com/docker/cli/cli/config#Load) to parse the config file and invoke any necessary credential helpers. This handles the logic of taking a [`ConfigFile`](https://github.com/docker/cli/blob/ba63a92655c0bea4857b8d6cc4991498858b3c60/cli/config/configfile/file.go#L25-L54) + registry domain and producing an [`AuthConfig`](https://github.com/docker/cli/blob/ba63a92655c0bea4857b8d6cc4991498858b3c60/cli/config/types/authconfig.go#L3-L22), which determines how we authenticate to the registry.
|
||||
|
||||
## Credential Helpers
|
||||
|
||||
The [credential helper protocol](https://github.com/docker/docker-credential-helpers) allows you to configure a binary that supplies credentials for the registry, rather than hard-coding them in the config file.
|
||||
|
||||
The protocol has several verbs, but the one we most care about is `get`.
|
||||
|
||||
For example, using the following config file:
|
||||
```json
|
||||
{
|
||||
"credHelpers": {
|
||||
"gcr.io": "gcr",
|
||||
"eu.gcr.io": "gcr"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
To acquire credentials for `gcr.io`, we look in the `credHelpers` map to find
|
||||
the credential helper for `gcr.io` is `gcr`. By appending that value to
|
||||
`docker-credential-`, we can get the name of the binary we need to use.
|
||||
|
||||
For this example, that's `docker-credential-gcr`, which must be on our `$PATH`.
|
||||
We'll then invoke that binary to get credentials:
|
||||
|
||||
```bash
|
||||
$ echo "gcr.io" | docker-credential-gcr get
|
||||
{"Username":"_token","Secret":"<long access token>"}
|
||||
```
|
||||
|
||||
You can configure the same credential helper for multiple registries, which is
|
||||
why we need to pass the domain in via STDIN, e.g. if we were trying to access
|
||||
`eu.gcr.io`, we'd do this instead:
|
||||
|
||||
```bash
|
||||
$ echo "eu.gcr.io" | docker-credential-gcr get
|
||||
{"Username":"_token","Secret":"<long access token>"}
|
||||
```
|
||||
|
||||
### Debugging credential helpers
|
||||
|
||||
If a credential helper is configured but doesn't seem to be working, it can be
|
||||
challenging to debug. Implementing a fake credential helper lets you poke around
|
||||
to make it easier to see where the failure is happening.
|
||||
|
||||
This "implements" a credential helper with hard-coded values:
|
||||
```
|
||||
#!/usr/bin/env bash
|
||||
echo '{"Username":"<token>","Secret":"hunter2"}'
|
||||
```
|
||||
|
||||
|
||||
This implements a credential helper that prints the output of
|
||||
`docker-credential-gcr` to both stderr and whatever called it, which allows you
|
||||
to snoop on another credential helper:
|
||||
```
|
||||
#!/usr/bin/env bash
|
||||
docker-credential-gcr $@ | tee >(cat 1>&2)
|
||||
```
|
||||
|
||||
Put those files somewhere on your path, naming them e.g.
|
||||
`docker-credential-hardcoded` and `docker-credential-tee`, then modify the
|
||||
config file to use them:
|
||||
|
||||
```json
|
||||
{
|
||||
"credHelpers": {
|
||||
"gcr.io": "tee",
|
||||
"eu.gcr.io": "hardcoded"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `docker-credential-tee` trick works with both `crane` and `docker`:
|
||||
|
||||
```bash
|
||||
$ crane manifest gcr.io/google-containers/pause > /dev/null
|
||||
{"ServerURL":"","Username":"_dcgcr_1_5_0_token","Secret":"<redacted>"}
|
||||
|
||||
$ docker pull gcr.io/google-containers/pause
|
||||
Using default tag: latest
|
||||
{"ServerURL":"","Username":"_dcgcr_1_5_0_token","Secret":"<redacted>"}
|
||||
latest: Pulling from google-containers/pause
|
||||
a3ed95caeb02: Pull complete
|
||||
4964c72cd024: Pull complete
|
||||
Digest: sha256:a78c2d6208eff9b672de43f880093100050983047b7b0afe0217d3656e1b0d5f
|
||||
Status: Downloaded newer image for gcr.io/google-containers/pause:latest
|
||||
gcr.io/google-containers/pause:latest
|
||||
```
|
||||
|
||||
## The Registry
|
||||
|
||||
There are two methods for authenticating against a registry:
|
||||
[token](https://docs.docker.com/registry/spec/auth/token/) and
|
||||
[oauth2](https://docs.docker.com/registry/spec/auth/oauth/).
|
||||
|
||||
Both methods are used to acquire an opaque `Bearer` token (or
|
||||
[RegistryToken](https://github.com/docker/cli/blob/ba63a92655c0bea4857b8d6cc4991498858b3c60/cli/config/types/authconfig.go#L21))
|
||||
to use in the `Authorization` header. The registry will return a `401
|
||||
Unauthorized` during the [version
|
||||
check](https://github.com/opencontainers/distribution-spec/blob/2c3975d1f03b67c9a0203199038adea0413f0573/spec.md#api-version-check)
|
||||
(or during normal operations) with
|
||||
[Www-Authenticate](https://tools.ietf.org/html/rfc7235#section-4.1) challenge
|
||||
indicating how to proceed.
|
||||
|
||||
### Token
|
||||
|
||||
If we get back an `AuthConfig` containing a [`Username/Password`](https://github.com/docker/cli/blob/ba63a92655c0bea4857b8d6cc4991498858b3c60/cli/config/types/authconfig.go#L5-L6)
|
||||
or
|
||||
[`Auth`](https://github.com/docker/cli/blob/ba63a92655c0bea4857b8d6cc4991498858b3c60/cli/config/types/authconfig.go#L7),
|
||||
we'll use the token method for authentication:
|
||||
|
||||

|
||||
|
||||
### OAuth 2
|
||||
|
||||
If we get back an `AuthConfig` containing an [`IdentityToken`](https://github.com/docker/cli/blob/ba63a92655c0bea4857b8d6cc4991498858b3c60/cli/config/types/authconfig.go#L18)
|
||||
we'll use the oauth2 method for authentication:
|
||||
|
||||

|
||||
|
||||
This happens when a credential helper returns a response with the
|
||||
[`Username`](https://github.com/docker/docker-credential-helpers/blob/f78081d1f7fef6ad74ad6b79368de6348386e591/credentials/credentials.go#L16)
|
||||
set to `<token>` (no, that's not a placeholder, the literal string `"<token>"`).
|
||||
It is unclear why: [moby/moby#36926](https://github.com/moby/moby/issues/36926).
|
||||
|
||||
We only support the oauth2 `grant_type` for `refresh_token` ([#629](https://github.com/google/go-containerregistry/issues/629)),
|
||||
since it's impossible to determine from the registry response whether we should
|
||||
use oauth, and the token method for authentication is widely implemented by
|
||||
registries.
|
||||
+6
-3
@@ -17,12 +17,12 @@ package k8schain
|
||||
import (
|
||||
"github.com/google/go-containerregistry/pkg/authn"
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
credentialprovider "github.com/vdemeester/k8s-pkg-credentialprovider"
|
||||
credentialprovidersecrets "github.com/vdemeester/k8s-pkg-credentialprovider/secrets"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/kubernetes/pkg/credentialprovider"
|
||||
credentialprovidersecrets "k8s.io/kubernetes/pkg/credentialprovider/secrets"
|
||||
)
|
||||
|
||||
// Options holds configuration data for guiding credential resolution.
|
||||
@@ -38,6 +38,9 @@ type Options struct {
|
||||
ImagePullSecrets []string
|
||||
}
|
||||
|
||||
// origKeyRing is a variable so that testing can adjust it.
|
||||
var origKeyRing = credentialprovider.NewDockerKeyring()
|
||||
|
||||
// New returns a new authn.Keychain suitable for resolving image references as
|
||||
// scoped by the provided Options. It speaks to Kubernetes through the provided
|
||||
// client interface.
|
||||
@@ -82,7 +85,7 @@ func New(client kubernetes.Interface, opt Options) (authn.Keychain, error) {
|
||||
}
|
||||
|
||||
// Third, extend the default keyring with the pull secrets.
|
||||
kr, err := credentialprovidersecrets.MakeDockerKeyring(pullSecrets, credentialprovider.NewDockerKeyring())
|
||||
kr, err := credentialprovidersecrets.MakeDockerKeyring(pullSecrets, origKeyRing)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
+1
-1
@@ -17,5 +17,5 @@ package k8schain
|
||||
|
||||
import (
|
||||
// aws credential provider
|
||||
_ "github.com/vdemeester/k8s-pkg-credentialprovider/aws"
|
||||
_ "k8s.io/kubernetes/pkg/credentialprovider/aws"
|
||||
)
|
||||
|
||||
Generated
Vendored
+1
-1
@@ -17,5 +17,5 @@ package k8schain
|
||||
|
||||
import (
|
||||
// azure credential provider
|
||||
_ "github.com/vdemeester/k8s-pkg-credentialprovider/azure"
|
||||
_ "k8s.io/kubernetes/pkg/credentialprovider/azure"
|
||||
)
|
||||
|
||||
+1
-1
@@ -17,5 +17,5 @@ package k8schain
|
||||
|
||||
import (
|
||||
// gcp credential provider
|
||||
_ "github.com/vdemeester/k8s-pkg-credentialprovider/gcp"
|
||||
_ "k8s.io/kubernetes/pkg/credentialprovider/gcp"
|
||||
)
|
||||
|
||||
+2
-4
@@ -52,9 +52,7 @@ var (
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultAuthKey is the key used for dockerhub in config files, which
|
||||
// is hardcoded for historical reasons.
|
||||
DefaultAuthKey = "https://" + name.DefaultRegistry + "/v1/"
|
||||
defaultAuthKey = "https://" + name.DefaultRegistry + "/v1/"
|
||||
)
|
||||
|
||||
// Resolve implements Keychain.
|
||||
@@ -69,7 +67,7 @@ func (dk *defaultKeychain) Resolve(target Resource) (Authenticator, error) {
|
||||
// https://github.com/moby/moby/blob/fc01c2b481097a6057bec3cd1ab2d7b4488c50c4/registry/config.go#L397-L404
|
||||
key := target.RegistryStr()
|
||||
if key == name.DefaultRegistry {
|
||||
key = DefaultAuthKey
|
||||
key = defaultAuthKey
|
||||
}
|
||||
|
||||
cfg, err := cf.GetAuthConfig(key)
|
||||
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
# `name`
|
||||
|
||||
[](https://godoc.org/github.com/google/go-containerregistry/pkg/name)
|
||||
+1
-4
@@ -23,9 +23,6 @@ import (
|
||||
// ConfigFile is the configuration file that holds the metadata describing
|
||||
// how to launch a container. See:
|
||||
// https://github.com/opencontainers/image-spec/blob/master/config.md
|
||||
//
|
||||
// docker_version and os.version are not part of the spec but included
|
||||
// for backwards compatibility.
|
||||
type ConfigFile struct {
|
||||
Architecture string `json:"architecture"`
|
||||
Author string `json:"author,omitempty"`
|
||||
@@ -36,7 +33,7 @@ type ConfigFile struct {
|
||||
OS string `json:"os"`
|
||||
RootFS RootFS `json:"rootfs"`
|
||||
Config Config `json:"config"`
|
||||
OSVersion string `json:"os.version,omitempty"`
|
||||
OSVersion string `json:"osversion,omitempty"`
|
||||
}
|
||||
|
||||
// History is one entry of a list recording how this container image was built.
|
||||
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
# `daemon`
|
||||
|
||||
[](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/daemon)
|
||||
|
||||
The `daemon` package enables reading/writing images from/to the docker daemon.
|
||||
|
||||
It is not fully fleshed out, but is useful for interoperability, see various issues:
|
||||
|
||||
* https://github.com/google/go-containerregistry/issues/205
|
||||
* https://github.com/google/go-containerregistry/issues/552
|
||||
* https://github.com/google/go-containerregistry/issues/627
|
||||
+28
-20
@@ -36,7 +36,6 @@ var _ v1.Image = (*image)(nil)
|
||||
type imageOpener struct {
|
||||
ref name.Reference
|
||||
buffered bool
|
||||
client Client
|
||||
}
|
||||
|
||||
// ImageOption is a functional option for Image.
|
||||
@@ -46,16 +45,14 @@ func (i *imageOpener) Open() (v1.Image, error) {
|
||||
var opener tarball.Opener
|
||||
var err error
|
||||
if i.buffered {
|
||||
opener, err = i.bufferedOpener(i.ref)
|
||||
opener, err = bufferedOpener(i.ref)
|
||||
} else {
|
||||
opener, err = i.unbufferedOpener(i.ref)
|
||||
opener, err = unbufferedOpener(i.ref)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
i.client.NegotiateAPIVersion(context.Background())
|
||||
|
||||
tb, err := tarball.Image(opener, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -66,13 +63,33 @@ func (i *imageOpener) Open() (v1.Image, error) {
|
||||
return img, nil
|
||||
}
|
||||
|
||||
func (i *imageOpener) saveImage(ref name.Reference) (io.ReadCloser, error) {
|
||||
return i.client.ImageSave(context.Background(), []string{ref.Name()})
|
||||
// ImageSaver is an interface for testing.
|
||||
type ImageSaver interface {
|
||||
ImageSave(context.Context, []string) (io.ReadCloser, error)
|
||||
}
|
||||
|
||||
func (i *imageOpener) bufferedOpener(ref name.Reference) (tarball.Opener, error) {
|
||||
// This is a variable so we can override in tests.
|
||||
var getImageSaver = func() (ImageSaver, error) {
|
||||
cli, err := client.NewClientWithOpts(client.FromEnv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cli.NegotiateAPIVersion(context.Background())
|
||||
return cli, nil
|
||||
}
|
||||
|
||||
func saveImage(ref name.Reference) (io.ReadCloser, error) {
|
||||
cli, err := getImageSaver()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cli.ImageSave(context.Background(), []string{ref.Name()})
|
||||
}
|
||||
|
||||
func bufferedOpener(ref name.Reference) (tarball.Opener, error) {
|
||||
// Store the tarball in memory and return a new reader into the bytes each time we need to access something.
|
||||
rc, err := i.saveImage(ref)
|
||||
rc, err := saveImage(ref)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -89,10 +106,10 @@ func (i *imageOpener) bufferedOpener(ref name.Reference) (tarball.Opener, error)
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (i *imageOpener) unbufferedOpener(ref name.Reference) (tarball.Opener, error) {
|
||||
func unbufferedOpener(ref name.Reference) (tarball.Opener, error) {
|
||||
// To avoid storing the tarball in memory, do a save every time we need to access something.
|
||||
return func() (io.ReadCloser, error) {
|
||||
return i.saveImage(ref)
|
||||
return saveImage(ref)
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -109,14 +126,5 @@ func Image(ref name.Reference, options ...ImageOption) (v1.Image, error) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if i.client == nil {
|
||||
var err error
|
||||
i.client, err = client.NewClientWithOpts(client.FromEnv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return i.Open()
|
||||
}
|
||||
|
||||
-26
@@ -14,13 +14,6 @@
|
||||
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
)
|
||||
|
||||
// WithBufferedOpener buffers the image.
|
||||
func WithBufferedOpener() ImageOption {
|
||||
return func(i *imageOpener) error {
|
||||
@@ -39,22 +32,3 @@ func (i *imageOpener) setBuffered(buffer bool) error {
|
||||
i.buffered = buffer
|
||||
return nil
|
||||
}
|
||||
|
||||
// WithClient is a functional option to allow injecting a docker client.
|
||||
//
|
||||
// By default, github.com/docker/docker/client.FromEnv is used.
|
||||
func WithClient(client Client) ImageOption {
|
||||
return func(i *imageOpener) error {
|
||||
i.client = client
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Client represents the subset of a docker client that the daemon
|
||||
// package uses.
|
||||
type Client interface {
|
||||
NegotiateAPIVersion(ctx context.Context)
|
||||
ImageSave(context.Context, []string) (io.ReadCloser, error)
|
||||
ImageLoad(context.Context, io.Reader, bool) (types.ImageLoadResponse, error)
|
||||
ImageTag(context.Context, string, string) error
|
||||
}
|
||||
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
# `empty`
|
||||
|
||||
[](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/empty)
|
||||
|
||||
The empty packages provides an empty base for constructing a `v1.Image` or `v1.ImageIndex`.
|
||||
This is especially useful when paired with the [`mutate`](/pkg/v1/mutate) package,
|
||||
see [`mutate.Append`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/mutate#Append)
|
||||
and [`mutate.AppendManifests`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/mutate#AppendManifests).
|
||||
+2
-3
@@ -31,14 +31,13 @@ type Image interface {
|
||||
// Size returns the size of the manifest.
|
||||
Size() (int64, error)
|
||||
|
||||
// ConfigName returns the hash of the image's config file, also known as
|
||||
// the Image ID.
|
||||
// ConfigName returns the hash of the image's config file.
|
||||
ConfigName() (Hash, error)
|
||||
|
||||
// ConfigFile returns this image's config file.
|
||||
ConfigFile() (*ConfigFile, error)
|
||||
|
||||
// RawConfigFile returns the serialized bytes of ConfigFile().
|
||||
// RawConfigFile returns the serialized bytes of ConfigFile()
|
||||
RawConfigFile() ([]byte, error)
|
||||
|
||||
// Digest returns the sha256 of this image's manifest.
|
||||
|
||||
-5
@@ -1,5 +0,0 @@
|
||||
# `layout`
|
||||
|
||||
[](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/layout)
|
||||
|
||||
The `layout` package implements support for interacting with an [OCI Image Layout](https://github.com/opencontainers/image-spec/blob/master/image-layout.md).
|
||||
+6
-10
@@ -124,19 +124,15 @@ func (l Path) AppendDescriptor(desc v1.Descriptor) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return l.WriteFile("index.json", rawIndex, os.ModePerm)
|
||||
return l.writeFile("index.json", rawIndex)
|
||||
}
|
||||
|
||||
// WriteFile write a file with arbitrary data at an arbitrary location in a v1
|
||||
// layout. Used mostly internally to write files like "oci-layout" and
|
||||
// "index.json", also can be used to write other arbitrary files. Do *not* use
|
||||
// this to write blobs. Use only WriteBlob() for that.
|
||||
func (l Path) WriteFile(name string, data []byte, perm os.FileMode) error {
|
||||
func (l Path) writeFile(name string, data []byte) error {
|
||||
if err := os.MkdirAll(l.path(), os.ModePerm); err != nil && !os.IsExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
return ioutil.WriteFile(l.path(name), data, perm)
|
||||
return ioutil.WriteFile(l.path(name), data, os.ModePerm)
|
||||
|
||||
}
|
||||
|
||||
@@ -263,12 +259,12 @@ func (l Path) writeIndexToFile(indexFile string, ii v1.ImageIndex) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return l.WriteFile(indexFile, rawIndex, os.ModePerm)
|
||||
return l.writeFile(indexFile, rawIndex)
|
||||
}
|
||||
|
||||
func (l Path) writeIndex(ii v1.ImageIndex) error {
|
||||
// Always just write oci-layout file, since it's small.
|
||||
if err := l.WriteFile("oci-layout", []byte(layoutFile), os.ModePerm); err != nil {
|
||||
if err := l.writeFile("oci-layout", []byte(layoutFile)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -295,7 +291,7 @@ func (l Path) writeIndex(ii v1.ImageIndex) error {
|
||||
func Write(path string, ii v1.ImageIndex) (Path, error) {
|
||||
lp := Path(path)
|
||||
// Always just write oci-layout file, since it's small.
|
||||
if err := lp.WriteFile("oci-layout", []byte(layoutFile), os.ModePerm); err != nil {
|
||||
if err := lp.writeFile("oci-layout", []byte(layoutFile)); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
|
||||
-56
@@ -1,56 +0,0 @@
|
||||
# `mutate`
|
||||
|
||||
[](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/mutate)
|
||||
|
||||
The `v1.Image`, `v1.ImageIndex`, and `v1.Layer` interfaces provide only
|
||||
accessor methods, so they are essentially immutable. If you want to change
|
||||
something about them, you need to produce a new instance of that interface.
|
||||
|
||||
A common use case for this library is to read an image from somewhere (a source),
|
||||
change something about it, and write the image somewhere else (a sink).
|
||||
|
||||
Graphically, this looks something like:
|
||||
|
||||
<p align="center">
|
||||
<img src="/images/mutate.dot.svg" />
|
||||
</p>
|
||||
|
||||
## Mutations
|
||||
|
||||
This is obviously not a comprehensive set of useful transformations (PRs welcome!),
|
||||
but a rough summary of what the `mutate` package currently does:
|
||||
|
||||
### `Config` and `ConfigFile`
|
||||
|
||||
These allow you to change the [image configuration](https://github.com/opencontainers/image-spec/blob/master/config.md#properties),
|
||||
e.g. to change the entrypoint, environment, author, etc.
|
||||
|
||||
### `Time`, `Canonical`, and `CreatedAt`
|
||||
|
||||
These are useful in the context of [reproducible builds](https://reproducible-builds.org/),
|
||||
where you may want to strip timestamps and other non-reproducible information.
|
||||
|
||||
### `Append`, `AppendLayers`, and `AppendManifests`
|
||||
|
||||
These functions allow the extension of a `v1.Image` or `v1.ImageIndex` with
|
||||
new layers or manifests.
|
||||
|
||||
For constructing an image `FROM scratch`, see the [`empty`](/pkg/v1/empty) package.
|
||||
|
||||
### `MediaType` and `IndexMediaType`
|
||||
|
||||
Sometimes, it is necessary to change the media type of an image or index,
|
||||
e.g. to appease a registry with strict validation of images (_looking at you, GCR_).
|
||||
|
||||
### `Rebase`
|
||||
|
||||
Rebase has [its own README](/cmd/crane/rebase.md).
|
||||
|
||||
This is the underlying implementation of [`crane rebase`](https://github.com/google/go-containerregistry/blob/master/cmd/crane/doc/crane_rebase.md).
|
||||
|
||||
### `Extract`
|
||||
|
||||
Extract will flatten an image filesystem into a single tar stream,
|
||||
respecting whiteout files.
|
||||
|
||||
This is the underlying implementation of [`crane export`](https://github.com/google/go-containerregistry/blob/master/cmd/crane/doc/crane_export.md).
|
||||
+17
-16
@@ -85,21 +85,26 @@ func (i *image) compute() error {
|
||||
manifest := m.DeepCopy()
|
||||
manifestLayers := manifest.Layers
|
||||
for _, add := range i.adds {
|
||||
desc, err := partial.Descriptor(add.Layer)
|
||||
if err != nil {
|
||||
d := v1.Descriptor{}
|
||||
var err error
|
||||
|
||||
if d.Size, err = add.Layer.Size(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Fields in the addendum override the original descriptor.
|
||||
if len(add.Annotations) != 0 {
|
||||
desc.Annotations = add.Annotations
|
||||
}
|
||||
if len(add.URLs) != 0 {
|
||||
desc.URLs = add.URLs
|
||||
if d.Digest, err = add.Layer.Digest(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
manifestLayers = append(manifestLayers, *desc)
|
||||
digestMap[desc.Digest] = add.Layer
|
||||
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
|
||||
@@ -120,12 +125,8 @@ func (i *image) compute() error {
|
||||
|
||||
// With OCI media types, this should not be set, see discussion:
|
||||
// https://github.com/opencontainers/image-spec/pull/795
|
||||
if i.mediaType != nil {
|
||||
if strings.Contains(string(*i.mediaType), types.OCIVendorPrefix) {
|
||||
manifest.MediaType = ""
|
||||
} else if strings.Contains(string(*i.mediaType), types.DockerVendorPrefix) {
|
||||
manifest.MediaType = *i.mediaType
|
||||
}
|
||||
if i.mediaType != nil && strings.Contains(string(*i.mediaType), types.OCIVendorPrefix) {
|
||||
manifest.MediaType = ""
|
||||
}
|
||||
|
||||
i.configFile = configFile
|
||||
|
||||
+21
-27
@@ -24,33 +24,31 @@ import (
|
||||
"github.com/google/go-containerregistry/pkg/v1/types"
|
||||
)
|
||||
|
||||
func computeDescriptor(ia IndexAddendum) (*v1.Descriptor, error) {
|
||||
desc, err := partial.Descriptor(ia.Add)
|
||||
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 Descriptor values.
|
||||
if ia.Descriptor.Size != 0 {
|
||||
desc.Size = ia.Descriptor.Size
|
||||
// The IndexAddendum allows overriding These values.
|
||||
if desc.Size == 0 {
|
||||
desc.Size = sz
|
||||
}
|
||||
if string(ia.Descriptor.MediaType) != "" {
|
||||
desc.MediaType = ia.Descriptor.MediaType
|
||||
if string(desc.MediaType) == "" {
|
||||
desc.MediaType = mt
|
||||
}
|
||||
if ia.Descriptor.Digest != (v1.Hash{}) {
|
||||
desc.Digest = ia.Descriptor.Digest
|
||||
if desc.Digest == (v1.Hash{}) {
|
||||
desc.Digest = d
|
||||
}
|
||||
if ia.Descriptor.Platform != nil {
|
||||
desc.Platform = ia.Descriptor.Platform
|
||||
}
|
||||
if len(ia.Descriptor.URLs) != 0 {
|
||||
desc.URLs = ia.Descriptor.URLs
|
||||
}
|
||||
if len(ia.Descriptor.Annotations) != 0 {
|
||||
desc.Annotations = ia.Descriptor.Annotations
|
||||
}
|
||||
|
||||
return desc, nil
|
||||
return &desc, nil
|
||||
}
|
||||
|
||||
type index struct {
|
||||
@@ -91,7 +89,7 @@ func (i *index) compute() error {
|
||||
manifest := m.DeepCopy()
|
||||
manifests := manifest.Manifests
|
||||
for _, add := range i.adds {
|
||||
desc, err := computeDescriptor(add)
|
||||
desc, err := computeDescriptor(add.Descriptor, add.Add)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -109,12 +107,8 @@ func (i *index) compute() error {
|
||||
|
||||
// With OCI media types, this should not be set, see discussion:
|
||||
// https://github.com/opencontainers/image-spec/pull/795
|
||||
if i.mediaType != nil {
|
||||
if strings.Contains(string(*i.mediaType), types.OCIVendorPrefix) {
|
||||
manifest.MediaType = ""
|
||||
} else if strings.Contains(string(*i.mediaType), types.DockerVendorPrefix) {
|
||||
manifest.MediaType = *i.mediaType
|
||||
}
|
||||
if i.mediaType != nil && strings.Contains(string(*i.mediaType), types.OCIVendorPrefix) {
|
||||
manifest.MediaType = ""
|
||||
}
|
||||
|
||||
i.manifest = manifest
|
||||
|
||||
+2
-4
@@ -140,6 +140,8 @@ func CreatedAt(base v1.Image, created v1.Time) (v1.Image, error) {
|
||||
//
|
||||
// If a caller doesn't read the full contents, they should Close it to free up
|
||||
// resources used during extraction.
|
||||
//
|
||||
// Adapted from https://github.com/google/containerregistry/blob/master/client/v2_2/docker_image_.py#L731
|
||||
func Extract(img v1.Image) io.ReadCloser {
|
||||
pr, pw := io.Pipe()
|
||||
|
||||
@@ -154,7 +156,6 @@ func Extract(img v1.Image) io.ReadCloser {
|
||||
return pr
|
||||
}
|
||||
|
||||
// Adapted from https://github.com/google/containerregistry/blob/da03b395ccdc4e149e34fbb540483efce962dc64/client/v2_2/docker_image_.py#L816
|
||||
func extract(img v1.Image, w io.Writer) error {
|
||||
tarWriter := tar.NewWriter(w)
|
||||
defer tarWriter.Close()
|
||||
@@ -279,9 +280,6 @@ func Time(img v1.Image, t time.Time) (v1.Image, error) {
|
||||
cfg := cf.DeepCopy()
|
||||
|
||||
// Copy basic config over
|
||||
cfg.Architecture = ocf.Architecture
|
||||
cfg.Author = ocf.Author
|
||||
cfg.OS = ocf.OS
|
||||
cfg.Config = ocf.Config
|
||||
|
||||
// Strip away timestamps from the config file
|
||||
|
||||
-66
@@ -1,66 +0,0 @@
|
||||
# `partial`
|
||||
|
||||
[](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/partial)
|
||||
|
||||
## Partial Implementations
|
||||
|
||||
There are roughly two kinds of image representations: compressed and uncompressed.
|
||||
|
||||
The implementations for these kinds of images are almost identical, with the only
|
||||
major difference being how blobs (config and layers) are fetched. This common
|
||||
code lives in this package, where you provide a _partial_ implementation of a
|
||||
compressed or uncompressed image, and you get back a full `v1.Image` implementation.
|
||||
|
||||
### Examples
|
||||
|
||||
In a registry, blobs are compressed, so it's easiest to implement a `v1.Image` in terms
|
||||
of compressed layers. `remote.remoteImage` does this by implementing `CompressedImageCore`:
|
||||
|
||||
```go
|
||||
type CompressedImageCore interface {
|
||||
RawConfigFile() ([]byte, error)
|
||||
MediaType() (types.MediaType, error)
|
||||
RawManifest() ([]byte, error)
|
||||
LayerByDigest(v1.Hash) (CompressedLayer, error)
|
||||
}
|
||||
```
|
||||
|
||||
In a tarball, blobs are (often) uncompressed, so it's easiest to implement a `v1.Image` in terms
|
||||
of uncompressed layers. `tarball.uncompressedImage` does this by implementing `UncompressedImageCore`:
|
||||
|
||||
```go
|
||||
type CompressedImageCore interface {
|
||||
RawConfigFile() ([]byte, error)
|
||||
MediaType() (types.MediaType, error)
|
||||
LayerByDiffID(v1.Hash) (UncompressedLayer, error)
|
||||
}
|
||||
```
|
||||
|
||||
## Optional Methods
|
||||
|
||||
Where possible, we access some information via optional methods as an optimization.
|
||||
|
||||
### [`partial.Descriptor`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/partial#Descriptor)
|
||||
|
||||
There are some properties of a [`Descriptor`](https://github.com/opencontainers/image-spec/blob/master/descriptor.md#properties) that aren't derivable from just image data:
|
||||
|
||||
* `MediaType`
|
||||
* `Platform`
|
||||
* `URLs`
|
||||
* `Annotations`
|
||||
|
||||
For example, in a `tarball.Image`, there is a `LayerSources` field that contains
|
||||
an entire layer descriptor with `URLs` information for foreign layers. This
|
||||
information can be passed through to callers by implementing this optional
|
||||
`Descriptor` method.
|
||||
|
||||
See [`#654`](https://github.com/google/go-containerregistry/pull/654).
|
||||
|
||||
### [`partial.UncompressedSize`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/partial#UncompressedSize)
|
||||
|
||||
Usually, you don't need to know the uncompressed size of a layer, since that
|
||||
information isn't stored in a config file (just he sha256 is needed); however,
|
||||
there are cases where it is very helpful to know the layer size, e.g. when
|
||||
writing the uncompressed layer into a tarball.
|
||||
|
||||
See [`#655`](https://github.com/google/go-containerregistry/pull/655).
|
||||
+7
-7
@@ -44,22 +44,22 @@ type compressedLayerExtender struct {
|
||||
}
|
||||
|
||||
// Uncompressed implements v1.Layer
|
||||
func (cle *compressedLayerExtender) Uncompressed() (io.ReadCloser, error) {
|
||||
r, err := cle.Compressed()
|
||||
func (ule *compressedLayerExtender) Uncompressed() (io.ReadCloser, error) {
|
||||
u, err := ule.Compressed()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return v1util.GunzipReadCloser(r)
|
||||
return v1util.GunzipReadCloser(u)
|
||||
}
|
||||
|
||||
// DiffID implements v1.Layer
|
||||
func (cle *compressedLayerExtender) DiffID() (v1.Hash, error) {
|
||||
func (ule *compressedLayerExtender) DiffID() (v1.Hash, error) {
|
||||
// If our nested CompressedLayer implements DiffID,
|
||||
// then delegate to it instead.
|
||||
if wdi, ok := cle.CompressedLayer.(WithDiffID); ok {
|
||||
if wdi, ok := ule.CompressedLayer.(WithDiffID); ok {
|
||||
return wdi.DiffID()
|
||||
}
|
||||
r, err := cle.Uncompressed()
|
||||
r, err := ule.Uncompressed()
|
||||
if err != nil {
|
||||
return v1.Hash{}, err
|
||||
}
|
||||
@@ -76,7 +76,7 @@ func CompressedToLayer(ul CompressedLayer) (v1.Layer, error) {
|
||||
// CompressedImageCore represents the base minimum interface a natively
|
||||
// compressed image must implement for us to produce a v1.Image.
|
||||
type CompressedImageCore interface {
|
||||
ImageCore
|
||||
imageCore
|
||||
|
||||
// RawManifest returns the serialized bytes of the manifest.
|
||||
RawManifest() ([]byte, error)
|
||||
|
||||
+2
-2
@@ -18,8 +18,8 @@ import (
|
||||
"github.com/google/go-containerregistry/pkg/v1/types"
|
||||
)
|
||||
|
||||
// ImageCore is the core set of properties without which we cannot build a v1.Image
|
||||
type ImageCore interface {
|
||||
// imageCore is the core set of properties without which we cannot build a v1.Image
|
||||
type imageCore interface {
|
||||
// RawConfigFile returns the serialized bytes of this image's config file.
|
||||
RawConfigFile() ([]byte, error)
|
||||
|
||||
|
||||
+44
-3
@@ -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
|
||||
@@ -89,7 +113,7 @@ func UncompressedToLayer(ul UncompressedLayer) (v1.Layer, error) {
|
||||
// UncompressedImageCore represents the bare minimum interface a natively
|
||||
// uncompressed image must implement for us to produce a v1.Image
|
||||
type UncompressedImageCore interface {
|
||||
ImageCore
|
||||
imageCore
|
||||
|
||||
// LayerByDiffID is a variation on the v1.Image method, which returns
|
||||
// an UncompressedLayer instead.
|
||||
@@ -155,12 +179,29 @@ func (i *uncompressedImageExtender) Manifest() (*v1.Manifest, error) {
|
||||
|
||||
m.Layers = make([]v1.Descriptor, len(ls))
|
||||
for i, l := range ls {
|
||||
desc, err := Descriptor(l)
|
||||
// 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
|
||||
}
|
||||
h, err := l.Digest()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m.Layers[i] = *desc
|
||||
m.Layers[i] = v1.Descriptor{
|
||||
MediaType: types.DockerLayer,
|
||||
Size: sz,
|
||||
Digest: h,
|
||||
}
|
||||
}
|
||||
|
||||
i.manifest = m
|
||||
|
||||
+11
-102
@@ -129,6 +129,12 @@ func RawConfigFile(i WithConfigFile) ([]byte, error) {
|
||||
return json.Marshal(cfg)
|
||||
}
|
||||
|
||||
// WithUncompressedLayer defines the subset of v1.Image used by these helper methods
|
||||
type WithUncompressedLayer interface {
|
||||
// UncompressedLayer is like UncompressedBlob, but takes the "diff id".
|
||||
UncompressedLayer(v1.Hash) (io.ReadCloser, error)
|
||||
}
|
||||
|
||||
// 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.
|
||||
@@ -201,22 +207,22 @@ func BlobSize(i WithManifest, h v1.Hash) (int64, error) {
|
||||
}
|
||||
|
||||
// BlobDescriptor is a helper for implementing v1.Image
|
||||
func BlobDescriptor(i WithManifest, h v1.Hash) (*v1.Descriptor, error) {
|
||||
func BlobDescriptor(i WithManifest, h v1.Hash) (v1.Descriptor, error) {
|
||||
m, err := i.Manifest()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return v1.Descriptor{}, err
|
||||
}
|
||||
|
||||
if m.Config.Digest == h {
|
||||
return &m.Config, nil
|
||||
return m.Config, nil
|
||||
}
|
||||
|
||||
for _, l := range m.Layers {
|
||||
if l.Digest == h {
|
||||
return &l, nil
|
||||
return l, nil
|
||||
}
|
||||
}
|
||||
return nil, 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
|
||||
@@ -283,100 +289,3 @@ type WithDiffID interface {
|
||||
type withDescriptor interface {
|
||||
Descriptor() (*v1.Descriptor, error)
|
||||
}
|
||||
|
||||
// Describable represents something for which we can produce a v1.Descriptor.
|
||||
type Describable interface {
|
||||
Digest() (v1.Hash, error)
|
||||
MediaType() (types.MediaType, error)
|
||||
Size() (int64, error)
|
||||
}
|
||||
|
||||
// Descriptor returns a v1.Descriptor given a Describable. It also encodes
|
||||
// some logic for unwrapping things that have been wrapped by
|
||||
// CompressedToLayer, UncompressedToLayer, CompressedToImage, or
|
||||
// UncompressedToImage.
|
||||
func Descriptor(d Describable) (*v1.Descriptor, error) {
|
||||
// If Describable implements Descriptor itself, return that.
|
||||
if wd, ok := d.(withDescriptor); ok {
|
||||
return wd.Descriptor()
|
||||
}
|
||||
|
||||
// Otherwise, try to unwrap any partial implementations to see
|
||||
// if the wrapped struct implements Descriptor.
|
||||
if ule, ok := d.(*uncompressedLayerExtender); ok {
|
||||
if wd, ok := ule.UncompressedLayer.(withDescriptor); ok {
|
||||
return wd.Descriptor()
|
||||
}
|
||||
}
|
||||
if cle, ok := d.(*compressedLayerExtender); ok {
|
||||
if wd, ok := cle.CompressedLayer.(withDescriptor); ok {
|
||||
return wd.Descriptor()
|
||||
}
|
||||
}
|
||||
if uie, ok := d.(*uncompressedImageExtender); ok {
|
||||
if wd, ok := uie.UncompressedImageCore.(withDescriptor); ok {
|
||||
return wd.Descriptor()
|
||||
}
|
||||
}
|
||||
if cie, ok := d.(*compressedImageExtender); ok {
|
||||
if wd, ok := cie.CompressedImageCore.(withDescriptor); ok {
|
||||
return wd.Descriptor()
|
||||
}
|
||||
}
|
||||
|
||||
// If all else fails, compute the descriptor from the individual methods.
|
||||
var (
|
||||
desc v1.Descriptor
|
||||
err error
|
||||
)
|
||||
|
||||
if desc.Size, err = d.Size(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if desc.Digest, err = d.Digest(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if desc.MediaType, err = d.MediaType(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &desc, nil
|
||||
}
|
||||
|
||||
type withUncompressedSize interface {
|
||||
UncompressedSize() (int64, error)
|
||||
}
|
||||
|
||||
// UncompressedSize returns the size of the Uncompressed layer. If the
|
||||
// underlying implementation doesn't implement UncompressedSize directly,
|
||||
// this will compute the uncompressedSize by reading everything returned
|
||||
// by Compressed(). This is potentially expensive and may consume the contents
|
||||
// for streaming layers.
|
||||
func UncompressedSize(l v1.Layer) (int64, error) {
|
||||
// If the layer implements UncompressedSize itself, return that.
|
||||
if wus, ok := l.(withUncompressedSize); ok {
|
||||
return wus.UncompressedSize()
|
||||
}
|
||||
|
||||
// Otherwise, try to unwrap any partial implementations to see
|
||||
// if the wrapped struct implements UncompressedSize.
|
||||
if ule, ok := l.(*uncompressedLayerExtender); ok {
|
||||
if wus, ok := ule.UncompressedLayer.(withUncompressedSize); ok {
|
||||
return wus.UncompressedSize()
|
||||
}
|
||||
}
|
||||
if cle, ok := l.(*compressedLayerExtender); ok {
|
||||
if wus, ok := cle.CompressedLayer.(withUncompressedSize); ok {
|
||||
return wus.UncompressedSize()
|
||||
}
|
||||
}
|
||||
|
||||
// The layer doesn't implement UncompressedSize, we need to compute it.
|
||||
rc, err := l.Uncompressed()
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
return io.Copy(ioutil.Discard, rc)
|
||||
}
|
||||
|
||||
-117
@@ -1,117 +0,0 @@
|
||||
# `remote`
|
||||
|
||||
[](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/remote)
|
||||
|
||||
The `remote` package implements a client for accessing a registry,
|
||||
per the [OCI distribution spec](https://github.com/opencontainers/distribution-spec/blob/master/spec.md).
|
||||
|
||||
It leans heavily on the lower level [`transport`](/pkg/v1/remote/transport) package, which handles the
|
||||
authentication handshake and structured errors.
|
||||
|
||||
## Usage
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/google/go-containerregistry/pkg/authn"
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ref, err := name.ParseReference("gcr.io/google-containers/pause")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
img, err := remote.Image(ref, remote.WithAuthFromKeychain(authn.DefaultKeychain))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// do stuff with img
|
||||
}
|
||||
```
|
||||
|
||||
## Structure
|
||||
|
||||
<p align="center">
|
||||
<img src="/images/remote.dot.svg" />
|
||||
</p>
|
||||
|
||||
|
||||
## Background
|
||||
|
||||
There are a lot of confusingly similar terms that come up when talking about images in registries.
|
||||
|
||||
### Anatomy of an image
|
||||
|
||||
In general...
|
||||
|
||||
* A tag refers to an image manifest.
|
||||
* An image manifest references a config file and an orderered list of _compressed_ layers by sha256 digest.
|
||||
* A config file references an ordered list of _uncompressed_ layers by sha256 digest and contains runtime configuration.
|
||||
* The sha256 digest of the config file is the [image id](https://github.com/opencontainers/image-spec/blob/master/config.md#imageid) for the image.
|
||||
|
||||
For example, an image with two layers would look something like this:
|
||||
|
||||

|
||||
|
||||
### Anatomy of an index
|
||||
|
||||
In the normal case, an [index](https://github.com/opencontainers/image-spec/blob/master/image-index.md) is used to represent a multi-platform image.
|
||||
This was the original use case for a [manifest
|
||||
list](https://docs.docker.com/registry/spec/manifest-v2-2/#manifest-list).
|
||||
|
||||

|
||||
|
||||
It is possible for an index to reference another index, per the OCI
|
||||
[image-spec](https://github.com/opencontainers/image-spec/blob/master/media-types.md#compatibility-matrix).
|
||||
In theory, both an image and image index can reference arbitrary things via
|
||||
[descriptors](https://github.com/opencontainers/image-spec/blob/master/descriptor.md),
|
||||
e.g. see the [image layout
|
||||
example](https://github.com/opencontainers/image-spec/blob/master/image-layout.md#index-example),
|
||||
which references an application/xml file from an image index.
|
||||
|
||||
That could look something like this:
|
||||
|
||||

|
||||
|
||||
Using a recursive index like this might not be possible with all registries,
|
||||
but this flexibility allows for some interesting applications, e.g. the
|
||||
[OCI Artifacts](https://github.com/opencontainers/artifacts) effort.
|
||||
|
||||
### Anatomy of an image upload
|
||||
|
||||
The structure of an image requires a delicate ordering when uploading an image to a registry.
|
||||
Below is a (slightly simplified) figure that describes how an image is prepared for upload
|
||||
to a registry and how the data flows between various artifacts:
|
||||
|
||||

|
||||
|
||||
Note that:
|
||||
|
||||
* A config file references the uncompressed layer contents by sha256.
|
||||
* A manifest references the compressed layer contents by sha256 and the size of the layer.
|
||||
* A manifest references the config file contents by sha256 and the size of the file.
|
||||
|
||||
It follows that during an upload, we need to upload layers before the config file,
|
||||
and we need to upload the config file before the manifest.
|
||||
|
||||
Sometimes, we know all of this information ahead of time, (e.g. when copying from remote.Image),
|
||||
so the ordering is less important.
|
||||
|
||||
In other cases, e.g. when using a [`stream.Layer`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/stream#Layer),
|
||||
we can't compute anything until we have already uploaded the layer, so we need to be careful about ordering.
|
||||
|
||||
## Caveats
|
||||
|
||||
### schema 1
|
||||
|
||||
This package does not support schema 1 images, see [`#377`](https://github.com/google/go-containerregistry/issues/377),
|
||||
however, it's possible to do _something_ useful with them via [`remote.Get`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/remote#Get),
|
||||
which doesn't try to interpret what is returned by the registry.
|
||||
|
||||
[`crane.Copy`](https://godoc.org/github.com/google/go-containerregistry/pkg/crane#Copy) takes advantage of this to implement support for copying schema 1 images,
|
||||
see [here](https://github.com/google/go-containerregistry/blob/master/pkg/internal/legacy/copy.go).
|
||||
+1
-1
@@ -34,7 +34,7 @@ func CheckPushPermission(ref name.Reference, kc authn.Keychain, t http.RoundTrip
|
||||
// authorize a push. Figure out how to return early here when we can,
|
||||
// to avoid a roundtrip for spec-compliant registries.
|
||||
w := writer{
|
||||
repo: ref.Context(),
|
||||
ref: ref,
|
||||
client: &http.Client{Transport: tr},
|
||||
}
|
||||
loc, _, err := w.initiateUpload("", "")
|
||||
|
||||
+8
-8
@@ -77,12 +77,14 @@ func (d *Descriptor) RawManifest() ([]byte, error) {
|
||||
// querying what kind of artifact a reference represents.
|
||||
func Get(ref name.Reference, options ...Option) (*Descriptor, error) {
|
||||
acceptable := []types.MediaType{
|
||||
types.DockerManifestSchema2,
|
||||
types.OCIManifestSchema1,
|
||||
types.DockerManifestList,
|
||||
types.OCIImageIndex,
|
||||
// Just to look at them.
|
||||
types.DockerManifestSchema1,
|
||||
types.DockerManifestSchema1Signed,
|
||||
}
|
||||
acceptable = append(acceptable, acceptableImageMediaTypes...)
|
||||
acceptable = append(acceptable, acceptableIndexMediaTypes...)
|
||||
return get(ref, acceptable, options...)
|
||||
}
|
||||
|
||||
@@ -172,9 +174,8 @@ func (d *Descriptor) remoteImage() *remoteImage {
|
||||
Ref: d.Ref,
|
||||
Client: d.Client,
|
||||
},
|
||||
manifest: d.Manifest,
|
||||
mediaType: d.MediaType,
|
||||
descriptor: &d.Descriptor,
|
||||
manifest: d.Manifest,
|
||||
mediaType: d.MediaType,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,9 +185,8 @@ func (d *Descriptor) remoteIndex() *remoteIndex {
|
||||
Ref: d.Ref,
|
||||
Client: d.Client,
|
||||
},
|
||||
manifest: d.Manifest,
|
||||
mediaType: d.MediaType,
|
||||
descriptor: &d.Descriptor,
|
||||
manifest: d.Manifest,
|
||||
mediaType: d.MediaType,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+5
-25
@@ -29,11 +29,6 @@ import (
|
||||
"github.com/google/go-containerregistry/pkg/v1/v1util"
|
||||
)
|
||||
|
||||
var acceptableImageMediaTypes = []types.MediaType{
|
||||
types.DockerManifestSchema2,
|
||||
types.OCIManifestSchema1,
|
||||
}
|
||||
|
||||
// remoteImage accesses an image from a remote registry
|
||||
type remoteImage struct {
|
||||
fetcher
|
||||
@@ -42,7 +37,6 @@ type remoteImage struct {
|
||||
configLock sync.Mutex // Protects config
|
||||
config []byte
|
||||
mediaType types.MediaType
|
||||
descriptor *v1.Descriptor
|
||||
}
|
||||
|
||||
var _ partial.CompressedImageCore = (*remoteImage)(nil)
|
||||
@@ -74,14 +68,15 @@ func (r *remoteImage) RawManifest() ([]byte, error) {
|
||||
// NOTE(jonjohnsonjr): We should never get here because the public entrypoints
|
||||
// do type-checking via remote.Descriptor. I've left this here for tests that
|
||||
// directly instantiate a remoteImage.
|
||||
manifest, desc, err := r.fetchManifest(r.Ref, acceptableImageMediaTypes)
|
||||
acceptable := []types.MediaType{
|
||||
types.DockerManifestSchema2,
|
||||
types.OCIManifestSchema1,
|
||||
}
|
||||
manifest, desc, err := r.fetchManifest(r.Ref, acceptable)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if r.descriptor == nil {
|
||||
r.descriptor = desc
|
||||
}
|
||||
r.mediaType = desc.MediaType
|
||||
r.manifest = manifest
|
||||
return r.manifest, nil
|
||||
@@ -112,15 +107,6 @@ func (r *remoteImage) RawConfigFile() ([]byte, error) {
|
||||
return r.config, nil
|
||||
}
|
||||
|
||||
// Descriptor retains the original descriptor from an index manifest.
|
||||
// See partial.Descriptor.
|
||||
func (r *remoteImage) Descriptor() (*v1.Descriptor, error) {
|
||||
// kind of a hack, but RawManifest does appropriate locking/memoization
|
||||
// and makes sure r.descriptor is populated.
|
||||
_, err := r.RawManifest()
|
||||
return r.descriptor, err
|
||||
}
|
||||
|
||||
// remoteImageLayer implements partial.CompressedLayer
|
||||
type remoteImageLayer struct {
|
||||
ri *remoteImage
|
||||
@@ -206,12 +192,6 @@ func (rl *remoteImageLayer) DiffID() (v1.Hash, error) {
|
||||
return partial.BlobToDiffID(rl, rl.digest)
|
||||
}
|
||||
|
||||
// Descriptor retains the original descriptor from an image manifest.
|
||||
// See partial.Descriptor.
|
||||
func (rl *remoteImageLayer) Descriptor() (*v1.Descriptor, error) {
|
||||
return partial.BlobDescriptor(rl, rl.digest)
|
||||
}
|
||||
|
||||
// LayerByDigest implements partial.CompressedLayer
|
||||
func (r *remoteImage) LayerByDigest(h v1.Hash) (partial.CompressedLayer, error) {
|
||||
return &remoteImageLayer{
|
||||
|
||||
+13
-22
@@ -25,23 +25,22 @@ import (
|
||||
"github.com/google/go-containerregistry/pkg/v1/types"
|
||||
)
|
||||
|
||||
var acceptableIndexMediaTypes = []types.MediaType{
|
||||
types.DockerManifestList,
|
||||
types.OCIImageIndex,
|
||||
}
|
||||
|
||||
// remoteIndex accesses an index from a remote registry
|
||||
type remoteIndex struct {
|
||||
fetcher
|
||||
manifestLock sync.Mutex // Protects manifest
|
||||
manifest []byte
|
||||
mediaType types.MediaType
|
||||
descriptor *v1.Descriptor
|
||||
}
|
||||
|
||||
// Index provides access to a remote index reference.
|
||||
func Index(ref name.Reference, options ...Option) (v1.ImageIndex, error) {
|
||||
desc, err := get(ref, acceptableIndexMediaTypes, options...)
|
||||
acceptable := []types.MediaType{
|
||||
types.DockerManifestList,
|
||||
types.OCIImageIndex,
|
||||
}
|
||||
|
||||
desc, err := get(ref, acceptable, options...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -74,14 +73,15 @@ func (r *remoteIndex) RawManifest() ([]byte, error) {
|
||||
// NOTE(jonjohnsonjr): We should never get here because the public entrypoints
|
||||
// do type-checking via remote.Descriptor. I've left this here for tests that
|
||||
// directly instantiate a remoteIndex.
|
||||
manifest, desc, err := r.fetchManifest(r.Ref, acceptableIndexMediaTypes)
|
||||
acceptable := []types.MediaType{
|
||||
types.DockerManifestList,
|
||||
types.OCIImageIndex,
|
||||
}
|
||||
manifest, desc, err := r.fetchManifest(r.Ref, acceptable)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if r.descriptor == nil {
|
||||
r.descriptor = desc
|
||||
}
|
||||
r.mediaType = desc.MediaType
|
||||
r.manifest = manifest
|
||||
return r.manifest, nil
|
||||
@@ -105,15 +105,6 @@ func (r *remoteIndex) Image(h v1.Hash) (v1.Image, error) {
|
||||
return desc.Image()
|
||||
}
|
||||
|
||||
// Descriptor retains the original descriptor from an index manifest.
|
||||
// See partial.Descriptor.
|
||||
func (r *remoteIndex) Descriptor() (*v1.Descriptor, error) {
|
||||
// kind of a hack, but RawManifest does appropriate locking/memoization
|
||||
// and makes sure r.descriptor is populated.
|
||||
_, err := r.RawManifest()
|
||||
return r.descriptor, err
|
||||
}
|
||||
|
||||
func (r *remoteIndex) ImageIndex(h v1.Hash) (v1.ImageIndex, error) {
|
||||
desc, err := r.childByHash(h)
|
||||
if err != nil {
|
||||
@@ -174,7 +165,7 @@ func (r *remoteIndex) childByHash(h v1.Hash) (*Descriptor, error) {
|
||||
// Convert one of this index's child's v1.Descriptor into a remote.Descriptor, with the given platform option.
|
||||
func (r *remoteIndex) childDescriptor(child v1.Descriptor, platform v1.Platform) (*Descriptor, error) {
|
||||
ref := r.Ref.Context().Digest(child.Digest.String())
|
||||
manifest, _, err := r.fetchManifest(ref, []types.MediaType{child.MediaType})
|
||||
manifest, desc, err := r.fetchManifest(ref, []types.MediaType{child.MediaType})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -184,7 +175,7 @@ func (r *remoteIndex) childDescriptor(child v1.Descriptor, platform v1.Platform)
|
||||
Client: r.Client,
|
||||
},
|
||||
Manifest: manifest,
|
||||
Descriptor: child,
|
||||
Descriptor: *desc,
|
||||
platform: platform,
|
||||
}, nil
|
||||
}
|
||||
|
||||
+4
-6
@@ -50,12 +50,10 @@ func ListWithContext(ctx context.Context, repo name.Repository, options ...Optio
|
||||
}
|
||||
|
||||
uri := &url.URL{
|
||||
Scheme: repo.Registry.Scheme(),
|
||||
Host: repo.Registry.RegistryStr(),
|
||||
Path: fmt.Sprintf("/v2/%s/tags/list", repo.RepositoryStr()),
|
||||
// ECR returns an error if n > 1000:
|
||||
// https://github.com/google/go-containerregistry/issues/681
|
||||
RawQuery: "n=1000",
|
||||
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}
|
||||
|
||||
-13
@@ -17,7 +17,6 @@ package remote
|
||||
import (
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/partial"
|
||||
)
|
||||
|
||||
// MountableLayer wraps a v1.Layer in a shim that enables the layer to be
|
||||
@@ -28,12 +27,6 @@ type MountableLayer struct {
|
||||
Reference name.Reference
|
||||
}
|
||||
|
||||
// Descriptor retains the original descriptor from an image manifest.
|
||||
// See partial.Descriptor.
|
||||
func (ml *MountableLayer) Descriptor() (*v1.Descriptor, error) {
|
||||
return partial.Descriptor(ml.Layer)
|
||||
}
|
||||
|
||||
// mountableImage wraps the v1.Layer references returned by the embedded v1.Image
|
||||
// in MountableLayer's so that remote.Write might attempt to mount them from their
|
||||
// source repository.
|
||||
@@ -82,9 +75,3 @@ func (mi *mountableImage) LayerByDiffID(d v1.Hash) (v1.Layer, error) {
|
||||
Reference: mi.Reference,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Descriptor retains the original descriptor from an index manifest.
|
||||
// See partial.Descriptor.
|
||||
func (mi *mountableImage) Descriptor() (*v1.Descriptor, error) {
|
||||
return partial.Descriptor(mi.Image)
|
||||
}
|
||||
|
||||
+1
-3
@@ -1,8 +1,6 @@
|
||||
# `transport`
|
||||
|
||||
[](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/transport)
|
||||
|
||||
The [distribution protocol](https://github.com/opencontainers/distribution-spec) is fairly simple, but correctly [implementing authentication](../../../authn/README.md) is **hard**.
|
||||
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:
|
||||
|
||||
+10
-3
@@ -33,7 +33,7 @@ type bearerTransport struct {
|
||||
// Basic credentials that we exchange for bearer tokens.
|
||||
basic authn.Authenticator
|
||||
// Holds the bearer response from the token service.
|
||||
bearer authn.AuthConfig
|
||||
bearer *authn.Bearer
|
||||
// Registry to which we send bearer tokens.
|
||||
registry name.Registry
|
||||
// See https://tools.ietf.org/html/rfc6750#section-3
|
||||
@@ -61,7 +61,11 @@ func (bt *bearerTransport) RoundTrip(in *http.Request) (*http.Response, error) {
|
||||
// the registry with which we are interacting.
|
||||
// In case of redirect http.Client can use an empty Host, check URL too.
|
||||
if matchesHost(bt.registry, in, bt.scheme) {
|
||||
hdr := fmt.Sprintf("Bearer %s", bt.bearer.RegistryToken)
|
||||
auth, err := bt.bearer.Authorization()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hdr := fmt.Sprintf("Bearer %s", auth.RegistryToken)
|
||||
in.Header.Set("Authorization", hdr)
|
||||
}
|
||||
return bt.inner.RoundTrip(in)
|
||||
@@ -130,8 +134,9 @@ func (bt *bearerTransport) refresh() error {
|
||||
}
|
||||
|
||||
// Find a token to turn into a Bearer authenticator
|
||||
var bearer authn.Bearer
|
||||
if response.Token != "" {
|
||||
bt.bearer.RegistryToken = response.Token
|
||||
bearer = authn.Bearer{Token: response.Token}
|
||||
} else {
|
||||
return fmt.Errorf("no token in bearer response:\n%s", content)
|
||||
}
|
||||
@@ -143,6 +148,8 @@ func (bt *bearerTransport) refresh() error {
|
||||
})
|
||||
}
|
||||
|
||||
// Replace our old bearer authenticator (if we had one) with our newly refreshed authenticator.
|
||||
bt.bearer = &bearer
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
+1
-45
@@ -19,27 +19,9 @@ import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// The set of query string keys that we expect to send as part of the registry
|
||||
// protocol. Anything else is potentially dangerous to leak, as it's probably
|
||||
// from a redirect. These redirects often included tokens or signed URLs.
|
||||
var paramWhitelist = map[string]struct{}{
|
||||
// Token exchange
|
||||
"scope": struct{}{},
|
||||
"service": struct{}{},
|
||||
// Cross-repo mounting
|
||||
"mount": struct{}{},
|
||||
"from": struct{}{},
|
||||
// Layer PUT
|
||||
"digest": struct{}{},
|
||||
// Listing tags and catalog
|
||||
"n": struct{}{},
|
||||
"last": struct{}{},
|
||||
}
|
||||
|
||||
// Error implements error to support the following error specification:
|
||||
// https://github.com/docker/distribution/blob/master/docs/spec/api.md#errors
|
||||
type Error struct {
|
||||
@@ -48,8 +30,6 @@ type Error struct {
|
||||
StatusCode int
|
||||
// The raw body if we couldn't understand it.
|
||||
rawBody string
|
||||
// The request that failed.
|
||||
request *http.Request
|
||||
}
|
||||
|
||||
// Check that Error implements error
|
||||
@@ -57,14 +37,6 @@ var _ error = (*Error)(nil)
|
||||
|
||||
// Error implements error
|
||||
func (e *Error) Error() string {
|
||||
prefix := ""
|
||||
if e.request != nil {
|
||||
prefix = fmt.Sprintf("%s %s: ", e.request.Method, redact(e.request.URL))
|
||||
}
|
||||
return prefix + e.responseErr()
|
||||
}
|
||||
|
||||
func (e *Error) responseErr() string {
|
||||
switch len(e.Errors) {
|
||||
case 0:
|
||||
if len(e.rawBody) == 0 {
|
||||
@@ -79,7 +51,7 @@ func (e *Error) responseErr() string {
|
||||
errors = append(errors, d.String())
|
||||
}
|
||||
return fmt.Sprintf("multiple errors returned: %s",
|
||||
strings.Join(errors, "; "))
|
||||
strings.Join(errors, ";"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,21 +69,6 @@ func (e *Error) Temporary() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func redact(original *url.URL) *url.URL {
|
||||
qs := original.Query()
|
||||
for k, v := range qs {
|
||||
for i := range v {
|
||||
if _, ok := paramWhitelist[k]; !ok {
|
||||
// key is not in the whitelist
|
||||
v[i] = "REDACTED"
|
||||
}
|
||||
}
|
||||
}
|
||||
redacted := *original
|
||||
redacted.RawQuery = qs.Encode()
|
||||
return &redacted
|
||||
}
|
||||
|
||||
// Diagnostic represents a single error returned by a Docker registry interaction.
|
||||
type Diagnostic struct {
|
||||
Code ErrorCode `json:"code"`
|
||||
@@ -170,6 +127,5 @@ func CheckError(resp *http.Response, codes ...int) error {
|
||||
structuredError.rawBody = string(b)
|
||||
}
|
||||
structuredError.StatusCode = resp.StatusCode
|
||||
structuredError.request = resp.Request
|
||||
return structuredError
|
||||
}
|
||||
|
||||
+26
-26
@@ -50,13 +50,13 @@ func Write(ref name.Reference, img v1.Image, options ...Option) error {
|
||||
return err
|
||||
}
|
||||
|
||||
scopes := scopesForUploadingImage(ref.Context(), ls)
|
||||
scopes := scopesForUploadingImage(ref, ls)
|
||||
tr, err := transport.New(ref.Context().Registry, o.auth, o.transport, scopes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
w := writer{
|
||||
repo: ref.Context(),
|
||||
ref: ref,
|
||||
client: &http.Client{Transport: tr},
|
||||
}
|
||||
|
||||
@@ -125,20 +125,20 @@ func Write(ref name.Reference, img v1.Image, options ...Option) error {
|
||||
|
||||
// With all of the constituent elements uploaded, upload the manifest
|
||||
// to commit the image.
|
||||
return w.commitImage(img, ref)
|
||||
return w.commitImage(img)
|
||||
}
|
||||
|
||||
// writer writes the elements of an image to a remote image reference.
|
||||
type writer struct {
|
||||
repo name.Repository
|
||||
ref name.Reference
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
// url returns a url.Url for the specified path in the context of this remote image reference.
|
||||
func (w *writer) url(path string) url.URL {
|
||||
return url.URL{
|
||||
Scheme: w.repo.Registry.Scheme(),
|
||||
Host: w.repo.RegistryStr(),
|
||||
Scheme: w.ref.Context().Registry.Scheme(),
|
||||
Host: w.ref.Context().RegistryStr(),
|
||||
Path: path,
|
||||
}
|
||||
}
|
||||
@@ -164,7 +164,7 @@ func (w *writer) nextLocation(resp *http.Response) (string, error) {
|
||||
// initiation if "mount" is specified, even if no "from" sources are specified.
|
||||
// However, this is not broadly applicable to all registries, e.g. ECR.
|
||||
func (w *writer) checkExistingBlob(h v1.Hash) (bool, error) {
|
||||
u := w.url(fmt.Sprintf("/v2/%s/blobs/%s", w.repo.RepositoryStr(), h.String()))
|
||||
u := w.url(fmt.Sprintf("/v2/%s/blobs/%s", w.ref.Context().RepositoryStr(), h.String()))
|
||||
|
||||
resp, err := w.client.Head(u.String())
|
||||
if err != nil {
|
||||
@@ -182,7 +182,7 @@ func (w *writer) checkExistingBlob(h v1.Hash) (bool, error) {
|
||||
// checkExistingManifest checks if a manifest exists already in the repository
|
||||
// by making a HEAD request to the manifest API.
|
||||
func (w *writer) checkExistingManifest(h v1.Hash, mt types.MediaType) (bool, error) {
|
||||
u := w.url(fmt.Sprintf("/v2/%s/manifests/%s", w.repo.RepositoryStr(), h.String()))
|
||||
u := w.url(fmt.Sprintf("/v2/%s/manifests/%s", w.ref.Context().RepositoryStr(), h.String()))
|
||||
|
||||
req, err := http.NewRequest(http.MethodHead, u.String(), nil)
|
||||
if err != nil {
|
||||
@@ -210,7 +210,7 @@ func (w *writer) checkExistingManifest(h v1.Hash, mt types.MediaType) (bool, err
|
||||
// upload was initiated and the body of that blob should be sent to the returned
|
||||
// location.
|
||||
func (w *writer) initiateUpload(from, mount string) (location string, mounted bool, err error) {
|
||||
u := w.url(fmt.Sprintf("/v2/%s/blobs/uploads/", w.repo.RepositoryStr()))
|
||||
u := w.url(fmt.Sprintf("/v2/%s/blobs/uploads/", w.ref.Context().RepositoryStr()))
|
||||
uv := url.Values{}
|
||||
if mount != "" && from != "" {
|
||||
// Quay will fail if we specify a "mount" without a "from".
|
||||
@@ -313,7 +313,7 @@ func (w *writer) uploadOne(l v1.Layer) error {
|
||||
mount = h.String()
|
||||
}
|
||||
if ml, ok := l.(*MountableLayer); ok {
|
||||
if w.repo.RegistryStr() == ml.Reference.Context().RegistryStr() {
|
||||
if w.ref.Context().RegistryStr() == ml.Reference.Context().RegistryStr() {
|
||||
from = ml.Reference.Context().RepositoryStr()
|
||||
}
|
||||
}
|
||||
@@ -407,7 +407,7 @@ func unpackTaggable(t Taggable) (*v1.Descriptor, error) {
|
||||
}
|
||||
|
||||
// commitImage does a PUT of the image's manifest.
|
||||
func (w *writer) commitImage(t Taggable, ref name.Reference) error {
|
||||
func (w *writer) commitImage(t Taggable) error {
|
||||
raw, err := t.RawManifest()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -417,7 +417,7 @@ func (w *writer) commitImage(t Taggable, ref name.Reference) error {
|
||||
return err
|
||||
}
|
||||
|
||||
u := w.url(fmt.Sprintf("/v2/%s/manifests/%s", w.repo.RepositoryStr(), ref.Identifier()))
|
||||
u := w.url(fmt.Sprintf("/v2/%s/manifests/%s", w.ref.Context().RepositoryStr(), w.ref.Identifier()))
|
||||
|
||||
// Make the request to PUT the serialized manifest
|
||||
req, err := http.NewRequest(http.MethodPut, u.String(), bytes.NewBuffer(raw))
|
||||
@@ -437,11 +437,11 @@ func (w *writer) commitImage(t Taggable, ref name.Reference) error {
|
||||
}
|
||||
|
||||
// The image was successfully pushed!
|
||||
logs.Progress.Printf("%v: digest: %v size: %d", ref, desc.Digest, len(raw))
|
||||
logs.Progress.Printf("%v: digest: %v size: %d", w.ref, desc.Digest, len(raw))
|
||||
return nil
|
||||
}
|
||||
|
||||
func scopesForUploadingImage(repo name.Repository, layers []v1.Layer) []string {
|
||||
func scopesForUploadingImage(ref name.Reference, layers []v1.Layer) []string {
|
||||
// use a map as set to remove duplicates scope strings
|
||||
scopeSet := map[string]struct{}{}
|
||||
|
||||
@@ -449,7 +449,7 @@ func scopesForUploadingImage(repo name.Repository, layers []v1.Layer) []string {
|
||||
if ml, ok := l.(*MountableLayer); ok {
|
||||
// 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().String() != repo.String() && ml.Reference.Context().Registry.String() == repo.Registry.String() {
|
||||
if ml.Reference.Context() != ref.Context() && ml.Reference.Context().Registry == ref.Context().Registry {
|
||||
scopeSet[ml.Reference.Scope(transport.PullScope)] = struct{}{}
|
||||
}
|
||||
}
|
||||
@@ -457,7 +457,7 @@ func scopesForUploadingImage(repo name.Repository, layers []v1.Layer) []string {
|
||||
|
||||
scopes := make([]string, 0)
|
||||
// Push scope should be the first element because a few registries just look at the first scope to determine access.
|
||||
scopes = append(scopes, repo.Scope(transport.PushScope))
|
||||
scopes = append(scopes, ref.Scope(transport.PushScope))
|
||||
|
||||
for scope := range scopeSet {
|
||||
scopes = append(scopes, scope)
|
||||
@@ -485,7 +485,7 @@ func WriteIndex(ref name.Reference, ii v1.ImageIndex, options ...Option) error {
|
||||
return err
|
||||
}
|
||||
w := writer{
|
||||
repo: ref.Context(),
|
||||
ref: ref,
|
||||
client: &http.Client{Transport: tr},
|
||||
}
|
||||
|
||||
@@ -523,22 +523,22 @@ func WriteIndex(ref name.Reference, ii v1.ImageIndex, options ...Option) error {
|
||||
|
||||
// With all of the constituent elements uploaded, upload the manifest
|
||||
// to commit the image.
|
||||
return w.commitImage(ii, ref)
|
||||
return w.commitImage(ii)
|
||||
}
|
||||
|
||||
// WriteLayer uploads the provided Layer to the specified repo.
|
||||
func WriteLayer(repo name.Repository, layer v1.Layer, options ...Option) error {
|
||||
o, err := makeOptions(repo, options...)
|
||||
// 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(repo, []v1.Layer{layer})
|
||||
tr, err := transport.New(repo.Registry, o.auth, o.transport, scopes)
|
||||
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{
|
||||
repo: repo,
|
||||
ref: ref,
|
||||
client: &http.Client{Transport: tr},
|
||||
}
|
||||
|
||||
@@ -564,9 +564,9 @@ func Tag(tag name.Tag, t Taggable, options ...Option) error {
|
||||
return err
|
||||
}
|
||||
w := writer{
|
||||
repo: tag.Context(),
|
||||
ref: tag,
|
||||
client: &http.Client{Transport: tr},
|
||||
}
|
||||
|
||||
return w.commitImage(t, tag)
|
||||
return w.commitImage(t)
|
||||
}
|
||||
|
||||
-68
@@ -1,68 +0,0 @@
|
||||
# `stream`
|
||||
|
||||
[](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/stream)
|
||||
|
||||
The `stream` package contains an implementation of
|
||||
[`v1.Layer`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1#Layer)
|
||||
that supports _streaming_ access, i.e. the layer contents are read once and not
|
||||
buffered.
|
||||
|
||||
## Usage
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||
"github.com/google/go-containerregistry/pkg/v1/stream"
|
||||
)
|
||||
|
||||
// upload the contents of stdin as a layer to a local registry
|
||||
func main() {
|
||||
repo, err := name.NewRepository("localhost:5000/stream")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
layer := stream.NewLayer(os.Stdin)
|
||||
|
||||
if err := remote.WriteLayer(repo, layer); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Structure
|
||||
|
||||
This implements the layer portion of an [image
|
||||
upload](/pkg/v1/remote#anatomy-of-an-image-upload). We launch a goroutine that
|
||||
is responsible for hashing the uncompressed contents to compute the `DiffID`,
|
||||
gzipping them to produce the `Compressed` contents, and hashing/counting the
|
||||
bytes to produce the `Digest`/`Size`. This goroutine writes to an
|
||||
`io.PipeWriter`, which blocks until `Compressed` reads the gzipped contents from
|
||||
the corresponding `io.PipeReader`.
|
||||
|
||||
<p align="center">
|
||||
<img src="/images/stream.dot.svg" />
|
||||
</p>
|
||||
|
||||
## Caveats
|
||||
|
||||
This assumes that you have an uncompressed layer (i.e. a tarball) and would like
|
||||
to compress it. Calling `Uncompressed` is always an error. Likewise, other
|
||||
methods are invalid until the contents of `Compressed` have been completely
|
||||
consumed and `Close`d.
|
||||
|
||||
Using a `stream.Layer` will likely not work without careful consideration. For
|
||||
example, in the `mutate` package, we defer computing the manifest and config
|
||||
file until they are actually called. This allows you to `mutate.Append` a
|
||||
streaming layer to an image without accidentally consuming it. Similarly, in
|
||||
`remote.Write`, if calling `Digest` on a layer fails, we attempt to upload the
|
||||
layer anyway, understanding that we may be dealing with a `stream.Layer` whose
|
||||
contents need to be uploaded before we can upload the config file.
|
||||
|
||||
Given the [structure](#structure) of how this is implemented, forgetting to
|
||||
`Close` a `stream.Layer` will leak a goroutine.
|
||||
-280
@@ -1,280 +0,0 @@
|
||||
# `tarball`
|
||||
|
||||
[](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/tarball)
|
||||
|
||||
This package produces tarballs that can consumed via `docker load`. Note
|
||||
that this is a _different_ format from the [`legacy`](/pkg/legacy/tarball)
|
||||
tarballs that are produced by `docker save`, but this package is still able to
|
||||
read the legacy tarballs produced by `docker save`.
|
||||
|
||||
## Usage
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
"github.com/google/go-containerregistry/pkg/v1/tarball"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Read a tarball from os.Args[1] that contains ubuntu.
|
||||
tag, err := name.NewTag("ubuntu")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
img, err := tarball.ImageFromPath(os.Args[1], &tag)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Write that tarball to os.Args[2] with a different tag.
|
||||
newTag, err := name.NewTag("ubuntu:newest")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
f, err := os.Create(os.Args[2])
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if err := tarball.Write(newTag, img, f); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Structure
|
||||
|
||||
<p align="center">
|
||||
<img src="/images/tarball.dot.svg" />
|
||||
</p>
|
||||
|
||||
Let's look at what happens when we write out a tarball:
|
||||
|
||||
|
||||
### `ubuntu:latest`
|
||||
|
||||
```
|
||||
$ crane pull ubuntu ubuntu.tar && mkdir ubuntu && tar xf ubuntu.tar -C ubuntu && rm ubuntu.tar
|
||||
$ tree ubuntu/
|
||||
ubuntu/
|
||||
├── 423ae2b273f4c17ceee9e8482fa8d071d90c7d052ae208e1fe4963fceb3d6954.tar.gz
|
||||
├── b6b53be908de2c0c78070fff0a9f04835211b3156c4e73785747af365e71a0d7.tar.gz
|
||||
├── de83a2304fa1f7c4a13708a0d15b9704f5945c2be5cbb2b3ed9b2ccb718d0b3d.tar.gz
|
||||
├── f9a83bce3af0648efaa60b9bb28225b09136d2d35d0bed25ac764297076dec1b.tar.gz
|
||||
├── manifest.json
|
||||
└── sha256:72300a873c2ca11c70d0c8642177ce76ff69ae04d61a5813ef58d40ff66e3e7c
|
||||
|
||||
0 directories, 6 files
|
||||
```
|
||||
|
||||
There are a couple interesting files here.
|
||||
|
||||
`manifest.json` is the entrypoint: a list of [`tarball.Descriptor`s](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/tarball#Descriptor)
|
||||
that describe the images containd in this tarball.
|
||||
|
||||
For each image, this has the `RepoTags` (how it was pulled), a `Config` file
|
||||
that points to the image's config file, a list of `Layers`, and (optionally)
|
||||
`LayerSources`.
|
||||
|
||||
```
|
||||
$ jq < ubuntu/manifest.json
|
||||
[
|
||||
{
|
||||
"Config": "sha256:72300a873c2ca11c70d0c8642177ce76ff69ae04d61a5813ef58d40ff66e3e7c",
|
||||
"RepoTags": [
|
||||
"ubuntu"
|
||||
],
|
||||
"Layers": [
|
||||
"423ae2b273f4c17ceee9e8482fa8d071d90c7d052ae208e1fe4963fceb3d6954.tar.gz",
|
||||
"de83a2304fa1f7c4a13708a0d15b9704f5945c2be5cbb2b3ed9b2ccb718d0b3d.tar.gz",
|
||||
"f9a83bce3af0648efaa60b9bb28225b09136d2d35d0bed25ac764297076dec1b.tar.gz",
|
||||
"b6b53be908de2c0c78070fff0a9f04835211b3156c4e73785747af365e71a0d7.tar.gz"
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
The config file and layers are exactly what you would expect, and match the
|
||||
registry representations of the same artifacts. You'll notice that the
|
||||
`manifest.json` contains similar information as the registry manifest, but isn't
|
||||
quite the same:
|
||||
|
||||
```
|
||||
$ crane manifest ubuntu@sha256:0925d086715714114c1988f7c947db94064fd385e171a63c07730f1fa014e6f9
|
||||
{
|
||||
"schemaVersion": 2,
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
|
||||
"config": {
|
||||
"mediaType": "application/vnd.docker.container.image.v1+json",
|
||||
"size": 3408,
|
||||
"digest": "sha256:72300a873c2ca11c70d0c8642177ce76ff69ae04d61a5813ef58d40ff66e3e7c"
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
|
||||
"size": 26692096,
|
||||
"digest": "sha256:423ae2b273f4c17ceee9e8482fa8d071d90c7d052ae208e1fe4963fceb3d6954"
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
|
||||
"size": 35365,
|
||||
"digest": "sha256:de83a2304fa1f7c4a13708a0d15b9704f5945c2be5cbb2b3ed9b2ccb718d0b3d"
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
|
||||
"size": 852,
|
||||
"digest": "sha256:f9a83bce3af0648efaa60b9bb28225b09136d2d35d0bed25ac764297076dec1b"
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
|
||||
"size": 163,
|
||||
"digest": "sha256:b6b53be908de2c0c78070fff0a9f04835211b3156c4e73785747af365e71a0d7"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
This makes it difficult to maintain image digests when roundtripping images
|
||||
through the tarball format, so it's not a great format if you care about
|
||||
provenance.
|
||||
|
||||
The ubuntu example didn't have any `LayerSources` -- let's look at another image
|
||||
that does.
|
||||
|
||||
### `hello-world:nanoserver`
|
||||
|
||||
```
|
||||
$ crane pull hello-world:nanoserver@sha256:63c287625c2b0b72900e562de73c0e381472a83b1b39217aef3856cd398eca0b nanoserver.tar
|
||||
$ mkdir nanoserver && tar xf nanoserver.tar -C nanoserver && rm nanoserver.tar
|
||||
$ tree nanoserver/
|
||||
nanoserver/
|
||||
├── 10d1439be4eb8819987ec2e9c140d44d74d6b42a823d57fe1953bd99948e1bc0.tar.gz
|
||||
├── a35da61c356213336e646756218539950461ff2bf096badf307a23add6e70053.tar.gz
|
||||
├── be21f08f670160cbae227e3053205b91d6bfa3de750b90c7e00bd2c511ccb63a.tar.gz
|
||||
├── manifest.json
|
||||
└── sha256:bc5d255ea81f83c8c38a982a6d29a6f2198427d258aea5f166e49856896b2da6
|
||||
|
||||
0 directories, 5 files
|
||||
|
||||
$ jq < nanoserver/manifest.json
|
||||
[
|
||||
{
|
||||
"Config": "sha256:bc5d255ea81f83c8c38a982a6d29a6f2198427d258aea5f166e49856896b2da6",
|
||||
"RepoTags": [
|
||||
"index.docker.io/library/hello-world:i-was-a-digest"
|
||||
],
|
||||
"Layers": [
|
||||
"a35da61c356213336e646756218539950461ff2bf096badf307a23add6e70053.tar.gz",
|
||||
"be21f08f670160cbae227e3053205b91d6bfa3de750b90c7e00bd2c511ccb63a.tar.gz",
|
||||
"10d1439be4eb8819987ec2e9c140d44d74d6b42a823d57fe1953bd99948e1bc0.tar.gz"
|
||||
],
|
||||
"LayerSources": {
|
||||
"sha256:26fd2d9d4c64a4f965bbc77939a454a31b607470f430b5d69fc21ded301fa55e": {
|
||||
"mediaType": "application/vnd.docker.image.rootfs.foreign.diff.tar.gzip",
|
||||
"size": 101145811,
|
||||
"digest": "sha256:a35da61c356213336e646756218539950461ff2bf096badf307a23add6e70053",
|
||||
"urls": [
|
||||
"https://mcr.microsoft.com/v2/windows/nanoserver/blobs/sha256:a35da61c356213336e646756218539950461ff2bf096badf307a23add6e70053"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
A couple things to note about this `manifest.json` versus the other:
|
||||
* The `RepoTags` field is a bit weird here. `hello-world` is a multi-platform
|
||||
image, so We had to pull this image by digest, since we're (I'm) on
|
||||
amd64/linux and wanted to grab a windows image. Since the tarball format
|
||||
expects a tag under `RepoTags`, and we didn't pull by tag, we replace the
|
||||
digest with a sentinel `i-was-a-digest` "tag" to appease docker.
|
||||
* The `LayerSources` has enough information to reconstruct the foreign layers
|
||||
pointer when pushing/pulling from the registry. For legal reasons, microsoft
|
||||
doesn't want anyone but them to serve windows base images, so the mediaType
|
||||
here indicates a "foreign" or "non-distributable" layer with an URL for where
|
||||
you can download it from microsoft (see the [OCI
|
||||
image-spec](https://github.com/opencontainers/image-spec/blob/master/layer.md#non-distributable-layers)).
|
||||
|
||||
We can look at what's in the registry to explain both of these things:
|
||||
```
|
||||
$ crane manifest hello-world:nanoserver | jq .
|
||||
{
|
||||
"manifests": [
|
||||
{
|
||||
"digest": "sha256:63c287625c2b0b72900e562de73c0e381472a83b1b39217aef3856cd398eca0b",
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
|
||||
"platform": {
|
||||
"architecture": "amd64",
|
||||
"os": "windows",
|
||||
"os.version": "10.0.17763.1040"
|
||||
},
|
||||
"size": 1124
|
||||
}
|
||||
],
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.list.v2+json",
|
||||
"schemaVersion": 2
|
||||
}
|
||||
|
||||
|
||||
# Note the media type and "urls" field.
|
||||
$ crane manifest hello-world:nanoserver@sha256:63c287625c2b0b72900e562de73c0e381472a83b1b39217aef3856cd398eca0b | jq .
|
||||
{
|
||||
"schemaVersion": 2,
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
|
||||
"config": {
|
||||
"mediaType": "application/vnd.docker.container.image.v1+json",
|
||||
"size": 1721,
|
||||
"digest": "sha256:bc5d255ea81f83c8c38a982a6d29a6f2198427d258aea5f166e49856896b2da6"
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"mediaType": "application/vnd.docker.image.rootfs.foreign.diff.tar.gzip",
|
||||
"size": 101145811,
|
||||
"digest": "sha256:a35da61c356213336e646756218539950461ff2bf096badf307a23add6e70053",
|
||||
"urls": [
|
||||
"https://mcr.microsoft.com/v2/windows/nanoserver/blobs/sha256:a35da61c356213336e646756218539950461ff2bf096badf307a23add6e70053"
|
||||
]
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
|
||||
"size": 1669,
|
||||
"digest": "sha256:be21f08f670160cbae227e3053205b91d6bfa3de750b90c7e00bd2c511ccb63a"
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
|
||||
"size": 949,
|
||||
"digest": "sha256:10d1439be4eb8819987ec2e9c140d44d74d6b42a823d57fe1953bd99948e1bc0"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The `LayerSources` map is keyed by the diffid. Note that `sha256:26fd2d9d4c64a4f965bbc77939a454a31b607470f430b5d69fc21ded301fa55e` matches the first layer in the config file:
|
||||
```
|
||||
$ jq '.[0].LayerSources' < nanoserver/manifest.json
|
||||
{
|
||||
"sha256:26fd2d9d4c64a4f965bbc77939a454a31b607470f430b5d69fc21ded301fa55e": {
|
||||
"mediaType": "application/vnd.docker.image.rootfs.foreign.diff.tar.gzip",
|
||||
"size": 101145811,
|
||||
"digest": "sha256:a35da61c356213336e646756218539950461ff2bf096badf307a23add6e70053",
|
||||
"urls": [
|
||||
"https://mcr.microsoft.com/v2/windows/nanoserver/blobs/sha256:a35da61c356213336e646756218539950461ff2bf096badf307a23add6e70053"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
$ jq < nanoserver/sha256\:bc5d255ea81f83c8c38a982a6d29a6f2198427d258aea5f166e49856896b2da6 | jq .rootfs
|
||||
{
|
||||
"type": "layers",
|
||||
"diff_ids": [
|
||||
"sha256:26fd2d9d4c64a4f965bbc77939a454a31b607470f430b5d69fc21ded301fa55e",
|
||||
"sha256:601cf7d78c62e4b4d32a7bbf96a17606a9cea5bd9d22ffa6f34aa431d056b0e8",
|
||||
"sha256:a1e1a3bf6529adcce4d91dce2cad86c2604a66b507ccbc4d2239f3da0ec5aab9"
|
||||
]
|
||||
}
|
||||
```
|
||||
+8
-10
@@ -79,17 +79,15 @@ func Image(opener Opener, tag *name.Tag) (v1.Image, error) {
|
||||
}
|
||||
|
||||
// Peek at the first layer and see if it's compressed.
|
||||
if len(img.imgDescriptor.Layers) > 0 {
|
||||
compressed, err := img.areLayersCompressed()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if compressed {
|
||||
c := compressedImage{
|
||||
image: img,
|
||||
}
|
||||
return partial.CompressedToImage(&c)
|
||||
compressed, err := img.areLayersCompressed()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if compressed {
|
||||
c := compressedImage{
|
||||
image: img,
|
||||
}
|
||||
return partial.CompressedToImage(&c)
|
||||
}
|
||||
|
||||
uc := uncompressedImage{
|
||||
|
||||
+2
-2
@@ -122,7 +122,7 @@ func LayerFromOpener(opener Opener, opts ...LayerOption) (v1.Layer, error) {
|
||||
}
|
||||
|
||||
// LayerFromReader returns a v1.Layer given a io.Reader.
|
||||
func LayerFromReader(reader io.Reader, opts ...LayerOption) (v1.Layer, error) {
|
||||
func LayerFromReader(reader io.Reader) (v1.Layer, error) {
|
||||
// Buffering due to Opener requiring multiple calls.
|
||||
a, err := ioutil.ReadAll(reader)
|
||||
if err != nil {
|
||||
@@ -130,7 +130,7 @@ func LayerFromReader(reader io.Reader, opts ...LayerOption) (v1.Layer, error) {
|
||||
}
|
||||
return LayerFromOpener(func() (io.ReadCloser, error) {
|
||||
return ioutil.NopCloser(bytes.NewReader(a)), nil
|
||||
}, opts...)
|
||||
})
|
||||
}
|
||||
|
||||
func computeDigest(opener Opener, compressed bool, compression int) (v1.Hash, int64, error) {
|
||||
|
||||
+1
-1
@@ -146,7 +146,7 @@ func MultiRefWrite(refToImage map[name.Reference]v1.Image, w io.Writer) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
layerSources[diffid] = *desc
|
||||
layerSources[diffid] = desc
|
||||
}
|
||||
|
||||
r, err := l.Compressed()
|
||||
|
||||
Reference in New Issue
Block a user