working commit
This commit is contained in:
-152
@@ -1,152 +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 v1
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 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"`
|
||||
Container string `json:"container,omitempty"`
|
||||
Created Time `json:"created,omitempty"`
|
||||
// Deprecated: This field is deprecated and will be removed in the next release.
|
||||
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:"os.version,omitempty"`
|
||||
Variant string `json:"variant,omitempty"`
|
||||
OSFeatures []string `json:"os.features,omitempty"`
|
||||
}
|
||||
|
||||
// Platform attempts to generates a Platform from the ConfigFile fields.
|
||||
func (cf *ConfigFile) Platform() *Platform {
|
||||
if cf.OS == "" && cf.Architecture == "" && cf.OSVersion == "" && cf.Variant == "" && len(cf.OSFeatures) == 0 {
|
||||
return nil
|
||||
}
|
||||
return &Platform{
|
||||
OS: cf.OS,
|
||||
Architecture: cf.Architecture,
|
||||
OSVersion: cf.OSVersion,
|
||||
Variant: cf.Variant,
|
||||
OSFeatures: cf.OSFeatures,
|
||||
}
|
||||
}
|
||||
|
||||
// History is one entry of a list recording how this container image was built.
|
||||
type History struct {
|
||||
Author string `json:"author,omitempty"`
|
||||
Created Time `json:"created,omitempty"`
|
||||
CreatedBy string `json:"created_by,omitempty"`
|
||||
Comment string `json:"comment,omitempty"`
|
||||
EmptyLayer bool `json:"empty_layer,omitempty"`
|
||||
}
|
||||
|
||||
// Time is a wrapper around time.Time to help with deep copying
|
||||
type Time struct {
|
||||
time.Time
|
||||
}
|
||||
|
||||
// DeepCopyInto creates a deep-copy of the Time value. The underlying time.Time
|
||||
// type is effectively immutable in the time API, so it is safe to
|
||||
// copy-by-assign, despite the presence of (unexported) Pointer fields.
|
||||
func (t *Time) DeepCopyInto(out *Time) {
|
||||
*out = *t
|
||||
}
|
||||
|
||||
// RootFS holds the ordered list of file system deltas that comprise the
|
||||
// container image's root filesystem.
|
||||
type RootFS struct {
|
||||
Type string `json:"type"`
|
||||
DiffIDs []Hash `json:"diff_ids"`
|
||||
}
|
||||
|
||||
// HealthConfig holds configuration settings for the HEALTHCHECK feature.
|
||||
type HealthConfig struct {
|
||||
// Test is the test to perform to check that the container is healthy.
|
||||
// An empty slice means to inherit the default.
|
||||
// The options are:
|
||||
// {} : inherit healthcheck
|
||||
// {"NONE"} : disable healthcheck
|
||||
// {"CMD", args...} : exec arguments directly
|
||||
// {"CMD-SHELL", command} : run command with system's default shell
|
||||
Test []string `json:",omitempty"`
|
||||
|
||||
// Zero means to inherit. Durations are expressed as integer nanoseconds.
|
||||
Interval time.Duration `json:",omitempty"` // Interval is the time to wait between checks.
|
||||
Timeout time.Duration `json:",omitempty"` // Timeout is the time to wait before considering the check to have hung.
|
||||
StartPeriod time.Duration `json:",omitempty"` // The start period for the container to initialize before the retries starts to count down.
|
||||
|
||||
// Retries is the number of consecutive failures needed to consider a container as unhealthy.
|
||||
// Zero means inherit.
|
||||
Retries int `json:",omitempty"`
|
||||
}
|
||||
|
||||
// Config is a submessage of the config file described as:
|
||||
//
|
||||
// The execution parameters which SHOULD be used as a base when running
|
||||
// a container using the image.
|
||||
//
|
||||
// The names of the fields in this message are chosen to reflect the JSON
|
||||
// payload of the Config as defined here:
|
||||
// https://git.io/vrAET
|
||||
// and
|
||||
// https://github.com/opencontainers/image-spec/blob/master/config.md
|
||||
type Config struct {
|
||||
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.
|
||||
func ParseConfigFile(r io.Reader) (*ConfigFile, error) {
|
||||
cf := ConfigFile{}
|
||||
if err := json.NewDecoder(r).Decode(&cf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &cf, nil
|
||||
}
|
||||
-18
@@ -1,18 +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.
|
||||
|
||||
// +k8s:deepcopy-gen=package
|
||||
|
||||
// Package v1 defines structured types for OCI v1 images
|
||||
package v1
|
||||
-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).
|
||||
-16
@@ -1,16 +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 empty provides an implementation of v1.Image equivalent to "FROM scratch".
|
||||
package empty
|
||||
-52
@@ -1,52 +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 empty
|
||||
|
||||
import (
|
||||
"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, _ = 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)
|
||||
}
|
||||
-65
@@ -1,65 +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 empty
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/partial"
|
||||
"github.com/google/go-containerregistry/pkg/v1/types"
|
||||
)
|
||||
|
||||
// Index is a singleton empty index, think: FROM scratch.
|
||||
var Index = emptyIndex{}
|
||||
|
||||
type emptyIndex struct{}
|
||||
|
||||
func (i emptyIndex) MediaType() (types.MediaType, error) {
|
||||
return types.OCIImageIndex, nil
|
||||
}
|
||||
|
||||
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 base(), nil
|
||||
}
|
||||
|
||||
func (i emptyIndex) RawManifest() ([]byte, error) {
|
||||
return json.Marshal(base())
|
||||
}
|
||||
|
||||
func (i emptyIndex) Image(v1.Hash) (v1.Image, error) {
|
||||
return nil, errors.New("empty index")
|
||||
}
|
||||
|
||||
func (i emptyIndex) ImageIndex(v1.Hash) (v1.ImageIndex, error) {
|
||||
return nil, errors.New("empty index")
|
||||
}
|
||||
|
||||
func base() *v1.IndexManifest {
|
||||
return &v1.IndexManifest{
|
||||
SchemaVersion: 2,
|
||||
MediaType: types.OCIImageIndex,
|
||||
Manifests: []v1.Descriptor{},
|
||||
}
|
||||
}
|
||||
-130
@@ -1,130 +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 v1
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"encoding"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"hash"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Hash is an unqualified digest of some content, e.g. sha256:deadbeef
|
||||
type Hash struct {
|
||||
// Algorithm holds the algorithm used to compute the hash.
|
||||
Algorithm string
|
||||
|
||||
// Hex holds the hex portion of the content hash.
|
||||
Hex string
|
||||
}
|
||||
|
||||
var _ encoding.TextMarshaler = (*Hash)(nil)
|
||||
var _ encoding.TextUnmarshaler = (*Hash)(nil)
|
||||
var _ json.Marshaler = (*Hash)(nil)
|
||||
var _ json.Unmarshaler = (*Hash)(nil)
|
||||
|
||||
// String reverses NewHash returning the string-form of the hash.
|
||||
func (h Hash) String() string {
|
||||
return fmt.Sprintf("%s:%s", h.Algorithm, h.Hex)
|
||||
}
|
||||
|
||||
// NewHash validates the input string is a hash and returns a strongly type Hash object.
|
||||
func NewHash(s string) (Hash, error) {
|
||||
h := Hash{}
|
||||
if err := h.parse(s); err != nil {
|
||||
return Hash{}, err
|
||||
}
|
||||
return h, nil
|
||||
}
|
||||
|
||||
// MarshalJSON implements json.Marshaler
|
||||
func (h Hash) MarshalJSON() ([]byte, error) { return json.Marshal(h.String()) }
|
||||
|
||||
// UnmarshalJSON implements json.Unmarshaler
|
||||
func (h *Hash) UnmarshalJSON(data []byte) error {
|
||||
var s string
|
||||
if err := json.Unmarshal(data, &s); err != nil {
|
||||
return err
|
||||
}
|
||||
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() ([]byte, 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 {
|
||||
case "sha256":
|
||||
return crypto.SHA256.New(), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported hash: %q", name)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Hash) parse(unquoted string) error {
|
||||
algo, body, ok := strings.Cut(unquoted, ":")
|
||||
if !ok || algo == "" || body == "" {
|
||||
return fmt.Errorf("cannot parse hash: %q", unquoted)
|
||||
}
|
||||
|
||||
rest := strings.TrimLeft(body, "0123456789abcdef")
|
||||
if len(rest) != 0 {
|
||||
return fmt.Errorf("found non-hex character in hash: %c", rest[0])
|
||||
}
|
||||
|
||||
var wantBytes int
|
||||
switch algo {
|
||||
case "sha256":
|
||||
wantBytes = crypto.SHA256.Size()
|
||||
default:
|
||||
hasher, err := Hasher(algo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
wantBytes = hasher.Size()
|
||||
}
|
||||
|
||||
// Compare the hex to the expected size (2 hex characters per byte)
|
||||
if len(body) != hex.EncodedLen(wantBytes) {
|
||||
return fmt.Errorf("wrong number of hex digits for %s: %s", algo, body)
|
||||
}
|
||||
|
||||
h.Algorithm = algo
|
||||
h.Hex = body
|
||||
return nil
|
||||
}
|
||||
|
||||
// SHA256 computes the Hash of the provided io.Reader's content.
|
||||
func SHA256(r io.Reader) (Hash, int64, error) {
|
||||
hasher := crypto.SHA256.New()
|
||||
n, err := io.Copy(hasher, r)
|
||||
if err != nil {
|
||||
return Hash{}, 0, err
|
||||
}
|
||||
return Hash{
|
||||
Algorithm: "sha256",
|
||||
Hex: hex.EncodeToString(hasher.Sum(make([]byte, 0, hasher.Size()))),
|
||||
}, n, nil
|
||||
}
|
||||
-59
@@ -1,59 +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 v1
|
||||
|
||||
import (
|
||||
"github.com/google/go-containerregistry/pkg/v1/types"
|
||||
)
|
||||
|
||||
// Image defines the interface for interacting with an OCI v1 image.
|
||||
type Image interface {
|
||||
// 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.
|
||||
Layers() ([]Layer, error)
|
||||
|
||||
// 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, also known as
|
||||
// the Image ID.
|
||||
ConfigName() (Hash, error)
|
||||
|
||||
// ConfigFile returns this image's config file.
|
||||
ConfigFile() (*ConfigFile, error)
|
||||
|
||||
// RawConfigFile returns the serialized bytes of ConfigFile().
|
||||
RawConfigFile() ([]byte, error)
|
||||
|
||||
// Digest returns the sha256 of this image's manifest.
|
||||
Digest() (Hash, error)
|
||||
|
||||
// Manifest returns this image's Manifest object.
|
||||
Manifest() (*Manifest, error)
|
||||
|
||||
// RawManifest returns the serialized bytes of Manifest()
|
||||
RawManifest() ([]byte, error)
|
||||
|
||||
// LayerByDigest returns a Layer for interacting with a particular layer of
|
||||
// the image, looking it up by "digest" (the compressed hash).
|
||||
LayerByDigest(Hash) (Layer, error)
|
||||
|
||||
// LayerByDiffID is an analog to LayerByDigest, looking up by "diff id"
|
||||
// (the uncompressed hash).
|
||||
LayerByDiffID(Hash) (Layer, error)
|
||||
}
|
||||
-43
@@ -1,43 +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 v1
|
||||
|
||||
import (
|
||||
"github.com/google/go-containerregistry/pkg/v1/types"
|
||||
)
|
||||
|
||||
// ImageIndex defines the interface for interacting with an OCI image index.
|
||||
type ImageIndex interface {
|
||||
// MediaType of this image's manifest.
|
||||
MediaType() (types.MediaType, error)
|
||||
|
||||
// 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)
|
||||
|
||||
// RawManifest returns the serialized bytes of IndexManifest().
|
||||
RawManifest() ([]byte, error)
|
||||
|
||||
// Image returns a v1.Image that this ImageIndex references.
|
||||
Image(Hash) (Image, error)
|
||||
|
||||
// ImageIndex returns a v1.ImageIndex that this ImageIndex references.
|
||||
ImageIndex(Hash) (ImageIndex, error)
|
||||
}
|
||||
-42
@@ -1,42 +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 v1
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/google/go-containerregistry/pkg/v1/types"
|
||||
)
|
||||
|
||||
// Layer is an interface for accessing the properties of a particular layer of a v1.Image
|
||||
type Layer interface {
|
||||
// Digest returns the Hash of the compressed layer.
|
||||
Digest() (Hash, error)
|
||||
|
||||
// DiffID returns the Hash of the uncompressed layer.
|
||||
DiffID() (Hash, error)
|
||||
|
||||
// Compressed returns an io.ReadCloser for the compressed layer contents.
|
||||
Compressed() (io.ReadCloser, error)
|
||||
|
||||
// Uncompressed returns an io.ReadCloser for the uncompressed layer contents.
|
||||
Uncompressed() (io.ReadCloser, error)
|
||||
|
||||
// Size returns the compressed size of the Layer.
|
||||
Size() (int64, error)
|
||||
|
||||
// MediaType returns the media type of the Layer.
|
||||
MediaType() (types.MediaType, error)
|
||||
}
|
||||
-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).
|
||||
-37
@@ -1,37 +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 layout
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
)
|
||||
|
||||
// Blob returns a blob with the given hash from the Path.
|
||||
func (l Path) Blob(h v1.Hash) (io.ReadCloser, error) {
|
||||
return os.Open(l.blobPath(h))
|
||||
}
|
||||
|
||||
// Bytes is a convenience function to return a blob from the Path as
|
||||
// a byte slice.
|
||||
func (l Path) Bytes(h v1.Hash) ([]byte, error) {
|
||||
return os.ReadFile(l.blobPath(h))
|
||||
}
|
||||
|
||||
func (l Path) blobPath(h v1.Hash) string {
|
||||
return l.path("blobs", h.Algorithm, h.Hex)
|
||||
}
|
||||
-19
@@ -1,19 +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 layout provides facilities for reading/writing artifacts from/to
|
||||
// an OCI image layout on disk, see:
|
||||
//
|
||||
// https://github.com/opencontainers/image-spec/blob/master/image-layout.md
|
||||
package layout
|
||||
-137
@@ -1,137 +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.
|
||||
|
||||
// This is an EXPERIMENTAL package, and may change in arbitrary ways without notice.
|
||||
package layout
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
)
|
||||
|
||||
// GarbageCollect removes unreferenced blobs from the oci-layout
|
||||
//
|
||||
// This is an experimental api, and not subject to any stability guarantees
|
||||
// We may abandon it at any time, without prior notice.
|
||||
// Deprecated: Use it at your own risk!
|
||||
func (l Path) GarbageCollect() ([]v1.Hash, error) {
|
||||
idx, err := l.ImageIndex()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
blobsToKeep := map[string]bool{}
|
||||
if err := l.garbageCollectImageIndex(idx, blobsToKeep); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
blobsDir := l.path("blobs")
|
||||
removedBlobs := []v1.Hash{}
|
||||
|
||||
err = filepath.WalkDir(blobsDir, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
rel, err := filepath.Rel(blobsDir, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hashString := strings.Replace(rel, "/", ":", 1)
|
||||
if present := blobsToKeep[hashString]; !present {
|
||||
h, err := v1.NewHash(hashString)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
removedBlobs = append(removedBlobs, h)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return removedBlobs, nil
|
||||
}
|
||||
|
||||
func (l Path) garbageCollectImageIndex(index v1.ImageIndex, blobsToKeep map[string]bool) error {
|
||||
idxm, err := index.IndexManifest()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
h, err := index.Digest()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
blobsToKeep[h.String()] = true
|
||||
|
||||
for _, descriptor := range idxm.Manifests {
|
||||
if descriptor.MediaType.IsImage() {
|
||||
img, err := index.Image(descriptor.Digest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := l.garbageCollectImage(img, blobsToKeep); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if descriptor.MediaType.IsIndex() {
|
||||
idx, err := index.ImageIndex(descriptor.Digest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := l.garbageCollectImageIndex(idx, blobsToKeep); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("gc: unknown media type: %s", descriptor.MediaType)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l Path) garbageCollectImage(image v1.Image, blobsToKeep map[string]bool) error {
|
||||
h, err := image.Digest()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
blobsToKeep[h.String()] = true
|
||||
|
||||
h, err = image.ConfigName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
blobsToKeep[h.String()] = true
|
||||
|
||||
ls, err := image.Layers()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, l := range ls {
|
||||
h, err := l.Digest()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
blobsToKeep[h.String()] = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
-139
@@ -1,139 +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 layout
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/partial"
|
||||
"github.com/google/go-containerregistry/pkg/v1/types"
|
||||
)
|
||||
|
||||
type layoutImage struct {
|
||||
path Path
|
||||
desc v1.Descriptor
|
||||
manifestLock sync.Mutex // Protects rawManifest
|
||||
rawManifest []byte
|
||||
}
|
||||
|
||||
var _ partial.CompressedImageCore = (*layoutImage)(nil)
|
||||
|
||||
// Image reads a v1.Image with digest h from the Path.
|
||||
func (l Path) Image(h v1.Hash) (v1.Image, error) {
|
||||
ii, err := l.ImageIndex()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ii.Image(h)
|
||||
}
|
||||
|
||||
func (li *layoutImage) MediaType() (types.MediaType, error) {
|
||||
return li.desc.MediaType, nil
|
||||
}
|
||||
|
||||
// Implements WithManifest for partial.Blobset.
|
||||
func (li *layoutImage) Manifest() (*v1.Manifest, error) {
|
||||
return partial.Manifest(li)
|
||||
}
|
||||
|
||||
func (li *layoutImage) RawManifest() ([]byte, error) {
|
||||
li.manifestLock.Lock()
|
||||
defer li.manifestLock.Unlock()
|
||||
if li.rawManifest != nil {
|
||||
return li.rawManifest, nil
|
||||
}
|
||||
|
||||
b, err := li.path.Bytes(li.desc.Digest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
li.rawManifest = b
|
||||
return li.rawManifest, nil
|
||||
}
|
||||
|
||||
func (li *layoutImage) RawConfigFile() ([]byte, error) {
|
||||
manifest, err := li.Manifest()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return li.path.Bytes(manifest.Config.Digest)
|
||||
}
|
||||
|
||||
func (li *layoutImage) LayerByDigest(h v1.Hash) (partial.CompressedLayer, error) {
|
||||
manifest, err := li.Manifest()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if h == manifest.Config.Digest {
|
||||
return &compressedBlob{
|
||||
path: li.path,
|
||||
desc: manifest.Config,
|
||||
}, nil
|
||||
}
|
||||
|
||||
for _, desc := range manifest.Layers {
|
||||
if h == desc.Digest {
|
||||
return &compressedBlob{
|
||||
path: li.path,
|
||||
desc: desc,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("could not find layer in image: %s", h)
|
||||
}
|
||||
|
||||
type compressedBlob struct {
|
||||
path Path
|
||||
desc v1.Descriptor
|
||||
}
|
||||
|
||||
func (b *compressedBlob) Digest() (v1.Hash, error) {
|
||||
return b.desc.Digest, nil
|
||||
}
|
||||
|
||||
func (b *compressedBlob) Compressed() (io.ReadCloser, error) {
|
||||
return b.path.Blob(b.desc.Digest)
|
||||
}
|
||||
|
||||
func (b *compressedBlob) Size() (int64, error) {
|
||||
return b.desc.Size, nil
|
||||
}
|
||||
|
||||
func (b *compressedBlob) MediaType() (types.MediaType, error) {
|
||||
return b.desc.MediaType, nil
|
||||
}
|
||||
|
||||
// Descriptor implements partial.withDescriptor.
|
||||
func (b *compressedBlob) Descriptor() (*v1.Descriptor, error) {
|
||||
return &b.desc, nil
|
||||
}
|
||||
|
||||
// See partial.Exists.
|
||||
func (b *compressedBlob) Exists() (bool, error) {
|
||||
_, err := os.Stat(b.path.blobPath(b.desc.Digest))
|
||||
if os.IsNotExist(err) {
|
||||
return false, nil
|
||||
}
|
||||
return err == nil, err
|
||||
}
|
||||
-161
@@ -1,161 +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 layout
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/partial"
|
||||
"github.com/google/go-containerregistry/pkg/v1/types"
|
||||
)
|
||||
|
||||
var _ v1.ImageIndex = (*layoutIndex)(nil)
|
||||
|
||||
type layoutIndex struct {
|
||||
mediaType types.MediaType
|
||||
path Path
|
||||
rawIndex []byte
|
||||
}
|
||||
|
||||
// ImageIndexFromPath is a convenience function which constructs a Path and returns its v1.ImageIndex.
|
||||
func ImageIndexFromPath(path string) (v1.ImageIndex, error) {
|
||||
lp, err := FromPath(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return lp.ImageIndex()
|
||||
}
|
||||
|
||||
// ImageIndex returns a v1.ImageIndex for the Path.
|
||||
func (l Path) ImageIndex() (v1.ImageIndex, error) {
|
||||
rawIndex, err := os.ReadFile(l.path("index.json"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
idx := &layoutIndex{
|
||||
mediaType: types.OCIImageIndex,
|
||||
path: l,
|
||||
rawIndex: rawIndex,
|
||||
}
|
||||
|
||||
return idx, nil
|
||||
}
|
||||
|
||||
func (i *layoutIndex) MediaType() (types.MediaType, error) {
|
||||
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)
|
||||
return &index, err
|
||||
}
|
||||
|
||||
func (i *layoutIndex) RawManifest() ([]byte, error) {
|
||||
return i.rawIndex, nil
|
||||
}
|
||||
|
||||
func (i *layoutIndex) Image(h v1.Hash) (v1.Image, error) {
|
||||
// Look up the digest in our manifest first to return a better error.
|
||||
desc, err := i.findDescriptor(h)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !isExpectedMediaType(desc.MediaType, types.OCIManifestSchema1, types.DockerManifestSchema2) {
|
||||
return nil, fmt.Errorf("unexpected media type for %v: %s", h, desc.MediaType)
|
||||
}
|
||||
|
||||
img := &layoutImage{
|
||||
path: i.path,
|
||||
desc: *desc,
|
||||
}
|
||||
return partial.CompressedToImage(img)
|
||||
}
|
||||
|
||||
func (i *layoutIndex) ImageIndex(h v1.Hash) (v1.ImageIndex, error) {
|
||||
// Look up the digest in our manifest first to return a better error.
|
||||
desc, err := i.findDescriptor(h)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !isExpectedMediaType(desc.MediaType, types.OCIImageIndex, types.DockerManifestList) {
|
||||
return nil, fmt.Errorf("unexpected media type for %v: %s", h, desc.MediaType)
|
||||
}
|
||||
|
||||
rawIndex, err := i.path.Bytes(h)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &layoutIndex{
|
||||
mediaType: desc.MediaType,
|
||||
path: i.path,
|
||||
rawIndex: rawIndex,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (i *layoutIndex) Blob(h v1.Hash) (io.ReadCloser, error) {
|
||||
return i.path.Blob(h)
|
||||
}
|
||||
|
||||
func (i *layoutIndex) findDescriptor(h v1.Hash) (*v1.Descriptor, error) {
|
||||
im, err := i.IndexManifest()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if h == (v1.Hash{}) {
|
||||
if len(im.Manifests) != 1 {
|
||||
return nil, errors.New("oci layout must contain only a single image to be used with layout.Image")
|
||||
}
|
||||
return &(im.Manifests)[0], nil
|
||||
}
|
||||
|
||||
for _, desc := range im.Manifests {
|
||||
if desc.Digest == h {
|
||||
return &desc, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("could not find descriptor in index: %s", h)
|
||||
}
|
||||
|
||||
// TODO: Pull this out into methods on types.MediaType? e.g. instead, have:
|
||||
// * mt.IsIndex()
|
||||
// * mt.IsImage()
|
||||
func isExpectedMediaType(mt types.MediaType, expected ...types.MediaType) bool {
|
||||
for _, allowed := range expected {
|
||||
if mt == allowed {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
// Copyright 2019 The original author or 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 layout
|
||||
|
||||
import "path/filepath"
|
||||
|
||||
// Path represents an OCI image layout rooted in a file system path
|
||||
type Path string
|
||||
|
||||
func (l Path) path(elem ...string) string {
|
||||
complete := []string{string(l)}
|
||||
return filepath.Join(append(complete, elem...)...)
|
||||
}
|
||||
-71
@@ -1,71 +0,0 @@
|
||||
// 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 layout
|
||||
|
||||
import v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
|
||||
// Option is a functional option for Layout.
|
||||
type Option func(*options)
|
||||
|
||||
type options struct {
|
||||
descOpts []descriptorOption
|
||||
}
|
||||
|
||||
func makeOptions(opts ...Option) *options {
|
||||
o := &options{
|
||||
descOpts: []descriptorOption{},
|
||||
}
|
||||
for _, apply := range opts {
|
||||
apply(o)
|
||||
}
|
||||
return o
|
||||
}
|
||||
|
||||
type descriptorOption func(*v1.Descriptor)
|
||||
|
||||
// WithAnnotations adds annotations to the artifact descriptor.
|
||||
func WithAnnotations(annotations map[string]string) Option {
|
||||
return func(o *options) {
|
||||
o.descOpts = append(o.descOpts, func(desc *v1.Descriptor) {
|
||||
if desc.Annotations == nil {
|
||||
desc.Annotations = make(map[string]string)
|
||||
}
|
||||
for k, v := range annotations {
|
||||
desc.Annotations[k] = v
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// WithURLs adds urls to the artifact descriptor.
|
||||
func WithURLs(urls []string) Option {
|
||||
return func(o *options) {
|
||||
o.descOpts = append(o.descOpts, func(desc *v1.Descriptor) {
|
||||
if desc.URLs == nil {
|
||||
desc.URLs = []string{}
|
||||
}
|
||||
desc.URLs = append(desc.URLs, urls...)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// WithPlatform sets the platform of the artifact descriptor.
|
||||
func WithPlatform(platform v1.Platform) Option {
|
||||
return func(o *options) {
|
||||
o.descOpts = append(o.descOpts, func(desc *v1.Descriptor) {
|
||||
desc.Platform = &platform
|
||||
})
|
||||
}
|
||||
}
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
// Copyright 2019 The original author or 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 layout
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// FromPath reads an OCI image layout at path and constructs a layout.Path.
|
||||
func FromPath(path string) (Path, error) {
|
||||
// TODO: check oci-layout exists
|
||||
|
||||
_, err := os.Stat(filepath.Join(path, "index.json"))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return Path(path), nil
|
||||
}
|
||||
-492
@@ -1,492 +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 layout
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync"
|
||||
|
||||
"github.com/google/go-containerregistry/pkg/logs"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/match"
|
||||
"github.com/google/go-containerregistry/pkg/v1/mutate"
|
||||
"github.com/google/go-containerregistry/pkg/v1/partial"
|
||||
"github.com/google/go-containerregistry/pkg/v1/stream"
|
||||
"github.com/google/go-containerregistry/pkg/v1/types"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
var layoutFile = `{
|
||||
"imageLayoutVersion": "1.0.0"
|
||||
}`
|
||||
|
||||
// renameMutex guards os.Rename calls in AppendImage on Windows only.
|
||||
var renameMutex sync.Mutex
|
||||
|
||||
// AppendImage writes a v1.Image to the Path and updates
|
||||
// the index.json to reference it.
|
||||
func (l Path) AppendImage(img v1.Image, options ...Option) error {
|
||||
if err := l.WriteImage(img); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
desc, err := partial.Descriptor(img)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o := makeOptions(options...)
|
||||
for _, opt := range o.descOpts {
|
||||
opt(desc)
|
||||
}
|
||||
|
||||
return l.AppendDescriptor(*desc)
|
||||
}
|
||||
|
||||
// AppendIndex writes a v1.ImageIndex to the Path and updates
|
||||
// the index.json to reference it.
|
||||
func (l Path) AppendIndex(ii v1.ImageIndex, options ...Option) error {
|
||||
if err := l.WriteIndex(ii); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
desc, err := partial.Descriptor(ii)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o := makeOptions(options...)
|
||||
for _, opt := range o.descOpts {
|
||||
opt(desc)
|
||||
}
|
||||
|
||||
return l.AppendDescriptor(*desc)
|
||||
}
|
||||
|
||||
// AppendDescriptor adds a descriptor to the index.json of the Path.
|
||||
func (l Path) AppendDescriptor(desc v1.Descriptor) error {
|
||||
ii, err := l.ImageIndex()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
index, err := ii.IndexManifest()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
index.Manifests = append(index.Manifests, desc)
|
||||
|
||||
rawIndex, err := json.MarshalIndent(index, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return l.WriteFile("index.json", rawIndex, os.ModePerm)
|
||||
}
|
||||
|
||||
// ReplaceImage writes a v1.Image to the Path and updates
|
||||
// the index.json to reference it, replacing any existing one that matches matcher, if found.
|
||||
func (l Path) ReplaceImage(img v1.Image, matcher match.Matcher, options ...Option) error {
|
||||
if err := l.WriteImage(img); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return l.replaceDescriptor(img, matcher, options...)
|
||||
}
|
||||
|
||||
// ReplaceIndex writes a v1.ImageIndex to the Path and updates
|
||||
// the index.json to reference it, replacing any existing one that matches matcher, if found.
|
||||
func (l Path) ReplaceIndex(ii v1.ImageIndex, matcher match.Matcher, options ...Option) error {
|
||||
if err := l.WriteIndex(ii); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return l.replaceDescriptor(ii, matcher, options...)
|
||||
}
|
||||
|
||||
// replaceDescriptor adds a descriptor to the index.json of the Path, replacing
|
||||
// any one matching matcher, if found.
|
||||
func (l Path) replaceDescriptor(appendable mutate.Appendable, matcher match.Matcher, options ...Option) error {
|
||||
ii, err := l.ImageIndex()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
desc, err := partial.Descriptor(appendable)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o := makeOptions(options...)
|
||||
for _, opt := range o.descOpts {
|
||||
opt(desc)
|
||||
}
|
||||
|
||||
add := mutate.IndexAddendum{
|
||||
Add: appendable,
|
||||
Descriptor: *desc,
|
||||
}
|
||||
ii = mutate.AppendManifests(mutate.RemoveManifests(ii, matcher), add)
|
||||
|
||||
index, err := ii.IndexManifest()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rawIndex, err := json.MarshalIndent(index, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return l.WriteFile("index.json", rawIndex, os.ModePerm)
|
||||
}
|
||||
|
||||
// RemoveDescriptors removes any descriptors that match the match.Matcher from the index.json of the Path.
|
||||
func (l Path) RemoveDescriptors(matcher match.Matcher) error {
|
||||
ii, err := l.ImageIndex()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ii = mutate.RemoveManifests(ii, matcher)
|
||||
|
||||
index, err := ii.IndexManifest()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rawIndex, err := json.MarshalIndent(index, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return l.WriteFile("index.json", rawIndex, os.ModePerm)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if err := os.MkdirAll(l.path(), os.ModePerm); err != nil && !os.IsExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(l.path(name), data, perm)
|
||||
}
|
||||
|
||||
// WriteBlob copies a file to the blobs/ directory in the Path from the given ReadCloser at
|
||||
// blobs/{hash.Algorithm}/{hash.Hex}.
|
||||
func (l Path) WriteBlob(hash v1.Hash, r io.ReadCloser) error {
|
||||
return l.writeBlob(hash, -1, r, nil)
|
||||
}
|
||||
|
||||
func (l Path) writeBlob(hash v1.Hash, size int64, rc io.ReadCloser, renamer func() (v1.Hash, error)) error {
|
||||
defer rc.Close()
|
||||
if hash.Hex == "" && renamer == nil {
|
||||
panic("writeBlob called an invalid hash and no renamer")
|
||||
}
|
||||
|
||||
dir := l.path("blobs", hash.Algorithm)
|
||||
if err := os.MkdirAll(dir, os.ModePerm); err != nil && !os.IsExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if blob already exists and is the correct size
|
||||
file := filepath.Join(dir, hash.Hex)
|
||||
if s, err := os.Stat(file); err == nil && !s.IsDir() && (s.Size() == size || size == -1) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// If a renamer func was provided write to a temporary file
|
||||
open := func() (*os.File, error) { return os.Create(file) }
|
||||
if renamer != nil {
|
||||
open = func() (*os.File, error) { return os.CreateTemp(dir, hash.Hex) }
|
||||
}
|
||||
w, err := open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if renamer != nil {
|
||||
// Delete temp file if an error is encountered before renaming
|
||||
defer func() {
|
||||
if err := os.Remove(w.Name()); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
logs.Warn.Printf("error removing temporary file after encountering an error while writing blob: %v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
defer w.Close()
|
||||
|
||||
// Write to file and exit if not renaming
|
||||
if n, err := io.Copy(w, rc); err != nil || renamer == nil {
|
||||
return err
|
||||
} else if size != -1 && n != size {
|
||||
return fmt.Errorf("expected blob size %d, but only wrote %d", size, n)
|
||||
}
|
||||
|
||||
// Always close reader before renaming, since Close computes the digest in
|
||||
// the case of streaming layers. If Close is not called explicitly, it will
|
||||
// occur in a goroutine that is not guaranteed to succeed before renamer is
|
||||
// called. When renamer is the layer's Digest method, it can return
|
||||
// ErrNotComputed.
|
||||
if err := rc.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Always close file before renaming
|
||||
if err := w.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Rename file based on the final hash
|
||||
finalHash, err := renamer()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting final digest of layer: %w", err)
|
||||
}
|
||||
|
||||
renamePath := l.path("blobs", finalHash.Algorithm, finalHash.Hex)
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
renameMutex.Lock()
|
||||
defer renameMutex.Unlock()
|
||||
}
|
||||
return os.Rename(w.Name(), renamePath)
|
||||
}
|
||||
|
||||
// writeLayer writes the compressed layer to a blob. Unlike WriteBlob it will
|
||||
// write to a temporary file (suffixed with .tmp) within the layout until the
|
||||
// compressed reader is fully consumed and written to disk. Also unlike
|
||||
// WriteBlob, it will not skip writing and exit without error when a blob file
|
||||
// exists, but does not have the correct size. (The blob hash is not
|
||||
// considered, because it may be expensive to compute.)
|
||||
func (l Path) writeLayer(layer v1.Layer) error {
|
||||
d, err := layer.Digest()
|
||||
if errors.Is(err, stream.ErrNotComputed) {
|
||||
// Allow digest errors, since streams may not have calculated the hash
|
||||
// yet. Instead, use an empty value, which will be transformed into a
|
||||
// random file name with `os.CreateTemp` and the final digest will be
|
||||
// calculated after writing to a temp file and before renaming to the
|
||||
// final path.
|
||||
d = v1.Hash{Algorithm: "sha256", Hex: ""}
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s, err := layer.Size()
|
||||
if errors.Is(err, stream.ErrNotComputed) {
|
||||
// Allow size errors, since streams may not have calculated the size
|
||||
// yet. Instead, use zero as a sentinel value meaning that no size
|
||||
// comparison can be done and any sized blob file should be considered
|
||||
// valid and not overwritten.
|
||||
//
|
||||
// TODO: Provide an option to always overwrite blobs.
|
||||
s = -1
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r, err := layer.Compressed()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := l.writeBlob(d, s, r, layer.Digest); err != nil {
|
||||
return fmt.Errorf("error writing layer: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveBlob removes a file from the blobs directory in the Path
|
||||
// at blobs/{hash.Algorithm}/{hash.Hex}
|
||||
// It does *not* remove any reference to it from other manifests or indexes, or
|
||||
// from the root index.json.
|
||||
func (l Path) RemoveBlob(hash v1.Hash) error {
|
||||
dir := l.path("blobs", hash.Algorithm)
|
||||
err := os.Remove(filepath.Join(dir, hash.Hex))
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteImage writes an image, including its manifest, config and all of its
|
||||
// layers, to the blobs directory. If any blob already exists, as determined by
|
||||
// the hash filename, does not write it.
|
||||
// This function does *not* update the `index.json` file. If you want to write the
|
||||
// image and also update the `index.json`, call AppendImage(), which wraps this
|
||||
// and also updates the `index.json`.
|
||||
func (l Path) WriteImage(img v1.Image) error {
|
||||
layers, err := img.Layers()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Write the layers concurrently.
|
||||
var g errgroup.Group
|
||||
for _, layer := range layers {
|
||||
layer := layer
|
||||
g.Go(func() error {
|
||||
return l.writeLayer(layer)
|
||||
})
|
||||
}
|
||||
if err := g.Wait(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Write the config.
|
||||
cfgName, err := img.ConfigName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfgBlob, err := img.RawConfigFile()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := l.WriteBlob(cfgName, io.NopCloser(bytes.NewReader(cfgBlob))); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Write the img manifest.
|
||||
d, err := img.Digest()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
manifest, err := img.RawManifest()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return l.WriteBlob(d, io.NopCloser(bytes.NewReader(manifest)))
|
||||
}
|
||||
|
||||
type withLayer interface {
|
||||
Layer(v1.Hash) (v1.Layer, error)
|
||||
}
|
||||
|
||||
type withBlob interface {
|
||||
Blob(v1.Hash) (io.ReadCloser, error)
|
||||
}
|
||||
|
||||
func (l Path) writeIndexToFile(indexFile string, ii v1.ImageIndex) error {
|
||||
index, err := ii.IndexManifest()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Walk the descriptors and write any v1.Image or v1.ImageIndex that we find.
|
||||
// If we come across something we don't expect, just write it as a blob.
|
||||
for _, desc := range index.Manifests {
|
||||
switch desc.MediaType {
|
||||
case types.OCIImageIndex, types.DockerManifestList:
|
||||
ii, err := ii.ImageIndex(desc.Digest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := l.WriteIndex(ii); err != nil {
|
||||
return err
|
||||
}
|
||||
case types.OCIManifestSchema1, types.DockerManifestSchema2:
|
||||
img, err := ii.Image(desc.Digest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := l.WriteImage(img); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
// TODO: The layout could reference arbitrary things, which we should
|
||||
// probably just pass through.
|
||||
|
||||
var blob io.ReadCloser
|
||||
// Workaround for #819.
|
||||
if wl, ok := ii.(withLayer); ok {
|
||||
layer, lerr := wl.Layer(desc.Digest)
|
||||
if lerr != nil {
|
||||
return lerr
|
||||
}
|
||||
blob, err = layer.Compressed()
|
||||
} else if wb, ok := ii.(withBlob); ok {
|
||||
blob, err = wb.Blob(desc.Digest)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := l.WriteBlob(desc.Digest, blob); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rawIndex, err := ii.RawManifest()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return l.WriteFile(indexFile, rawIndex, os.ModePerm)
|
||||
}
|
||||
|
||||
// WriteIndex writes an index to the blobs directory. Walks down the children,
|
||||
// including its children manifests and/or indexes, and down the tree until all of
|
||||
// config and all layers, have been written. If any blob already exists, as determined by
|
||||
// the hash filename, does not write it.
|
||||
// This function does *not* update the `index.json` file. If you want to write the
|
||||
// index and also update the `index.json`, call AppendIndex(), which wraps this
|
||||
// and also updates the `index.json`.
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
||||
h, err := ii.Digest()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
indexFile := filepath.Join("blobs", h.Algorithm, h.Hex)
|
||||
return l.writeIndexToFile(indexFile, ii)
|
||||
}
|
||||
|
||||
// Write constructs a Path at path from an ImageIndex.
|
||||
//
|
||||
// The contents are written in the following format:
|
||||
// At the top level, there is:
|
||||
//
|
||||
// One oci-layout file containing the version of this image-layout.
|
||||
// One index.json file listing descriptors for the contained images.
|
||||
//
|
||||
// Under blobs/, there is, for each image:
|
||||
//
|
||||
// One file for each layer, named after the layer's SHA.
|
||||
// One file for each config blob, named after its SHA.
|
||||
// One file for each manifest blob, named after its SHA.
|
||||
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 {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// TODO create blobs/ in case there is a blobs file which would prevent the directory from being created
|
||||
|
||||
return lp, lp.writeIndexToFile("index.json", ii)
|
||||
}
|
||||
-71
@@ -1,71 +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 v1
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
|
||||
"github.com/google/go-containerregistry/pkg/v1/types"
|
||||
)
|
||||
|
||||
// Manifest represents the OCI image manifest in a structured way.
|
||||
type Manifest struct {
|
||||
SchemaVersion int64 `json:"schemaVersion"`
|
||||
MediaType types.MediaType `json:"mediaType,omitempty"`
|
||||
Config Descriptor `json:"config"`
|
||||
Layers []Descriptor `json:"layers"`
|
||||
Annotations map[string]string `json:"annotations,omitempty"`
|
||||
Subject *Descriptor `json:"subject,omitempty"`
|
||||
}
|
||||
|
||||
// IndexManifest represents an OCI image index in a structured way.
|
||||
type IndexManifest struct {
|
||||
SchemaVersion int64 `json:"schemaVersion"`
|
||||
MediaType types.MediaType `json:"mediaType,omitempty"`
|
||||
Manifests []Descriptor `json:"manifests"`
|
||||
Annotations map[string]string `json:"annotations,omitempty"`
|
||||
Subject *Descriptor `json:"subject,omitempty"`
|
||||
}
|
||||
|
||||
// Descriptor holds a reference from the manifest to one of its constituent elements.
|
||||
type Descriptor struct {
|
||||
MediaType types.MediaType `json:"mediaType"`
|
||||
Size int64 `json:"size"`
|
||||
Digest Hash `json:"digest"`
|
||||
Data []byte `json:"data,omitempty"`
|
||||
URLs []string `json:"urls,omitempty"`
|
||||
Annotations map[string]string `json:"annotations,omitempty"`
|
||||
Platform *Platform `json:"platform,omitempty"`
|
||||
ArtifactType string `json:"artifactType,omitempty"`
|
||||
}
|
||||
|
||||
// ParseManifest parses the io.Reader's contents into a Manifest.
|
||||
func ParseManifest(r io.Reader) (*Manifest, error) {
|
||||
m := Manifest{}
|
||||
if err := json.NewDecoder(r).Decode(&m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &m, nil
|
||||
}
|
||||
|
||||
// ParseIndexManifest parses the io.Reader's contents into an IndexManifest.
|
||||
func ParseIndexManifest(r io.Reader) (*IndexManifest, error) {
|
||||
im := IndexManifest{}
|
||||
if err := json.NewDecoder(r).Decode(&im); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &im, nil
|
||||
}
|
||||
-92
@@ -1,92 +0,0 @@
|
||||
// Copyright 2020 Google LLC All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package match provides functionality for conveniently matching a v1.Descriptor.
|
||||
package match
|
||||
|
||||
import (
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
imagespec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
||||
|
||||
// Matcher function that is given a v1.Descriptor, and returns whether or
|
||||
// not it matches a given rule. Can match on anything it wants in the Descriptor.
|
||||
type Matcher func(desc v1.Descriptor) bool
|
||||
|
||||
// Name returns a match.Matcher that matches based on the value of the
|
||||
//
|
||||
// "org.opencontainers.image.ref.name" annotation:
|
||||
//
|
||||
// github.com/opencontainers/image-spec/blob/v1.0.1/annotations.md#pre-defined-annotation-keys
|
||||
func Name(name string) Matcher {
|
||||
return Annotation(imagespec.AnnotationRefName, name)
|
||||
}
|
||||
|
||||
// Annotation returns a match.Matcher that matches based on the provided annotation.
|
||||
func Annotation(key, value string) Matcher {
|
||||
return func(desc v1.Descriptor) bool {
|
||||
if desc.Annotations == nil {
|
||||
return false
|
||||
}
|
||||
if aValue, ok := desc.Annotations[key]; ok && aValue == value {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Platforms returns a match.Matcher that matches on any one of the provided platforms.
|
||||
// Ignores any descriptors that do not have a platform.
|
||||
func Platforms(platforms ...v1.Platform) Matcher {
|
||||
return func(desc v1.Descriptor) bool {
|
||||
if desc.Platform == nil {
|
||||
return false
|
||||
}
|
||||
for _, platform := range platforms {
|
||||
if desc.Platform.Equals(platform) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// MediaTypes returns a match.Matcher that matches at least one of the provided media types.
|
||||
func MediaTypes(mediaTypes ...string) Matcher {
|
||||
mts := map[string]bool{}
|
||||
for _, media := range mediaTypes {
|
||||
mts[media] = true
|
||||
}
|
||||
return func(desc v1.Descriptor) bool {
|
||||
if desc.MediaType == "" {
|
||||
return false
|
||||
}
|
||||
if _, ok := mts[string(desc.MediaType)]; ok {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Digests returns a match.Matcher that matches at least one of the provided Digests
|
||||
func Digests(digests ...v1.Hash) Matcher {
|
||||
digs := map[v1.Hash]bool{}
|
||||
for _, digest := range digests {
|
||||
digs[digest] = true
|
||||
}
|
||||
return func(desc v1.Descriptor) bool {
|
||||
_, ok := digs[desc.Digest]
|
||||
return ok
|
||||
}
|
||||
}
|
||||
-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/main/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/main/cmd/crane/doc/crane_export.md).
|
||||
-16
@@ -1,16 +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 mutate provides facilities for mutating v1.Images of any kind.
|
||||
package mutate
|
||||
-293
@@ -1,293 +0,0 @@
|
||||
// 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"
|
||||
"sync"
|
||||
|
||||
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
|
||||
annotations map[string]string
|
||||
mediaType *types.MediaType
|
||||
configMediaType *types.MediaType
|
||||
diffIDMap map[v1.Hash]v1.Layer
|
||||
digestMap map[v1.Hash]v1.Layer
|
||||
subject *v1.Descriptor
|
||||
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
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 {
|
||||
i.Lock()
|
||||
defer i.Unlock()
|
||||
|
||||
// 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 {
|
||||
history = append(history, add.History)
|
||||
if add.Layer != nil {
|
||||
diffID, err := add.Layer.DiffID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
diffIDs = append(diffIDs, diffID)
|
||||
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 {
|
||||
if add.Layer == nil {
|
||||
// Empty layers include only history in manifest.
|
||||
continue
|
||||
}
|
||||
|
||||
desc, err := partial.Descriptor(add.Layer)
|
||||
if 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 add.MediaType != "" {
|
||||
desc.MediaType = add.MediaType
|
||||
}
|
||||
|
||||
manifestLayers = append(manifestLayers, *desc)
|
||||
digestMap[desc.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
|
||||
|
||||
// If Data was set in the base image, we need to update it in the mutated image.
|
||||
if m.Config.Data != nil {
|
||||
manifest.Config.Data = rcfg
|
||||
}
|
||||
|
||||
// If the user wants to mutate the media type of the config
|
||||
if i.configMediaType != nil {
|
||||
manifest.Config.MediaType = *i.configMediaType
|
||||
}
|
||||
|
||||
if i.mediaType != nil {
|
||||
manifest.MediaType = *i.mediaType
|
||||
}
|
||||
|
||||
if i.annotations != nil {
|
||||
if manifest.Annotations == nil {
|
||||
manifest.Annotations = map[string]string{}
|
||||
}
|
||||
|
||||
for k, v := range i.annotations {
|
||||
manifest.Annotations[k] = v
|
||||
}
|
||||
}
|
||||
manifest.Subject = i.subject
|
||||
|
||||
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(); errors.Is(err, stream.ErrNotComputed) {
|
||||
// Image contains a streamable layer which has not yet been
|
||||
// consumed. Just return the layers we have in case the caller
|
||||
// is going to consume the layers.
|
||||
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.DeepCopy(), 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.DeepCopy(), 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 && !add.History.EmptyLayer {
|
||||
return errors.New("unable to add a nil layer to the image")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
-232
@@ -1,232 +0,0 @@
|
||||
// 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"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/google/go-containerregistry/pkg/logs"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/match"
|
||||
"github.com/google/go-containerregistry/pkg/v1/partial"
|
||||
"github.com/google/go-containerregistry/pkg/v1/stream"
|
||||
"github.com/google/go-containerregistry/pkg/v1/types"
|
||||
)
|
||||
|
||||
func computeDescriptor(ia IndexAddendum) (*v1.Descriptor, error) {
|
||||
desc, err := partial.Descriptor(ia.Add)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// The IndexAddendum allows overriding Descriptor values.
|
||||
if ia.Size != 0 {
|
||||
desc.Size = ia.Size
|
||||
}
|
||||
if string(ia.MediaType) != "" {
|
||||
desc.MediaType = ia.MediaType
|
||||
}
|
||||
if ia.Digest != (v1.Hash{}) {
|
||||
desc.Digest = ia.Digest
|
||||
}
|
||||
if ia.Platform != nil {
|
||||
desc.Platform = ia.Platform
|
||||
}
|
||||
if len(ia.URLs) != 0 {
|
||||
desc.URLs = ia.URLs
|
||||
}
|
||||
if len(ia.Annotations) != 0 {
|
||||
desc.Annotations = ia.Annotations
|
||||
}
|
||||
if ia.Data != nil {
|
||||
desc.Data = ia.Data
|
||||
}
|
||||
|
||||
return desc, nil
|
||||
}
|
||||
|
||||
type index struct {
|
||||
base v1.ImageIndex
|
||||
adds []IndexAddendum
|
||||
// remove is removed before adds
|
||||
remove match.Matcher
|
||||
|
||||
computed bool
|
||||
manifest *v1.IndexManifest
|
||||
annotations map[string]string
|
||||
mediaType *types.MediaType
|
||||
imageMap map[v1.Hash]v1.Image
|
||||
indexMap map[v1.Hash]v1.ImageIndex
|
||||
layerMap map[v1.Hash]v1.Layer
|
||||
subject *v1.Descriptor
|
||||
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
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 {
|
||||
i.Lock()
|
||||
defer i.Unlock()
|
||||
|
||||
// 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)
|
||||
i.layerMap = make(map[v1.Hash]v1.Layer)
|
||||
|
||||
m, err := i.base.IndexManifest()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
manifest := m.DeepCopy()
|
||||
manifests := manifest.Manifests
|
||||
|
||||
if i.remove != nil {
|
||||
var cleanedManifests []v1.Descriptor
|
||||
for _, m := range manifests {
|
||||
if !i.remove(m) {
|
||||
cleanedManifests = append(cleanedManifests, m)
|
||||
}
|
||||
}
|
||||
manifests = cleanedManifests
|
||||
}
|
||||
|
||||
for _, add := range i.adds {
|
||||
desc, err := computeDescriptor(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 if l, ok := add.Add.(v1.Layer); ok {
|
||||
i.layerMap[desc.Digest] = l
|
||||
} else {
|
||||
logs.Warn.Printf("Unexpected index addendum: %T", add.Add)
|
||||
}
|
||||
}
|
||||
|
||||
manifest.Manifests = manifests
|
||||
|
||||
if i.mediaType != nil {
|
||||
manifest.MediaType = *i.mediaType
|
||||
}
|
||||
|
||||
if i.annotations != nil {
|
||||
if manifest.Annotations == nil {
|
||||
manifest.Annotations = map[string]string{}
|
||||
}
|
||||
for k, v := range i.annotations {
|
||||
manifest.Annotations[k] = v
|
||||
}
|
||||
}
|
||||
manifest.Subject = i.subject
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
type withLayer interface {
|
||||
Layer(v1.Hash) (v1.Layer, error)
|
||||
}
|
||||
|
||||
// Workaround for #819.
|
||||
func (i *index) Layer(h v1.Hash) (v1.Layer, error) {
|
||||
if layer, ok := i.layerMap[h]; ok {
|
||||
return layer, nil
|
||||
}
|
||||
if wl, ok := i.base.(withLayer); ok {
|
||||
return wl.Layer(h)
|
||||
}
|
||||
return nil, fmt.Errorf("layer not found: %s", h)
|
||||
}
|
||||
|
||||
// Digest returns the sha256 of this image's manifest.
|
||||
func (i *index) Digest() (v1.Hash, error) {
|
||||
if err := i.compute(); err != nil {
|
||||
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.DeepCopy(), 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)
|
||||
}
|
||||
|
||||
func (i *index) Manifests() ([]partial.Describable, error) {
|
||||
if err := i.compute(); errors.Is(err, stream.ErrNotComputed) {
|
||||
// Index contains a streamable layer which has not yet been
|
||||
// consumed. Just return the manifests we have in case the caller
|
||||
// is going to consume the streamable layers.
|
||||
manifests, err := partial.Manifests(i.base)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, add := range i.adds {
|
||||
manifests = append(manifests, add.Add)
|
||||
}
|
||||
return manifests, nil
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return partial.ComputeManifests(i)
|
||||
}
|
||||
-546
@@ -1,546 +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 mutate
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"maps"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-containerregistry/internal/gzip"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/empty"
|
||||
"github.com/google/go-containerregistry/pkg/v1/match"
|
||||
"github.com/google/go-containerregistry/pkg/v1/partial"
|
||||
"github.com/google/go-containerregistry/pkg/v1/tarball"
|
||||
"github.com/google/go-containerregistry/pkg/v1/types"
|
||||
)
|
||||
|
||||
const whiteoutPrefix = ".wh."
|
||||
|
||||
// Addendum contains layers and history to be appended
|
||||
// to a base image
|
||||
type Addendum struct {
|
||||
Layer v1.Layer
|
||||
History v1.History
|
||||
URLs []string
|
||||
Annotations map[string]string
|
||||
MediaType types.MediaType
|
||||
}
|
||||
|
||||
// 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 {
|
||||
additions = append(additions, Addendum{Layer: layer})
|
||||
}
|
||||
|
||||
return Append(base, additions...)
|
||||
}
|
||||
|
||||
// Append will apply the list of addendums to the base image
|
||||
func Append(base v1.Image, adds ...Addendum) (v1.Image, error) {
|
||||
if len(adds) == 0 {
|
||||
return base, nil
|
||||
}
|
||||
if err := validate(adds); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &image{
|
||||
base: base,
|
||||
adds: adds,
|
||||
}, 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,
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveManifests removes any descriptors that match the match.Matcher.
|
||||
func RemoveManifests(base v1.ImageIndex, matcher match.Matcher) v1.ImageIndex {
|
||||
return &index{
|
||||
base: base,
|
||||
remove: matcher,
|
||||
}
|
||||
}
|
||||
|
||||
// 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()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cf.Config = cfg
|
||||
|
||||
return ConfigFile(base, cf)
|
||||
}
|
||||
|
||||
// Subject mutates the subject on an image or index manifest.
|
||||
//
|
||||
// The input is expected to be a v1.Image or v1.ImageIndex, and
|
||||
// returns the same type. You can type-assert the result like so:
|
||||
//
|
||||
// img := Subject(empty.Image, subj).(v1.Image)
|
||||
//
|
||||
// Or for an index:
|
||||
//
|
||||
// idx := Subject(empty.Index, subj).(v1.ImageIndex)
|
||||
//
|
||||
// If the input is not an Image or ImageIndex, the result will
|
||||
// attempt to lazily annotate the raw manifest.
|
||||
func Subject(f partial.WithRawManifest, subject v1.Descriptor) partial.WithRawManifest {
|
||||
if img, ok := f.(v1.Image); ok {
|
||||
return &image{
|
||||
base: img,
|
||||
subject: &subject,
|
||||
}
|
||||
}
|
||||
if idx, ok := f.(v1.ImageIndex); ok {
|
||||
return &index{
|
||||
base: idx,
|
||||
subject: &subject,
|
||||
}
|
||||
}
|
||||
return arbitraryRawManifest{a: f, subject: &subject}
|
||||
}
|
||||
|
||||
// Annotations mutates the annotations on an annotatable image or index manifest.
|
||||
//
|
||||
// The annotatable input is expected to be a v1.Image or v1.ImageIndex, and
|
||||
// returns the same type. You can type-assert the result like so:
|
||||
//
|
||||
// img := Annotations(empty.Image, map[string]string{
|
||||
// "foo": "bar",
|
||||
// }).(v1.Image)
|
||||
//
|
||||
// Or for an index:
|
||||
//
|
||||
// idx := Annotations(empty.Index, map[string]string{
|
||||
// "foo": "bar",
|
||||
// }).(v1.ImageIndex)
|
||||
//
|
||||
// If the input Annotatable is not an Image or ImageIndex, the result will
|
||||
// attempt to lazily annotate the raw manifest.
|
||||
func Annotations(f partial.WithRawManifest, anns map[string]string) partial.WithRawManifest {
|
||||
if img, ok := f.(v1.Image); ok {
|
||||
return &image{
|
||||
base: img,
|
||||
annotations: maps.Clone(anns),
|
||||
}
|
||||
}
|
||||
if idx, ok := f.(v1.ImageIndex); ok {
|
||||
return &index{
|
||||
base: idx,
|
||||
annotations: maps.Clone(anns),
|
||||
}
|
||||
}
|
||||
return arbitraryRawManifest{a: f, anns: maps.Clone(anns)}
|
||||
}
|
||||
|
||||
type arbitraryRawManifest struct {
|
||||
a partial.WithRawManifest
|
||||
anns map[string]string
|
||||
subject *v1.Descriptor
|
||||
}
|
||||
|
||||
func (a arbitraryRawManifest) RawManifest() ([]byte, error) {
|
||||
b, err := a.a.RawManifest()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var m map[string]any
|
||||
if err := json.Unmarshal(b, &m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ann, ok := m["annotations"]; ok {
|
||||
if annm, ok := ann.(map[string]string); ok {
|
||||
for k, v := range a.anns {
|
||||
annm[k] = v
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf(".annotations is not a map: %T", ann)
|
||||
}
|
||||
} else {
|
||||
m["annotations"] = a.anns
|
||||
}
|
||||
if a.subject != nil {
|
||||
m["subject"] = a.subject
|
||||
}
|
||||
return json.Marshal(m)
|
||||
}
|
||||
|
||||
// ConfigFile mutates the provided v1.Image to have the provided v1.ConfigFile
|
||||
func ConfigFile(base v1.Image, cfg *v1.ConfigFile) (v1.Image, error) {
|
||||
m, err := base.Manifest()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
image := &image{
|
||||
base: base,
|
||||
manifest: m.DeepCopy(),
|
||||
configFile: cfg,
|
||||
}
|
||||
|
||||
return image, nil
|
||||
}
|
||||
|
||||
// CreatedAt mutates the provided v1.Image to have the provided v1.Time
|
||||
func CreatedAt(base v1.Image, created v1.Time) (v1.Image, error) {
|
||||
cf, err := base.ConfigFile()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfg := cf.DeepCopy()
|
||||
cfg.Created = created
|
||||
|
||||
return ConfigFile(base, cfg)
|
||||
}
|
||||
|
||||
// Extract takes an image and returns an io.ReadCloser containing the image's
|
||||
// flattened filesystem.
|
||||
//
|
||||
// Callers can read the filesystem contents by passing the reader to
|
||||
// tar.NewReader, or io.Copy it directly to some output.
|
||||
//
|
||||
// If a caller doesn't read the full contents, they should Close it to free up
|
||||
// resources used during extraction.
|
||||
func Extract(img v1.Image) io.ReadCloser {
|
||||
pr, pw := io.Pipe()
|
||||
|
||||
go func() {
|
||||
// Close the writer with any errors encountered during
|
||||
// extraction. These errors will be returned by the reader end
|
||||
// on subsequent reads. If err == nil, the reader will return
|
||||
// EOF.
|
||||
pw.CloseWithError(extract(img, pw))
|
||||
}()
|
||||
|
||||
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()
|
||||
|
||||
fileMap := map[string]bool{}
|
||||
|
||||
layers, err := img.Layers()
|
||||
if err != nil {
|
||||
return fmt.Errorf("retrieving image layers: %w", err)
|
||||
}
|
||||
|
||||
// we iterate through the layers in reverse order because it makes handling
|
||||
// whiteout layers more efficient, since we can just keep track of the removed
|
||||
// files as we see .wh. layers and ignore those in previous layers.
|
||||
for i := len(layers) - 1; i >= 0; i-- {
|
||||
layer := layers[i]
|
||||
layerReader, err := layer.Uncompressed()
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading layer contents: %w", err)
|
||||
}
|
||||
defer layerReader.Close()
|
||||
tarReader := tar.NewReader(layerReader)
|
||||
for {
|
||||
header, err := tarReader.Next()
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading tar: %w", err)
|
||||
}
|
||||
|
||||
// Some tools prepend everything with "./", so if we don't Clean the
|
||||
// name, we may have duplicate entries, which angers tar-split.
|
||||
header.Name = filepath.Clean(header.Name)
|
||||
// force PAX format to remove Name/Linkname length limit of 100 characters
|
||||
// required by USTAR and to not depend on internal tar package guess which
|
||||
// prefers USTAR over PAX
|
||||
header.Format = tar.FormatPAX
|
||||
|
||||
basename := filepath.Base(header.Name)
|
||||
dirname := filepath.Dir(header.Name)
|
||||
tombstone := strings.HasPrefix(basename, whiteoutPrefix)
|
||||
if tombstone {
|
||||
basename = basename[len(whiteoutPrefix):]
|
||||
}
|
||||
|
||||
// check if we have seen value before
|
||||
// if we're checking a directory, don't filepath.Join names
|
||||
var name string
|
||||
if header.Typeflag == tar.TypeDir {
|
||||
name = header.Name
|
||||
} else {
|
||||
name = filepath.Join(dirname, basename)
|
||||
}
|
||||
|
||||
if _, ok := fileMap[name]; ok && !tombstone {
|
||||
continue
|
||||
}
|
||||
|
||||
// check for a whited out parent directory
|
||||
if inWhiteoutDir(fileMap, name) {
|
||||
continue
|
||||
}
|
||||
|
||||
// mark file as handled. non-directory implicitly tombstones
|
||||
// any entries with a matching (or child) name
|
||||
fileMap[name] = tombstone || (header.Typeflag != tar.TypeDir)
|
||||
if !tombstone {
|
||||
if err := tarWriter.WriteHeader(header); err != nil {
|
||||
return err
|
||||
}
|
||||
if header.Size > 0 {
|
||||
if _, err := io.CopyN(tarWriter, tarReader, header.Size); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func inWhiteoutDir(fileMap map[string]bool, file string) bool {
|
||||
for file != "" {
|
||||
dirname := filepath.Dir(file)
|
||||
if file == dirname {
|
||||
break
|
||||
}
|
||||
if val, ok := fileMap[dirname]; ok && val {
|
||||
return true
|
||||
}
|
||||
file = dirname
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Time sets all timestamps in an image to the given timestamp.
|
||||
func Time(img v1.Image, t time.Time) (v1.Image, error) {
|
||||
newImage := empty.Image
|
||||
|
||||
layers, err := img.Layers()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting image layers: %w", err)
|
||||
}
|
||||
|
||||
ocf, err := img.ConfigFile()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting original config file: %w", err)
|
||||
}
|
||||
|
||||
addendums := make([]Addendum, max(len(ocf.History), len(layers)))
|
||||
var historyIdx, addendumIdx int
|
||||
for layerIdx := 0; layerIdx < len(layers); addendumIdx, layerIdx = addendumIdx+1, layerIdx+1 {
|
||||
newLayer, err := layerTime(layers[layerIdx], t)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("setting layer times: %w", err)
|
||||
}
|
||||
|
||||
// try to search for the history entry that corresponds to this layer
|
||||
for ; historyIdx < len(ocf.History); historyIdx++ {
|
||||
addendums[addendumIdx].History = ocf.History[historyIdx]
|
||||
// if it's an EmptyLayer, do not set the Layer and have the Addendum with just the History
|
||||
// and move on to the next History entry
|
||||
if ocf.History[historyIdx].EmptyLayer {
|
||||
addendumIdx++
|
||||
continue
|
||||
}
|
||||
// otherwise, we can exit from the cycle
|
||||
historyIdx++
|
||||
break
|
||||
}
|
||||
if addendumIdx < len(addendums) {
|
||||
addendums[addendumIdx].Layer = newLayer
|
||||
}
|
||||
}
|
||||
|
||||
// add all leftover History entries
|
||||
for ; historyIdx < len(ocf.History); historyIdx, addendumIdx = historyIdx+1, addendumIdx+1 {
|
||||
addendums[addendumIdx].History = ocf.History[historyIdx]
|
||||
}
|
||||
|
||||
newImage, err = Append(newImage, addendums...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("appending layers: %w", err)
|
||||
}
|
||||
|
||||
cf, err := newImage.ConfigFile()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("setting config file: %w", err)
|
||||
}
|
||||
|
||||
cfg := cf.DeepCopy()
|
||||
|
||||
// Copy basic config over
|
||||
cfg.Architecture = ocf.Architecture
|
||||
cfg.OS = ocf.OS
|
||||
cfg.OSVersion = ocf.OSVersion
|
||||
cfg.Config = ocf.Config
|
||||
|
||||
// Strip away timestamps from the config file
|
||||
cfg.Created = v1.Time{Time: t}
|
||||
|
||||
for i, h := range cfg.History {
|
||||
h.Created = v1.Time{Time: t}
|
||||
h.CreatedBy = ocf.History[i].CreatedBy
|
||||
h.Comment = ocf.History[i].Comment
|
||||
h.EmptyLayer = ocf.History[i].EmptyLayer
|
||||
// Explicitly ignore Author field; which hinders reproducibility
|
||||
h.Author = ""
|
||||
cfg.History[i] = h
|
||||
}
|
||||
|
||||
return ConfigFile(newImage, cfg)
|
||||
}
|
||||
|
||||
func layerTime(layer v1.Layer, t time.Time) (v1.Layer, error) {
|
||||
layerReader, err := layer.Uncompressed()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting layer: %w", err)
|
||||
}
|
||||
defer layerReader.Close()
|
||||
w := new(bytes.Buffer)
|
||||
tarWriter := tar.NewWriter(w)
|
||||
defer tarWriter.Close()
|
||||
|
||||
tarReader := tar.NewReader(layerReader)
|
||||
for {
|
||||
header, err := tarReader.Next()
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading layer: %w", err)
|
||||
}
|
||||
|
||||
header.ModTime = t
|
||||
|
||||
//PAX and GNU Format support additional timestamps in the header
|
||||
if header.Format == tar.FormatPAX || header.Format == tar.FormatGNU {
|
||||
header.AccessTime = t
|
||||
header.ChangeTime = t
|
||||
}
|
||||
|
||||
if err := tarWriter.WriteHeader(header); err != nil {
|
||||
return nil, fmt.Errorf("writing tar header: %w", err)
|
||||
}
|
||||
|
||||
if header.Typeflag == tar.TypeReg {
|
||||
// TODO(#1168): This should be lazy, and not buffer the entire layer contents.
|
||||
if _, err = io.CopyN(tarWriter, tarReader, header.Size); err != nil {
|
||||
return nil, fmt.Errorf("writing layer file: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := tarWriter.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
b := w.Bytes()
|
||||
// gzip the contents, then create the layer
|
||||
opener := func() (io.ReadCloser, error) {
|
||||
return gzip.ReadCloser(io.NopCloser(bytes.NewReader(b))), nil
|
||||
}
|
||||
layer, err = tarball.LayerFromOpener(opener)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating layer: %w", err)
|
||||
}
|
||||
|
||||
return layer, nil
|
||||
}
|
||||
|
||||
// Canonical is a helper function to combine Time and configFile
|
||||
// to remove any randomness during a docker build.
|
||||
func Canonical(img v1.Image) (v1.Image, error) {
|
||||
// Set all timestamps to 0
|
||||
created := time.Time{}
|
||||
img, err := Time(img, created)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cf, err := img.ConfigFile()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get rid of host-dependent random config
|
||||
cfg := cf.DeepCopy()
|
||||
|
||||
cfg.Container = ""
|
||||
cfg.Config.Hostname = ""
|
||||
cfg.DockerVersion = "" //nolint:staticcheck // Field will be removed in next release
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
// ConfigMediaType modifies the MediaType() of the given image's Config.
|
||||
//
|
||||
// If !mt.IsConfig(), this will be the image's artifactType in any indexes it's a part of.
|
||||
func ConfigMediaType(img v1.Image, mt types.MediaType) v1.Image {
|
||||
return &image{
|
||||
base: img,
|
||||
configMediaType: &mt,
|
||||
}
|
||||
}
|
||||
|
||||
// IndexMediaType modifies the MediaType() of the given index.
|
||||
func IndexMediaType(idx v1.ImageIndex, mt types.MediaType) v1.ImageIndex {
|
||||
return &index{
|
||||
base: idx,
|
||||
mediaType: &mt,
|
||||
}
|
||||
}
|
||||
-144
@@ -1,144 +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 mutate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/empty"
|
||||
)
|
||||
|
||||
// Rebase returns a new v1.Image where the oldBase in orig is replaced by newBase.
|
||||
func Rebase(orig, oldBase, newBase v1.Image) (v1.Image, error) {
|
||||
// Verify that oldBase's layers are present in orig, otherwise orig is
|
||||
// not based on oldBase at all.
|
||||
origLayers, err := orig.Layers()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get layers for original: %w", err)
|
||||
}
|
||||
oldBaseLayers, err := oldBase.Layers()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(oldBaseLayers) > len(origLayers) {
|
||||
return nil, fmt.Errorf("image %q is not based on %q (too few layers)", orig, oldBase)
|
||||
}
|
||||
for i, l := range oldBaseLayers {
|
||||
oldLayerDigest, err := l.Digest()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get digest of layer %d of %q: %w", i, oldBase, err)
|
||||
}
|
||||
origLayerDigest, err := origLayers[i].Digest()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get digest of layer %d of %q: %w", i, orig, err)
|
||||
}
|
||||
if oldLayerDigest != origLayerDigest {
|
||||
return nil, fmt.Errorf("image %q is not based on %q (layer %d mismatch)", orig, oldBase, i)
|
||||
}
|
||||
}
|
||||
|
||||
oldConfig, err := oldBase.ConfigFile()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get config for old base: %w", err)
|
||||
}
|
||||
|
||||
origConfig, err := orig.ConfigFile()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get config for original: %w", err)
|
||||
}
|
||||
|
||||
newConfig, err := newBase.ConfigFile()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not get config for new base: %w", err)
|
||||
}
|
||||
|
||||
// Stitch together an image that contains:
|
||||
// - original image's config
|
||||
// - new base image's os/arch properties
|
||||
// - new base image's layers + top of original image's layers
|
||||
// - new base image's history + top of original image's history
|
||||
rebasedImage, err := Config(empty.Image, *origConfig.Config.DeepCopy())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create empty image with original config: %w", err)
|
||||
}
|
||||
|
||||
// Add new config properties from existing images.
|
||||
rebasedConfig, err := rebasedImage.ConfigFile()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not get config for rebased image: %w", err)
|
||||
}
|
||||
// OS/Arch properties from new base
|
||||
rebasedConfig.Architecture = newConfig.Architecture
|
||||
rebasedConfig.OS = newConfig.OS
|
||||
rebasedConfig.OSVersion = newConfig.OSVersion
|
||||
|
||||
// Apply config properties to rebased.
|
||||
rebasedImage, err = ConfigFile(rebasedImage, rebasedConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to replace config for rebased image: %w", err)
|
||||
}
|
||||
|
||||
// Get new base layers and config for history.
|
||||
newBaseLayers, err := newBase.Layers()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not get new base layers for new base: %w", err)
|
||||
}
|
||||
// Add new base layers.
|
||||
rebasedImage, err = Append(rebasedImage, createAddendums(0, 0, newConfig.History, newBaseLayers)...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to append new base image: %w", err)
|
||||
}
|
||||
|
||||
// Add original layers above the old base.
|
||||
rebasedImage, err = Append(rebasedImage, createAddendums(len(oldConfig.History), len(oldBaseLayers)+1, origConfig.History, origLayers)...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to append original image: %w", err)
|
||||
}
|
||||
|
||||
return rebasedImage, nil
|
||||
}
|
||||
|
||||
// createAddendums makes a list of addendums from a history and layers starting from a specific history and layer
|
||||
// indexes.
|
||||
func createAddendums(startHistory, startLayer int, history []v1.History, layers []v1.Layer) []Addendum {
|
||||
var adds []Addendum
|
||||
// History should be a superset of layers; empty layers (e.g. ENV statements) only exist in history.
|
||||
// They cannot be iterated identically but must be walked independently, only advancing the iterator for layers
|
||||
// when a history entry for a non-empty layer is seen.
|
||||
layerIndex := 0
|
||||
for historyIndex := range history {
|
||||
var layer v1.Layer
|
||||
emptyLayer := history[historyIndex].EmptyLayer
|
||||
if !emptyLayer {
|
||||
layer = layers[layerIndex]
|
||||
layerIndex++
|
||||
}
|
||||
if historyIndex >= startHistory || layerIndex >= startLayer {
|
||||
adds = append(adds, Addendum{
|
||||
Layer: layer,
|
||||
History: history[historyIndex],
|
||||
})
|
||||
}
|
||||
}
|
||||
// In the event history was malformed or non-existent, append the remaining layers.
|
||||
for i := layerIndex; i < len(layers); i++ {
|
||||
if i >= startLayer {
|
||||
adds = append(adds, Addendum{Layer: layers[layerIndex]})
|
||||
}
|
||||
}
|
||||
|
||||
return adds
|
||||
}
|
||||
-82
@@ -1,82 +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 UncompressedImageCore 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).
|
||||
|
||||
### [`partial.Exists`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/partial#Exists)
|
||||
|
||||
We generally don't care about the existence of something as granular as a
|
||||
layer, and would rather ensure all the invariants of an image are upheld via
|
||||
the `validate` package. However, there are situations where we want to do a
|
||||
quick smoke test to ensure that the underlying storage engine hasn't been
|
||||
corrupted by something e.g. deleting files or blobs. Thus, we've exposed an
|
||||
optional `Exists` method that does an existence check without actually reading
|
||||
any bytes.
|
||||
|
||||
The `remote` package implements this via `HEAD` requests.
|
||||
|
||||
The `layout` package implements this via `os.Stat`.
|
||||
|
||||
See [`#838`](https://github.com/google/go-containerregistry/pull/838).
|
||||
-188
@@ -1,188 +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 partial
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/google/go-containerregistry/internal/and"
|
||||
"github.com/google/go-containerregistry/internal/compression"
|
||||
"github.com/google/go-containerregistry/internal/gzip"
|
||||
"github.com/google/go-containerregistry/internal/zstd"
|
||||
comp "github.com/google/go-containerregistry/pkg/compression"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/types"
|
||||
)
|
||||
|
||||
// CompressedLayer represents the bare minimum interface a natively
|
||||
// compressed layer must implement for us to produce a v1.Layer
|
||||
type CompressedLayer interface {
|
||||
// Digest returns the Hash of the compressed layer.
|
||||
Digest() (v1.Hash, error)
|
||||
|
||||
// Compressed returns an io.ReadCloser for the compressed layer contents.
|
||||
Compressed() (io.ReadCloser, error)
|
||||
|
||||
// Size returns the compressed size of the Layer.
|
||||
Size() (int64, error)
|
||||
|
||||
// Returns the mediaType for the compressed Layer
|
||||
MediaType() (types.MediaType, error)
|
||||
}
|
||||
|
||||
// compressedLayerExtender implements v1.Image using the compressed base properties.
|
||||
type compressedLayerExtender struct {
|
||||
CompressedLayer
|
||||
}
|
||||
|
||||
// Uncompressed implements v1.Layer
|
||||
func (cle *compressedLayerExtender) Uncompressed() (io.ReadCloser, error) {
|
||||
rc, err := cle.Compressed()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Often, the "compressed" bytes are not actually-compressed.
|
||||
// Peek at the first two bytes to determine whether it's correct to
|
||||
// wrap this with gzip.UnzipReadCloser or zstd.UnzipReadCloser.
|
||||
cp, pr, err := compression.PeekCompression(rc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
prc := &and.ReadCloser{
|
||||
Reader: pr,
|
||||
CloseFunc: rc.Close,
|
||||
}
|
||||
|
||||
switch cp {
|
||||
case comp.GZip:
|
||||
return gzip.UnzipReadCloser(prc)
|
||||
case comp.ZStd:
|
||||
return zstd.UnzipReadCloser(prc)
|
||||
default:
|
||||
return prc, nil
|
||||
}
|
||||
}
|
||||
|
||||
// DiffID implements v1.Layer
|
||||
func (cle *compressedLayerExtender) DiffID() (v1.Hash, error) {
|
||||
// If our nested CompressedLayer implements DiffID,
|
||||
// then delegate to it instead.
|
||||
if wdi, ok := cle.CompressedLayer.(WithDiffID); ok {
|
||||
return wdi.DiffID()
|
||||
}
|
||||
r, err := cle.Uncompressed()
|
||||
if err != nil {
|
||||
return v1.Hash{}, err
|
||||
}
|
||||
defer r.Close()
|
||||
h, _, err := v1.SHA256(r)
|
||||
return h, err
|
||||
}
|
||||
|
||||
// CompressedToLayer fills in the missing methods from a CompressedLayer so that it implements v1.Layer
|
||||
func CompressedToLayer(ul CompressedLayer) (v1.Layer, error) {
|
||||
return &compressedLayerExtender{ul}, nil
|
||||
}
|
||||
|
||||
// CompressedImageCore represents the base minimum interface a natively
|
||||
// compressed image must implement for us to produce a v1.Image.
|
||||
type CompressedImageCore interface {
|
||||
ImageCore
|
||||
|
||||
// RawManifest returns the serialized bytes of the manifest.
|
||||
RawManifest() ([]byte, error)
|
||||
|
||||
// LayerByDigest is a variation on the v1.Image method, which returns
|
||||
// a CompressedLayer instead.
|
||||
LayerByDigest(v1.Hash) (CompressedLayer, error)
|
||||
}
|
||||
|
||||
// compressedImageExtender implements v1.Image by extending CompressedImageCore with the
|
||||
// appropriate methods computed from the minimal core.
|
||||
type compressedImageExtender struct {
|
||||
CompressedImageCore
|
||||
}
|
||||
|
||||
// Assert that our extender type completes the v1.Image interface
|
||||
var _ v1.Image = (*compressedImageExtender)(nil)
|
||||
|
||||
// Digest implements v1.Image
|
||||
func (i *compressedImageExtender) Digest() (v1.Hash, error) {
|
||||
return Digest(i)
|
||||
}
|
||||
|
||||
// ConfigName implements v1.Image
|
||||
func (i *compressedImageExtender) ConfigName() (v1.Hash, error) {
|
||||
return ConfigName(i)
|
||||
}
|
||||
|
||||
// Layers implements v1.Image
|
||||
func (i *compressedImageExtender) Layers() ([]v1.Layer, error) {
|
||||
hs, err := FSLayers(i)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ls := make([]v1.Layer, 0, len(hs))
|
||||
for _, h := range hs {
|
||||
l, err := i.LayerByDigest(h)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ls = append(ls, l)
|
||||
}
|
||||
return ls, nil
|
||||
}
|
||||
|
||||
// LayerByDigest implements v1.Image
|
||||
func (i *compressedImageExtender) LayerByDigest(h v1.Hash) (v1.Layer, error) {
|
||||
cl, err := i.CompressedImageCore.LayerByDigest(h)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return CompressedToLayer(cl)
|
||||
}
|
||||
|
||||
// LayerByDiffID implements v1.Image
|
||||
func (i *compressedImageExtender) LayerByDiffID(h v1.Hash) (v1.Layer, error) {
|
||||
h, err := DiffIDToBlob(i, h)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return i.LayerByDigest(h)
|
||||
}
|
||||
|
||||
// ConfigFile implements v1.Image
|
||||
func (i *compressedImageExtender) ConfigFile() (*v1.ConfigFile, error) {
|
||||
return ConfigFile(i)
|
||||
}
|
||||
|
||||
// Manifest implements v1.Image
|
||||
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{
|
||||
CompressedImageCore: cic,
|
||||
}, nil
|
||||
}
|
||||
-17
@@ -1,17 +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 partial defines methods for building up a v1.Image from
|
||||
// minimal subsets that are sufficient for defining a v1.Image.
|
||||
package partial
|
||||
-28
@@ -1,28 +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 partial
|
||||
|
||||
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 {
|
||||
// RawConfigFile returns the serialized bytes of this image's config file.
|
||||
RawConfigFile() ([]byte, error)
|
||||
|
||||
// MediaType of this image's manifest.
|
||||
MediaType() (types.MediaType, error)
|
||||
}
|
||||
-165
@@ -1,165 +0,0 @@
|
||||
// Copyright 2020 Google LLC All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package partial
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/match"
|
||||
"github.com/google/go-containerregistry/pkg/v1/types"
|
||||
)
|
||||
|
||||
// FindManifests given a v1.ImageIndex, find the manifests that fit the matcher.
|
||||
func FindManifests(index v1.ImageIndex, matcher match.Matcher) ([]v1.Descriptor, error) {
|
||||
// get the actual manifest list
|
||||
indexManifest, err := index.IndexManifest()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get raw index: %w", err)
|
||||
}
|
||||
manifests := []v1.Descriptor{}
|
||||
// try to get the root of our image
|
||||
for _, manifest := range indexManifest.Manifests {
|
||||
if matcher(manifest) {
|
||||
manifests = append(manifests, manifest)
|
||||
}
|
||||
}
|
||||
return manifests, nil
|
||||
}
|
||||
|
||||
// FindImages given a v1.ImageIndex, find the images that fit the matcher. If a Descriptor
|
||||
// matches the provider Matcher, but the referenced item is not an Image, ignores it.
|
||||
// Only returns those that match the Matcher and are images.
|
||||
func FindImages(index v1.ImageIndex, matcher match.Matcher) ([]v1.Image, error) {
|
||||
matches := []v1.Image{}
|
||||
manifests, err := FindManifests(index, matcher)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, desc := range manifests {
|
||||
// if it is not an image, ignore it
|
||||
if !desc.MediaType.IsImage() {
|
||||
continue
|
||||
}
|
||||
img, err := index.Image(desc.Digest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
matches = append(matches, img)
|
||||
}
|
||||
return matches, nil
|
||||
}
|
||||
|
||||
// FindIndexes given a v1.ImageIndex, find the indexes that fit the matcher. If a Descriptor
|
||||
// matches the provider Matcher, but the referenced item is not an Index, ignores it.
|
||||
// Only returns those that match the Matcher and are indexes.
|
||||
func FindIndexes(index v1.ImageIndex, matcher match.Matcher) ([]v1.ImageIndex, error) {
|
||||
matches := []v1.ImageIndex{}
|
||||
manifests, err := FindManifests(index, matcher)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, desc := range manifests {
|
||||
if !desc.MediaType.IsIndex() {
|
||||
continue
|
||||
}
|
||||
// if it is not an index, ignore it
|
||||
idx, err := index.ImageIndex(desc.Digest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
matches = append(matches, idx)
|
||||
}
|
||||
return matches, nil
|
||||
}
|
||||
|
||||
type withManifests interface {
|
||||
Manifests() ([]Describable, error)
|
||||
}
|
||||
|
||||
type withLayer interface {
|
||||
Layer(v1.Hash) (v1.Layer, error)
|
||||
}
|
||||
|
||||
type describable struct {
|
||||
desc v1.Descriptor
|
||||
}
|
||||
|
||||
func (d describable) Digest() (v1.Hash, error) {
|
||||
return d.desc.Digest, nil
|
||||
}
|
||||
|
||||
func (d describable) Size() (int64, error) {
|
||||
return d.desc.Size, nil
|
||||
}
|
||||
|
||||
func (d describable) MediaType() (types.MediaType, error) {
|
||||
return d.desc.MediaType, nil
|
||||
}
|
||||
|
||||
func (d describable) Descriptor() (*v1.Descriptor, error) {
|
||||
return &d.desc, nil
|
||||
}
|
||||
|
||||
// Manifests is analogous to v1.Image.Layers in that it allows values in the
|
||||
// returned list to be lazily evaluated, which enables an index to contain
|
||||
// an image that contains a streaming layer.
|
||||
//
|
||||
// This should have been part of the v1.ImageIndex interface, but wasn't.
|
||||
// It is instead usable through this extension interface.
|
||||
func Manifests(idx v1.ImageIndex) ([]Describable, error) {
|
||||
if wm, ok := idx.(withManifests); ok {
|
||||
return wm.Manifests()
|
||||
}
|
||||
|
||||
return ComputeManifests(idx)
|
||||
}
|
||||
|
||||
// ComputeManifests provides a fallback implementation for Manifests.
|
||||
func ComputeManifests(idx v1.ImageIndex) ([]Describable, error) {
|
||||
m, err := idx.IndexManifest()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
manifests := []Describable{}
|
||||
for _, desc := range m.Manifests {
|
||||
switch {
|
||||
case desc.MediaType.IsImage():
|
||||
img, err := idx.Image(desc.Digest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
manifests = append(manifests, img)
|
||||
case desc.MediaType.IsIndex():
|
||||
idx, err := idx.ImageIndex(desc.Digest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
manifests = append(manifests, idx)
|
||||
default:
|
||||
if wl, ok := idx.(withLayer); ok {
|
||||
layer, err := wl.Layer(desc.Digest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
manifests = append(manifests, layer)
|
||||
} else {
|
||||
manifests = append(manifests, describable{desc})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return manifests, nil
|
||||
}
|
||||
-223
@@ -1,223 +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 partial
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
"github.com/google/go-containerregistry/internal/gzip"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/types"
|
||||
)
|
||||
|
||||
// UncompressedLayer represents the bare minimum interface a natively
|
||||
// uncompressed layer must implement for us to produce a v1.Layer
|
||||
type UncompressedLayer interface {
|
||||
// DiffID returns the Hash of the uncompressed layer.
|
||||
DiffID() (v1.Hash, error)
|
||||
|
||||
// Uncompressed returns an io.ReadCloser for the uncompressed layer contents.
|
||||
Uncompressed() (io.ReadCloser, error)
|
||||
|
||||
// Returns the mediaType for the compressed Layer
|
||||
MediaType() (types.MediaType, error)
|
||||
}
|
||||
|
||||
// uncompressedLayerExtender implements v1.Image using the uncompressed base properties.
|
||||
type uncompressedLayerExtender struct {
|
||||
UncompressedLayer
|
||||
// Memoize size/hash so that the methods aren't twice as
|
||||
// expensive as doing this manually.
|
||||
hash v1.Hash
|
||||
size int64
|
||||
hashSizeError error
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
// Compressed implements v1.Layer
|
||||
func (ule *uncompressedLayerExtender) Compressed() (io.ReadCloser, error) {
|
||||
u, err := ule.Uncompressed()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return gzip.ReadCloser(u), nil
|
||||
}
|
||||
|
||||
// Digest implements v1.Layer
|
||||
func (ule *uncompressedLayerExtender) Digest() (v1.Hash, error) {
|
||||
ule.calcSizeHash()
|
||||
return ule.hash, ule.hashSizeError
|
||||
}
|
||||
|
||||
// Size implements v1.Layer
|
||||
func (ule *uncompressedLayerExtender) Size() (int64, error) {
|
||||
ule.calcSizeHash()
|
||||
return ule.size, ule.hashSizeError
|
||||
}
|
||||
|
||||
func (ule *uncompressedLayerExtender) calcSizeHash() {
|
||||
ule.once.Do(func() {
|
||||
var r io.ReadCloser
|
||||
r, ule.hashSizeError = ule.Compressed()
|
||||
if ule.hashSizeError != nil {
|
||||
return
|
||||
}
|
||||
defer r.Close()
|
||||
ule.hash, ule.size, ule.hashSizeError = v1.SHA256(r)
|
||||
})
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// UncompressedImageCore represents the bare minimum interface a natively
|
||||
// uncompressed image must implement for us to produce a v1.Image
|
||||
type UncompressedImageCore interface {
|
||||
ImageCore
|
||||
|
||||
// LayerByDiffID is a variation on the v1.Image method, which returns
|
||||
// an UncompressedLayer instead.
|
||||
LayerByDiffID(v1.Hash) (UncompressedLayer, error)
|
||||
}
|
||||
|
||||
// UncompressedToImage fills in the missing methods from an UncompressedImageCore so that it implements v1.Image.
|
||||
func UncompressedToImage(uic UncompressedImageCore) (v1.Image, error) {
|
||||
return &uncompressedImageExtender{
|
||||
UncompressedImageCore: uic,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// uncompressedImageExtender implements v1.Image by extending UncompressedImageCore with the
|
||||
// appropriate methods computed from the minimal core.
|
||||
type uncompressedImageExtender struct {
|
||||
UncompressedImageCore
|
||||
|
||||
lock sync.Mutex
|
||||
manifest *v1.Manifest
|
||||
}
|
||||
|
||||
// Assert that our extender type completes the v1.Image interface
|
||||
var _ v1.Image = (*uncompressedImageExtender)(nil)
|
||||
|
||||
// Digest implements v1.Image
|
||||
func (i *uncompressedImageExtender) Digest() (v1.Hash, error) {
|
||||
return Digest(i)
|
||||
}
|
||||
|
||||
// Manifest implements v1.Image
|
||||
func (i *uncompressedImageExtender) Manifest() (*v1.Manifest, error) {
|
||||
i.lock.Lock()
|
||||
defer i.lock.Unlock()
|
||||
if i.manifest != nil {
|
||||
return i.manifest, nil
|
||||
}
|
||||
|
||||
b, err := i.RawConfigFile()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfgHash, cfgSize, err := v1.SHA256(bytes.NewReader(b))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m := &v1.Manifest{
|
||||
SchemaVersion: 2,
|
||||
MediaType: types.DockerManifestSchema2,
|
||||
Config: v1.Descriptor{
|
||||
MediaType: types.DockerConfigJSON,
|
||||
Size: cfgSize,
|
||||
Digest: cfgHash,
|
||||
},
|
||||
}
|
||||
|
||||
ls, err := i.Layers()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m.Layers = make([]v1.Descriptor, len(ls))
|
||||
for i, l := range ls {
|
||||
desc, err := Descriptor(l)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m.Layers[i] = *desc
|
||||
}
|
||||
|
||||
i.manifest = m
|
||||
return i.manifest, nil
|
||||
}
|
||||
|
||||
// RawManifest implements v1.Image
|
||||
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)
|
||||
}
|
||||
|
||||
// ConfigFile implements v1.Image
|
||||
func (i *uncompressedImageExtender) ConfigFile() (*v1.ConfigFile, error) {
|
||||
return ConfigFile(i)
|
||||
}
|
||||
|
||||
// Layers implements v1.Image
|
||||
func (i *uncompressedImageExtender) Layers() ([]v1.Layer, error) {
|
||||
diffIDs, err := 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
|
||||
}
|
||||
|
||||
// LayerByDiffID implements v1.Image
|
||||
func (i *uncompressedImageExtender) LayerByDiffID(diffID v1.Hash) (v1.Layer, error) {
|
||||
ul, err := i.UncompressedImageCore.LayerByDiffID(diffID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return UncompressedToLayer(ul)
|
||||
}
|
||||
|
||||
// LayerByDigest implements v1.Image
|
||||
func (i *uncompressedImageExtender) LayerByDigest(h v1.Hash) (v1.Layer, error) {
|
||||
diffID, err := BlobToDiffID(i, h)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return i.LayerByDiffID(diffID)
|
||||
}
|
||||
-436
@@ -1,436 +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 partial
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/types"
|
||||
)
|
||||
|
||||
// WithRawConfigFile defines the subset of v1.Image used by these helper methods
|
||||
type WithRawConfigFile interface {
|
||||
// RawConfigFile returns the serialized bytes of this image's config file.
|
||||
RawConfigFile() ([]byte, error)
|
||||
}
|
||||
|
||||
// ConfigFile is a helper for implementing v1.Image
|
||||
func ConfigFile(i WithRawConfigFile) (*v1.ConfigFile, error) {
|
||||
b, err := i.RawConfigFile()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return v1.ParseConfigFile(bytes.NewReader(b))
|
||||
}
|
||||
|
||||
// ConfigName is a helper for implementing v1.Image
|
||||
func ConfigName(i WithRawConfigFile) (v1.Hash, error) {
|
||||
b, err := i.RawConfigFile()
|
||||
if err != nil {
|
||||
return v1.Hash{}, err
|
||||
}
|
||||
h, _, err := v1.SHA256(bytes.NewReader(b))
|
||||
return h, err
|
||||
}
|
||||
|
||||
type configLayer struct {
|
||||
hash v1.Hash
|
||||
content []byte
|
||||
}
|
||||
|
||||
// Digest implements v1.Layer
|
||||
func (cl *configLayer) Digest() (v1.Hash, error) {
|
||||
return cl.hash, nil
|
||||
}
|
||||
|
||||
// DiffID implements v1.Layer
|
||||
func (cl *configLayer) DiffID() (v1.Hash, error) {
|
||||
return cl.hash, nil
|
||||
}
|
||||
|
||||
// Uncompressed implements v1.Layer
|
||||
func (cl *configLayer) Uncompressed() (io.ReadCloser, error) {
|
||||
return io.NopCloser(bytes.NewBuffer(cl.content)), nil
|
||||
}
|
||||
|
||||
// Compressed implements v1.Layer
|
||||
func (cl *configLayer) Compressed() (io.ReadCloser, error) {
|
||||
return io.NopCloser(bytes.NewBuffer(cl.content)), nil
|
||||
}
|
||||
|
||||
// Size implements v1.Layer
|
||||
func (cl *configLayer) Size() (int64, error) {
|
||||
return int64(len(cl.content)), nil
|
||||
}
|
||||
|
||||
func (cl *configLayer) MediaType() (types.MediaType, error) {
|
||||
// Defaulting this to OCIConfigJSON as it should remain
|
||||
// backwards compatible with DockerConfigJSON
|
||||
return types.OCIConfigJSON, nil
|
||||
}
|
||||
|
||||
var _ v1.Layer = (*configLayer)(nil)
|
||||
|
||||
// withConfigLayer allows partial image implementations to provide a layer
|
||||
// for their config file.
|
||||
type withConfigLayer interface {
|
||||
ConfigLayer() (v1.Layer, error)
|
||||
}
|
||||
|
||||
// ConfigLayer implements v1.Layer from the raw config bytes.
|
||||
// This is so that clients (e.g. remote) can access the config as a blob.
|
||||
//
|
||||
// Images that want to return a specific layer implementation can implement
|
||||
// withConfigLayer.
|
||||
func ConfigLayer(i WithRawConfigFile) (v1.Layer, error) {
|
||||
if wcl, ok := unwrap(i).(withConfigLayer); ok {
|
||||
return wcl.ConfigLayer()
|
||||
}
|
||||
|
||||
h, err := ConfigName(i)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rcfg, err := i.RawConfigFile()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &configLayer{
|
||||
hash: h,
|
||||
content: rcfg,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// WithConfigFile defines the subset of v1.Image used by these helper methods
|
||||
type WithConfigFile interface {
|
||||
// ConfigFile returns this image's config file.
|
||||
ConfigFile() (*v1.ConfigFile, error)
|
||||
}
|
||||
|
||||
// DiffIDs is a helper for implementing v1.Image
|
||||
func DiffIDs(i WithConfigFile) ([]v1.Hash, error) {
|
||||
cfg, err := i.ConfigFile()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cfg.RootFS.DiffIDs, nil
|
||||
}
|
||||
|
||||
// RawConfigFile is a helper for implementing v1.Image
|
||||
func RawConfigFile(i WithConfigFile) ([]byte, error) {
|
||||
cfg, err := i.ConfigFile()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return json.Marshal(cfg)
|
||||
}
|
||||
|
||||
// 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.
|
||||
RawManifest() ([]byte, error)
|
||||
}
|
||||
|
||||
// Digest is a helper for implementing v1.Image
|
||||
func Digest(i WithRawManifest) (v1.Hash, error) {
|
||||
mb, err := i.RawManifest()
|
||||
if err != nil {
|
||||
return v1.Hash{}, err
|
||||
}
|
||||
digest, _, err := v1.SHA256(bytes.NewReader(mb))
|
||||
return digest, err
|
||||
}
|
||||
|
||||
// Manifest is a helper for implementing v1.Image
|
||||
func Manifest(i WithRawManifest) (*v1.Manifest, error) {
|
||||
b, err := i.RawManifest()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return v1.ParseManifest(bytes.NewReader(b))
|
||||
}
|
||||
|
||||
// WithManifest defines the subset of v1.Image used by these helper methods
|
||||
type WithManifest interface {
|
||||
// Manifest returns this image's Manifest object.
|
||||
Manifest() (*v1.Manifest, error)
|
||||
}
|
||||
|
||||
// RawManifest is a helper for implementing v1.Image
|
||||
func RawManifest(i WithManifest) ([]byte, error) {
|
||||
m, err := i.Manifest()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fsl := make([]v1.Hash, len(m.Layers))
|
||||
for i, l := range m.Layers {
|
||||
fsl[i] = l.Digest
|
||||
}
|
||||
return fsl, nil
|
||||
}
|
||||
|
||||
// BlobSize is a helper for implementing v1.Image
|
||||
func BlobSize(i WithManifest, h v1.Hash) (int64, error) {
|
||||
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 nil, err
|
||||
}
|
||||
|
||||
if m.Config.Digest == h {
|
||||
return &m.Config, nil
|
||||
}
|
||||
|
||||
for _, l := range m.Layers {
|
||||
if l.Digest == h {
|
||||
return &l, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("blob %v not found", h)
|
||||
}
|
||||
|
||||
// WithManifestAndConfigFile defines the subset of v1.Image used by these helper methods
|
||||
type WithManifestAndConfigFile interface {
|
||||
WithConfigFile
|
||||
|
||||
// Manifest returns this image's Manifest object.
|
||||
Manifest() (*v1.Manifest, error)
|
||||
}
|
||||
|
||||
// BlobToDiffID is a helper for mapping between compressed
|
||||
// and uncompressed blob hashes.
|
||||
func BlobToDiffID(i WithManifestAndConfigFile, h v1.Hash) (v1.Hash, error) {
|
||||
blobs, err := FSLayers(i)
|
||||
if err != nil {
|
||||
return v1.Hash{}, err
|
||||
}
|
||||
diffIDs, err := DiffIDs(i)
|
||||
if err != nil {
|
||||
return v1.Hash{}, err
|
||||
}
|
||||
if len(blobs) != len(diffIDs) {
|
||||
return v1.Hash{}, fmt.Errorf("mismatched fs layers (%d) and diff ids (%d)", len(blobs), len(diffIDs))
|
||||
}
|
||||
for i, blob := range blobs {
|
||||
if blob == h {
|
||||
return diffIDs[i], nil
|
||||
}
|
||||
}
|
||||
return v1.Hash{}, fmt.Errorf("unknown blob %v", h)
|
||||
}
|
||||
|
||||
// DiffIDToBlob is a helper for mapping between uncompressed
|
||||
// and compressed blob hashes.
|
||||
func DiffIDToBlob(wm WithManifestAndConfigFile, h v1.Hash) (v1.Hash, error) {
|
||||
blobs, err := FSLayers(wm)
|
||||
if err != nil {
|
||||
return v1.Hash{}, err
|
||||
}
|
||||
diffIDs, err := DiffIDs(wm)
|
||||
if err != nil {
|
||||
return v1.Hash{}, err
|
||||
}
|
||||
if len(blobs) != len(diffIDs) {
|
||||
return v1.Hash{}, fmt.Errorf("mismatched fs layers (%d) and diff ids (%d)", len(blobs), len(diffIDs))
|
||||
}
|
||||
for i, diffID := range diffIDs {
|
||||
if diffID == h {
|
||||
return blobs[i], nil
|
||||
}
|
||||
}
|
||||
return v1.Hash{}, fmt.Errorf("unknown diffID %v", h)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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 := unwrap(d).(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
|
||||
}
|
||||
if wat, ok := d.(withArtifactType); ok {
|
||||
if desc.ArtifactType, err = wat.ArtifactType(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
if wrm, ok := d.(WithRawManifest); ok && desc.MediaType.IsImage() {
|
||||
mf, _ := Manifest(wrm)
|
||||
// Failing to parse as a manifest should just be ignored.
|
||||
// The manifest might not be valid, and that's okay.
|
||||
if mf != nil && !mf.Config.MediaType.IsConfig() {
|
||||
desc.ArtifactType = string(mf.Config.MediaType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &desc, nil
|
||||
}
|
||||
|
||||
type withArtifactType interface {
|
||||
ArtifactType() (string, error)
|
||||
}
|
||||
|
||||
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 := unwrap(l).(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(io.Discard, rc)
|
||||
}
|
||||
|
||||
type withExists interface {
|
||||
Exists() (bool, error)
|
||||
}
|
||||
|
||||
// Exists checks to see if a layer exists. This is a hack to work around the
|
||||
// mistakes of the partial package. Don't use this.
|
||||
func Exists(l v1.Layer) (bool, error) {
|
||||
// If the layer implements Exists itself, return that.
|
||||
if we, ok := unwrap(l).(withExists); ok {
|
||||
return we.Exists()
|
||||
}
|
||||
|
||||
// The layer doesn't implement Exists, so we hope that calling Compressed()
|
||||
// is enough to trigger an error if the layer does not exist.
|
||||
rc, err := l.Compressed()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
// We may want to try actually reading a single byte, but if we need to do
|
||||
// that, we should just fix this hack.
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Recursively unwrap our wrappers so that we can check for the original implementation.
|
||||
// We might want to expose this?
|
||||
func unwrap(i any) any {
|
||||
if ule, ok := i.(*uncompressedLayerExtender); ok {
|
||||
return unwrap(ule.UncompressedLayer)
|
||||
}
|
||||
if cle, ok := i.(*compressedLayerExtender); ok {
|
||||
return unwrap(cle.CompressedLayer)
|
||||
}
|
||||
if uie, ok := i.(*uncompressedImageExtender); ok {
|
||||
return unwrap(uie.UncompressedImageCore)
|
||||
}
|
||||
if cie, ok := i.(*compressedImageExtender); ok {
|
||||
return unwrap(cie.CompressedImageCore)
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
// ArtifactType returns the artifact type for the given manifest.
|
||||
//
|
||||
// If the manifest reports its own artifact type, that's returned, otherwise
|
||||
// the manifest is parsed and, if successful, its config.mediaType is returned.
|
||||
func ArtifactType(w WithManifest) (string, error) {
|
||||
if wat, ok := w.(withArtifactType); ok {
|
||||
return wat.ArtifactType()
|
||||
}
|
||||
mf, _ := w.Manifest()
|
||||
// Failing to parse as a manifest should just be ignored.
|
||||
// The manifest might not be valid, and that's okay.
|
||||
if mf != nil && !mf.Config.MediaType.IsConfig() {
|
||||
return string(mf.Config.MediaType), nil
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
-149
@@ -1,149 +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 v1
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Platform represents the target os/arch for an image.
|
||||
type Platform struct {
|
||||
Architecture string `json:"architecture"`
|
||||
OS string `json:"os"`
|
||||
OSVersion string `json:"os.version,omitempty"`
|
||||
OSFeatures []string `json:"os.features,omitempty"`
|
||||
Variant string `json:"variant,omitempty"`
|
||||
Features []string `json:"features,omitempty"`
|
||||
}
|
||||
|
||||
func (p Platform) String() string {
|
||||
if p.OS == "" {
|
||||
return ""
|
||||
}
|
||||
var b strings.Builder
|
||||
b.WriteString(p.OS)
|
||||
if p.Architecture != "" {
|
||||
b.WriteString("/")
|
||||
b.WriteString(p.Architecture)
|
||||
}
|
||||
if p.Variant != "" {
|
||||
b.WriteString("/")
|
||||
b.WriteString(p.Variant)
|
||||
}
|
||||
if p.OSVersion != "" {
|
||||
b.WriteString(":")
|
||||
b.WriteString(p.OSVersion)
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// ParsePlatform parses a string representing a Platform, if possible.
|
||||
func ParsePlatform(s string) (*Platform, error) {
|
||||
var p Platform
|
||||
parts := strings.Split(strings.TrimSpace(s), ":")
|
||||
if len(parts) == 2 {
|
||||
p.OSVersion = parts[1]
|
||||
}
|
||||
parts = strings.Split(parts[0], "/")
|
||||
if len(parts) > 0 {
|
||||
p.OS = parts[0]
|
||||
}
|
||||
if len(parts) > 1 {
|
||||
p.Architecture = parts[1]
|
||||
}
|
||||
if len(parts) > 2 {
|
||||
p.Variant = parts[2]
|
||||
}
|
||||
if len(parts) > 3 {
|
||||
return nil, fmt.Errorf("too many slashes in platform spec: %s", s)
|
||||
}
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
// Equals returns true if the given platform is semantically equivalent to this one.
|
||||
// The order of Features and OSFeatures is not important.
|
||||
func (p Platform) Equals(o Platform) bool {
|
||||
return p.OS == o.OS &&
|
||||
p.Architecture == o.Architecture &&
|
||||
p.Variant == o.Variant &&
|
||||
p.OSVersion == o.OSVersion &&
|
||||
stringSliceEqualIgnoreOrder(p.OSFeatures, o.OSFeatures) &&
|
||||
stringSliceEqualIgnoreOrder(p.Features, o.Features)
|
||||
}
|
||||
|
||||
// Satisfies returns true if this Platform "satisfies" the given spec Platform.
|
||||
//
|
||||
// Note that this is different from Equals and that Satisfies is not reflexive.
|
||||
//
|
||||
// The given spec represents "requirements" such that any missing values in the
|
||||
// spec are not compared.
|
||||
//
|
||||
// For OSFeatures and Features, Satisfies will return true if this Platform's
|
||||
// fields contain a superset of the values in the spec's fields (order ignored).
|
||||
func (p Platform) Satisfies(spec Platform) bool {
|
||||
return satisfies(spec.OS, p.OS) &&
|
||||
satisfies(spec.Architecture, p.Architecture) &&
|
||||
satisfies(spec.Variant, p.Variant) &&
|
||||
satisfies(spec.OSVersion, p.OSVersion) &&
|
||||
satisfiesList(spec.OSFeatures, p.OSFeatures) &&
|
||||
satisfiesList(spec.Features, p.Features)
|
||||
}
|
||||
|
||||
func satisfies(want, have string) bool {
|
||||
return want == "" || want == have
|
||||
}
|
||||
|
||||
func satisfiesList(want, have []string) bool {
|
||||
if len(want) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
set := map[string]struct{}{}
|
||||
for _, h := range have {
|
||||
set[h] = struct{}{}
|
||||
}
|
||||
|
||||
for _, w := range want {
|
||||
if _, ok := set[w]; !ok {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// stringSliceEqual compares 2 string slices and returns if their contents are identical.
|
||||
func stringSliceEqual(a, b []string) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i, elm := range a {
|
||||
if elm != b[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// stringSliceEqualIgnoreOrder compares 2 string slices and returns if their contents are identical, ignoring order
|
||||
func stringSliceEqualIgnoreOrder(a, b []string) bool {
|
||||
if a != nil && b != nil {
|
||||
sort.Strings(a)
|
||||
sort.Strings(b)
|
||||
}
|
||||
return stringSliceEqual(a, b)
|
||||
}
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
// Copyright 2020 Google LLC All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package v1
|
||||
|
||||
// Update representation of an update of transfer progress. Some functions
|
||||
// in this module can take a channel to which updates will be sent while a
|
||||
// transfer is in progress.
|
||||
// +k8s:deepcopy-gen=false
|
||||
type Update struct {
|
||||
Total int64
|
||||
Complete int64
|
||||
Error error
|
||||
}
|
||||
-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/main/pkg/internal/legacy/copy.go).
|
||||
-159
@@ -1,159 +0,0 @@
|
||||
// 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 Catalogs struct {
|
||||
Repos []string `json:"repositories"`
|
||||
Next string `json:"next,omitempty"`
|
||||
}
|
||||
|
||||
// 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(options...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
f, err := newPuller(o).fetcher(o.context, target)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
uri := url.URL{
|
||||
Scheme: target.Scheme(),
|
||||
Host: target.RegistryStr(),
|
||||
Path: "/v2/_catalog",
|
||||
RawQuery: fmt.Sprintf("last=%s&n=%d", url.QueryEscape(last), n),
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, uri.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := f.client.Do(req.WithContext(o.context))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if err := transport.CheckError(resp, http.StatusOK); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var parsed Catalogs
|
||||
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(options...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// WithContext overrides the ctx passed directly.
|
||||
if o.context != context.Background() {
|
||||
ctx = o.context
|
||||
}
|
||||
|
||||
return newPuller(o).catalog(ctx, target, o.pageSize)
|
||||
}
|
||||
|
||||
func (f *fetcher) catalogPage(ctx context.Context, reg name.Registry, next string, pageSize int) (*Catalogs, error) {
|
||||
if next == "" {
|
||||
uri := &url.URL{
|
||||
Scheme: reg.Scheme(),
|
||||
Host: reg.RegistryStr(),
|
||||
Path: "/v2/_catalog",
|
||||
}
|
||||
if pageSize > 0 {
|
||||
uri.RawQuery = fmt.Sprintf("n=%d", pageSize)
|
||||
}
|
||||
next = uri.String()
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", next, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := f.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := transport.CheckError(resp, http.StatusOK); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
parsed := Catalogs{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&parsed); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := resp.Body.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
uri, err := getNextPageURL(resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if uri != nil {
|
||||
parsed.Next = uri.String()
|
||||
}
|
||||
|
||||
return &parsed, nil
|
||||
}
|
||||
|
||||
type Catalogger struct {
|
||||
f *fetcher
|
||||
reg name.Registry
|
||||
pageSize int
|
||||
|
||||
page *Catalogs
|
||||
err error
|
||||
|
||||
needMore bool
|
||||
}
|
||||
|
||||
func (l *Catalogger) Next(ctx context.Context) (*Catalogs, error) {
|
||||
if l.needMore {
|
||||
l.page, l.err = l.f.catalogPage(ctx, l.reg, l.page.Next, l.pageSize)
|
||||
} else {
|
||||
l.needMore = true
|
||||
}
|
||||
return l.page, l.err
|
||||
}
|
||||
|
||||
func (l *Catalogger) HasNext() bool {
|
||||
return l.page != nil && (!l.needMore || l.page.Next != "")
|
||||
}
|
||||
-72
@@ -1,72 +0,0 @@
|
||||
// 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"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/google/go-containerregistry/pkg/authn"
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
"github.com/google/go-containerregistry/pkg/v1/remote/transport"
|
||||
)
|
||||
|
||||
// CheckPushPermission returns an error if the given keychain cannot authorize
|
||||
// a push operation to the given ref.
|
||||
//
|
||||
// This can be useful to check whether the caller has permission to push an
|
||||
// image before doing work to construct the image.
|
||||
//
|
||||
// TODO(#412): Remove the need for this method.
|
||||
func CheckPushPermission(ref name.Reference, kc authn.Keychain, t http.RoundTripper) error {
|
||||
auth, err := kc.Resolve(ref.Context().Registry)
|
||||
if err != nil {
|
||||
return fmt.Errorf("resolving authorization for %v failed: %w", ref.Context().Registry, err)
|
||||
}
|
||||
|
||||
scopes := []string{ref.Scope(transport.PushScope)}
|
||||
tr, err := transport.NewWithContext(context.TODO(), ref.Context().Registry, auth, t, scopes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating push check transport for %v failed: %w", ref.Context().Registry, err)
|
||||
}
|
||||
// TODO(jasonhall): Against GCR, just doing the token handshake is
|
||||
// enough, but this doesn't extend to Dockerhub
|
||||
// (https://github.com/docker/hub-feedback/issues/1771), so we actually
|
||||
// need to initiate an upload to tell whether the credentials can
|
||||
// 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(),
|
||||
client: &http.Client{Transport: tr},
|
||||
}
|
||||
loc, _, err := w.initiateUpload(context.Background(), "", "", "")
|
||||
if loc != "" {
|
||||
// Since we're only initiating the upload to check whether we
|
||||
// can, we should attempt to cancel it, in case initiating
|
||||
// reserves some resources on the server. We shouldn't wait for
|
||||
// cancelling to complete, and we don't care if it fails.
|
||||
go w.cancelUpload(loc)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (w *writer) cancelUpload(loc string) {
|
||||
req, err := http.NewRequest(http.MethodDelete, loc, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_, _ = w.client.Do(req)
|
||||
}
|
||||
-28
@@ -1,28 +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 remote
|
||||
|
||||
import (
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
)
|
||||
|
||||
// Delete removes the specified image reference from the remote registry.
|
||||
func Delete(ref name.Reference, options ...Option) error {
|
||||
o, err := makeOptions(options...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return newPusher(o).Delete(o.context, ref)
|
||||
}
|
||||
-198
@@ -1,198 +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 remote
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"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/types"
|
||||
)
|
||||
|
||||
var allManifestMediaTypes = append(append([]types.MediaType{
|
||||
types.DockerManifestSchema1,
|
||||
types.DockerManifestSchema1Signed,
|
||||
}, acceptableImageMediaTypes...), acceptableIndexMediaTypes...)
|
||||
|
||||
// ErrSchema1 indicates that we received a schema1 manifest from the registry.
|
||||
// This library doesn't have plans to support this legacy image format:
|
||||
// https://github.com/google/go-containerregistry/issues/377
|
||||
var ErrSchema1 = errors.New("see https://github.com/google/go-containerregistry/issues/377")
|
||||
|
||||
// newErrSchema1 returns an ErrSchema1 with the unexpected MediaType.
|
||||
func newErrSchema1(schema types.MediaType) error {
|
||||
return fmt.Errorf("unsupported MediaType: %q, %w", schema, ErrSchema1)
|
||||
}
|
||||
|
||||
// Descriptor provides access to metadata about remote artifact and accessors
|
||||
// for efficiently converting it into a v1.Image or v1.ImageIndex.
|
||||
type Descriptor struct {
|
||||
fetcher fetcher
|
||||
v1.Descriptor
|
||||
|
||||
ref name.Reference
|
||||
Manifest []byte
|
||||
ctx context.Context
|
||||
|
||||
// So we can share this implementation with Image.
|
||||
platform v1.Platform
|
||||
}
|
||||
|
||||
func (d *Descriptor) toDesc() v1.Descriptor {
|
||||
return d.Descriptor
|
||||
}
|
||||
|
||||
// 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.
|
||||
//
|
||||
// See Head if you don't need the response body.
|
||||
func Get(ref name.Reference, options ...Option) (*Descriptor, error) {
|
||||
return get(ref, allManifestMediaTypes, options...)
|
||||
}
|
||||
|
||||
// Head returns a v1.Descriptor for the given reference by issuing a HEAD
|
||||
// request.
|
||||
//
|
||||
// Note that the server response will not have a body, so any errors encountered
|
||||
// should be retried with Get to get more details.
|
||||
func Head(ref name.Reference, options ...Option) (*v1.Descriptor, error) {
|
||||
o, err := makeOptions(options...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return newPuller(o).Head(o.context, ref)
|
||||
}
|
||||
|
||||
// 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(options...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newPuller(o).get(o.context, ref, acceptable, o.platform)
|
||||
}
|
||||
|
||||
// Image converts the Descriptor into a v1.Image.
|
||||
//
|
||||
// If the fetched artifact is already an image, it will just return it.
|
||||
//
|
||||
// If the fetched artifact is an index, it will attempt to resolve the index to
|
||||
// a child image with the appropriate platform.
|
||||
//
|
||||
// See WithPlatform to set the desired platform.
|
||||
func (d *Descriptor) Image() (v1.Image, error) {
|
||||
switch d.MediaType {
|
||||
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)
|
||||
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)
|
||||
case types.OCIManifestSchema1, types.DockerManifestSchema2:
|
||||
// These are expected. Enumerated here to allow a default case.
|
||||
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...
|
||||
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
|
||||
// remote.Write calls to facilitate cross-repo "mounting".
|
||||
imgCore, err := partial.CompressedToImage(d.remoteImage())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &mountableImage{
|
||||
Image: imgCore,
|
||||
Reference: d.ref,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Schema1 converts the Descriptor into a v1.Image for v2 schema 1 media types.
|
||||
//
|
||||
// The v1.Image returned by this method does not implement the entire interface because it would be inefficient.
|
||||
// This exists mostly to make it easier to copy schema 1 images around or look at their filesystems.
|
||||
// This is separate from Image() to avoid a backward incompatible change for callers expecting ErrSchema1.
|
||||
func (d *Descriptor) Schema1() (v1.Image, error) {
|
||||
i := &schema1{
|
||||
ref: d.ref,
|
||||
fetcher: d.fetcher,
|
||||
ctx: d.ctx,
|
||||
manifest: d.Manifest,
|
||||
mediaType: d.MediaType,
|
||||
descriptor: &d.Descriptor,
|
||||
}
|
||||
|
||||
return &mountableImage{
|
||||
Image: i,
|
||||
Reference: d.ref,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ImageIndex converts the Descriptor into a v1.ImageIndex.
|
||||
func (d *Descriptor) ImageIndex() (v1.ImageIndex, error) {
|
||||
switch d.MediaType {
|
||||
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)
|
||||
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)
|
||||
case types.OCIImageIndex, types.DockerManifestList:
|
||||
// These are expected.
|
||||
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...
|
||||
logs.Warn.Printf("Unexpected media type for ImageIndex(): %s", d.MediaType)
|
||||
}
|
||||
return d.remoteIndex(), nil
|
||||
}
|
||||
|
||||
func (d *Descriptor) remoteImage() *remoteImage {
|
||||
return &remoteImage{
|
||||
ref: d.ref,
|
||||
ctx: d.ctx,
|
||||
fetcher: d.fetcher,
|
||||
manifest: d.Manifest,
|
||||
mediaType: d.MediaType,
|
||||
descriptor: &d.Descriptor,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Descriptor) remoteIndex() *remoteIndex {
|
||||
return &remoteIndex{
|
||||
ref: d.ref,
|
||||
ctx: d.ctx,
|
||||
fetcher: d.fetcher,
|
||||
manifest: d.Manifest,
|
||||
mediaType: d.MediaType,
|
||||
descriptor: &d.Descriptor,
|
||||
}
|
||||
}
|
||||
-17
@@ -1,17 +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 remote provides facilities for reading/writing v1.Images from/to
|
||||
// a remote image registry.
|
||||
package remote
|
||||
-317
@@ -1,317 +0,0 @@
|
||||
// Copyright 2023 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 (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/google/go-containerregistry/internal/redact"
|
||||
"github.com/google/go-containerregistry/internal/verify"
|
||||
"github.com/google/go-containerregistry/pkg/authn"
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/remote/transport"
|
||||
"github.com/google/go-containerregistry/pkg/v1/types"
|
||||
)
|
||||
|
||||
const (
|
||||
kib = 1024
|
||||
mib = 1024 * kib
|
||||
manifestLimit = 100 * mib
|
||||
)
|
||||
|
||||
// fetcher implements methods for reading from a registry.
|
||||
type fetcher struct {
|
||||
target resource
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func makeFetcher(ctx context.Context, target resource, o *options) (*fetcher, error) {
|
||||
auth := o.auth
|
||||
if o.keychain != nil {
|
||||
kauth, err := authn.Resolve(ctx, o.keychain, target)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
auth = kauth
|
||||
}
|
||||
|
||||
reg, ok := target.(name.Registry)
|
||||
if !ok {
|
||||
repo, ok := target.(name.Repository)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected resource: %T", target)
|
||||
}
|
||||
reg = repo.Registry
|
||||
}
|
||||
|
||||
tr, err := transport.NewWithContext(ctx, reg, auth, o.transport, []string{target.Scope(transport.PullScope)})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &fetcher{
|
||||
target: target,
|
||||
client: &http.Client{Transport: tr},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (f *fetcher) Do(req *http.Request) (*http.Response, error) {
|
||||
return f.client.Do(req)
|
||||
}
|
||||
|
||||
type resource interface {
|
||||
Scheme() string
|
||||
RegistryStr() string
|
||||
Scope(string) string
|
||||
|
||||
authn.Resource
|
||||
}
|
||||
|
||||
// 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 {
|
||||
u := url.URL{
|
||||
Scheme: f.target.Scheme(),
|
||||
Host: f.target.RegistryStr(),
|
||||
// Default path if this is not a repository.
|
||||
Path: "/v2/_catalog",
|
||||
}
|
||||
if repo, ok := f.target.(name.Repository); ok {
|
||||
u.Path = fmt.Sprintf("/v2/%s/%s/%s", repo.RepositoryStr(), resource, identifier)
|
||||
}
|
||||
return u
|
||||
}
|
||||
|
||||
func (f *fetcher) get(ctx context.Context, ref name.Reference, acceptable []types.MediaType, platform v1.Platform) (*Descriptor, error) {
|
||||
b, desc, err := f.fetchManifest(ctx, ref, acceptable)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Descriptor{
|
||||
ref: ref,
|
||||
ctx: ctx,
|
||||
fetcher: *f,
|
||||
Manifest: b,
|
||||
Descriptor: *desc,
|
||||
platform: platform,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (f *fetcher) fetchManifest(ctx context.Context, ref name.Reference, acceptable []types.MediaType) ([]byte, *v1.Descriptor, error) {
|
||||
u := f.url("manifests", ref.Identifier())
|
||||
req, err := http.NewRequest(http.MethodGet, u.String(), nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
accept := []string{}
|
||||
for _, mt := range acceptable {
|
||||
accept = append(accept, string(mt))
|
||||
}
|
||||
req.Header.Set("Accept", strings.Join(accept, ","))
|
||||
|
||||
resp, err := f.client.Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if err := transport.CheckError(resp, http.StatusOK); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
manifest, err := io.ReadAll(io.LimitReader(resp.Body, manifestLimit))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
digest, size, err := v1.SHA256(bytes.NewReader(manifest))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
mediaType := types.MediaType(resp.Header.Get("Content-Type"))
|
||||
contentDigest, err := v1.NewHash(resp.Header.Get("Docker-Content-Digest"))
|
||||
if err == nil && mediaType == types.DockerManifestSchema1Signed {
|
||||
// If we can parse the digest from the header, and it's a signed schema 1
|
||||
// manifest, let's use that for the digest to appease older registries.
|
||||
digest = contentDigest
|
||||
}
|
||||
|
||||
// Validate the digest matches what we asked for, if pulling by digest.
|
||||
if dgst, ok := ref.(name.Digest); ok {
|
||||
if digest.String() != dgst.DigestStr() {
|
||||
return nil, nil, fmt.Errorf("manifest digest: %q does not match requested digest: %q for %q", digest, dgst.DigestStr(), ref)
|
||||
}
|
||||
}
|
||||
|
||||
var artifactType string
|
||||
mf, _ := v1.ParseManifest(bytes.NewReader(manifest))
|
||||
// Failing to parse as a manifest should just be ignored.
|
||||
// The manifest might not be valid, and that's okay.
|
||||
if mf != nil && !mf.Config.MediaType.IsConfig() {
|
||||
artifactType = string(mf.Config.MediaType)
|
||||
}
|
||||
|
||||
// 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{
|
||||
Digest: digest,
|
||||
Size: size,
|
||||
MediaType: mediaType,
|
||||
ArtifactType: artifactType,
|
||||
}
|
||||
|
||||
return manifest, &desc, nil
|
||||
}
|
||||
|
||||
func (f *fetcher) headManifest(ctx context.Context, ref name.Reference, acceptable []types.MediaType) (*v1.Descriptor, error) {
|
||||
u := f.url("manifests", ref.Identifier())
|
||||
req, err := http.NewRequest(http.MethodHead, u.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
accept := []string{}
|
||||
for _, mt := range acceptable {
|
||||
accept = append(accept, string(mt))
|
||||
}
|
||||
req.Header.Set("Accept", strings.Join(accept, ","))
|
||||
|
||||
resp, err := f.client.Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if err := transport.CheckError(resp, http.StatusOK); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mth := resp.Header.Get("Content-Type")
|
||||
if mth == "" {
|
||||
return nil, fmt.Errorf("HEAD %s: response did not include Content-Type header", u.String())
|
||||
}
|
||||
mediaType := types.MediaType(mth)
|
||||
|
||||
size := resp.ContentLength
|
||||
if size == -1 {
|
||||
return nil, fmt.Errorf("GET %s: response did not include Content-Length header", u.String())
|
||||
}
|
||||
|
||||
dh := resp.Header.Get("Docker-Content-Digest")
|
||||
if dh == "" {
|
||||
return nil, fmt.Errorf("HEAD %s: response did not include Docker-Content-Digest header", u.String())
|
||||
}
|
||||
digest, err := v1.NewHash(dh)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Validate the digest matches what we asked for, if pulling by digest.
|
||||
if dgst, ok := ref.(name.Digest); ok {
|
||||
if digest.String() != dgst.DigestStr() {
|
||||
return nil, fmt.Errorf("manifest digest: %q does not match requested digest: %q for %q", digest, dgst.DigestStr(), ref)
|
||||
}
|
||||
}
|
||||
|
||||
// Return all this info since we have to calculate it anyway.
|
||||
return &v1.Descriptor{
|
||||
Digest: digest,
|
||||
Size: size,
|
||||
MediaType: mediaType,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (f *fetcher) fetchBlob(ctx context.Context, size int64, h v1.Hash) (io.ReadCloser, error) {
|
||||
u := f.url("blobs", h.String())
|
||||
req, err := http.NewRequest(http.MethodGet, u.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := f.client.Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
return nil, redact.Error(err)
|
||||
}
|
||||
|
||||
if err := transport.CheckError(resp, http.StatusOK); err != nil {
|
||||
resp.Body.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Do whatever we can.
|
||||
// If we have an expected size and Content-Length doesn't match, return an error.
|
||||
// If we don't have an expected size and we do have a Content-Length, use Content-Length.
|
||||
if hsize := resp.ContentLength; hsize != -1 {
|
||||
if size == verify.SizeUnknown {
|
||||
size = hsize
|
||||
} else if hsize != size {
|
||||
return nil, fmt.Errorf("GET %s: Content-Length header %d does not match expected size %d", u.String(), hsize, size)
|
||||
}
|
||||
}
|
||||
|
||||
return verify.ReadCloser(resp.Body, size, h)
|
||||
}
|
||||
|
||||
func (f *fetcher) headBlob(ctx context.Context, h v1.Hash) (*http.Response, error) {
|
||||
u := f.url("blobs", h.String())
|
||||
req, err := http.NewRequest(http.MethodHead, u.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := f.client.Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
return nil, redact.Error(err)
|
||||
}
|
||||
|
||||
if err := transport.CheckError(resp, http.StatusOK); err != nil {
|
||||
resp.Body.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (f *fetcher) blobExists(ctx context.Context, h v1.Hash) (bool, error) {
|
||||
u := f.url("blobs", h.String())
|
||||
req, err := http.NewRequest(http.MethodHead, u.String(), nil)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
resp, err := f.client.Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
return false, redact.Error(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if err := transport.CheckError(resp, http.StatusOK, http.StatusNotFound); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return resp.StatusCode == http.StatusOK, nil
|
||||
}
|
||||
-277
@@ -1,277 +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 remote
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sync"
|
||||
|
||||
"github.com/google/go-containerregistry/internal/redact"
|
||||
"github.com/google/go-containerregistry/internal/verify"
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/partial"
|
||||
"github.com/google/go-containerregistry/pkg/v1/remote/transport"
|
||||
"github.com/google/go-containerregistry/pkg/v1/types"
|
||||
)
|
||||
|
||||
var acceptableImageMediaTypes = []types.MediaType{
|
||||
types.DockerManifestSchema2,
|
||||
types.OCIManifestSchema1,
|
||||
}
|
||||
|
||||
// remoteImage accesses an image from a remote registry
|
||||
type remoteImage struct {
|
||||
fetcher fetcher
|
||||
ref name.Reference
|
||||
ctx context.Context
|
||||
manifestLock sync.Mutex // Protects manifest
|
||||
manifest []byte
|
||||
configLock sync.Mutex // Protects config
|
||||
config []byte
|
||||
mediaType types.MediaType
|
||||
descriptor *v1.Descriptor
|
||||
}
|
||||
|
||||
func (r *remoteImage) ArtifactType() (string, error) {
|
||||
// kind of a hack, but RawManifest does appropriate locking/memoization
|
||||
// and makes sure r.descriptor is populated.
|
||||
if _, err := r.RawManifest(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return r.descriptor.ArtifactType, nil
|
||||
}
|
||||
|
||||
var _ partial.CompressedImageCore = (*remoteImage)(nil)
|
||||
|
||||
// Image provides access to a remote image reference.
|
||||
func Image(ref name.Reference, options ...Option) (v1.Image, error) {
|
||||
desc, err := Get(ref, options...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return desc.Image()
|
||||
}
|
||||
|
||||
func (r *remoteImage) MediaType() (types.MediaType, error) {
|
||||
if string(r.mediaType) != "" {
|
||||
return r.mediaType, nil
|
||||
}
|
||||
return types.DockerManifestSchema2, nil
|
||||
}
|
||||
|
||||
func (r *remoteImage) RawManifest() ([]byte, error) {
|
||||
r.manifestLock.Lock()
|
||||
defer r.manifestLock.Unlock()
|
||||
if r.manifest != nil {
|
||||
return r.manifest, nil
|
||||
}
|
||||
|
||||
// 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.fetcher.fetchManifest(r.ctx, r.ref, acceptableImageMediaTypes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if r.descriptor == nil {
|
||||
r.descriptor = desc
|
||||
}
|
||||
r.mediaType = desc.MediaType
|
||||
r.manifest = manifest
|
||||
return r.manifest, nil
|
||||
}
|
||||
|
||||
func (r *remoteImage) RawConfigFile() ([]byte, error) {
|
||||
r.configLock.Lock()
|
||||
defer r.configLock.Unlock()
|
||||
if r.config != nil {
|
||||
return r.config, nil
|
||||
}
|
||||
|
||||
m, err := partial.Manifest(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if m.Config.Data != nil {
|
||||
if err := verify.Descriptor(m.Config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.config = m.Config.Data
|
||||
return r.config, nil
|
||||
}
|
||||
|
||||
body, err := r.fetcher.fetchBlob(r.ctx, m.Config.Size, m.Config.Digest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer body.Close()
|
||||
|
||||
r.config, err = io.ReadAll(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
func (r *remoteImage) ConfigLayer() (v1.Layer, error) {
|
||||
if _, err := r.RawManifest(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m, err := partial.Manifest(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return partial.CompressedToLayer(&remoteImageLayer{
|
||||
ri: r,
|
||||
ctx: r.ctx,
|
||||
digest: m.Config.Digest,
|
||||
})
|
||||
}
|
||||
|
||||
// remoteImageLayer implements partial.CompressedLayer
|
||||
type remoteImageLayer struct {
|
||||
ri *remoteImage
|
||||
ctx context.Context
|
||||
digest v1.Hash
|
||||
}
|
||||
|
||||
// Digest implements partial.CompressedLayer
|
||||
func (rl *remoteImageLayer) Digest() (v1.Hash, error) {
|
||||
return rl.digest, nil
|
||||
}
|
||||
|
||||
// Compressed implements partial.CompressedLayer
|
||||
func (rl *remoteImageLayer) Compressed() (io.ReadCloser, error) {
|
||||
urls := []url.URL{rl.ri.fetcher.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 d.Data != nil {
|
||||
return verify.ReadCloser(io.NopCloser(bytes.NewReader(d.Data)), d.Size, d.Digest)
|
||||
}
|
||||
|
||||
// We don't want to log binary layers -- this can break terminals.
|
||||
ctx := redact.NewContext(rl.ctx, "omitting binary blobs from logs")
|
||||
|
||||
for _, s := range d.URLs {
|
||||
u, err := url.Parse(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
urls = append(urls, *u)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
req, err := http.NewRequest(http.MethodGet, u.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := rl.ri.fetcher.Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
|
||||
if err := transport.CheckError(resp, http.StatusOK); err != nil {
|
||||
resp.Body.Close()
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
|
||||
return verify.ReadCloser(resp.Body, d.Size, rl.digest)
|
||||
}
|
||||
|
||||
return nil, lastErr
|
||||
}
|
||||
|
||||
// Manifest implements partial.WithManifest so that we can use partial.BlobSize below.
|
||||
func (rl *remoteImageLayer) Manifest() (*v1.Manifest, error) {
|
||||
return partial.Manifest(rl.ri)
|
||||
}
|
||||
|
||||
// MediaType implements v1.Layer
|
||||
func (rl *remoteImageLayer) MediaType() (types.MediaType, error) {
|
||||
bd, err := partial.BlobDescriptor(rl, rl.digest)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return bd.MediaType, nil
|
||||
}
|
||||
|
||||
// Size implements partial.CompressedLayer
|
||||
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 *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 *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)
|
||||
}
|
||||
|
||||
// See partial.Exists.
|
||||
func (rl *remoteImageLayer) Exists() (bool, error) {
|
||||
return rl.ri.fetcher.blobExists(rl.ri.ctx, rl.digest)
|
||||
}
|
||||
|
||||
// LayerByDigest implements partial.CompressedLayer
|
||||
func (r *remoteImage) LayerByDigest(h v1.Hash) (partial.CompressedLayer, error) {
|
||||
return &remoteImageLayer{
|
||||
ri: r,
|
||||
ctx: r.ctx,
|
||||
digest: h,
|
||||
}, nil
|
||||
}
|
||||
-287
@@ -1,287 +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 remote
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/google/go-containerregistry/internal/verify"
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/partial"
|
||||
"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 fetcher
|
||||
ref name.Reference
|
||||
ctx context.Context
|
||||
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...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return desc.ImageIndex()
|
||||
}
|
||||
|
||||
func (r *remoteIndex) MediaType() (types.MediaType, error) {
|
||||
if string(r.mediaType) != "" {
|
||||
return r.mediaType, nil
|
||||
}
|
||||
return types.DockerManifestList, nil
|
||||
}
|
||||
|
||||
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()
|
||||
if r.manifest != nil {
|
||||
return r.manifest, nil
|
||||
}
|
||||
|
||||
// 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.fetcher.fetchManifest(r.ctx, r.ref, acceptableIndexMediaTypes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if r.descriptor == nil {
|
||||
r.descriptor = desc
|
||||
}
|
||||
r.mediaType = desc.MediaType
|
||||
r.manifest = manifest
|
||||
return r.manifest, nil
|
||||
}
|
||||
|
||||
func (r *remoteIndex) IndexManifest() (*v1.IndexManifest, error) {
|
||||
b, err := r.RawManifest()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return v1.ParseIndexManifest(bytes.NewReader(b))
|
||||
}
|
||||
|
||||
func (r *remoteIndex) Image(h v1.Hash) (v1.Image, error) {
|
||||
desc, err := r.childByHash(h)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Descriptor.Image will handle coercing nested indexes into an Image.
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
return desc.ImageIndex()
|
||||
}
|
||||
|
||||
// Workaround for #819.
|
||||
func (r *remoteIndex) Layer(h v1.Hash) (v1.Layer, error) {
|
||||
index, err := r.IndexManifest()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, childDesc := range index.Manifests {
|
||||
if h == childDesc.Digest {
|
||||
l, err := partial.CompressedToLayer(&remoteLayer{
|
||||
fetcher: r.fetcher,
|
||||
ctx: r.ctx,
|
||||
digest: h,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &MountableLayer{
|
||||
Layer: l,
|
||||
Reference: r.ref.Context().Digest(h.String()),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("layer not found: %s", h)
|
||||
}
|
||||
|
||||
func (r *remoteIndex) imageByPlatform(platform v1.Platform) (v1.Image, error) {
|
||||
desc, err := r.childByPlatform(platform)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Descriptor.Image will handle coercing nested indexes into an Image.
|
||||
return desc.Image()
|
||||
}
|
||||
|
||||
// This naively matches the first manifest with matching platform attributes.
|
||||
//
|
||||
// We should probably use this instead:
|
||||
//
|
||||
// github.com/containerd/containerd/platforms
|
||||
//
|
||||
// But first we'd need to migrate to:
|
||||
//
|
||||
// github.com/opencontainers/image-spec/specs-go/v1
|
||||
func (r *remoteIndex) childByPlatform(platform v1.Platform) (*Descriptor, error) {
|
||||
index, err := r.IndexManifest()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, childDesc := range index.Manifests {
|
||||
// If platform is missing from child descriptor, assume it's amd64/linux.
|
||||
p := defaultPlatform
|
||||
if childDesc.Platform != nil {
|
||||
p = *childDesc.Platform
|
||||
}
|
||||
|
||||
if matchesPlatform(p, platform) {
|
||||
return r.childDescriptor(childDesc, platform)
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("no child with platform %+v in index %s", platform, r.ref)
|
||||
}
|
||||
|
||||
func (r *remoteIndex) childByHash(h v1.Hash) (*Descriptor, error) {
|
||||
index, err := r.IndexManifest()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, childDesc := range index.Manifests {
|
||||
if h == childDesc.Digest {
|
||||
return r.childDescriptor(childDesc, defaultPlatform)
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("no child with digest %s in index %s", h, r.ref)
|
||||
}
|
||||
|
||||
// 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())
|
||||
var (
|
||||
manifest []byte
|
||||
err error
|
||||
)
|
||||
if child.Data != nil {
|
||||
if err := verify.Descriptor(child); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
manifest = child.Data
|
||||
} else {
|
||||
manifest, _, err = r.fetcher.fetchManifest(r.ctx, ref, []types.MediaType{child.MediaType})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if child.MediaType.IsImage() {
|
||||
mf, _ := v1.ParseManifest(bytes.NewReader(manifest))
|
||||
// Failing to parse as a manifest should just be ignored.
|
||||
// The manifest might not be valid, and that's okay.
|
||||
if mf != nil && !mf.Config.MediaType.IsConfig() {
|
||||
child.ArtifactType = string(mf.Config.MediaType)
|
||||
}
|
||||
}
|
||||
|
||||
return &Descriptor{
|
||||
ref: ref,
|
||||
ctx: r.ctx,
|
||||
fetcher: r.fetcher,
|
||||
Manifest: manifest,
|
||||
Descriptor: child,
|
||||
platform: platform,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// matchesPlatform checks if the given platform matches the required platforms.
|
||||
// The given platform matches the required platform if
|
||||
// - architecture and OS are identical.
|
||||
// - OS version and variant are identical if provided.
|
||||
// - features and OS features of the required platform are subsets of those of the given platform.
|
||||
func matchesPlatform(given, required v1.Platform) bool {
|
||||
// Required fields that must be identical.
|
||||
if given.Architecture != required.Architecture || given.OS != required.OS {
|
||||
return false
|
||||
}
|
||||
|
||||
// Optional fields that may be empty, but must be identical if provided.
|
||||
if required.OSVersion != "" && given.OSVersion != required.OSVersion {
|
||||
return false
|
||||
}
|
||||
if required.Variant != "" && given.Variant != required.Variant {
|
||||
return false
|
||||
}
|
||||
|
||||
// Verify required platform's features are a subset of given platform's features.
|
||||
if !isSubset(given.OSFeatures, required.OSFeatures) {
|
||||
return false
|
||||
}
|
||||
if !isSubset(given.Features, required.Features) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// isSubset checks if the required array of strings is a subset of the given lst.
|
||||
func isSubset(lst, required []string) bool {
|
||||
set := make(map[string]bool)
|
||||
for _, value := range lst {
|
||||
set[value] = true
|
||||
}
|
||||
|
||||
for _, value := range required {
|
||||
if _, ok := set[value]; !ok {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
-77
@@ -1,77 +0,0 @@
|
||||
// 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"
|
||||
"io"
|
||||
|
||||
"github.com/google/go-containerregistry/internal/redact"
|
||||
"github.com/google/go-containerregistry/internal/verify"
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/types"
|
||||
)
|
||||
|
||||
// remoteImagelayer implements partial.CompressedLayer
|
||||
type remoteLayer struct {
|
||||
ctx context.Context
|
||||
fetcher fetcher
|
||||
digest v1.Hash
|
||||
}
|
||||
|
||||
// Compressed implements partial.CompressedLayer
|
||||
func (rl *remoteLayer) Compressed() (io.ReadCloser, error) {
|
||||
// We don't want to log binary layers -- this can break terminals.
|
||||
ctx := redact.NewContext(rl.ctx, "omitting binary blobs from logs")
|
||||
return rl.fetcher.fetchBlob(ctx, verify.SizeUnknown, rl.digest)
|
||||
}
|
||||
|
||||
// Compressed implements partial.CompressedLayer
|
||||
func (rl *remoteLayer) Size() (int64, error) {
|
||||
resp, err := rl.fetcher.headBlob(rl.ctx, rl.digest)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
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
|
||||
}
|
||||
|
||||
// See partial.Exists.
|
||||
func (rl *remoteLayer) Exists() (bool, error) {
|
||||
return rl.fetcher.blobExists(rl.ctx, rl.digest)
|
||||
}
|
||||
|
||||
// Layer reads the given blob reference from a registry as a Layer. A blob
|
||||
// reference here is just a punned name.Digest where the digest portion is the
|
||||
// digest of the blob to be read and the repository portion is the repo where
|
||||
// that blob lives.
|
||||
func Layer(ref name.Digest, options ...Option) (v1.Layer, error) {
|
||||
o, err := makeOptions(options...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newPuller(o).Layer(o.context, ref)
|
||||
}
|
||||
-152
@@ -1,152 +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 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"
|
||||
)
|
||||
|
||||
// ListWithContext calls List with the given context.
|
||||
//
|
||||
// Deprecated: Use List and WithContext. This will be removed in a future release.
|
||||
func ListWithContext(ctx context.Context, repo name.Repository, options ...Option) ([]string, error) {
|
||||
return List(repo, append(options, WithContext(ctx))...)
|
||||
}
|
||||
|
||||
// List calls /tags/list for the given repository, returning the list of tags
|
||||
// in the "tags" property.
|
||||
func List(repo name.Repository, options ...Option) ([]string, error) {
|
||||
o, err := makeOptions(options...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newPuller(o).List(o.context, repo)
|
||||
}
|
||||
|
||||
type Tags struct {
|
||||
Name string `json:"name"`
|
||||
Tags []string `json:"tags"`
|
||||
Next string `json:"next,omitempty"`
|
||||
}
|
||||
|
||||
func (f *fetcher) listPage(ctx context.Context, repo name.Repository, next string, pageSize int) (*Tags, error) {
|
||||
if next == "" {
|
||||
uri := &url.URL{
|
||||
Scheme: repo.Scheme(),
|
||||
Host: repo.RegistryStr(),
|
||||
Path: fmt.Sprintf("/v2/%s/tags/list", repo.RepositoryStr()),
|
||||
}
|
||||
if pageSize > 0 {
|
||||
uri.RawQuery = fmt.Sprintf("n=%d", pageSize)
|
||||
}
|
||||
next = uri.String()
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", next, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := f.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := transport.CheckError(resp, http.StatusOK); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
parsed := Tags{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&parsed); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := resp.Body.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
uri, err := getNextPageURL(resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if uri != nil {
|
||||
parsed.Next = uri.String()
|
||||
}
|
||||
|
||||
return &parsed, nil
|
||||
}
|
||||
|
||||
// getNextPageURL checks if there is a Link header in a http.Response which
|
||||
// contains a link to the next page. If yes it returns the url.URL of the next
|
||||
// page otherwise it returns nil.
|
||||
func getNextPageURL(resp *http.Response) (*url.URL, error) {
|
||||
link := resp.Header.Get("Link")
|
||||
if link == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if link[0] != '<' {
|
||||
return nil, fmt.Errorf("failed to parse link header: missing '<' in: %s", link)
|
||||
}
|
||||
|
||||
end := strings.Index(link, ">")
|
||||
if end == -1 {
|
||||
return nil, fmt.Errorf("failed to parse link header: missing '>' in: %s", link)
|
||||
}
|
||||
link = link[1:end]
|
||||
|
||||
linkURL, err := url.Parse(link)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.Request == nil || resp.Request.URL == nil {
|
||||
return nil, nil
|
||||
}
|
||||
linkURL = resp.Request.URL.ResolveReference(linkURL)
|
||||
return linkURL, nil
|
||||
}
|
||||
|
||||
type Lister struct {
|
||||
f *fetcher
|
||||
repo name.Repository
|
||||
pageSize int
|
||||
|
||||
page *Tags
|
||||
err error
|
||||
|
||||
needMore bool
|
||||
}
|
||||
|
||||
func (l *Lister) Next(ctx context.Context) (*Tags, error) {
|
||||
if l.needMore {
|
||||
l.page, l.err = l.f.listPage(ctx, l.repo, l.page.Next, l.pageSize)
|
||||
} else {
|
||||
l.needMore = true
|
||||
}
|
||||
return l.page, l.err
|
||||
}
|
||||
|
||||
func (l *Lister) HasNext() bool {
|
||||
return l.page != nil && (!l.needMore || l.page.Next != "")
|
||||
}
|
||||
-108
@@ -1,108 +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 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
|
||||
// "mounted" when published to another registry.
|
||||
type MountableLayer struct {
|
||||
v1.Layer
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// Exists is a hack. See partial.Exists.
|
||||
func (ml *MountableLayer) Exists() (bool, error) {
|
||||
return partial.Exists(ml.Layer)
|
||||
}
|
||||
|
||||
// mountableImage wraps the v1.Layer references returned by the embedded v1.Image
|
||||
// in MountableLayer's so that remote.Write might attempt to mount them from their
|
||||
// source repository.
|
||||
type mountableImage struct {
|
||||
v1.Image
|
||||
|
||||
Reference name.Reference
|
||||
}
|
||||
|
||||
// Layers implements v1.Image
|
||||
func (mi *mountableImage) Layers() ([]v1.Layer, error) {
|
||||
ls, err := mi.Image.Layers()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mls := make([]v1.Layer, 0, len(ls))
|
||||
for _, l := range ls {
|
||||
mls = append(mls, &MountableLayer{
|
||||
Layer: l,
|
||||
Reference: mi.Reference,
|
||||
})
|
||||
}
|
||||
return mls, nil
|
||||
}
|
||||
|
||||
// LayerByDigest implements v1.Image
|
||||
func (mi *mountableImage) LayerByDigest(d v1.Hash) (v1.Layer, error) {
|
||||
l, err := mi.Image.LayerByDigest(d)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &MountableLayer{
|
||||
Layer: l,
|
||||
Reference: mi.Reference,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// LayerByDiffID implements v1.Image
|
||||
func (mi *mountableImage) LayerByDiffID(d v1.Hash) (v1.Layer, error) {
|
||||
l, err := mi.Image.LayerByDiffID(d)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &MountableLayer{
|
||||
Layer: l,
|
||||
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)
|
||||
}
|
||||
|
||||
// ConfigLayer retains the original reference so that it can be mounted.
|
||||
// See partial.ConfigLayer.
|
||||
func (mi *mountableImage) ConfigLayer() (v1.Layer, error) {
|
||||
l, err := partial.ConfigLayer(mi.Image)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &MountableLayer{
|
||||
Layer: l,
|
||||
Reference: mi.Reference,
|
||||
}, nil
|
||||
}
|
||||
-46
@@ -1,46 +0,0 @@
|
||||
// Copyright 2020 Google LLC All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package remote
|
||||
|
||||
import (
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
// MultiWrite writes the given Images or ImageIndexes to the given refs, as
|
||||
// efficiently as possible, by deduping shared layer blobs while uploading them
|
||||
// in parallel.
|
||||
func MultiWrite(todo map[name.Reference]Taggable, options ...Option) (rerr error) {
|
||||
o, err := makeOptions(options...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if o.progress != nil {
|
||||
defer func() { o.progress.Close(rerr) }()
|
||||
}
|
||||
p := newPusher(o)
|
||||
|
||||
g, ctx := errgroup.WithContext(o.context)
|
||||
g.SetLimit(o.jobs)
|
||||
|
||||
for ref, t := range todo {
|
||||
ref, t := ref, t
|
||||
g.Go(func() error {
|
||||
return p.Push(ctx, ref, t)
|
||||
})
|
||||
}
|
||||
|
||||
return g.Wait()
|
||||
}
|
||||
-354
@@ -1,354 +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 remote
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-containerregistry/internal/retry"
|
||||
"github.com/google/go-containerregistry/pkg/authn"
|
||||
"github.com/google/go-containerregistry/pkg/logs"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/remote/transport"
|
||||
)
|
||||
|
||||
// Option is a functional option for remote operations.
|
||||
type Option func(*options) error
|
||||
|
||||
type options struct {
|
||||
auth authn.Authenticator
|
||||
keychain authn.Keychain
|
||||
transport http.RoundTripper
|
||||
context context.Context
|
||||
jobs int
|
||||
userAgent string
|
||||
allowNondistributableArtifacts bool
|
||||
progress *progress
|
||||
retryBackoff Backoff
|
||||
retryPredicate retry.Predicate
|
||||
retryStatusCodes []int
|
||||
|
||||
// Only these options can overwrite Reuse()d options.
|
||||
platform v1.Platform
|
||||
pageSize int
|
||||
filter map[string]string
|
||||
|
||||
// Set by Reuse, we currently store one or the other.
|
||||
puller *Puller
|
||||
pusher *Pusher
|
||||
}
|
||||
|
||||
var defaultPlatform = v1.Platform{
|
||||
Architecture: "amd64",
|
||||
OS: "linux",
|
||||
}
|
||||
|
||||
// Backoff is an alias of retry.Backoff to expose this configuration option to consumers of this lib
|
||||
type Backoff = retry.Backoff
|
||||
|
||||
var defaultRetryPredicate retry.Predicate = func(err error) bool {
|
||||
// Various failure modes here, as we're often reading from and writing to
|
||||
// the network.
|
||||
if retry.IsTemporary(err) || errors.Is(err, io.ErrUnexpectedEOF) || errors.Is(err, io.EOF) || errors.Is(err, syscall.EPIPE) || errors.Is(err, syscall.ECONNRESET) || errors.Is(err, net.ErrClosed) {
|
||||
logs.Warn.Printf("retrying %v", err)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Try this three times, waiting 1s after first failure, 3s after second.
|
||||
var defaultRetryBackoff = Backoff{
|
||||
Duration: 1.0 * time.Second,
|
||||
Factor: 3.0,
|
||||
Jitter: 0.1,
|
||||
Steps: 3,
|
||||
}
|
||||
|
||||
// Useful for tests
|
||||
var fastBackoff = Backoff{
|
||||
Duration: 1.0 * time.Millisecond,
|
||||
Factor: 3.0,
|
||||
Jitter: 0.1,
|
||||
Steps: 3,
|
||||
}
|
||||
|
||||
var defaultRetryStatusCodes = []int{
|
||||
http.StatusRequestTimeout,
|
||||
http.StatusInternalServerError,
|
||||
http.StatusBadGateway,
|
||||
http.StatusServiceUnavailable,
|
||||
http.StatusGatewayTimeout,
|
||||
499, // nginx-specific, client closed request
|
||||
522, // Cloudflare-specific, connection timeout
|
||||
}
|
||||
|
||||
const (
|
||||
defaultJobs = 4
|
||||
|
||||
// ECR returns an error if n > 1000:
|
||||
// https://github.com/google/go-containerregistry/issues/1091
|
||||
defaultPageSize = 1000
|
||||
)
|
||||
|
||||
// DefaultTransport is based on http.DefaultTransport with modifications
|
||||
// documented inline below.
|
||||
var DefaultTransport http.RoundTripper = &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
}).DialContext,
|
||||
ForceAttemptHTTP2: true,
|
||||
MaxIdleConns: 100,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
// We usually are dealing with 2 hosts (at most), split MaxIdleConns between them.
|
||||
MaxIdleConnsPerHost: 50,
|
||||
}
|
||||
|
||||
func makeOptions(opts ...Option) (*options, error) {
|
||||
o := &options{
|
||||
transport: DefaultTransport,
|
||||
platform: defaultPlatform,
|
||||
context: context.Background(),
|
||||
jobs: defaultJobs,
|
||||
pageSize: defaultPageSize,
|
||||
retryPredicate: defaultRetryPredicate,
|
||||
retryBackoff: defaultRetryBackoff,
|
||||
retryStatusCodes: defaultRetryStatusCodes,
|
||||
}
|
||||
|
||||
for _, option := range opts {
|
||||
if err := option(o); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
switch {
|
||||
case o.auth != nil && o.keychain != nil:
|
||||
// It is a better experience to explicitly tell a caller their auth is misconfigured
|
||||
// than potentially fail silently when the correct auth is overridden by option misuse.
|
||||
return nil, errors.New("provide an option for either authn.Authenticator or authn.Keychain, not both")
|
||||
case o.auth == nil:
|
||||
o.auth = authn.Anonymous
|
||||
}
|
||||
|
||||
// transport.Wrapper is a signal that consumers are opt-ing into providing their own transport without any additional wrapping.
|
||||
// This is to allow consumers full control over the transports logic, such as providing retry logic.
|
||||
if _, ok := o.transport.(*transport.Wrapper); !ok {
|
||||
// Wrap the transport in something that logs requests and responses.
|
||||
// It's expensive to generate the dumps, so skip it if we're writing
|
||||
// to nothing.
|
||||
if logs.Enabled(logs.Debug) {
|
||||
o.transport = transport.NewLogger(o.transport)
|
||||
}
|
||||
|
||||
// Using customized retry predicate if provided, and fallback to default if not.
|
||||
predicate := o.retryPredicate
|
||||
if predicate == nil {
|
||||
predicate = defaultRetryPredicate
|
||||
}
|
||||
|
||||
// Wrap the transport in something that can retry network flakes.
|
||||
o.transport = transport.NewRetry(o.transport, transport.WithRetryBackoff(o.retryBackoff), transport.WithRetryPredicate(predicate), transport.WithRetryStatusCodes(o.retryStatusCodes...))
|
||||
// Wrap this last to prevent transport.New from double-wrapping.
|
||||
if o.userAgent != "" {
|
||||
o.transport = transport.NewUserAgent(o.transport, o.userAgent)
|
||||
}
|
||||
}
|
||||
|
||||
return o, nil
|
||||
}
|
||||
|
||||
// WithTransport is a functional option for overriding the default transport
|
||||
// for remote operations.
|
||||
// If transport.Wrapper is provided, this signals that the consumer does *not* want any further wrapping to occur.
|
||||
// i.e. logging, retry and useragent
|
||||
//
|
||||
// The default transport is DefaultTransport.
|
||||
func WithTransport(t http.RoundTripper) Option {
|
||||
return func(o *options) error {
|
||||
o.transport = t
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithAuth is a functional option for overriding the default authenticator
|
||||
// for remote operations.
|
||||
// It is an error to use both WithAuth and WithAuthFromKeychain in the same Option set.
|
||||
//
|
||||
// The default authenticator is authn.Anonymous.
|
||||
func WithAuth(auth authn.Authenticator) Option {
|
||||
return func(o *options) error {
|
||||
o.auth = auth
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithAuthFromKeychain is a functional option for overriding the default
|
||||
// authenticator for remote operations, using an authn.Keychain to find
|
||||
// credentials.
|
||||
// It is an error to use both WithAuth and WithAuthFromKeychain in the same Option set.
|
||||
//
|
||||
// The default authenticator is authn.Anonymous.
|
||||
func WithAuthFromKeychain(keys authn.Keychain) Option {
|
||||
return func(o *options) error {
|
||||
o.keychain = keys
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithPlatform is a functional option for overriding the default platform
|
||||
// that Image and Descriptor.Image use for resolving an index to an image.
|
||||
//
|
||||
// The default platform is amd64/linux.
|
||||
func WithPlatform(p v1.Platform) Option {
|
||||
return func(o *options) error {
|
||||
o.platform = p
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithContext is a functional option for setting the context in http requests
|
||||
// performed by a given function. Note that this context is used for _all_
|
||||
// http requests, not just the initial volley. E.g., for remote.Image, the
|
||||
// context will be set on http requests generated by subsequent calls to
|
||||
// RawConfigFile() and even methods on layers returned by Layers().
|
||||
//
|
||||
// The default context is context.Background().
|
||||
func WithContext(ctx context.Context) Option {
|
||||
return func(o *options) error {
|
||||
o.context = ctx
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithJobs is a functional option for setting the parallelism of remote
|
||||
// operations performed by a given function. Note that not all remote
|
||||
// operations support parallelism.
|
||||
//
|
||||
// The default value is 4.
|
||||
func WithJobs(jobs int) Option {
|
||||
return func(o *options) error {
|
||||
if jobs <= 0 {
|
||||
return errors.New("jobs must be greater than zero")
|
||||
}
|
||||
o.jobs = jobs
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithUserAgent adds the given string to the User-Agent header for any HTTP
|
||||
// requests. This header will also include "go-containerregistry/${version}".
|
||||
//
|
||||
// If you want to completely overwrite the User-Agent header, use WithTransport.
|
||||
func WithUserAgent(ua string) Option {
|
||||
return func(o *options) error {
|
||||
o.userAgent = ua
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithNondistributable includes non-distributable (foreign) layers
|
||||
// when writing images, see:
|
||||
// https://github.com/opencontainers/image-spec/blob/master/layer.md#non-distributable-layers
|
||||
//
|
||||
// The default behaviour is to skip these layers
|
||||
func WithNondistributable(o *options) error {
|
||||
o.allowNondistributableArtifacts = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// WithProgress takes a channel that will receive progress updates as bytes are written.
|
||||
//
|
||||
// Sending updates to an unbuffered channel will block writes, so callers
|
||||
// should provide a buffered channel to avoid potential deadlocks.
|
||||
func WithProgress(updates chan<- v1.Update) Option {
|
||||
return func(o *options) error {
|
||||
o.progress = &progress{updates: updates}
|
||||
o.progress.lastUpdate = &v1.Update{}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithPageSize sets the given size as the value of parameter 'n' in the request.
|
||||
//
|
||||
// To omit the `n` parameter entirely, use WithPageSize(0).
|
||||
// The default value is 1000.
|
||||
func WithPageSize(size int) Option {
|
||||
return func(o *options) error {
|
||||
o.pageSize = size
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithRetryBackoff sets the httpBackoff for retry HTTP operations.
|
||||
func WithRetryBackoff(backoff Backoff) Option {
|
||||
return func(o *options) error {
|
||||
o.retryBackoff = backoff
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithRetryPredicate sets the predicate for retry HTTP operations.
|
||||
func WithRetryPredicate(predicate retry.Predicate) Option {
|
||||
return func(o *options) error {
|
||||
o.retryPredicate = predicate
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithRetryStatusCodes sets which http response codes will be retried.
|
||||
func WithRetryStatusCodes(codes ...int) Option {
|
||||
return func(o *options) error {
|
||||
o.retryStatusCodes = codes
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithFilter sets the filter querystring for HTTP operations.
|
||||
func WithFilter(key string, value string) Option {
|
||||
return func(o *options) error {
|
||||
if o.filter == nil {
|
||||
o.filter = map[string]string{}
|
||||
}
|
||||
o.filter[key] = value
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Reuse takes a Puller or Pusher and reuses it for remote interactions
|
||||
// rather than starting from a clean slate. For example, it will reuse token exchanges
|
||||
// when possible and avoid sending redundant HEAD requests.
|
||||
//
|
||||
// Reuse will take precedence over other options passed to most remote functions because
|
||||
// most options deal with setting up auth and transports, which Reuse intetionally skips.
|
||||
func Reuse[I *Puller | *Pusher](i I) Option {
|
||||
return func(o *options) error {
|
||||
if puller, ok := any(i).(*Puller); ok {
|
||||
o.puller = puller
|
||||
} else if pusher, ok := any(i).(*Pusher); ok {
|
||||
o.pusher = pusher
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
-76
@@ -1,76 +0,0 @@
|
||||
// Copyright 2022 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"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
)
|
||||
|
||||
type progress struct {
|
||||
sync.Mutex
|
||||
updates chan<- v1.Update
|
||||
lastUpdate *v1.Update
|
||||
}
|
||||
|
||||
func (p *progress) total(delta int64) {
|
||||
p.Lock()
|
||||
defer p.Unlock()
|
||||
atomic.AddInt64(&p.lastUpdate.Total, delta)
|
||||
}
|
||||
|
||||
func (p *progress) complete(delta int64) {
|
||||
p.Lock()
|
||||
defer p.Unlock()
|
||||
p.updates <- v1.Update{
|
||||
Total: p.lastUpdate.Total,
|
||||
Complete: atomic.AddInt64(&p.lastUpdate.Complete, delta),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *progress) err(err error) error {
|
||||
if err != nil && p.updates != nil {
|
||||
p.updates <- v1.Update{Error: err}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *progress) Close(err error) {
|
||||
_ = p.err(err)
|
||||
close(p.updates)
|
||||
}
|
||||
|
||||
type progressReader struct {
|
||||
rc io.ReadCloser
|
||||
|
||||
count *int64 // number of bytes this reader has read, to support resetting on retry.
|
||||
progress *progress
|
||||
}
|
||||
|
||||
func (r *progressReader) Read(b []byte) (int, error) {
|
||||
n, err := r.rc.Read(b)
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
atomic.AddInt64(r.count, int64(n))
|
||||
// TODO: warn/debug log if sending takes too long, or if sending is blocked while context is canceled.
|
||||
r.progress.complete(int64(n))
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (r *progressReader) Close() error { return r.rc.Close() }
|
||||
-222
@@ -1,222 +0,0 @@
|
||||
// Copyright 2023 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"
|
||||
"sync"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
type Puller struct {
|
||||
o *options
|
||||
|
||||
// map[resource]*reader
|
||||
readers sync.Map
|
||||
}
|
||||
|
||||
func NewPuller(options ...Option) (*Puller, error) {
|
||||
o, err := makeOptions(options...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return newPuller(o), nil
|
||||
}
|
||||
|
||||
func newPuller(o *options) *Puller {
|
||||
if o.puller != nil {
|
||||
return o.puller
|
||||
}
|
||||
return &Puller{
|
||||
o: o,
|
||||
}
|
||||
}
|
||||
|
||||
type reader struct {
|
||||
// in
|
||||
target resource
|
||||
o *options
|
||||
|
||||
// f()
|
||||
once sync.Once
|
||||
|
||||
// out
|
||||
f *fetcher
|
||||
err error
|
||||
}
|
||||
|
||||
// this will run once per reader instance
|
||||
func (r *reader) init(ctx context.Context) error {
|
||||
r.once.Do(func() {
|
||||
r.f, r.err = makeFetcher(ctx, r.target, r.o)
|
||||
})
|
||||
return r.err
|
||||
}
|
||||
|
||||
func (p *Puller) fetcher(ctx context.Context, target resource) (*fetcher, error) {
|
||||
v, _ := p.readers.LoadOrStore(target, &reader{
|
||||
target: target,
|
||||
o: p.o,
|
||||
})
|
||||
rr := v.(*reader)
|
||||
return rr.f, rr.init(ctx)
|
||||
}
|
||||
|
||||
// Head is like remote.Head, but avoids re-authenticating when possible.
|
||||
func (p *Puller) Head(ctx context.Context, ref name.Reference) (*v1.Descriptor, error) {
|
||||
f, err := p.fetcher(ctx, ref.Context())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return f.headManifest(ctx, ref, allManifestMediaTypes)
|
||||
}
|
||||
|
||||
// Get is like remote.Get, but avoids re-authenticating when possible.
|
||||
func (p *Puller) Get(ctx context.Context, ref name.Reference) (*Descriptor, error) {
|
||||
return p.get(ctx, ref, allManifestMediaTypes, p.o.platform)
|
||||
}
|
||||
|
||||
func (p *Puller) get(ctx context.Context, ref name.Reference, acceptable []types.MediaType, platform v1.Platform) (*Descriptor, error) {
|
||||
f, err := p.fetcher(ctx, ref.Context())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return f.get(ctx, ref, acceptable, platform)
|
||||
}
|
||||
|
||||
// Layer is like remote.Layer, but avoids re-authenticating when possible.
|
||||
func (p *Puller) Layer(ctx context.Context, ref name.Digest) (v1.Layer, error) {
|
||||
f, err := p.fetcher(ctx, ref.Context())
|
||||
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,
|
||||
ctx: ctx,
|
||||
digest: h,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &MountableLayer{
|
||||
Layer: l,
|
||||
Reference: ref,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// List lists tags in a repo and handles pagination, returning the full list of tags.
|
||||
func (p *Puller) List(ctx context.Context, repo name.Repository) ([]string, error) {
|
||||
lister, err := p.Lister(ctx, repo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tagList := []string{}
|
||||
for lister.HasNext() {
|
||||
tags, err := lister.Next(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tagList = append(tagList, tags.Tags...)
|
||||
}
|
||||
|
||||
return tagList, nil
|
||||
}
|
||||
|
||||
// Lister lists tags in a repo and returns a Lister for paginating through the results.
|
||||
func (p *Puller) Lister(ctx context.Context, repo name.Repository) (*Lister, error) {
|
||||
return p.lister(ctx, repo, p.o.pageSize)
|
||||
}
|
||||
|
||||
func (p *Puller) lister(ctx context.Context, repo name.Repository, pageSize int) (*Lister, error) {
|
||||
f, err := p.fetcher(ctx, repo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
page, err := f.listPage(ctx, repo, "", pageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Lister{
|
||||
f: f,
|
||||
repo: repo,
|
||||
pageSize: pageSize,
|
||||
page: page,
|
||||
err: err,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Catalog lists repos in a registry and handles pagination, returning the full list of repos.
|
||||
func (p *Puller) Catalog(ctx context.Context, reg name.Registry) ([]string, error) {
|
||||
return p.catalog(ctx, reg, p.o.pageSize)
|
||||
}
|
||||
|
||||
func (p *Puller) catalog(ctx context.Context, reg name.Registry, pageSize int) ([]string, error) {
|
||||
catalogger, err := p.catalogger(ctx, reg, pageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
repoList := []string{}
|
||||
for catalogger.HasNext() {
|
||||
repos, err := catalogger.Next(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
repoList = append(repoList, repos.Repos...)
|
||||
}
|
||||
return repoList, nil
|
||||
}
|
||||
|
||||
// Catalogger lists repos in a registry and returns a Catalogger for paginating through the results.
|
||||
func (p *Puller) Catalogger(ctx context.Context, reg name.Registry) (*Catalogger, error) {
|
||||
return p.catalogger(ctx, reg, p.o.pageSize)
|
||||
}
|
||||
|
||||
func (p *Puller) catalogger(ctx context.Context, reg name.Registry, pageSize int) (*Catalogger, error) {
|
||||
f, err := p.fetcher(ctx, reg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
page, err := f.catalogPage(ctx, reg, "", pageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Catalogger{
|
||||
f: f,
|
||||
reg: reg,
|
||||
pageSize: pageSize,
|
||||
page: page,
|
||||
err: err,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *Puller) referrers(ctx context.Context, d name.Digest, filter map[string]string) (v1.ImageIndex, error) {
|
||||
f, err := p.fetcher(ctx, d.Context())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return f.fetchReferrers(ctx, filter, d)
|
||||
}
|
||||
-573
@@ -1,573 +0,0 @@
|
||||
// Copyright 2023 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 (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sync"
|
||||
|
||||
"github.com/google/go-containerregistry/pkg/logs"
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/partial"
|
||||
"github.com/google/go-containerregistry/pkg/v1/remote/transport"
|
||||
"github.com/google/go-containerregistry/pkg/v1/stream"
|
||||
"github.com/google/go-containerregistry/pkg/v1/types"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
type manifest interface {
|
||||
Taggable
|
||||
partial.Describable
|
||||
}
|
||||
|
||||
// key is either v1.Hash or v1.Layer (for stream.Layer)
|
||||
type workers struct {
|
||||
// map[v1.Hash|v1.Layer]*sync.Once
|
||||
onces sync.Map
|
||||
|
||||
// map[v1.Hash|v1.Layer]error
|
||||
errors sync.Map
|
||||
}
|
||||
|
||||
func nop() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *workers) err(digest v1.Hash) error {
|
||||
v, ok := w.errors.Load(digest)
|
||||
if !ok || v == nil {
|
||||
return nil
|
||||
}
|
||||
return v.(error)
|
||||
}
|
||||
|
||||
func (w *workers) Do(digest v1.Hash, f func() error) error {
|
||||
// We don't care if it was loaded or not because the sync.Once will do it for us.
|
||||
once, _ := w.onces.LoadOrStore(digest, &sync.Once{})
|
||||
|
||||
once.(*sync.Once).Do(func() {
|
||||
w.errors.Store(digest, f())
|
||||
})
|
||||
|
||||
err := w.err(digest)
|
||||
if err != nil {
|
||||
// Allow this to be retried by another caller.
|
||||
w.onces.Delete(digest)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (w *workers) Stream(layer v1.Layer, f func() error) error {
|
||||
// We don't care if it was loaded or not because the sync.Once will do it for us.
|
||||
once, _ := w.onces.LoadOrStore(layer, &sync.Once{})
|
||||
|
||||
once.(*sync.Once).Do(func() {
|
||||
w.errors.Store(layer, f())
|
||||
})
|
||||
|
||||
v, ok := w.errors.Load(layer)
|
||||
if !ok || v == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return v.(error)
|
||||
}
|
||||
|
||||
type Pusher struct {
|
||||
o *options
|
||||
|
||||
// map[name.Repository]*repoWriter
|
||||
writers sync.Map
|
||||
}
|
||||
|
||||
func NewPusher(options ...Option) (*Pusher, error) {
|
||||
o, err := makeOptions(options...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return newPusher(o), nil
|
||||
}
|
||||
|
||||
func newPusher(o *options) *Pusher {
|
||||
if o.pusher != nil {
|
||||
return o.pusher
|
||||
}
|
||||
return &Pusher{
|
||||
o: o,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Pusher) writer(ctx context.Context, repo name.Repository, o *options) (*repoWriter, error) {
|
||||
v, _ := p.writers.LoadOrStore(repo, &repoWriter{
|
||||
repo: repo,
|
||||
o: o,
|
||||
})
|
||||
rw := v.(*repoWriter)
|
||||
return rw, rw.init(ctx)
|
||||
}
|
||||
|
||||
func (p *Pusher) Put(ctx context.Context, ref name.Reference, t Taggable) error {
|
||||
w, err := p.writer(ctx, ref.Context(), p.o)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m, err := taggableToManifest(t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return w.commitManifest(ctx, ref, m)
|
||||
}
|
||||
|
||||
func (p *Pusher) Push(ctx context.Context, ref name.Reference, t Taggable) error {
|
||||
w, err := p.writer(ctx, ref.Context(), p.o)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return w.writeManifest(ctx, ref, t)
|
||||
}
|
||||
|
||||
func (p *Pusher) Upload(ctx context.Context, repo name.Repository, l v1.Layer) error {
|
||||
w, err := p.writer(ctx, repo, p.o)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return w.writeLayer(ctx, l)
|
||||
}
|
||||
|
||||
func (p *Pusher) Delete(ctx context.Context, ref name.Reference) error {
|
||||
w, err := p.writer(ctx, ref.Context(), p.o)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
u := url.URL{
|
||||
Scheme: ref.Context().Scheme(),
|
||||
Host: ref.Context().RegistryStr(),
|
||||
Path: fmt.Sprintf("/v2/%s/manifests/%s", ref.Context().RepositoryStr(), ref.Identifier()),
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodDelete, u.String(), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := w.w.client.Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
return transport.CheckError(resp, http.StatusOK, http.StatusAccepted)
|
||||
|
||||
// TODO(jason): If the manifest had a `subject`, and if the registry
|
||||
// doesn't support Referrers, update the index pointed to by the
|
||||
// subject's fallback tag to remove the descriptor for this manifest.
|
||||
}
|
||||
|
||||
type repoWriter struct {
|
||||
repo name.Repository
|
||||
o *options
|
||||
once sync.Once
|
||||
|
||||
w *writer
|
||||
err error
|
||||
|
||||
work *workers
|
||||
}
|
||||
|
||||
// this will run once per repoWriter instance
|
||||
func (rw *repoWriter) init(ctx context.Context) error {
|
||||
rw.once.Do(func() {
|
||||
rw.work = &workers{}
|
||||
rw.w, rw.err = makeWriter(ctx, rw.repo, nil, rw.o)
|
||||
})
|
||||
return rw.err
|
||||
}
|
||||
|
||||
func (rw *repoWriter) writeDeps(ctx context.Context, m manifest) error {
|
||||
if img, ok := m.(v1.Image); ok {
|
||||
return rw.writeLayers(ctx, img)
|
||||
}
|
||||
|
||||
if idx, ok := m.(v1.ImageIndex); ok {
|
||||
return rw.writeChildren(ctx, idx)
|
||||
}
|
||||
|
||||
// This has no deps, not an error (e.g. something you want to just PUT).
|
||||
return nil
|
||||
}
|
||||
|
||||
type describable struct {
|
||||
desc v1.Descriptor
|
||||
}
|
||||
|
||||
func (d describable) Digest() (v1.Hash, error) {
|
||||
return d.desc.Digest, nil
|
||||
}
|
||||
|
||||
func (d describable) Size() (int64, error) {
|
||||
return d.desc.Size, nil
|
||||
}
|
||||
|
||||
func (d describable) MediaType() (types.MediaType, error) {
|
||||
return d.desc.MediaType, nil
|
||||
}
|
||||
|
||||
type tagManifest struct {
|
||||
Taggable
|
||||
partial.Describable
|
||||
}
|
||||
|
||||
func taggableToManifest(t Taggable) (manifest, error) {
|
||||
if m, ok := t.(manifest); ok {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
if d, ok := t.(*Descriptor); ok {
|
||||
if d.MediaType.IsIndex() {
|
||||
return d.ImageIndex()
|
||||
}
|
||||
|
||||
if d.MediaType.IsImage() {
|
||||
return d.Image()
|
||||
}
|
||||
|
||||
if d.MediaType.IsSchema1() {
|
||||
return d.Schema1()
|
||||
}
|
||||
|
||||
return tagManifest{t, describable{d.toDesc()}}, nil
|
||||
}
|
||||
|
||||
desc := v1.Descriptor{
|
||||
// A reasonable default if Taggable doesn't implement MediaType.
|
||||
MediaType: types.DockerManifestSchema2,
|
||||
}
|
||||
|
||||
b, err := t.RawManifest()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if wmt, ok := t.(withMediaType); ok {
|
||||
desc.MediaType, err = wmt.MediaType()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
desc.Digest, desc.Size, err = v1.SHA256(bytes.NewReader(b))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tagManifest{t, describable{desc}}, nil
|
||||
}
|
||||
|
||||
func (rw *repoWriter) writeManifest(ctx context.Context, ref name.Reference, t Taggable) error {
|
||||
m, err := taggableToManifest(t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
needDeps := true
|
||||
|
||||
digest, err := m.Digest()
|
||||
if errors.Is(err, stream.ErrNotComputed) {
|
||||
if err := rw.writeDeps(ctx, m); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
needDeps = false
|
||||
|
||||
digest, err = m.Digest()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// This may be a lazy child where we have no ref until digest is computed.
|
||||
if ref == nil {
|
||||
ref = rw.repo.Digest(digest.String())
|
||||
}
|
||||
|
||||
// For tags, we want to do this check outside of our Work.Do closure because
|
||||
// we don't want to dedupe based on the manifest digest.
|
||||
_, byTag := ref.(name.Tag)
|
||||
if byTag {
|
||||
if exists, err := rw.manifestExists(ctx, ref, t); err != nil {
|
||||
return err
|
||||
} else if exists {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// The following work.Do will get deduped by digest, so it won't happen unless
|
||||
// this tag happens to be the first commitManifest to run for that digest.
|
||||
needPut := byTag
|
||||
|
||||
if err := rw.work.Do(digest, func() error {
|
||||
if !byTag {
|
||||
if exists, err := rw.manifestExists(ctx, ref, t); err != nil {
|
||||
return err
|
||||
} else if exists {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if needDeps {
|
||||
if err := rw.writeDeps(ctx, m); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
needPut = false
|
||||
return rw.commitManifest(ctx, ref, m)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !needPut {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Only runs for tags that got deduped by digest.
|
||||
return rw.commitManifest(ctx, ref, m)
|
||||
}
|
||||
|
||||
func (rw *repoWriter) writeChildren(ctx context.Context, idx v1.ImageIndex) error {
|
||||
children, err := partial.Manifests(idx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
g, ctx := errgroup.WithContext(ctx)
|
||||
g.SetLimit(rw.o.jobs)
|
||||
|
||||
for _, child := range children {
|
||||
child := child
|
||||
if err := rw.writeChild(ctx, child, g); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return g.Wait()
|
||||
}
|
||||
|
||||
func (rw *repoWriter) writeChild(ctx context.Context, child partial.Describable, g *errgroup.Group) error {
|
||||
switch child := child.(type) {
|
||||
case v1.ImageIndex:
|
||||
// For recursive index, we want to do a depth-first launching of goroutines
|
||||
// to avoid deadlocking.
|
||||
//
|
||||
// Note that this is rare, so the impact of this should be really small.
|
||||
return rw.writeManifest(ctx, nil, child)
|
||||
case v1.Image:
|
||||
g.Go(func() error {
|
||||
return rw.writeManifest(ctx, nil, child)
|
||||
})
|
||||
case v1.Layer:
|
||||
g.Go(func() error {
|
||||
return rw.writeLayer(ctx, child)
|
||||
})
|
||||
default:
|
||||
// This can't happen.
|
||||
return fmt.Errorf("encountered unknown child: %T", child)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO: Consider caching some representation of the tags/digests in the destination
|
||||
// repository as a hint to avoid this optimistic check in cases where we will most
|
||||
// likely have to do a PUT anyway, e.g. if we are overwriting a tag we just wrote.
|
||||
func (rw *repoWriter) manifestExists(ctx context.Context, ref name.Reference, t Taggable) (bool, error) {
|
||||
f := &fetcher{
|
||||
target: ref.Context(),
|
||||
client: rw.w.client,
|
||||
}
|
||||
|
||||
m, err := taggableToManifest(t)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
digest, err := m.Digest()
|
||||
if err != nil {
|
||||
// Possibly due to streaming layers.
|
||||
return false, nil
|
||||
}
|
||||
got, err := f.headManifest(ctx, ref, allManifestMediaTypes)
|
||||
if err != nil {
|
||||
var terr *transport.Error
|
||||
if errors.As(err, &terr) {
|
||||
if terr.StatusCode == http.StatusNotFound {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// We treat a 403 here as non-fatal because this existence check is an optimization and
|
||||
// some registries will return a 403 instead of a 404 in certain situations.
|
||||
// E.g. https://jfrog.atlassian.net/browse/RTFACT-13797
|
||||
if terr.StatusCode == http.StatusForbidden {
|
||||
logs.Debug.Printf("manifestExists unexpected 403: %v", err)
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, err
|
||||
}
|
||||
|
||||
if digest != got.Digest {
|
||||
// Mark that we saw this digest in the registry so we don't have to check it again.
|
||||
rw.work.Do(got.Digest, nop)
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if tag, ok := ref.(name.Tag); ok {
|
||||
logs.Progress.Printf("existing manifest: %s@%s", tag.Identifier(), got.Digest)
|
||||
} else {
|
||||
logs.Progress.Print("existing manifest: ", got.Digest)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (rw *repoWriter) commitManifest(ctx context.Context, ref name.Reference, m manifest) error {
|
||||
if rw.o.progress != nil {
|
||||
size, err := m.Size()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rw.o.progress.total(size)
|
||||
}
|
||||
|
||||
return rw.w.commitManifest(ctx, m, ref)
|
||||
}
|
||||
|
||||
func (rw *repoWriter) writeLayers(pctx context.Context, img v1.Image) error {
|
||||
ls, err := img.Layers()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
g, ctx := errgroup.WithContext(pctx)
|
||||
g.SetLimit(rw.o.jobs)
|
||||
|
||||
for _, l := range ls {
|
||||
l := l
|
||||
|
||||
g.Go(func() error {
|
||||
return rw.writeLayer(ctx, l)
|
||||
})
|
||||
}
|
||||
|
||||
mt, err := img.MediaType()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if mt.IsSchema1() {
|
||||
return g.Wait()
|
||||
}
|
||||
|
||||
cl, err := partial.ConfigLayer(img)
|
||||
if errors.Is(err, stream.ErrNotComputed) {
|
||||
if err := g.Wait(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cl, err := partial.ConfigLayer(img)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return rw.writeLayer(pctx, cl)
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
g.Go(func() error {
|
||||
return rw.writeLayer(ctx, cl)
|
||||
})
|
||||
|
||||
return g.Wait()
|
||||
}
|
||||
|
||||
func (rw *repoWriter) writeLayer(ctx context.Context, l v1.Layer) error {
|
||||
// Skip any non-distributable things.
|
||||
mt, err := l.MediaType()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !mt.IsDistributable() && !rw.o.allowNondistributableArtifacts {
|
||||
return nil
|
||||
}
|
||||
|
||||
digest, err := l.Digest()
|
||||
if err != nil {
|
||||
if errors.Is(err, stream.ErrNotComputed) {
|
||||
return rw.lazyWriteLayer(ctx, l)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return rw.work.Do(digest, func() error {
|
||||
if rw.o.progress != nil {
|
||||
size, err := l.Size()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rw.o.progress.total(size)
|
||||
}
|
||||
return rw.w.uploadOne(ctx, l)
|
||||
})
|
||||
}
|
||||
|
||||
func (rw *repoWriter) lazyWriteLayer(ctx context.Context, l v1.Layer) error {
|
||||
return rw.work.Stream(l, func() error {
|
||||
if err := rw.w.uploadOne(ctx, l); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Mark this upload completed.
|
||||
digest, err := l.Digest()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rw.work.Do(digest, nop)
|
||||
|
||||
if rw.o.progress != nil {
|
||||
size, err := l.Size()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rw.o.progress.total(size)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
-117
@@ -1,117 +0,0 @@
|
||||
// Copyright 2023 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 (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/empty"
|
||||
"github.com/google/go-containerregistry/pkg/v1/mutate"
|
||||
"github.com/google/go-containerregistry/pkg/v1/remote/transport"
|
||||
"github.com/google/go-containerregistry/pkg/v1/types"
|
||||
)
|
||||
|
||||
// Referrers returns a list of descriptors that refer to the given manifest digest.
|
||||
//
|
||||
// The subject manifest doesn't have to exist in the registry for there to be descriptors that refer to it.
|
||||
func Referrers(d name.Digest, options ...Option) (v1.ImageIndex, error) {
|
||||
o, err := makeOptions(options...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newPuller(o).referrers(o.context, d, o.filter)
|
||||
}
|
||||
|
||||
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#referrers-tag-schema
|
||||
func fallbackTag(d name.Digest) name.Tag {
|
||||
return d.Context().Tag(strings.Replace(d.DigestStr(), ":", "-", 1))
|
||||
}
|
||||
|
||||
func (f *fetcher) fetchReferrers(ctx context.Context, filter map[string]string, d name.Digest) (v1.ImageIndex, error) {
|
||||
// Check the Referrers API endpoint first.
|
||||
u := f.url("referrers", d.DigestStr())
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Accept", string(types.OCIImageIndex))
|
||||
|
||||
resp, err := f.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if err := transport.CheckError(resp, http.StatusOK, http.StatusNotFound, http.StatusBadRequest, http.StatusNotAcceptable); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var b []byte
|
||||
if resp.StatusCode == http.StatusOK && resp.Header.Get("Content-Type") == string(types.OCIImageIndex) {
|
||||
b, err = io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
// The registry doesn't support the Referrers API endpoint, so we'll use the fallback tag scheme.
|
||||
b, _, err = f.fetchManifest(ctx, fallbackTag(d), []types.MediaType{types.OCIImageIndex})
|
||||
var terr *transport.Error
|
||||
if errors.As(err, &terr) && terr.StatusCode == http.StatusNotFound {
|
||||
// Not found just means there are no attachments yet. Start with an empty manifest.
|
||||
return empty.Index, nil
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
h, sz, err := v1.SHA256(bytes.NewReader(b))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
idx := &remoteIndex{
|
||||
fetcher: *f,
|
||||
ctx: ctx,
|
||||
manifest: b,
|
||||
mediaType: types.OCIImageIndex,
|
||||
descriptor: &v1.Descriptor{
|
||||
Digest: h,
|
||||
MediaType: types.OCIImageIndex,
|
||||
Size: sz,
|
||||
},
|
||||
}
|
||||
return filterReferrersResponse(filter, idx), nil
|
||||
}
|
||||
|
||||
// If filter applied, filter out by artifactType.
|
||||
// See https://github.com/opencontainers/distribution-spec/blob/main/spec.md#listing-referrers
|
||||
func filterReferrersResponse(filter map[string]string, in v1.ImageIndex) v1.ImageIndex {
|
||||
if filter == nil {
|
||||
return in
|
||||
}
|
||||
v, ok := filter["artifactType"]
|
||||
if !ok {
|
||||
return in
|
||||
}
|
||||
return mutate.RemoveManifests(in, func(desc v1.Descriptor) bool {
|
||||
return desc.ArtifactType != v
|
||||
})
|
||||
}
|
||||
-118
@@ -1,118 +0,0 @@
|
||||
// Copyright 2023 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 (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
type schema1 struct {
|
||||
ref name.Reference
|
||||
ctx context.Context
|
||||
fetcher fetcher
|
||||
manifest []byte
|
||||
mediaType types.MediaType
|
||||
descriptor *v1.Descriptor
|
||||
}
|
||||
|
||||
func (s *schema1) Layers() ([]v1.Layer, error) {
|
||||
m := schema1Manifest{}
|
||||
if err := json.NewDecoder(bytes.NewReader(s.manifest)).Decode(&m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
layers := []v1.Layer{}
|
||||
for i := len(m.FSLayers) - 1; i >= 0; i-- {
|
||||
fsl := m.FSLayers[i]
|
||||
|
||||
h, err := v1.NewHash(fsl.BlobSum)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
l, err := s.LayerByDigest(h)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
layers = append(layers, l)
|
||||
}
|
||||
|
||||
return layers, nil
|
||||
}
|
||||
|
||||
func (s *schema1) MediaType() (types.MediaType, error) {
|
||||
return s.mediaType, nil
|
||||
}
|
||||
|
||||
func (s *schema1) Size() (int64, error) {
|
||||
return s.descriptor.Size, nil
|
||||
}
|
||||
|
||||
func (s *schema1) ConfigName() (v1.Hash, error) {
|
||||
return partial.ConfigName(s)
|
||||
}
|
||||
|
||||
func (s *schema1) ConfigFile() (*v1.ConfigFile, error) {
|
||||
return nil, newErrSchema1(s.mediaType)
|
||||
}
|
||||
|
||||
func (s *schema1) RawConfigFile() ([]byte, error) {
|
||||
return []byte("{}"), nil
|
||||
}
|
||||
|
||||
func (s *schema1) Digest() (v1.Hash, error) {
|
||||
return s.descriptor.Digest, nil
|
||||
}
|
||||
|
||||
func (s *schema1) Manifest() (*v1.Manifest, error) {
|
||||
return nil, newErrSchema1(s.mediaType)
|
||||
}
|
||||
|
||||
func (s *schema1) RawManifest() ([]byte, error) {
|
||||
return s.manifest, nil
|
||||
}
|
||||
|
||||
func (s *schema1) LayerByDigest(h v1.Hash) (v1.Layer, error) {
|
||||
l, err := partial.CompressedToLayer(&remoteLayer{
|
||||
fetcher: s.fetcher,
|
||||
ctx: s.ctx,
|
||||
digest: h,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &MountableLayer{
|
||||
Layer: l,
|
||||
Reference: s.ref.Context().Digest(h.String()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *schema1) LayerByDiffID(v1.Hash) (v1.Layer, error) {
|
||||
return nil, newErrSchema1(s.mediaType)
|
||||
}
|
||||
|
||||
type fslayer struct {
|
||||
BlobSum string `json:"blobSum"`
|
||||
}
|
||||
|
||||
type schema1Manifest struct {
|
||||
FSLayers []fslayer `json:"fsLayers"`
|
||||
}
|
||||
-129
@@ -1,129 +0,0 @@
|
||||
# `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**.
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
```
|
||||
-62
@@ -1,62 +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 transport
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/google/go-containerregistry/pkg/authn"
|
||||
)
|
||||
|
||||
type basicTransport struct {
|
||||
inner http.RoundTripper
|
||||
auth authn.Authenticator
|
||||
target string
|
||||
}
|
||||
|
||||
var _ http.RoundTripper = (*basicTransport)(nil)
|
||||
|
||||
// RoundTrip implements http.RoundTripper
|
||||
func (bt *basicTransport) RoundTrip(in *http.Request) (*http.Response, error) {
|
||||
if bt.auth != authn.Anonymous {
|
||||
auth, err := authn.Authorization(in.Context(), bt.auth)
|
||||
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 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
return bt.inner.RoundTrip(in)
|
||||
}
|
||||
-407
@@ -1,407 +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 transport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
authchallenge "github.com/docker/distribution/registry/client/auth/challenge"
|
||||
|
||||
"github.com/google/go-containerregistry/internal/redact"
|
||||
"github.com/google/go-containerregistry/pkg/authn"
|
||||
"github.com/google/go-containerregistry/pkg/logs"
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
)
|
||||
|
||||
type Token struct {
|
||||
Token string `json:"token"`
|
||||
AccessToken string `json:"access_token,omitempty"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
}
|
||||
|
||||
// Exchange requests a registry Token with the given scopes.
|
||||
func Exchange(ctx context.Context, reg name.Registry, auth authn.Authenticator, t http.RoundTripper, scopes []string, pr *Challenge) (*Token, error) {
|
||||
if strings.ToLower(pr.Scheme) != "bearer" {
|
||||
// TODO: Pretend token for basic?
|
||||
return nil, fmt.Errorf("challenge scheme %q is not bearer", pr.Scheme)
|
||||
}
|
||||
bt, err := fromChallenge(reg, auth, t, pr, scopes...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
authcfg, err := authn.Authorization(ctx, auth)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tok, err := bt.Refresh(ctx, authcfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tok, nil
|
||||
}
|
||||
|
||||
// FromToken returns a transport given a Challenge + Token.
|
||||
func FromToken(reg name.Registry, auth authn.Authenticator, t http.RoundTripper, pr *Challenge, tok *Token) (http.RoundTripper, error) {
|
||||
if strings.ToLower(pr.Scheme) != "bearer" {
|
||||
return &Wrapper{&basicTransport{inner: t, auth: auth, target: reg.RegistryStr()}}, nil
|
||||
}
|
||||
bt, err := fromChallenge(reg, auth, t, pr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tok.Token != "" {
|
||||
bt.bearer.RegistryToken = tok.Token
|
||||
}
|
||||
return &Wrapper{bt}, nil
|
||||
}
|
||||
|
||||
func fromChallenge(reg name.Registry, auth authn.Authenticator, t http.RoundTripper, pr *Challenge, scopes ...string) (*bearerTransport, error) {
|
||||
// We require the realm, which tells us where to send our Basic auth to turn it into Bearer auth.
|
||||
realm, ok := pr.Parameters["realm"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("malformed www-authenticate, missing realm: %v", pr.Parameters)
|
||||
}
|
||||
service := pr.Parameters["service"]
|
||||
scheme := "https"
|
||||
if pr.Insecure {
|
||||
scheme = "http"
|
||||
}
|
||||
return &bearerTransport{
|
||||
inner: t,
|
||||
basic: auth,
|
||||
realm: realm,
|
||||
registry: reg,
|
||||
service: service,
|
||||
scopes: scopes,
|
||||
scheme: scheme,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type bearerTransport struct {
|
||||
mx sync.RWMutex
|
||||
// Wrapped by bearerTransport.
|
||||
inner http.RoundTripper
|
||||
// Basic credentials that we exchange for bearer tokens.
|
||||
basic authn.Authenticator
|
||||
// Holds the bearer response from the token service.
|
||||
bearer authn.AuthConfig
|
||||
// Registry to which we send bearer tokens.
|
||||
registry name.Registry
|
||||
// See https://tools.ietf.org/html/rfc6750#section-3
|
||||
realm string
|
||||
// See https://docs.docker.com/registry/spec/auth/token/
|
||||
service string
|
||||
scopes []string
|
||||
// Scheme we should use, determined by ping response.
|
||||
scheme string
|
||||
}
|
||||
|
||||
var _ http.RoundTripper = (*bearerTransport)(nil)
|
||||
|
||||
var portMap = map[string]string{
|
||||
"http": "80",
|
||||
"https": "443",
|
||||
}
|
||||
|
||||
func stringSet(ss []string) map[string]struct{} {
|
||||
set := make(map[string]struct{})
|
||||
for _, s := range ss {
|
||||
set[s] = struct{}{}
|
||||
}
|
||||
return set
|
||||
}
|
||||
|
||||
// RoundTrip implements http.RoundTripper
|
||||
func (bt *bearerTransport) RoundTrip(in *http.Request) (*http.Response, error) {
|
||||
sendRequest := func() (*http.Response, error) {
|
||||
// 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.
|
||||
if matchesHost(bt.registry.RegistryStr(), in, bt.scheme) {
|
||||
bt.mx.RLock()
|
||||
localToken := bt.bearer.RegistryToken
|
||||
bt.mx.RUnlock()
|
||||
hdr := fmt.Sprintf("Bearer %s", localToken)
|
||||
in.Header.Set("Authorization", hdr)
|
||||
}
|
||||
return bt.inner.RoundTrip(in)
|
||||
}
|
||||
|
||||
res, err := sendRequest()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If we hit a WWW-Authenticate challenge, it might be due to expired tokens or insufficient scope.
|
||||
if challenges := authchallenge.ResponseChallenges(res); len(challenges) != 0 {
|
||||
// close out old response, since we will not return it.
|
||||
res.Body.Close()
|
||||
|
||||
newScopes := []string{}
|
||||
bt.mx.Lock()
|
||||
got := stringSet(bt.scopes)
|
||||
for _, wac := range challenges {
|
||||
// TODO(jonjohnsonjr): Should we also update "realm" or "service"?
|
||||
if want, ok := wac.Parameters["scope"]; ok {
|
||||
// Add any scopes that we don't already request.
|
||||
if _, ok := got[want]; !ok {
|
||||
newScopes = append(newScopes, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Some registries seem to only look at the first scope parameter during a token exchange.
|
||||
// If a request fails because it's missing a scope, we should put those at the beginning,
|
||||
// otherwise the registry might just ignore it :/
|
||||
newScopes = append(newScopes, bt.scopes...)
|
||||
bt.scopes = newScopes
|
||||
bt.mx.Unlock()
|
||||
|
||||
// TODO(jonjohnsonjr): Teach transport.Error about "error" and "error_description" from challenge.
|
||||
|
||||
// Retry the request to attempt to get a valid token.
|
||||
if err = bt.refresh(in.Context()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return sendRequest()
|
||||
}
|
||||
|
||||
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(ctx context.Context) error {
|
||||
auth, err := authn.Authorization(ctx, bt.basic)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if auth.RegistryToken != "" {
|
||||
bt.mx.Lock()
|
||||
bt.bearer.RegistryToken = auth.RegistryToken
|
||||
bt.mx.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
response, err := bt.Refresh(ctx, auth)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Some registries set access_token instead of token. See #54.
|
||||
if response.AccessToken != "" {
|
||||
response.Token = response.AccessToken
|
||||
}
|
||||
|
||||
// Find a token to turn into a Bearer authenticator
|
||||
if response.Token != "" {
|
||||
bt.mx.Lock()
|
||||
bt.bearer.RegistryToken = response.Token
|
||||
bt.mx.Unlock()
|
||||
}
|
||||
|
||||
// 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,
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bt *bearerTransport) Refresh(ctx context.Context, auth *authn.AuthConfig) (*Token, error) {
|
||||
var (
|
||||
content []byte
|
||||
err error
|
||||
)
|
||||
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(ctx)
|
||||
var terr *Error
|
||||
if errors.As(err, &terr) && terr.StatusCode == http.StatusNotFound {
|
||||
// Note: Not all token servers implement oauth2.
|
||||
// If the request to the endpoint returns 404 using the HTTP POST method,
|
||||
// refer to Token Documentation for using the HTTP GET method supported by all token servers.
|
||||
content, err = bt.refreshBasic(ctx)
|
||||
}
|
||||
} else {
|
||||
content, err = bt.refreshBasic(ctx)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var response Token
|
||||
if err := json.Unmarshal(content, &response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if response.Token == "" && response.AccessToken == "" {
|
||||
return &response, fmt.Errorf("no token in bearer response:\n%s", content)
|
||||
}
|
||||
|
||||
return &response, nil
|
||||
}
|
||||
|
||||
func matchesHost(host string, in *http.Request, scheme string) bool {
|
||||
canonicalHeaderHost := canonicalAddress(in.Host, scheme)
|
||||
canonicalURLHost := canonicalAddress(in.URL.Host, scheme)
|
||||
canonicalRegistryHost := canonicalAddress(host, scheme)
|
||||
return canonicalHeaderHost == canonicalRegistryHost || canonicalURLHost == canonicalRegistryHost
|
||||
}
|
||||
|
||||
func canonicalAddress(host, scheme string) (address string) {
|
||||
// The host may be any one of:
|
||||
// - hostname
|
||||
// - hostname:port
|
||||
// - ipv4
|
||||
// - ipv4:port
|
||||
// - ipv6
|
||||
// - [ipv6]:port
|
||||
// As net.SplitHostPort returns an error if the host does not contain a port, we should only attempt
|
||||
// to call it when we know that the address contains a port
|
||||
if strings.Count(host, ":") == 1 || (strings.Count(host, ":") >= 2 && strings.Contains(host, "]:")) {
|
||||
hostname, port, err := net.SplitHostPort(host)
|
||||
if err != nil {
|
||||
return host
|
||||
}
|
||||
if port == "" {
|
||||
port = portMap[scheme]
|
||||
}
|
||||
|
||||
return net.JoinHostPort(hostname, port)
|
||||
}
|
||||
|
||||
return net.JoinHostPort(host, portMap[scheme])
|
||||
}
|
||||
|
||||
// https://docs.docker.com/registry/spec/auth/oauth/
|
||||
func (bt *bearerTransport) refreshOauth(ctx context.Context) ([]byte, error) {
|
||||
auth, err := authn.Authorization(ctx, bt.basic)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
u, err := url.Parse(bt.realm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v := url.Values{}
|
||||
bt.mx.RLock()
|
||||
v.Set("scope", strings.Join(bt.scopes, " "))
|
||||
bt.mx.RUnlock()
|
||||
if bt.service != "" {
|
||||
v.Set("service", bt.service)
|
||||
}
|
||||
v.Set("client_id", defaultUserAgent)
|
||||
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}
|
||||
req, err := http.NewRequest(http.MethodPost, u.String(), strings.NewReader(v.Encode()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
// We don't want to log credentials.
|
||||
ctx = redact.NewContext(ctx, "oauth token response contains credentials")
|
||||
|
||||
resp, err := client.Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if err := CheckError(resp, http.StatusOK); err != nil {
|
||||
if bt.basic == authn.Anonymous {
|
||||
logs.Warn.Printf("No matching credentials were found for %q", bt.registry)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return io.ReadAll(resp.Body)
|
||||
}
|
||||
|
||||
// https://docs.docker.com/registry/spec/auth/token/
|
||||
func (bt *bearerTransport) refreshBasic(ctx context.Context) ([]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}
|
||||
|
||||
v := u.Query()
|
||||
bt.mx.RLock()
|
||||
v["scope"] = bt.scopes
|
||||
bt.mx.RUnlock()
|
||||
v.Set("service", bt.service)
|
||||
u.RawQuery = v.Encode()
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, u.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// We don't want to log credentials.
|
||||
ctx = redact.NewContext(ctx, "basic token response contains credentials")
|
||||
|
||||
resp, err := client.Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if err := CheckError(resp, http.StatusOK); err != nil {
|
||||
if bt.basic == authn.Anonymous {
|
||||
logs.Warn.Printf("No matching credentials were found for %q", bt.registry)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return io.ReadAll(resp.Body)
|
||||
}
|
||||
-18
@@ -1,18 +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 transport provides facilities for setting up an authenticated
|
||||
// http.RoundTripper given an Authenticator and base RoundTripper. See
|
||||
// transport.New for more information.
|
||||
package transport
|
||||
-196
@@ -1,196 +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 transport
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/google/go-containerregistry/internal/redact"
|
||||
)
|
||||
|
||||
// Error implements error to support the following error specification:
|
||||
// https://github.com/distribution/distribution/blob/aac2f6c8b7c5a6c60190848bab5cbeed2b5ba0a9/docs/spec/api.md#errors
|
||||
type Error struct {
|
||||
Errors []Diagnostic `json:"errors,omitempty"`
|
||||
// The http status code returned.
|
||||
StatusCode int
|
||||
// The request that failed.
|
||||
Request *http.Request
|
||||
// The raw body if we couldn't understand it.
|
||||
rawBody string
|
||||
|
||||
// Bit of a hack to make it easier to force a retry.
|
||||
temporary bool
|
||||
}
|
||||
|
||||
// Check that Error implements error
|
||||
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.URL(e.Request.URL))
|
||||
}
|
||||
return prefix + e.responseErr()
|
||||
}
|
||||
|
||||
func (e *Error) responseErr() string {
|
||||
switch len(e.Errors) {
|
||||
case 0:
|
||||
if len(e.rawBody) == 0 {
|
||||
if e.Request != nil && e.Request.Method == http.MethodHead {
|
||||
return fmt.Sprintf("unexpected status code %d %s (HEAD responses have no body, use GET for details)", e.StatusCode, http.StatusText(e.StatusCode))
|
||||
}
|
||||
return fmt.Sprintf("unexpected status code %d %s", e.StatusCode, http.StatusText(e.StatusCode))
|
||||
}
|
||||
return fmt.Sprintf("unexpected status code %d %s: %s", e.StatusCode, http.StatusText(e.StatusCode), e.rawBody)
|
||||
case 1:
|
||||
return e.Errors[0].String()
|
||||
default:
|
||||
var errors []string
|
||||
for _, d := range e.Errors {
|
||||
errors = append(errors, d.String())
|
||||
}
|
||||
return fmt.Sprintf("multiple errors returned: %s",
|
||||
strings.Join(errors, "; "))
|
||||
}
|
||||
}
|
||||
|
||||
// Temporary returns whether the request that preceded the error is temporary.
|
||||
func (e *Error) Temporary() bool {
|
||||
if e.temporary {
|
||||
return true
|
||||
}
|
||||
|
||||
if len(e.Errors) == 0 {
|
||||
_, ok := temporaryStatusCodes[e.StatusCode]
|
||||
return ok
|
||||
}
|
||||
for _, d := range e.Errors {
|
||||
if _, ok := temporaryErrorCodes[d.Code]; !ok {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Diagnostic represents a single error returned by a Docker registry interaction.
|
||||
type Diagnostic struct {
|
||||
Code ErrorCode `json:"code"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Detail any `json:"detail,omitempty"`
|
||||
}
|
||||
|
||||
// String stringifies the Diagnostic in the form: $Code: $Message[; $Detail]
|
||||
func (d Diagnostic) String() string {
|
||||
msg := fmt.Sprintf("%s: %s", d.Code, d.Message)
|
||||
if d.Detail != nil {
|
||||
msg = fmt.Sprintf("%s; %v", msg, d.Detail)
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
// ErrorCode is an enumeration of supported error codes.
|
||||
type ErrorCode string
|
||||
|
||||
// The set of error conditions a registry may return:
|
||||
// https://github.com/distribution/distribution/blob/aac2f6c8b7c5a6c60190848bab5cbeed2b5ba0a9/docs/spec/api.md#errors-2
|
||||
const (
|
||||
BlobUnknownErrorCode ErrorCode = "BLOB_UNKNOWN"
|
||||
BlobUploadInvalidErrorCode ErrorCode = "BLOB_UPLOAD_INVALID"
|
||||
BlobUploadUnknownErrorCode ErrorCode = "BLOB_UPLOAD_UNKNOWN"
|
||||
DigestInvalidErrorCode ErrorCode = "DIGEST_INVALID"
|
||||
ManifestBlobUnknownErrorCode ErrorCode = "MANIFEST_BLOB_UNKNOWN"
|
||||
ManifestInvalidErrorCode ErrorCode = "MANIFEST_INVALID"
|
||||
ManifestUnknownErrorCode ErrorCode = "MANIFEST_UNKNOWN"
|
||||
ManifestUnverifiedErrorCode ErrorCode = "MANIFEST_UNVERIFIED"
|
||||
NameInvalidErrorCode ErrorCode = "NAME_INVALID"
|
||||
NameUnknownErrorCode ErrorCode = "NAME_UNKNOWN"
|
||||
SizeInvalidErrorCode ErrorCode = "SIZE_INVALID"
|
||||
TagInvalidErrorCode ErrorCode = "TAG_INVALID"
|
||||
UnauthorizedErrorCode ErrorCode = "UNAUTHORIZED"
|
||||
DeniedErrorCode ErrorCode = "DENIED"
|
||||
UnsupportedErrorCode ErrorCode = "UNSUPPORTED"
|
||||
TooManyRequestsErrorCode ErrorCode = "TOOMANYREQUESTS"
|
||||
UnknownErrorCode ErrorCode = "UNKNOWN"
|
||||
|
||||
// This isn't defined by either docker or OCI spec, but is defined by docker/distribution:
|
||||
// https://github.com/distribution/distribution/blob/6a977a5a754baa213041443f841705888107362a/registry/api/errcode/register.go#L60
|
||||
UnavailableErrorCode ErrorCode = "UNAVAILABLE"
|
||||
)
|
||||
|
||||
// TODO: Include other error types.
|
||||
var temporaryErrorCodes = map[ErrorCode]struct{}{
|
||||
BlobUploadInvalidErrorCode: {},
|
||||
TooManyRequestsErrorCode: {},
|
||||
UnknownErrorCode: {},
|
||||
UnavailableErrorCode: {},
|
||||
}
|
||||
|
||||
var temporaryStatusCodes = map[int]struct{}{
|
||||
http.StatusRequestTimeout: {},
|
||||
http.StatusInternalServerError: {},
|
||||
http.StatusBadGateway: {},
|
||||
http.StatusServiceUnavailable: {},
|
||||
http.StatusGatewayTimeout: {},
|
||||
}
|
||||
|
||||
// CheckError returns a structured error if the response status is not in codes.
|
||||
func CheckError(resp *http.Response, codes ...int) error {
|
||||
for _, code := range codes {
|
||||
if resp.StatusCode == code {
|
||||
// This is one of the supported status codes.
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
b, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return makeError(resp, b)
|
||||
}
|
||||
|
||||
func makeError(resp *http.Response, body []byte) *Error {
|
||||
// https://github.com/distribution/distribution/blob/aac2f6c8b7c5a6c60190848bab5cbeed2b5ba0a9/docs/spec/api.md#errors
|
||||
structuredError := &Error{}
|
||||
|
||||
// This can fail if e.g. the response body is not valid JSON. That's fine,
|
||||
// we'll construct an appropriate error string from the body and status code.
|
||||
_ = json.Unmarshal(body, structuredError)
|
||||
|
||||
structuredError.rawBody = string(body)
|
||||
structuredError.StatusCode = resp.StatusCode
|
||||
structuredError.Request = resp.Request
|
||||
|
||||
return structuredError
|
||||
}
|
||||
|
||||
func retryError(resp *http.Response) error {
|
||||
b, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rerr := makeError(resp, b)
|
||||
rerr.temporary = true
|
||||
return rerr
|
||||
}
|
||||
-91
@@ -1,91 +0,0 @@
|
||||
// Copyright 2020 Google LLC All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package transport
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-containerregistry/internal/redact"
|
||||
"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
|
||||
|
||||
// We redact token responses and binary blobs in response/request.
|
||||
omitBody, reason := redact.FromContext(in.Context())
|
||||
if omitBody {
|
||||
logs.Debug.Printf("--> %s %s [body redacted: %s]", in.Method, in.URL, reason)
|
||||
} else {
|
||||
logs.Debug.Printf("--> %s %s", in.Method, in.URL)
|
||||
}
|
||||
|
||||
// Save these headers so we can redact Authorization.
|
||||
savedHeaders := in.Header.Clone()
|
||||
if in.Header != nil && in.Header.Get("authorization") != "" {
|
||||
in.Header.Set("authorization", "<redacted>")
|
||||
}
|
||||
|
||||
b, err := httputil.DumpRequestOut(in, !omitBody)
|
||||
if err == nil {
|
||||
logs.Debug.Println(string(b))
|
||||
} else {
|
||||
logs.Debug.Printf("Failed to dump request %s %s: %v", in.Method, in.URL, err)
|
||||
}
|
||||
|
||||
// Restore the non-redacted headers.
|
||||
in.Header = savedHeaders
|
||||
|
||||
start := time.Now()
|
||||
out, err = t.inner.RoundTrip(in)
|
||||
duration := time.Since(start)
|
||||
if err != nil {
|
||||
logs.Debug.Printf("<-- %v %s %s (%s)", err, in.Method, in.URL, duration)
|
||||
}
|
||||
if out != nil {
|
||||
msg := fmt.Sprintf("<-- %d", out.StatusCode)
|
||||
if out.Request != nil {
|
||||
msg = fmt.Sprintf("%s %s", msg, out.Request.URL)
|
||||
}
|
||||
msg = fmt.Sprintf("%s (%s)", msg, duration)
|
||||
|
||||
if omitBody {
|
||||
msg = fmt.Sprintf("%s [body redacted: %s]", msg, reason)
|
||||
}
|
||||
|
||||
logs.Debug.Print(msg)
|
||||
|
||||
b, err := httputil.DumpResponse(out, !omitBody)
|
||||
if err == nil {
|
||||
logs.Debug.Println(string(b))
|
||||
} else {
|
||||
logs.Debug.Printf("Failed to dump response %s %s: %v", in.Method, in.URL, err)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
-217
@@ -1,217 +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 transport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
authchallenge "github.com/docker/distribution/registry/client/auth/challenge"
|
||||
"github.com/google/go-containerregistry/pkg/logs"
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
)
|
||||
|
||||
// 300ms is the default fallback period for go's DNS dialer but we could make this configurable.
|
||||
var fallbackDelay = 300 * time.Millisecond
|
||||
|
||||
type Challenge struct {
|
||||
Scheme string
|
||||
|
||||
// Following the challenge there are often key/value pairs
|
||||
// e.g. Bearer service="gcr.io",realm="https://auth.gcr.io/v36/tokenz"
|
||||
Parameters map[string]string
|
||||
|
||||
// Whether we had to use http to complete the Ping.
|
||||
Insecure bool
|
||||
}
|
||||
|
||||
// Ping does a GET /v2/ against the registry and returns the response.
|
||||
func Ping(ctx context.Context, reg name.Registry, t http.RoundTripper) (*Challenge, error) {
|
||||
// This first attempts to use "https" for every request, falling back to http
|
||||
// if the registry matches our localhost heuristic or if it is intentionally
|
||||
// set to insecure via name.NewInsecureRegistry.
|
||||
schemes := []string{"https"}
|
||||
if reg.Scheme() == "http" {
|
||||
schemes = append(schemes, "http")
|
||||
}
|
||||
if len(schemes) == 1 {
|
||||
return pingSingle(ctx, reg, t, schemes[0])
|
||||
}
|
||||
return pingParallel(ctx, reg, t, schemes)
|
||||
}
|
||||
|
||||
func pingSingle(ctx context.Context, reg name.Registry, t http.RoundTripper, scheme string) (*Challenge, error) {
|
||||
client := http.Client{Transport: t}
|
||||
url := fmt.Sprintf("%s://%s/v2/", scheme, reg.RegistryStr())
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := client.Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
// By draining the body, make sure to reuse the connection made by
|
||||
// the ping for the following access to the registry
|
||||
io.Copy(io.Discard, resp.Body)
|
||||
resp.Body.Close()
|
||||
}()
|
||||
|
||||
insecure := scheme == "http"
|
||||
|
||||
switch resp.StatusCode {
|
||||
case http.StatusOK:
|
||||
// If we get a 200, then no authentication is needed.
|
||||
return &Challenge{
|
||||
Insecure: insecure,
|
||||
}, nil
|
||||
case http.StatusUnauthorized:
|
||||
if challenges := authchallenge.ResponseChallenges(resp); len(challenges) != 0 {
|
||||
// If we hit more than one, let's try to find one that we know how to handle.
|
||||
wac := pickFromMultipleChallenges(challenges)
|
||||
return &Challenge{
|
||||
Scheme: wac.Scheme,
|
||||
Parameters: wac.Parameters,
|
||||
Insecure: insecure,
|
||||
}, nil
|
||||
}
|
||||
// Otherwise, just return the challenge without parameters.
|
||||
return &Challenge{
|
||||
Scheme: resp.Header.Get("WWW-Authenticate"),
|
||||
Insecure: insecure,
|
||||
}, nil
|
||||
default:
|
||||
return nil, CheckError(resp, http.StatusOK, http.StatusUnauthorized)
|
||||
}
|
||||
}
|
||||
|
||||
// Based on the golang happy eyeballs dialParallel impl in net/dial.go.
|
||||
func pingParallel(ctx context.Context, reg name.Registry, t http.RoundTripper, schemes []string) (*Challenge, error) {
|
||||
returned := make(chan struct{})
|
||||
defer close(returned)
|
||||
|
||||
type pingResult struct {
|
||||
*Challenge
|
||||
error
|
||||
primary bool
|
||||
done bool
|
||||
}
|
||||
|
||||
results := make(chan pingResult)
|
||||
|
||||
startRacer := func(ctx context.Context, scheme string) {
|
||||
pr, err := pingSingle(ctx, reg, t, scheme)
|
||||
select {
|
||||
case results <- pingResult{Challenge: pr, error: err, primary: scheme == "https", done: true}:
|
||||
case <-returned:
|
||||
if pr != nil {
|
||||
logs.Debug.Printf("%s lost race", scheme)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var primary, fallback pingResult
|
||||
|
||||
primaryCtx, primaryCancel := context.WithCancel(ctx)
|
||||
defer primaryCancel()
|
||||
go startRacer(primaryCtx, schemes[0])
|
||||
|
||||
fallbackTimer := time.NewTimer(fallbackDelay)
|
||||
defer fallbackTimer.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-fallbackTimer.C:
|
||||
fallbackCtx, fallbackCancel := context.WithCancel(ctx)
|
||||
defer fallbackCancel()
|
||||
go startRacer(fallbackCtx, schemes[1])
|
||||
|
||||
case res := <-results:
|
||||
if res.error == nil {
|
||||
return res.Challenge, nil
|
||||
}
|
||||
if res.primary {
|
||||
primary = res
|
||||
} else {
|
||||
fallback = res
|
||||
}
|
||||
if primary.done && fallback.done {
|
||||
return nil, multierrs{primary.error, fallback.error}
|
||||
}
|
||||
if res.primary && fallbackTimer.Stop() {
|
||||
// Primary failed and we haven't started the fallback,
|
||||
// reset time to start fallback immediately.
|
||||
fallbackTimer.Reset(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func pickFromMultipleChallenges(challenges []authchallenge.Challenge) authchallenge.Challenge {
|
||||
// It might happen there are multiple www-authenticate headers, e.g. `Negotiate` and `Basic`.
|
||||
// Picking simply the first one could result eventually in `unrecognized challenge` error,
|
||||
// that's why we're looping through the challenges in search for one that can be handled.
|
||||
allowedSchemes := []string{"basic", "bearer"}
|
||||
|
||||
for _, wac := range challenges {
|
||||
currentScheme := strings.ToLower(wac.Scheme)
|
||||
for _, allowed := range allowedSchemes {
|
||||
if allowed == currentScheme {
|
||||
return wac
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return challenges[0]
|
||||
}
|
||||
|
||||
type multierrs []error
|
||||
|
||||
func (m multierrs) Error() string {
|
||||
var b strings.Builder
|
||||
hasWritten := false
|
||||
for _, err := range m {
|
||||
if hasWritten {
|
||||
b.WriteString("; ")
|
||||
}
|
||||
hasWritten = true
|
||||
b.WriteString(err.Error())
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m multierrs) As(target any) bool {
|
||||
for _, err := range m {
|
||||
if errors.As(err, target) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (m multierrs) Is(target error) bool {
|
||||
for _, err := range m {
|
||||
if errors.Is(err, target) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
-111
@@ -1,111 +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 transport
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-containerregistry/internal/retry"
|
||||
)
|
||||
|
||||
// Sleep for 0.1 then 0.3 seconds. This should cover networking blips.
|
||||
var defaultBackoff = retry.Backoff{
|
||||
Duration: 100 * time.Millisecond,
|
||||
Factor: 3.0,
|
||||
Jitter: 0.1,
|
||||
Steps: 3,
|
||||
}
|
||||
|
||||
var _ http.RoundTripper = (*retryTransport)(nil)
|
||||
|
||||
// retryTransport wraps a RoundTripper and retries temporary network errors.
|
||||
type retryTransport struct {
|
||||
inner http.RoundTripper
|
||||
backoff retry.Backoff
|
||||
predicate retry.Predicate
|
||||
codes []int
|
||||
}
|
||||
|
||||
// Option is a functional option for retryTransport.
|
||||
type Option func(*options)
|
||||
|
||||
type options struct {
|
||||
backoff retry.Backoff
|
||||
predicate retry.Predicate
|
||||
codes []int
|
||||
}
|
||||
|
||||
// Backoff is an alias of retry.Backoff to expose this configuration option to consumers of this lib
|
||||
type Backoff = retry.Backoff
|
||||
|
||||
// WithRetryBackoff sets the backoff for retry operations.
|
||||
func WithRetryBackoff(backoff Backoff) Option {
|
||||
return func(o *options) {
|
||||
o.backoff = backoff
|
||||
}
|
||||
}
|
||||
|
||||
// WithRetryPredicate sets the predicate for retry operations.
|
||||
func WithRetryPredicate(predicate func(error) bool) Option {
|
||||
return func(o *options) {
|
||||
o.predicate = predicate
|
||||
}
|
||||
}
|
||||
|
||||
// WithRetryStatusCodes sets which http response codes will be retried.
|
||||
func WithRetryStatusCodes(codes ...int) Option {
|
||||
return func(o *options) {
|
||||
o.codes = codes
|
||||
}
|
||||
}
|
||||
|
||||
// NewRetry returns a transport that retries errors.
|
||||
func NewRetry(inner http.RoundTripper, opts ...Option) http.RoundTripper {
|
||||
o := &options{
|
||||
backoff: defaultBackoff,
|
||||
predicate: retry.IsTemporary,
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(o)
|
||||
}
|
||||
|
||||
return &retryTransport{
|
||||
inner: inner,
|
||||
backoff: o.backoff,
|
||||
predicate: o.predicate,
|
||||
codes: o.codes,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *retryTransport) RoundTrip(in *http.Request) (out *http.Response, err error) {
|
||||
roundtrip := func() error {
|
||||
out, err = t.inner.RoundTrip(in)
|
||||
if !retry.Ever(in.Context()) {
|
||||
return nil
|
||||
}
|
||||
if out != nil {
|
||||
for _, code := range t.codes {
|
||||
if out.StatusCode == code {
|
||||
return retryError(out)
|
||||
}
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
retry.Retry(roundtrip, t.predicate, t.backoff)
|
||||
return
|
||||
}
|
||||
-44
@@ -1,44 +0,0 @@
|
||||
// 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.String(), in, st.scheme) {
|
||||
in.URL.Scheme = st.scheme
|
||||
}
|
||||
return st.inner.RoundTrip(in)
|
||||
}
|
||||
-24
@@ -1,24 +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 transport
|
||||
|
||||
// Scopes suitable to qualify each Repository
|
||||
const (
|
||||
PullScope string = "pull"
|
||||
PushScope string = "push,pull"
|
||||
// For now DELETE is PUSH, which is the read/write ACL.
|
||||
DeleteScope string = PushScope
|
||||
CatalogScope string = "catalog"
|
||||
)
|
||||
Generated
Vendored
-109
@@ -1,109 +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 transport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/google/go-containerregistry/pkg/authn"
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
)
|
||||
|
||||
// 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.
|
||||
//
|
||||
// Deprecated: Use NewWithContext.
|
||||
func New(reg name.Registry, auth authn.Authenticator, t http.RoundTripper, scopes []string) (http.RoundTripper, error) {
|
||||
return NewWithContext(context.Background(), reg, auth, t, scopes)
|
||||
}
|
||||
|
||||
// NewWithContext returns a new RoundTripper based on the provided RoundTripper that has been
|
||||
// set up to authenticate with the remote registry "reg", in the capacity
|
||||
// laid out by the specified scopes.
|
||||
// In case the RoundTripper is already of the type Wrapper it assumes
|
||||
// authentication was already done prior to this call, so it just returns
|
||||
// the provided RoundTripper without further action
|
||||
func NewWithContext(ctx context.Context, reg name.Registry, auth authn.Authenticator, t http.RoundTripper, scopes []string) (http.RoundTripper, error) {
|
||||
// When the transport provided is of the type Wrapper this function assumes that the caller already
|
||||
// executed the necessary login and check.
|
||||
switch t.(type) {
|
||||
case *Wrapper:
|
||||
return t, nil
|
||||
}
|
||||
// The handshake:
|
||||
// 1. Use "t" to ping() the registry for the authentication challenge.
|
||||
//
|
||||
// 2a. If we get back a 200, then simply use "t".
|
||||
//
|
||||
// 2b. If we get back a 401 with a Basic challenge, then use a transport
|
||||
// that just attachs auth each roundtrip.
|
||||
//
|
||||
// 2c. If we get back a 401 with a Bearer challenge, then use a transport
|
||||
// that attaches a bearer token to each request, and refreshes is on 401s.
|
||||
// Perform an initial refresh to seed the bearer token.
|
||||
|
||||
// First we ping the registry to determine the parameters of the authentication handshake
|
||||
// (if one is even necessary).
|
||||
pr, err := Ping(ctx, reg, t)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Wrap t with a useragent transport unless we already have one.
|
||||
if _, ok := t.(*userAgentTransport); !ok {
|
||||
t = NewUserAgent(t, "")
|
||||
}
|
||||
|
||||
scheme := "https"
|
||||
if pr.Insecure {
|
||||
scheme = "http"
|
||||
}
|
||||
|
||||
// Wrap t in a transport that selects the appropriate scheme based on the ping response.
|
||||
t = &schemeTransport{
|
||||
scheme: scheme,
|
||||
registry: reg,
|
||||
inner: t,
|
||||
}
|
||||
|
||||
if strings.ToLower(pr.Scheme) != "bearer" {
|
||||
return &Wrapper{&basicTransport{inner: t, auth: auth, target: reg.RegistryStr()}}, nil
|
||||
}
|
||||
|
||||
bt, err := fromChallenge(reg, auth, t, pr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bt.scopes = scopes
|
||||
|
||||
if err := bt.refresh(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Wrapper{bt}, nil
|
||||
}
|
||||
|
||||
// Wrapper results in *not* wrapping supplied transport with additional logic such as retries, useragent and debug logging
|
||||
// Consumers are opt-ing into providing their own transport without any additional wrapping.
|
||||
type Wrapper struct {
|
||||
inner http.RoundTripper
|
||||
}
|
||||
|
||||
// RoundTrip delegates to the inner RoundTripper
|
||||
func (w *Wrapper) RoundTrip(in *http.Request) (*http.Response, error) {
|
||||
return w.inner.RoundTrip(in)
|
||||
}
|
||||
Generated
Vendored
-94
@@ -1,94 +0,0 @@
|
||||
// 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 (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"runtime/debug"
|
||||
)
|
||||
|
||||
var (
|
||||
// Version can be set via:
|
||||
// -ldflags="-X 'github.com/google/go-containerregistry/pkg/v1/remote/transport.Version=$TAG'"
|
||||
Version string
|
||||
|
||||
ggcrVersion = defaultUserAgent
|
||||
)
|
||||
|
||||
const (
|
||||
defaultUserAgent = "go-containerregistry"
|
||||
moduleName = "github.com/google/go-containerregistry"
|
||||
)
|
||||
|
||||
type userAgentTransport struct {
|
||||
inner http.RoundTripper
|
||||
ua string
|
||||
}
|
||||
|
||||
func init() {
|
||||
if v := version(); v != "" {
|
||||
ggcrVersion = fmt.Sprintf("%s/%s", defaultUserAgent, v)
|
||||
}
|
||||
}
|
||||
|
||||
func version() string {
|
||||
if Version != "" {
|
||||
// Version was set via ldflags, just return it.
|
||||
return Version
|
||||
}
|
||||
|
||||
info, ok := debug.ReadBuildInfo()
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Happens for crane and gcrane.
|
||||
if info.Main.Path == moduleName {
|
||||
return info.Main.Version
|
||||
}
|
||||
|
||||
// Anything else.
|
||||
for _, dep := range info.Deps {
|
||||
if dep.Path == moduleName {
|
||||
return dep.Version
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// NewUserAgent returns an http.Roundtripper that sets the user agent to
|
||||
// The provided string plus additional go-containerregistry information,
|
||||
// e.g. if provided "crane/v0.1.4" and this modules was built at v0.1.4:
|
||||
//
|
||||
// User-Agent: crane/v0.1.4 go-containerregistry/v0.1.4
|
||||
func NewUserAgent(inner http.RoundTripper, ua string) http.RoundTripper {
|
||||
if ua == "" {
|
||||
ua = ggcrVersion
|
||||
} else {
|
||||
ua = fmt.Sprintf("%s %s", ua, ggcrVersion)
|
||||
}
|
||||
return &userAgentTransport{
|
||||
inner: inner,
|
||||
ua: ua,
|
||||
}
|
||||
}
|
||||
|
||||
// RoundTrip implements http.RoundTripper
|
||||
func (ut *userAgentTransport) RoundTrip(in *http.Request) (*http.Response, error) {
|
||||
in.Header.Set("User-Agent", ut.ua)
|
||||
return ut.inner.RoundTrip(in)
|
||||
}
|
||||
-711
@@ -1,711 +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 remote
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/google/go-containerregistry/internal/redact"
|
||||
"github.com/google/go-containerregistry/internal/retry"
|
||||
"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"
|
||||
"github.com/google/go-containerregistry/pkg/v1/stream"
|
||||
"github.com/google/go-containerregistry/pkg/v1/types"
|
||||
)
|
||||
|
||||
// Taggable is an interface that enables a manifest PUT (e.g. for tagging).
|
||||
type Taggable interface {
|
||||
RawManifest() ([]byte, error)
|
||||
}
|
||||
|
||||
// Write pushes the provided img to the specified image reference.
|
||||
func Write(ref name.Reference, img v1.Image, options ...Option) (rerr error) {
|
||||
return Push(ref, img, options...)
|
||||
}
|
||||
|
||||
// writer writes the elements of an image to a remote image reference.
|
||||
type writer struct {
|
||||
repo name.Repository
|
||||
auth authn.Authenticator
|
||||
transport http.RoundTripper
|
||||
|
||||
client *http.Client
|
||||
|
||||
progress *progress
|
||||
backoff Backoff
|
||||
predicate retry.Predicate
|
||||
|
||||
scopeLock sync.Mutex
|
||||
// Keep track of scopes that we have already requested.
|
||||
scopeSet map[string]struct{}
|
||||
scopes []string
|
||||
}
|
||||
|
||||
func makeWriter(ctx context.Context, repo name.Repository, ls []v1.Layer, o *options) (*writer, error) {
|
||||
auth := o.auth
|
||||
if o.keychain != nil {
|
||||
kauth, err := authn.Resolve(ctx, o.keychain, repo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
auth = kauth
|
||||
}
|
||||
scopes := scopesForUploadingImage(repo, ls)
|
||||
tr, err := transport.NewWithContext(ctx, repo.Registry, auth, o.transport, scopes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
scopeSet := map[string]struct{}{}
|
||||
for _, scope := range scopes {
|
||||
scopeSet[scope] = struct{}{}
|
||||
}
|
||||
return &writer{
|
||||
repo: repo,
|
||||
client: &http.Client{Transport: tr},
|
||||
auth: auth,
|
||||
transport: o.transport,
|
||||
progress: o.progress,
|
||||
backoff: o.retryBackoff,
|
||||
predicate: o.retryPredicate,
|
||||
scopes: scopes,
|
||||
scopeSet: scopeSet,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 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.Scheme(),
|
||||
Host: w.repo.RegistryStr(),
|
||||
Path: path,
|
||||
}
|
||||
}
|
||||
|
||||
func (w *writer) maybeUpdateScopes(ctx context.Context, ml *MountableLayer) error {
|
||||
if ml.Reference.Context().String() == w.repo.String() {
|
||||
return nil
|
||||
}
|
||||
if ml.Reference.Context().Registry.String() != w.repo.Registry.String() {
|
||||
return nil
|
||||
}
|
||||
|
||||
scope := ml.Reference.Scope(transport.PullScope)
|
||||
|
||||
w.scopeLock.Lock()
|
||||
defer w.scopeLock.Unlock()
|
||||
|
||||
if _, ok := w.scopeSet[scope]; !ok {
|
||||
w.scopeSet[scope] = struct{}{}
|
||||
w.scopes = append(w.scopes, scope)
|
||||
|
||||
logs.Debug.Printf("Refreshing token to add scope %q", scope)
|
||||
wt, err := transport.NewWithContext(ctx, w.repo.Registry, w.auth, w.transport, w.scopes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
w.client = &http.Client{Transport: wt}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// nextLocation extracts the fully-qualified URL to which we should send the next request in an upload sequence.
|
||||
func (w *writer) nextLocation(resp *http.Response) (string, error) {
|
||||
loc := resp.Header.Get("Location")
|
||||
if len(loc) == 0 {
|
||||
return "", errors.New("missing Location header")
|
||||
}
|
||||
u, err := url.Parse(loc)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// If the location header returned is just a url path, then fully qualify it.
|
||||
// We cannot simply call w.url, since there might be an embedded query string.
|
||||
return resp.Request.URL.ResolveReference(u).String(), nil
|
||||
}
|
||||
|
||||
// checkExistingBlob checks if a blob exists already in the repository by making a
|
||||
// HEAD request to the blob store API. GCR performs an existence check on the
|
||||
// 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(ctx context.Context, h v1.Hash) (bool, error) {
|
||||
u := w.url(fmt.Sprintf("/v2/%s/blobs/%s", w.repo.RepositoryStr(), h.String()))
|
||||
|
||||
req, err := http.NewRequest(http.MethodHead, u.String(), nil)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
resp, err := w.client.Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if err := transport.CheckError(resp, http.StatusOK, http.StatusNotFound); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return resp.StatusCode == http.StatusOK, nil
|
||||
}
|
||||
|
||||
// initiateUpload initiates the blob upload, which starts with a POST that can
|
||||
// optionally include the hash of the layer and a list of repositories from
|
||||
// which that layer might be read. On failure, an error is returned.
|
||||
// On success, the layer was either mounted (nothing more to do) or a blob
|
||||
// upload was initiated and the body of that blob should be sent to the returned
|
||||
// location.
|
||||
func (w *writer) initiateUpload(ctx context.Context, from, mount, origin string) (location string, mounted bool, err error) {
|
||||
u := w.url(fmt.Sprintf("/v2/%s/blobs/uploads/", w.repo.RepositoryStr()))
|
||||
uv := url.Values{}
|
||||
if mount != "" && from != "" {
|
||||
// Quay will fail if we specify a "mount" without a "from".
|
||||
uv.Set("mount", mount)
|
||||
uv.Set("from", from)
|
||||
if origin != "" {
|
||||
uv.Set("origin", origin)
|
||||
}
|
||||
}
|
||||
u.RawQuery = uv.Encode()
|
||||
|
||||
// Make the request to initiate the blob upload.
|
||||
req, err := http.NewRequest(http.MethodPost, u.String(), nil)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := w.client.Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
if from != "" {
|
||||
// https://github.com/google/go-containerregistry/issues/1679
|
||||
logs.Warn.Printf("retrying without mount: %v", err)
|
||||
return w.initiateUpload(ctx, "", "", "")
|
||||
}
|
||||
return "", false, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if err := transport.CheckError(resp, http.StatusCreated, http.StatusAccepted); err != nil {
|
||||
if from != "" {
|
||||
// https://github.com/google/go-containerregistry/issues/1404
|
||||
logs.Warn.Printf("retrying without mount: %v", err)
|
||||
return w.initiateUpload(ctx, "", "", "")
|
||||
}
|
||||
return "", false, err
|
||||
}
|
||||
|
||||
// Check the response code to determine the result.
|
||||
switch resp.StatusCode {
|
||||
case http.StatusCreated:
|
||||
// We're done, we were able to fast-path.
|
||||
return "", true, nil
|
||||
case http.StatusAccepted:
|
||||
// Proceed to PATCH, upload has begun.
|
||||
loc, err := w.nextLocation(resp)
|
||||
return loc, false, err
|
||||
default:
|
||||
panic("Unreachable: initiateUpload")
|
||||
}
|
||||
}
|
||||
|
||||
// streamBlob streams the contents of the blob to the specified location.
|
||||
// On failure, this will return an error. On success, this will return the location
|
||||
// header indicating how to commit the streamed blob.
|
||||
func (w *writer) streamBlob(ctx context.Context, layer v1.Layer, streamLocation string) (commitLocation string, rerr error) {
|
||||
reset := func() {}
|
||||
defer func() {
|
||||
if rerr != nil {
|
||||
reset()
|
||||
}
|
||||
}()
|
||||
blob, err := layer.Compressed()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
getBody := layer.Compressed
|
||||
if w.progress != nil {
|
||||
var count int64
|
||||
blob = &progressReader{rc: blob, progress: w.progress, count: &count}
|
||||
getBody = func() (io.ReadCloser, error) {
|
||||
blob, err := layer.Compressed()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &progressReader{rc: blob, progress: w.progress, count: &count}, nil
|
||||
}
|
||||
reset = func() {
|
||||
w.progress.complete(-count)
|
||||
}
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodPatch, streamLocation, blob)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if _, ok := layer.(*stream.Layer); !ok {
|
||||
// We can't retry streaming layers.
|
||||
req.GetBody = getBody
|
||||
|
||||
// If we know the size, set it.
|
||||
if size, err := layer.Size(); err == nil {
|
||||
req.ContentLength = size
|
||||
}
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/octet-stream")
|
||||
|
||||
resp, err := w.client.Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if err := transport.CheckError(resp, http.StatusNoContent, http.StatusAccepted, http.StatusCreated); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// The blob has been uploaded, return the location header indicating
|
||||
// how to commit this layer.
|
||||
return w.nextLocation(resp)
|
||||
}
|
||||
|
||||
// commitBlob commits this blob by sending a PUT to the location returned from
|
||||
// streaming the blob.
|
||||
func (w *writer) commitBlob(ctx context.Context, location, digest string) error {
|
||||
u, err := url.Parse(location)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
v := u.Query()
|
||||
v.Set("digest", digest)
|
||||
u.RawQuery = v.Encode()
|
||||
|
||||
req, err := http.NewRequest(http.MethodPut, u.String(), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/octet-stream")
|
||||
|
||||
resp, err := w.client.Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
return transport.CheckError(resp, http.StatusCreated)
|
||||
}
|
||||
|
||||
// incrProgress increments and sends a progress update, if WithProgress is used.
|
||||
func (w *writer) incrProgress(written int64) {
|
||||
if w.progress == nil {
|
||||
return
|
||||
}
|
||||
w.progress.complete(written)
|
||||
}
|
||||
|
||||
// uploadOne performs a complete upload of a single layer.
|
||||
func (w *writer) uploadOne(ctx context.Context, l v1.Layer) error {
|
||||
tryUpload := func() error {
|
||||
ctx := retry.Never(ctx)
|
||||
var from, mount, origin string
|
||||
if h, err := l.Digest(); err == nil {
|
||||
// If we know the digest, this isn't a streaming layer. Do an existence
|
||||
// check so we can skip uploading the layer if possible.
|
||||
existing, err := w.checkExistingBlob(ctx, h)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if existing {
|
||||
size, err := l.Size()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
w.incrProgress(size)
|
||||
logs.Progress.Printf("existing blob: %v", h)
|
||||
return nil
|
||||
}
|
||||
|
||||
mount = h.String()
|
||||
}
|
||||
if ml, ok := l.(*MountableLayer); ok {
|
||||
if err := w.maybeUpdateScopes(ctx, ml); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
from = ml.Reference.Context().RepositoryStr()
|
||||
origin = ml.Reference.Context().RegistryStr()
|
||||
|
||||
// This keeps breaking with DockerHub.
|
||||
// https://github.com/google/go-containerregistry/issues/1741
|
||||
if w.repo.RegistryStr() == name.DefaultRegistry && origin != w.repo.RegistryStr() {
|
||||
from = ""
|
||||
origin = ""
|
||||
}
|
||||
}
|
||||
|
||||
location, mounted, err := w.initiateUpload(ctx, from, mount, origin)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if mounted {
|
||||
size, err := l.Size()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
w.incrProgress(size)
|
||||
h, err := l.Digest()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logs.Progress.Printf("mounted blob: %s", h.String())
|
||||
return nil
|
||||
}
|
||||
|
||||
// Only log layers with +json or +yaml. We can let through other stuff if it becomes popular.
|
||||
// TODO(opencontainers/image-spec#791): Would be great to have an actual parser.
|
||||
mt, err := l.MediaType()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
smt := string(mt)
|
||||
if !strings.HasSuffix(smt, "+json") && !strings.HasSuffix(smt, "+yaml") {
|
||||
ctx = redact.NewContext(ctx, "omitting binary blobs from logs")
|
||||
}
|
||||
|
||||
location, err = w.streamBlob(ctx, l, location)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
h, err := l.Digest()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
digest := h.String()
|
||||
|
||||
if err := w.commitBlob(ctx, location, digest); err != nil {
|
||||
return err
|
||||
}
|
||||
logs.Progress.Printf("pushed blob: %s", digest)
|
||||
return nil
|
||||
}
|
||||
|
||||
return retry.Retry(tryUpload, w.predicate, w.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) ([]byte, *v1.Descriptor, error) {
|
||||
if d, ok := t.(*Descriptor); ok {
|
||||
return d.Manifest, &d.Descriptor, nil
|
||||
}
|
||||
b, err := t.RawManifest()
|
||||
if err != nil {
|
||||
return nil, 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, nil, err
|
||||
}
|
||||
mt = m
|
||||
}
|
||||
|
||||
h, sz, err := v1.SHA256(bytes.NewReader(b))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return b, &v1.Descriptor{
|
||||
MediaType: mt,
|
||||
Size: sz,
|
||||
Digest: h,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// commitSubjectReferrers is responsible for updating the fallback tag manifest to track descriptors referring to a subject for registries that don't yet support the Referrers API.
|
||||
// TODO: use conditional requests to avoid race conditions
|
||||
func (w *writer) commitSubjectReferrers(ctx context.Context, sub name.Digest, add v1.Descriptor) error {
|
||||
// Check if the registry supports Referrers API.
|
||||
// TODO: This should be done once per registry, not once per subject.
|
||||
u := w.url(fmt.Sprintf("/v2/%s/referrers/%s", w.repo.RepositoryStr(), sub.DigestStr()))
|
||||
req, err := http.NewRequest(http.MethodGet, u.String(), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Accept", string(types.OCIImageIndex))
|
||||
resp, err := w.client.Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if err := transport.CheckError(resp, http.StatusOK, http.StatusNotFound, http.StatusBadRequest); err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
// The registry supports Referrers API. The registry is responsible for updating the referrers list.
|
||||
return nil
|
||||
}
|
||||
|
||||
// The registry doesn't support Referrers API, we need to update the manifest tagged with the fallback tag.
|
||||
// Make the request to GET the current manifest.
|
||||
t := fallbackTag(sub)
|
||||
u = w.url(fmt.Sprintf("/v2/%s/manifests/%s", w.repo.RepositoryStr(), t.Identifier()))
|
||||
req, err = http.NewRequest(http.MethodGet, u.String(), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Accept", string(types.OCIImageIndex))
|
||||
resp, err = w.client.Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var im v1.IndexManifest
|
||||
if err := transport.CheckError(resp, http.StatusOK, http.StatusNotFound); err != nil {
|
||||
return err
|
||||
} else if resp.StatusCode == http.StatusNotFound {
|
||||
// Not found just means there are no attachments. Start with an empty index.
|
||||
im = v1.IndexManifest{
|
||||
SchemaVersion: 2,
|
||||
MediaType: types.OCIImageIndex,
|
||||
Manifests: []v1.Descriptor{add},
|
||||
}
|
||||
} else {
|
||||
if err := json.NewDecoder(resp.Body).Decode(&im); err != nil {
|
||||
return err
|
||||
}
|
||||
if im.SchemaVersion != 2 {
|
||||
return fmt.Errorf("fallback tag manifest is not a schema version 2: %d", im.SchemaVersion)
|
||||
}
|
||||
if im.MediaType != types.OCIImageIndex {
|
||||
return fmt.Errorf("fallback tag manifest is not an OCI image index: %s", im.MediaType)
|
||||
}
|
||||
for _, desc := range im.Manifests {
|
||||
if desc.Digest == add.Digest {
|
||||
// The digest is already attached, nothing to do.
|
||||
logs.Progress.Printf("fallback tag %s already had referrer", t.Identifier())
|
||||
return nil
|
||||
}
|
||||
}
|
||||
// Append the new descriptor to the index.
|
||||
im.Manifests = append(im.Manifests, add)
|
||||
}
|
||||
|
||||
// Sort the manifests for reproducibility.
|
||||
sort.Slice(im.Manifests, func(i, j int) bool {
|
||||
return im.Manifests[i].Digest.String() < im.Manifests[j].Digest.String()
|
||||
})
|
||||
logs.Progress.Printf("updating fallback tag %s with new referrer", t.Identifier())
|
||||
return w.commitManifest(ctx, fallbackTaggable{im}, t)
|
||||
}
|
||||
|
||||
type fallbackTaggable struct {
|
||||
im v1.IndexManifest
|
||||
}
|
||||
|
||||
func (f fallbackTaggable) RawManifest() ([]byte, error) { return json.Marshal(f.im) }
|
||||
func (f fallbackTaggable) MediaType() (types.MediaType, error) { return types.OCIImageIndex, nil }
|
||||
|
||||
// commitManifest does a PUT of the image's manifest.
|
||||
func (w *writer) commitManifest(ctx context.Context, t Taggable, ref name.Reference) error {
|
||||
// If the manifest refers to a subject, we need to check whether we need to update the fallback tag manifest.
|
||||
raw, err := t.RawManifest()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var mf struct {
|
||||
MediaType types.MediaType `json:"mediaType"`
|
||||
Subject *v1.Descriptor `json:"subject,omitempty"`
|
||||
Config struct {
|
||||
MediaType types.MediaType `json:"mediaType"`
|
||||
} `json:"config"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &mf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tryUpload := func() error {
|
||||
ctx := retry.Never(ctx)
|
||||
raw, desc, err := unpackTaggable(t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
u := w.url(fmt.Sprintf("/v2/%s/manifests/%s", w.repo.RepositoryStr(), ref.Identifier()))
|
||||
|
||||
// Make the request to PUT the serialized manifest
|
||||
req, err := http.NewRequest(http.MethodPut, u.String(), bytes.NewBuffer(raw))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", string(desc.MediaType))
|
||||
|
||||
resp, err := w.client.Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if err := transport.CheckError(resp, http.StatusOK, http.StatusCreated, http.StatusAccepted); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If the manifest referred to a subject, we may need to update the fallback tag manifest.
|
||||
// TODO: If this fails, we'll retry the whole upload. We should retry just this part.
|
||||
if mf.Subject != nil {
|
||||
h, size, err := v1.SHA256(bytes.NewReader(raw))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
desc := v1.Descriptor{
|
||||
ArtifactType: string(mf.Config.MediaType),
|
||||
MediaType: mf.MediaType,
|
||||
Digest: h,
|
||||
Size: size,
|
||||
}
|
||||
if err := w.commitSubjectReferrers(ctx,
|
||||
ref.Context().Digest(mf.Subject.Digest.String()),
|
||||
desc); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// The image was successfully pushed!
|
||||
logs.Progress.Printf("%v: digest: %v size: %d", ref, desc.Digest, desc.Size)
|
||||
w.incrProgress(int64(len(raw)))
|
||||
return nil
|
||||
}
|
||||
|
||||
return retry.Retry(tryUpload, w.predicate, w.backoff)
|
||||
}
|
||||
|
||||
func scopesForUploadingImage(repo name.Repository, layers []v1.Layer) []string {
|
||||
// use a map as set to remove duplicates scope strings
|
||||
scopeSet := map[string]struct{}{}
|
||||
|
||||
for _, l := range layers {
|
||||
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() {
|
||||
scopeSet[ml.Reference.Scope(transport.PullScope)] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scopes := make([]string, 0, len(scopeSet)+1)
|
||||
// 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))
|
||||
|
||||
for scope := range scopeSet {
|
||||
scopes = append(scopes, scope)
|
||||
}
|
||||
|
||||
return scopes
|
||||
}
|
||||
|
||||
// WriteIndex pushes the provided ImageIndex to the specified image reference.
|
||||
// WriteIndex will attempt to push all of the referenced manifests before
|
||||
// attempting to push the ImageIndex, to retain referential integrity.
|
||||
func WriteIndex(ref name.Reference, ii v1.ImageIndex, options ...Option) (rerr error) {
|
||||
return Push(ref, ii, options...)
|
||||
}
|
||||
|
||||
// WriteLayer uploads the provided Layer to the specified repo.
|
||||
func WriteLayer(repo name.Repository, layer v1.Layer, options ...Option) (rerr error) {
|
||||
o, err := makeOptions(options...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if o.progress != nil {
|
||||
defer func() { o.progress.Close(rerr) }()
|
||||
}
|
||||
return newPusher(o).Upload(o.context, repo, layer)
|
||||
}
|
||||
|
||||
// Tag adds a tag to the given Taggable via PUT /v2/.../manifests/<tag>
|
||||
//
|
||||
// Notable implementations of Taggable are v1.Image, v1.ImageIndex, and
|
||||
// remote.Descriptor.
|
||||
//
|
||||
// If t implements MediaType, we will use that for the Content-Type, otherwise
|
||||
// we will default to types.DockerManifestSchema2.
|
||||
//
|
||||
// Tag does not attempt to write anything other than the manifest, so callers
|
||||
// should ensure that all blobs or manifests that are referenced by t exist
|
||||
// in the target registry.
|
||||
func Tag(tag name.Tag, t Taggable, options ...Option) error {
|
||||
return Put(tag, t, options...)
|
||||
}
|
||||
|
||||
// Put adds a manifest from the given Taggable via PUT /v1/.../manifest/<ref>
|
||||
//
|
||||
// Notable implementations of Taggable are v1.Image, v1.ImageIndex, and
|
||||
// remote.Descriptor.
|
||||
//
|
||||
// If t implements MediaType, we will use that for the Content-Type, otherwise
|
||||
// we will default to types.DockerManifestSchema2.
|
||||
//
|
||||
// Put does not attempt to write anything other than the manifest, so callers
|
||||
// should ensure that all blobs or manifests that are referenced by t exist
|
||||
// in the target registry.
|
||||
func Put(ref name.Reference, t Taggable, options ...Option) error {
|
||||
o, err := makeOptions(options...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return newPusher(o).Put(o.context, ref, t)
|
||||
}
|
||||
|
||||
// Push uploads the given Taggable to the specified reference.
|
||||
func Push(ref name.Reference, t Taggable, options ...Option) (rerr error) {
|
||||
o, err := makeOptions(options...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if o.progress != nil {
|
||||
defer func() { o.progress.Close(rerr) }()
|
||||
}
|
||||
return newPusher(o).Push(o.context, ref, 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.
|
||||
-275
@@ -1,275 +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 stream implements a single-pass streaming v1.Layer.
|
||||
package stream
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"compress/gzip"
|
||||
"crypto"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"hash"
|
||||
"io"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/types"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrNotComputed is returned when the requested value is not yet
|
||||
// computed because the stream has not been consumed yet.
|
||||
ErrNotComputed = errors.New("value not computed until stream is consumed")
|
||||
|
||||
// ErrConsumed is returned by Compressed when the underlying stream has
|
||||
// already been consumed and closed.
|
||||
ErrConsumed = errors.New("stream was already consumed")
|
||||
)
|
||||
|
||||
// Layer is a streaming implementation of v1.Layer.
|
||||
type Layer struct {
|
||||
blob io.ReadCloser
|
||||
consumed bool
|
||||
compression int
|
||||
|
||||
mu sync.Mutex
|
||||
digest, diffID *v1.Hash
|
||||
size int64
|
||||
mediaType types.MediaType
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// WithMediaType is a functional option for overriding the layer's media type.
|
||||
func WithMediaType(mt types.MediaType) LayerOption {
|
||||
return func(l *Layer) {
|
||||
l.mediaType = mt
|
||||
}
|
||||
}
|
||||
|
||||
// NewLayer creates a Layer from an io.ReadCloser.
|
||||
func NewLayer(rc io.ReadCloser, opts ...LayerOption) *Layer {
|
||||
layer := &Layer{
|
||||
blob: rc,
|
||||
compression: gzip.BestSpeed,
|
||||
// We use DockerLayer for now as uncompressed layers
|
||||
// are unimplemented
|
||||
mediaType: types.DockerLayer,
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(layer)
|
||||
}
|
||||
|
||||
return layer
|
||||
}
|
||||
|
||||
// Digest implements v1.Layer.
|
||||
func (l *Layer) Digest() (v1.Hash, error) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
if l.digest == nil {
|
||||
return v1.Hash{}, ErrNotComputed
|
||||
}
|
||||
return *l.digest, nil
|
||||
}
|
||||
|
||||
// DiffID implements v1.Layer.
|
||||
func (l *Layer) DiffID() (v1.Hash, error) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
if l.diffID == nil {
|
||||
return v1.Hash{}, ErrNotComputed
|
||||
}
|
||||
return *l.diffID, nil
|
||||
}
|
||||
|
||||
// Size implements v1.Layer.
|
||||
func (l *Layer) Size() (int64, error) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
if l.size == 0 {
|
||||
return 0, ErrNotComputed
|
||||
}
|
||||
return l.size, nil
|
||||
}
|
||||
|
||||
// MediaType implements v1.Layer
|
||||
func (l *Layer) MediaType() (types.MediaType, error) {
|
||||
return l.mediaType, nil
|
||||
}
|
||||
|
||||
// Uncompressed implements v1.Layer.
|
||||
func (l *Layer) Uncompressed() (io.ReadCloser, error) {
|
||||
return nil, errors.New("NYI: stream.Layer.Uncompressed is not implemented")
|
||||
}
|
||||
|
||||
// Compressed implements v1.Layer.
|
||||
func (l *Layer) Compressed() (io.ReadCloser, error) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
if l.consumed {
|
||||
return nil, ErrConsumed
|
||||
}
|
||||
return newCompressedReader(l)
|
||||
}
|
||||
|
||||
// finalize sets the layer to consumed and computes all hash and size values.
|
||||
func (l *Layer) finalize(uncompressed, compressed hash.Hash, size int64) error {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
diffID, err := v1.NewHash("sha256:" + hex.EncodeToString(uncompressed.Sum(nil)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
l.diffID = &diffID
|
||||
|
||||
digest, err := v1.NewHash("sha256:" + hex.EncodeToString(compressed.Sum(nil)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
l.digest = &digest
|
||||
|
||||
l.size = size
|
||||
l.consumed = true
|
||||
return nil
|
||||
}
|
||||
|
||||
type compressedReader struct {
|
||||
pr io.Reader
|
||||
closer func() error
|
||||
}
|
||||
|
||||
func newCompressedReader(l *Layer) (*compressedReader, error) {
|
||||
// Collect digests of compressed and uncompressed stream and size of
|
||||
// compressed stream.
|
||||
h := crypto.SHA256.New()
|
||||
zh := crypto.SHA256.New()
|
||||
count := &countWriter{}
|
||||
|
||||
// gzip.Writer writes to the output stream via pipe, a hasher to
|
||||
// capture compressed digest, and a countWriter to capture compressed
|
||||
// size.
|
||||
pr, pw := io.Pipe()
|
||||
|
||||
// Write compressed bytes to be read by the pipe.Reader, hashed by zh, and counted by count.
|
||||
mw := io.MultiWriter(pw, zh, count)
|
||||
|
||||
// Buffer the output of the gzip writer so we don't have to wait on pr to keep writing.
|
||||
// 64K ought to be small enough for anybody.
|
||||
bw := bufio.NewWriterSize(mw, 2<<16)
|
||||
zw, err := gzip.NewWriterLevel(bw, l.compression)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
doneDigesting := make(chan struct{})
|
||||
|
||||
cr := &compressedReader{
|
||||
pr: pr,
|
||||
closer: func() error {
|
||||
// Immediately close pw without error. There are three ways to get
|
||||
// here.
|
||||
//
|
||||
// 1. There was a copy error due from the underlying reader, in which
|
||||
// case the error will not be overwritten.
|
||||
// 2. Copying from the underlying reader completed successfully.
|
||||
// 3. Close has been called before the underlying reader has been
|
||||
// fully consumed. In this case pw must be closed in order to
|
||||
// keep the flush of bw from blocking indefinitely.
|
||||
//
|
||||
// NOTE: pw.Close never returns an error. The signature is only to
|
||||
// implement io.Closer.
|
||||
_ = pw.Close()
|
||||
|
||||
// Close the inner ReadCloser.
|
||||
//
|
||||
// NOTE: net/http will call close on success, so if we've already
|
||||
// closed the inner rc, it's not an error.
|
||||
if err := l.blob.Close(); err != nil && !errors.Is(err, os.ErrClosed) {
|
||||
return err
|
||||
}
|
||||
|
||||
// Finalize layer with its digest and size values.
|
||||
<-doneDigesting
|
||||
return l.finalize(h, zh, count.n)
|
||||
},
|
||||
}
|
||||
go func() {
|
||||
// Copy blob into the gzip writer, which also hashes and counts the
|
||||
// size of the compressed output, and hasher of the raw contents.
|
||||
_, copyErr := io.Copy(io.MultiWriter(h, zw), l.blob)
|
||||
|
||||
// Close the gzip writer once copying is done. If this is done in the
|
||||
// Close method of compressedReader instead, then it can cause a panic
|
||||
// when the compressedReader is closed before the blob is fully
|
||||
// consumed and io.Copy in this goroutine is still blocking.
|
||||
closeErr := zw.Close()
|
||||
|
||||
// Check errors from writing and closing streams.
|
||||
if copyErr != nil {
|
||||
close(doneDigesting)
|
||||
pw.CloseWithError(copyErr)
|
||||
return
|
||||
}
|
||||
if closeErr != nil {
|
||||
close(doneDigesting)
|
||||
pw.CloseWithError(closeErr)
|
||||
return
|
||||
}
|
||||
|
||||
// Flush the buffer once all writes are complete to the gzip writer.
|
||||
if err := bw.Flush(); err != nil {
|
||||
close(doneDigesting)
|
||||
pw.CloseWithError(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Notify closer that digests are done being written.
|
||||
close(doneDigesting)
|
||||
|
||||
// Close the compressed reader to calculate digest/diffID/size. This
|
||||
// will cause pr to return EOF which will cause readers of the
|
||||
// Compressed stream to finish reading.
|
||||
pw.CloseWithError(cr.Close())
|
||||
}()
|
||||
|
||||
return cr, nil
|
||||
}
|
||||
|
||||
func (cr *compressedReader) Read(b []byte) (int, error) { return cr.pr.Read(b) }
|
||||
|
||||
func (cr *compressedReader) Close() error { return cr.closer() }
|
||||
|
||||
// countWriter counts bytes written to it.
|
||||
type countWriter struct{ n int64 }
|
||||
|
||||
func (c *countWriter) Write(p []byte) (int, error) {
|
||||
c.n += int64(len(p))
|
||||
return len(p), nil
|
||||
}
|
||||
-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 contained 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"
|
||||
]
|
||||
}
|
||||
```
|
||||
-17
@@ -1,17 +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 tarball provides facilities for reading/writing v1.Images from/to
|
||||
// a tarball on-disk.
|
||||
package tarball
|
||||
-440
@@ -1,440 +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 tarball
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
comp "github.com/google/go-containerregistry/internal/compression"
|
||||
"github.com/google/go-containerregistry/pkg/compression"
|
||||
"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"
|
||||
)
|
||||
|
||||
type image struct {
|
||||
opener Opener
|
||||
manifest *Manifest
|
||||
config []byte
|
||||
imgDescriptor *Descriptor
|
||||
|
||||
tag *name.Tag
|
||||
}
|
||||
|
||||
type uncompressedImage struct {
|
||||
*image
|
||||
}
|
||||
|
||||
type compressedImage struct {
|
||||
*image
|
||||
manifestLock sync.Mutex // Protects manifest
|
||||
manifest *v1.Manifest
|
||||
}
|
||||
|
||||
var _ partial.UncompressedImageCore = (*uncompressedImage)(nil)
|
||||
var _ partial.CompressedImageCore = (*compressedImage)(nil)
|
||||
|
||||
// Opener is a thunk for opening a tar file.
|
||||
type Opener func() (io.ReadCloser, error)
|
||||
|
||||
func pathOpener(path string) Opener {
|
||||
return func() (io.ReadCloser, error) {
|
||||
return os.Open(path)
|
||||
}
|
||||
}
|
||||
|
||||
// ImageFromPath returns a v1.Image from a tarball located on path.
|
||||
func ImageFromPath(path string, tag *name.Tag) (v1.Image, error) {
|
||||
return Image(pathOpener(path), tag)
|
||||
}
|
||||
|
||||
// LoadManifest load manifest
|
||||
func LoadManifest(opener Opener) (Manifest, error) {
|
||||
m, err := extractFileFromTar(opener, "manifest.json")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer m.Close()
|
||||
|
||||
var manifest Manifest
|
||||
|
||||
if err := json.NewDecoder(m).Decode(&manifest); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return manifest, nil
|
||||
}
|
||||
|
||||
// Image exposes an image from the tarball at the provided path.
|
||||
func Image(opener Opener, tag *name.Tag) (v1.Image, error) {
|
||||
img := &image{
|
||||
opener: opener,
|
||||
tag: tag,
|
||||
}
|
||||
if err := img.loadTarDescriptorAndConfig(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
uc := uncompressedImage{
|
||||
image: img,
|
||||
}
|
||||
return partial.UncompressedToImage(&uc)
|
||||
}
|
||||
|
||||
func (i *image) MediaType() (types.MediaType, error) {
|
||||
return types.DockerManifestSchema2, nil
|
||||
}
|
||||
|
||||
// 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"`
|
||||
}
|
||||
|
||||
// Manifest represents the manifests of all images as the `manifest.json` file in a `docker save` tarball.
|
||||
type Manifest []Descriptor
|
||||
|
||||
func (m Manifest) findDescriptor(tag *name.Tag) (*Descriptor, error) {
|
||||
if tag == nil {
|
||||
if len(m) != 1 {
|
||||
return nil, errors.New("tarball must contain only a single image to be used with tarball.Image")
|
||||
}
|
||||
return &(m)[0], nil
|
||||
}
|
||||
for _, img := range m {
|
||||
for _, tagStr := range img.RepoTags {
|
||||
repoTag, err := name.NewTag(tagStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Compare the resolved names, since there are several ways to specify the same tag.
|
||||
if repoTag.Name() == tag.Name() {
|
||||
return &img, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("tag %s not found in tarball", tag)
|
||||
}
|
||||
|
||||
func (i *image) areLayersCompressed() (bool, error) {
|
||||
if len(i.imgDescriptor.Layers) == 0 {
|
||||
return false, errors.New("0 layers found in image")
|
||||
}
|
||||
layer := i.imgDescriptor.Layers[0]
|
||||
blob, err := extractFileFromTar(i.opener, layer)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer blob.Close()
|
||||
|
||||
cp, _, err := comp.PeekCompression(blob)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return cp != compression.None, nil
|
||||
}
|
||||
|
||||
func (i *image) loadTarDescriptorAndConfig() error {
|
||||
m, err := extractFileFromTar(i.opener, "manifest.json")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer m.Close()
|
||||
|
||||
if err := json.NewDecoder(m).Decode(&i.manifest); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if i.manifest == nil {
|
||||
return errors.New("no valid manifest.json in tarball")
|
||||
}
|
||||
|
||||
i.imgDescriptor, err = i.manifest.findDescriptor(i.tag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg, err := extractFileFromTar(i.opener, i.imgDescriptor.Config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cfg.Close()
|
||||
|
||||
i.config, err = io.ReadAll(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *image) RawConfigFile() ([]byte, error) {
|
||||
return i.config, nil
|
||||
}
|
||||
|
||||
// tarFile represents a single file inside a tar. Closing it closes the tar itself.
|
||||
type tarFile struct {
|
||||
io.Reader
|
||||
io.Closer
|
||||
}
|
||||
|
||||
func extractFileFromTar(opener Opener, filePath string) (io.ReadCloser, error) {
|
||||
f, err := opener()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
needClose := true
|
||||
defer func() {
|
||||
if needClose {
|
||||
f.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
tf := tar.NewReader(f)
|
||||
for {
|
||||
hdr, err := tf.Next()
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if hdr.Name == filePath {
|
||||
if hdr.Typeflag == tar.TypeSymlink || hdr.Typeflag == tar.TypeLink {
|
||||
currentDir := filepath.Dir(filePath)
|
||||
return extractFileFromTar(opener, path.Join(currentDir, path.Clean(hdr.Linkname)))
|
||||
}
|
||||
needClose = false
|
||||
return tarFile{
|
||||
Reader: tf,
|
||||
Closer: f,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("file %s not found in tar", filePath)
|
||||
}
|
||||
|
||||
// uncompressedLayerFromTarball implements partial.UncompressedLayer
|
||||
type uncompressedLayerFromTarball struct {
|
||||
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
|
||||
func (ulft *uncompressedLayerFromTarball) DiffID() (v1.Hash, error) {
|
||||
return ulft.diffID, nil
|
||||
}
|
||||
|
||||
// Uncompressed implements partial.UncompressedLayer
|
||||
func (ulft *uncompressedLayerFromTarball) Uncompressed() (io.ReadCloser, error) {
|
||||
return extractFileFromTar(ulft.opener, ulft.filePath)
|
||||
}
|
||||
|
||||
func (ulft *uncompressedLayerFromTarball) MediaType() (types.MediaType, error) {
|
||||
return ulft.mediaType, nil
|
||||
}
|
||||
|
||||
func (i *uncompressedImage) LayerByDiffID(h v1.Hash) (partial.UncompressedLayer, error) {
|
||||
cfg, err := partial.ConfigFile(i)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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
|
||||
bd, ok := i.imgDescriptor.LayerSources[h]
|
||||
if ok {
|
||||
// This is janky, but we don't want to implement Descriptor for
|
||||
// uncompressed layers because it breaks a bunch of assumptions in partial.
|
||||
// See https://github.com/google/go-containerregistry/issues/1870
|
||||
docker25workaround := bd.MediaType == types.DockerUncompressedLayer || bd.MediaType == types.OCIUncompressedLayer
|
||||
|
||||
if !docker25workaround {
|
||||
// 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
|
||||
}
|
||||
|
||||
// Intentional fall through.
|
||||
}
|
||||
|
||||
return &uncompressedLayerFromTarball{
|
||||
diffID: diffID,
|
||||
mediaType: mt,
|
||||
opener: i.opener,
|
||||
filePath: i.imgDescriptor.Layers[idx],
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("diff id %q not found", h)
|
||||
}
|
||||
|
||||
func (c *compressedImage) Manifest() (*v1.Manifest, error) {
|
||||
c.manifestLock.Lock()
|
||||
defer c.manifestLock.Unlock()
|
||||
if c.manifest != nil {
|
||||
return c.manifest, nil
|
||||
}
|
||||
|
||||
b, err := c.RawConfigFile()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfgHash, cfgSize, err := v1.SHA256(bytes.NewReader(b))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.manifest = &v1.Manifest{
|
||||
SchemaVersion: 2,
|
||||
MediaType: types.DockerManifestSchema2,
|
||||
Config: v1.Descriptor{
|
||||
MediaType: types.DockerConfigJSON,
|
||||
Size: cfgSize,
|
||||
Digest: cfgHash,
|
||||
},
|
||||
}
|
||||
|
||||
for i, p := range c.imgDescriptor.Layers {
|
||||
cfg, err := partial.ConfigFile(c)
|
||||
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,
|
||||
})
|
||||
}
|
||||
}
|
||||
return c.manifest, nil
|
||||
}
|
||||
|
||||
func (c *compressedImage) RawManifest() ([]byte, error) {
|
||||
return partial.RawManifest(c)
|
||||
}
|
||||
|
||||
// compressedLayerFromTarball implements partial.CompressedLayer
|
||||
type compressedLayerFromTarball struct {
|
||||
desc v1.Descriptor
|
||||
opener Opener
|
||||
filePath string
|
||||
}
|
||||
|
||||
// Digest implements partial.CompressedLayer
|
||||
func (clft *compressedLayerFromTarball) Digest() (v1.Hash, error) {
|
||||
return clft.desc.Digest, nil
|
||||
}
|
||||
|
||||
// Compressed implements partial.CompressedLayer
|
||||
func (clft *compressedLayerFromTarball) Compressed() (io.ReadCloser, error) {
|
||||
return extractFileFromTar(clft.opener, clft.filePath)
|
||||
}
|
||||
|
||||
// MediaType implements partial.CompressedLayer
|
||||
func (clft *compressedLayerFromTarball) MediaType() (types.MediaType, error) {
|
||||
return clft.desc.MediaType, nil
|
||||
}
|
||||
|
||||
// Size implements partial.CompressedLayer
|
||||
func (clft *compressedLayerFromTarball) Size() (int64, error) {
|
||||
return clft.desc.Size, nil
|
||||
}
|
||||
|
||||
func (c *compressedImage) LayerByDigest(h v1.Hash) (partial.CompressedLayer, error) {
|
||||
m, err := c.Manifest()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i, l := range m.Layers {
|
||||
if l.Digest == h {
|
||||
fp := c.imgDescriptor.Layers[i]
|
||||
return &compressedLayerFromTarball{
|
||||
desc: l,
|
||||
opener: c.opener,
|
||||
filePath: fp,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("blob %v not found", h)
|
||||
}
|
||||
-354
@@ -1,354 +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 tarball
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/containerd/stargz-snapshotter/estargz"
|
||||
"github.com/google/go-containerregistry/internal/and"
|
||||
comp "github.com/google/go-containerregistry/internal/compression"
|
||||
gestargz "github.com/google/go-containerregistry/internal/estargz"
|
||||
ggzip "github.com/google/go-containerregistry/internal/gzip"
|
||||
"github.com/google/go-containerregistry/internal/zstd"
|
||||
"github.com/google/go-containerregistry/pkg/compression"
|
||||
"github.com/google/go-containerregistry/pkg/logs"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/types"
|
||||
)
|
||||
|
||||
type layer struct {
|
||||
digest v1.Hash
|
||||
diffID v1.Hash
|
||||
size int64
|
||||
compressedopener Opener
|
||||
uncompressedopener Opener
|
||||
compression compression.Compression
|
||||
compressionLevel int
|
||||
annotations map[string]string
|
||||
estgzopts []estargz.Option
|
||||
mediaType types.MediaType
|
||||
}
|
||||
|
||||
// Descriptor implements partial.withDescriptor.
|
||||
func (l *layer) Descriptor() (*v1.Descriptor, error) {
|
||||
digest, err := l.Digest()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &v1.Descriptor{
|
||||
Size: l.size,
|
||||
Digest: digest,
|
||||
Annotations: l.annotations,
|
||||
MediaType: l.mediaType,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Digest implements v1.Layer
|
||||
func (l *layer) Digest() (v1.Hash, error) {
|
||||
return l.digest, nil
|
||||
}
|
||||
|
||||
// DiffID implements v1.Layer
|
||||
func (l *layer) DiffID() (v1.Hash, error) {
|
||||
return l.diffID, nil
|
||||
}
|
||||
|
||||
// Compressed implements v1.Layer
|
||||
func (l *layer) Compressed() (io.ReadCloser, error) {
|
||||
return l.compressedopener()
|
||||
}
|
||||
|
||||
// Uncompressed implements v1.Layer
|
||||
func (l *layer) Uncompressed() (io.ReadCloser, error) {
|
||||
return l.uncompressedopener()
|
||||
}
|
||||
|
||||
// Size implements v1.Layer
|
||||
func (l *layer) Size() (int64, error) {
|
||||
return l.size, nil
|
||||
}
|
||||
|
||||
// MediaType implements v1.Layer
|
||||
func (l *layer) MediaType() (types.MediaType, error) {
|
||||
return l.mediaType, nil
|
||||
}
|
||||
|
||||
// LayerOption applies options to layer
|
||||
type LayerOption func(*layer)
|
||||
|
||||
// WithCompression is a functional option for overriding the default
|
||||
// compression algorithm used for compressing uncompressed tarballs.
|
||||
// Please note that WithCompression(compression.ZStd) should be used
|
||||
// in conjunction with WithMediaType(types.OCILayerZStd)
|
||||
func WithCompression(comp compression.Compression) LayerOption {
|
||||
return func(l *layer) {
|
||||
switch comp {
|
||||
case compression.ZStd:
|
||||
l.compression = compression.ZStd
|
||||
case compression.GZip:
|
||||
l.compression = compression.GZip
|
||||
case compression.None:
|
||||
logs.Warn.Printf("Compression type 'none' is not supported for tarball layers; using gzip compression.")
|
||||
l.compression = compression.GZip
|
||||
default:
|
||||
logs.Warn.Printf("Unexpected compression type for WithCompression(): %s; using gzip compression instead.", comp)
|
||||
l.compression = compression.GZip
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithCompressionLevel is a functional option for overriding the default
|
||||
// compression level used for compressing uncompressed tarballs.
|
||||
func WithCompressionLevel(level int) LayerOption {
|
||||
return func(l *layer) {
|
||||
l.compressionLevel = level
|
||||
}
|
||||
}
|
||||
|
||||
// WithMediaType is a functional option for overriding the layer's media type.
|
||||
func WithMediaType(mt types.MediaType) LayerOption {
|
||||
return func(l *layer) {
|
||||
l.mediaType = mt
|
||||
}
|
||||
}
|
||||
|
||||
// WithCompressedCaching is a functional option that overrides the
|
||||
// logic for accessing the compressed bytes to memoize the result
|
||||
// and avoid expensive repeated gzips.
|
||||
func WithCompressedCaching(l *layer) {
|
||||
var once sync.Once
|
||||
var err error
|
||||
|
||||
buf := bytes.NewBuffer(nil)
|
||||
og := l.compressedopener
|
||||
|
||||
l.compressedopener = func() (io.ReadCloser, error) {
|
||||
once.Do(func() {
|
||||
var rc io.ReadCloser
|
||||
rc, err = og()
|
||||
if err == nil {
|
||||
defer rc.Close()
|
||||
_, err = io.Copy(buf, rc)
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return io.NopCloser(bytes.NewBuffer(buf.Bytes())), nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithEstargzOptions is a functional option that allow the caller to pass
|
||||
// through estargz.Options to the underlying compression layer. This is
|
||||
// only meaningful when estargz is enabled.
|
||||
//
|
||||
// Deprecated: WithEstargz is deprecated, and will be removed in a future release.
|
||||
func WithEstargzOptions(opts ...estargz.Option) LayerOption {
|
||||
return func(l *layer) {
|
||||
l.estgzopts = opts
|
||||
}
|
||||
}
|
||||
|
||||
// WithEstargz is a functional option that explicitly enables estargz support.
|
||||
//
|
||||
// Deprecated: WithEstargz is deprecated, and will be removed in a future release.
|
||||
func WithEstargz(l *layer) {
|
||||
oguncompressed := l.uncompressedopener
|
||||
estargz := func() (io.ReadCloser, error) {
|
||||
crc, err := oguncompressed()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
eopts := append(l.estgzopts, estargz.WithCompressionLevel(l.compressionLevel))
|
||||
rc, h, err := gestargz.ReadCloser(crc, eopts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
l.annotations[estargz.TOCJSONDigestAnnotation] = h.String()
|
||||
return &and.ReadCloser{
|
||||
Reader: rc,
|
||||
CloseFunc: func() error {
|
||||
err := rc.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// As an optimization, leverage the DiffID exposed by the estargz ReadCloser
|
||||
l.diffID, err = v1.NewHash(rc.DiffID().String())
|
||||
return err
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
uncompressed := func() (io.ReadCloser, error) {
|
||||
urc, err := estargz()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ggzip.UnzipReadCloser(urc)
|
||||
}
|
||||
|
||||
l.compressedopener = estargz
|
||||
l.uncompressedopener = uncompressed
|
||||
}
|
||||
|
||||
// LayerFromFile returns a v1.Layer given a tarball
|
||||
func LayerFromFile(path string, opts ...LayerOption) (v1.Layer, error) {
|
||||
opener := func() (io.ReadCloser, error) {
|
||||
return os.Open(path)
|
||||
}
|
||||
return LayerFromOpener(opener, opts...)
|
||||
}
|
||||
|
||||
// LayerFromOpener returns a v1.Layer given an Opener function.
|
||||
// The Opener may return either an uncompressed tarball (common),
|
||||
// or a compressed tarball (uncommon).
|
||||
//
|
||||
// When using this in conjunction with something like remote.Write
|
||||
// the uncompressed path may end up gzipping things multiple times:
|
||||
// 1. Compute the layer SHA256
|
||||
// 2. Upload the compressed layer.
|
||||
//
|
||||
// Since gzip can be expensive, we support an option to memoize the
|
||||
// compression that can be passed here: tarball.WithCompressedCaching
|
||||
func LayerFromOpener(opener Opener, opts ...LayerOption) (v1.Layer, error) {
|
||||
comp, err := comp.GetCompression(opener)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
layer := &layer{
|
||||
compression: compression.GZip,
|
||||
compressionLevel: gzip.BestSpeed,
|
||||
annotations: make(map[string]string, 1),
|
||||
mediaType: types.DockerLayer,
|
||||
}
|
||||
|
||||
if estgz := os.Getenv("GGCR_EXPERIMENT_ESTARGZ"); estgz == "1" {
|
||||
logs.Warn.Println("GGCR_EXPERIMENT_ESTARGZ is deprecated, and will be removed in a future release.")
|
||||
opts = append([]LayerOption{WithEstargz}, opts...)
|
||||
}
|
||||
|
||||
switch comp {
|
||||
case compression.GZip:
|
||||
layer.compressedopener = opener
|
||||
layer.uncompressedopener = func() (io.ReadCloser, error) {
|
||||
urc, err := opener()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ggzip.UnzipReadCloser(urc)
|
||||
}
|
||||
case compression.ZStd:
|
||||
layer.compressedopener = opener
|
||||
layer.uncompressedopener = func() (io.ReadCloser, error) {
|
||||
urc, err := opener()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return zstd.UnzipReadCloser(urc)
|
||||
}
|
||||
default:
|
||||
layer.uncompressedopener = opener
|
||||
layer.compressedopener = func() (io.ReadCloser, error) {
|
||||
crc, err := opener()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if layer.compression == compression.ZStd {
|
||||
return zstd.ReadCloserLevel(crc, layer.compressionLevel), nil
|
||||
}
|
||||
|
||||
return ggzip.ReadCloserLevel(crc, layer.compressionLevel), nil
|
||||
}
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(layer)
|
||||
}
|
||||
|
||||
// Warn if media type does not match compression
|
||||
var mediaTypeMismatch = false
|
||||
switch layer.compression {
|
||||
case compression.GZip:
|
||||
mediaTypeMismatch =
|
||||
layer.mediaType != types.OCILayer &&
|
||||
layer.mediaType != types.OCIRestrictedLayer &&
|
||||
layer.mediaType != types.DockerLayer
|
||||
|
||||
case compression.ZStd:
|
||||
mediaTypeMismatch = layer.mediaType != types.OCILayerZStd
|
||||
}
|
||||
|
||||
if mediaTypeMismatch {
|
||||
logs.Warn.Printf("Unexpected mediaType (%s) for selected compression in %s in LayerFromOpener().", layer.mediaType, layer.compression)
|
||||
}
|
||||
|
||||
if layer.digest, layer.size, err = computeDigest(layer.compressedopener); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
empty := v1.Hash{}
|
||||
if layer.diffID == empty {
|
||||
if layer.diffID, err = computeDiffID(layer.uncompressedopener); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return layer, nil
|
||||
}
|
||||
|
||||
// LayerFromReader returns a v1.Layer given a io.Reader.
|
||||
//
|
||||
// The reader's contents are read and buffered to a temp file in the process.
|
||||
//
|
||||
// Deprecated: Use LayerFromOpener or stream.NewLayer instead, if possible.
|
||||
func LayerFromReader(reader io.Reader, opts ...LayerOption) (v1.Layer, error) {
|
||||
tmp, err := os.CreateTemp("", "")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating temp file to buffer reader: %w", err)
|
||||
}
|
||||
if _, err := io.Copy(tmp, reader); err != nil {
|
||||
return nil, fmt.Errorf("writing temp file to buffer reader: %w", err)
|
||||
}
|
||||
return LayerFromFile(tmp.Name(), opts...)
|
||||
}
|
||||
|
||||
func computeDigest(opener Opener) (v1.Hash, int64, error) {
|
||||
rc, err := opener()
|
||||
if err != nil {
|
||||
return v1.Hash{}, 0, err
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
return v1.SHA256(rc)
|
||||
}
|
||||
|
||||
func computeDiffID(opener Opener) (v1.Hash, error) {
|
||||
rc, err := opener()
|
||||
if err != nil {
|
||||
return v1.Hash{}, err
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
digest, _, err := v1.SHA256(rc)
|
||||
return digest, err
|
||||
}
|
||||
-457
@@ -1,457 +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 tarball
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"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.
|
||||
// This is just syntactic sugar wrapping tarball.Write with a new file.
|
||||
func WriteToFile(p string, ref name.Reference, img v1.Image, opts ...WriteOption) error {
|
||||
w, err := os.Create(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer w.Close()
|
||||
|
||||
return Write(ref, img, w, opts...)
|
||||
}
|
||||
|
||||
// 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, opts ...WriteOption) error {
|
||||
refToImage := make(map[name.Reference]v1.Image, len(tagToImage))
|
||||
for i, d := range tagToImage {
|
||||
refToImage[i] = d
|
||||
}
|
||||
return MultiRefWriteToFile(p, refToImage, opts...)
|
||||
}
|
||||
|
||||
// MultiRefWriteToFile writes in the compressed format to a tarball, on disk.
|
||||
// This is just syntactic sugar wrapping tarball.MultiRefWrite with a new file.
|
||||
func MultiRefWriteToFile(p string, refToImage map[name.Reference]v1.Image, opts ...WriteOption) error {
|
||||
w, err := os.Create(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer w.Close()
|
||||
|
||||
return MultiRefWrite(refToImage, w, opts...)
|
||||
}
|
||||
|
||||
// Write is a wrapper to write a single image and tag to a tarball.
|
||||
func Write(ref name.Reference, img v1.Image, w io.Writer, opts ...WriteOption) error {
|
||||
return MultiRefWrite(map[name.Reference]v1.Image{ref: img}, w, opts...)
|
||||
}
|
||||
|
||||
// MultiWrite writes the contents of each image to the provided writer, in the compressed format.
|
||||
// The contents are written in the following format:
|
||||
// One manifest.json file at the top level containing information about several images.
|
||||
// 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, opts ...WriteOption) error {
|
||||
refToImage := make(map[name.Reference]v1.Image, len(tagToImage))
|
||||
for i, d := range tagToImage {
|
||||
refToImage[i] = d
|
||||
}
|
||||
return MultiRefWrite(refToImage, w, opts...)
|
||||
}
|
||||
|
||||
// MultiRefWrite writes the contents of each image to the provided writer, in the compressed format.
|
||||
// The contents are written in the following format:
|
||||
// One manifest.json file at the top level containing information about several images.
|
||||
// One file for each layer, named after the layer's SHA.
|
||||
// One file for the config blob, named after its SHA.
|
||||
func MultiRefWrite(refToImage map[name.Reference]v1.Image, w io.Writer, opts ...WriteOption) error {
|
||||
// process options
|
||||
o := &writeOptions{
|
||||
updates: nil,
|
||||
}
|
||||
for _, option := range opts {
|
||||
if err := option(o); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
imageToTags := dedupRefToImage(refToImage)
|
||||
size, mBytes, err := getSizeAndManifest(imageToTags)
|
||||
if err != nil {
|
||||
return sendUpdateReturn(o, err)
|
||||
}
|
||||
|
||||
return writeImagesToTar(imageToTags, mBytes, size, w, o)
|
||||
}
|
||||
|
||||
// sendUpdateReturn return the passed in error message, also sending on update channel, if it exists
|
||||
func sendUpdateReturn(o *writeOptions, err error) error {
|
||||
if o != nil && o.updates != nil {
|
||||
o.updates <- v1.Update{
|
||||
Error: err,
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// sendProgressWriterReturn return the passed in error message, also sending on update channel, if it exists, along with downloaded information
|
||||
func sendProgressWriterReturn(pw *progressWriter, err error) error {
|
||||
if pw != nil {
|
||||
return pw.Error(err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// writeImagesToTar writes the images to the tarball
|
||||
func writeImagesToTar(imageToTags map[v1.Image][]string, m []byte, size int64, w io.Writer, o *writeOptions) (err error) {
|
||||
if w == nil {
|
||||
return sendUpdateReturn(o, errors.New("must pass valid writer"))
|
||||
}
|
||||
|
||||
tw := w
|
||||
var pw *progressWriter
|
||||
|
||||
// we only calculate the sizes and use a progressWriter if we were provided
|
||||
// an option with a progress channel
|
||||
if o != nil && o.updates != nil {
|
||||
pw = &progressWriter{
|
||||
w: w,
|
||||
updates: o.updates,
|
||||
size: size,
|
||||
}
|
||||
tw = pw
|
||||
}
|
||||
|
||||
tf := tar.NewWriter(tw)
|
||||
defer tf.Close()
|
||||
|
||||
seenLayerDigests := make(map[string]struct{})
|
||||
|
||||
for img := range imageToTags {
|
||||
// Write the config.
|
||||
cfgName, err := img.ConfigName()
|
||||
if err != nil {
|
||||
return sendProgressWriterReturn(pw, err)
|
||||
}
|
||||
cfgBlob, err := img.RawConfigFile()
|
||||
if err != nil {
|
||||
return sendProgressWriterReturn(pw, err)
|
||||
}
|
||||
if err := writeTarEntry(tf, cfgName.String(), bytes.NewReader(cfgBlob), int64(len(cfgBlob))); err != nil {
|
||||
return sendProgressWriterReturn(pw, err)
|
||||
}
|
||||
|
||||
// Write the layers.
|
||||
layers, err := img.Layers()
|
||||
if err != nil {
|
||||
return sendProgressWriterReturn(pw, err)
|
||||
}
|
||||
layerFiles := make([]string, len(layers))
|
||||
for i, l := range layers {
|
||||
d, err := l.Digest()
|
||||
if err != nil {
|
||||
return sendProgressWriterReturn(pw, err)
|
||||
}
|
||||
// Munge the file name to appease ancient technology.
|
||||
//
|
||||
// tar assumes anything with a colon is a remote tape drive:
|
||||
// https://www.gnu.org/software/tar/manual/html_section/tar_45.html
|
||||
// Drop the algorithm prefix, e.g. "sha256:"
|
||||
hex := d.Hex
|
||||
|
||||
// gunzip expects certain file extensions:
|
||||
// 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{}{}
|
||||
|
||||
r, err := l.Compressed()
|
||||
if err != nil {
|
||||
return sendProgressWriterReturn(pw, err)
|
||||
}
|
||||
blobSize, err := l.Size()
|
||||
if err != nil {
|
||||
return sendProgressWriterReturn(pw, err)
|
||||
}
|
||||
|
||||
if err := writeTarEntry(tf, layerFiles[i], r, blobSize); err != nil {
|
||||
return sendProgressWriterReturn(pw, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := writeTarEntry(tf, "manifest.json", bytes.NewReader(m), int64(len(m))); err != nil {
|
||||
return sendProgressWriterReturn(pw, err)
|
||||
}
|
||||
|
||||
// be sure to close the tar writer so everything is flushed out before we send our EOF
|
||||
if err := tf.Close(); err != nil {
|
||||
return sendProgressWriterReturn(pw, err)
|
||||
}
|
||||
// send an EOF to indicate finished on the channel, but nil as our return error
|
||||
_ = sendProgressWriterReturn(pw, io.EOF)
|
||||
return nil
|
||||
}
|
||||
|
||||
// calculateManifest calculates the manifest and optionally the size of the tar file
|
||||
func calculateManifest(imageToTags map[v1.Image][]string) (m Manifest, err error) {
|
||||
if len(imageToTags) == 0 {
|
||||
return nil, errors.New("set of images is empty")
|
||||
}
|
||||
|
||||
for img, tags := range imageToTags {
|
||||
cfgName, err := img.ConfigName()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Store foreign layer info.
|
||||
layerSources := make(map[v1.Hash]v1.Descriptor)
|
||||
|
||||
// Write the layers.
|
||||
layers, err := img.Layers()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
layerFiles := make([]string, len(layers))
|
||||
for i, l := range layers {
|
||||
d, err := l.Digest()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Munge the file name to appease ancient technology.
|
||||
//
|
||||
// tar assumes anything with a colon is a remote tape drive:
|
||||
// https://www.gnu.org/software/tar/manual/html_section/tar_45.html
|
||||
// Drop the algorithm prefix, e.g. "sha256:"
|
||||
hex := d.Hex
|
||||
|
||||
// gunzip expects certain file extensions:
|
||||
// https://www.gnu.org/software/gzip/manual/html_node/Overview.html
|
||||
layerFiles[i] = fmt.Sprintf("%s.tar.gz", hex)
|
||||
|
||||
// Add to LayerSources if it's a foreign layer.
|
||||
desc, err := partial.BlobDescriptor(img, d)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !desc.MediaType.IsDistributable() {
|
||||
diffid, err := partial.BlobToDiffID(img, d)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
layerSources[diffid] = *desc
|
||||
}
|
||||
}
|
||||
|
||||
// Generate the tar descriptor and write it.
|
||||
m = append(m, Descriptor{
|
||||
Config: cfgName.String(),
|
||||
RepoTags: tags,
|
||||
Layers: layerFiles,
|
||||
LayerSources: layerSources,
|
||||
})
|
||||
}
|
||||
// sort by name of the repotags so it is consistent. Alternatively, we could sort by hash of the
|
||||
// descriptor, but that would make it hard for humans to process
|
||||
sort.Slice(m, func(i, j int) bool {
|
||||
return strings.Join(m[i].RepoTags, ",") < strings.Join(m[j].RepoTags, ",")
|
||||
})
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// CalculateSize calculates the expected complete size of the output tar file
|
||||
func CalculateSize(refToImage map[name.Reference]v1.Image) (size int64, err error) {
|
||||
imageToTags := dedupRefToImage(refToImage)
|
||||
size, _, err = getSizeAndManifest(imageToTags)
|
||||
return size, err
|
||||
}
|
||||
|
||||
func getSizeAndManifest(imageToTags map[v1.Image][]string) (int64, []byte, error) {
|
||||
m, err := calculateManifest(imageToTags)
|
||||
if err != nil {
|
||||
return 0, nil, fmt.Errorf("unable to calculate manifest: %w", err)
|
||||
}
|
||||
mBytes, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
return 0, nil, fmt.Errorf("could not marshall manifest to bytes: %w", err)
|
||||
}
|
||||
|
||||
size, err := calculateTarballSize(imageToTags, mBytes)
|
||||
if err != nil {
|
||||
return 0, nil, fmt.Errorf("error calculating tarball size: %w", err)
|
||||
}
|
||||
return size, mBytes, nil
|
||||
}
|
||||
|
||||
// calculateTarballSize calculates the size of the tar file
|
||||
func calculateTarballSize(imageToTags map[v1.Image][]string, mBytes []byte) (size int64, err error) {
|
||||
seenLayerDigests := make(map[string]struct{})
|
||||
for img, name := range imageToTags {
|
||||
manifest, err := img.Manifest()
|
||||
if err != nil {
|
||||
return size, fmt.Errorf("unable to get manifest for img %s: %w", name, err)
|
||||
}
|
||||
size += calculateSingleFileInTarSize(manifest.Config.Size)
|
||||
for _, l := range manifest.Layers {
|
||||
hex := l.Digest.Hex
|
||||
if _, ok := seenLayerDigests[hex]; ok {
|
||||
continue
|
||||
}
|
||||
seenLayerDigests[hex] = struct{}{}
|
||||
size += calculateSingleFileInTarSize(l.Size)
|
||||
}
|
||||
}
|
||||
// add the manifest
|
||||
size += calculateSingleFileInTarSize(int64(len(mBytes)))
|
||||
|
||||
// add the two padding blocks that indicate end of a tar file
|
||||
size += 1024
|
||||
return size, nil
|
||||
}
|
||||
|
||||
func dedupRefToImage(refToImage map[name.Reference]v1.Image) map[v1.Image][]string {
|
||||
imageToTags := make(map[v1.Image][]string)
|
||||
|
||||
for ref, img := range refToImage {
|
||||
if tag, ok := ref.(name.Tag); ok {
|
||||
if tags, ok := imageToTags[img]; !ok || tags == nil {
|
||||
imageToTags[img] = []string{}
|
||||
}
|
||||
// Docker cannot load tarballs without an explicit tag:
|
||||
// https://github.com/google/go-containerregistry/issues/890
|
||||
//
|
||||
// We can't use the fully qualified tag.Name() because of rules_docker:
|
||||
// https://github.com/google/go-containerregistry/issues/527
|
||||
//
|
||||
// If the tag is "latest", but tag.String() doesn't end in ":latest",
|
||||
// just append it. Kind of gross, but should work for now.
|
||||
ts := tag.String()
|
||||
if tag.Identifier() == name.DefaultTag && !strings.HasSuffix(ts, ":"+name.DefaultTag) {
|
||||
ts = fmt.Sprintf("%s:%s", ts, name.DefaultTag)
|
||||
}
|
||||
imageToTags[img] = append(imageToTags[img], ts)
|
||||
} else if _, ok := imageToTags[img]; !ok {
|
||||
imageToTags[img] = nil
|
||||
}
|
||||
}
|
||||
|
||||
return imageToTags
|
||||
}
|
||||
|
||||
// writeTarEntry writes a file to the provided writer with a corresponding tar header
|
||||
func writeTarEntry(tf *tar.Writer, path string, r io.Reader, size int64) error {
|
||||
hdr := &tar.Header{
|
||||
Mode: 0644,
|
||||
Typeflag: tar.TypeReg,
|
||||
Size: size,
|
||||
Name: path,
|
||||
}
|
||||
if err := tf.WriteHeader(hdr); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := io.Copy(tf, r)
|
||||
return err
|
||||
}
|
||||
|
||||
// ComputeManifest get the manifest.json that will be written to the tarball
|
||||
// for multiple references
|
||||
func ComputeManifest(refToImage map[name.Reference]v1.Image) (Manifest, error) {
|
||||
imageToTags := dedupRefToImage(refToImage)
|
||||
return calculateManifest(imageToTags)
|
||||
}
|
||||
|
||||
// WriteOption a function option to pass to Write()
|
||||
type WriteOption func(*writeOptions) error
|
||||
type writeOptions struct {
|
||||
updates chan<- v1.Update
|
||||
}
|
||||
|
||||
// WithProgress create a WriteOption for passing to Write() that enables
|
||||
// a channel to receive updates as they are downloaded and written to disk.
|
||||
func WithProgress(updates chan<- v1.Update) WriteOption {
|
||||
return func(o *writeOptions) error {
|
||||
o.updates = updates
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// progressWriter is a writer which will send the download progress
|
||||
type progressWriter struct {
|
||||
w io.Writer
|
||||
updates chan<- v1.Update
|
||||
size, complete int64
|
||||
}
|
||||
|
||||
func (pw *progressWriter) Write(p []byte) (int, error) {
|
||||
n, err := pw.w.Write(p)
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
|
||||
pw.complete += int64(n)
|
||||
|
||||
pw.updates <- v1.Update{
|
||||
Total: pw.size,
|
||||
Complete: pw.complete,
|
||||
}
|
||||
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (pw *progressWriter) Error(err error) error {
|
||||
pw.updates <- v1.Update{
|
||||
Total: pw.size,
|
||||
Complete: pw.complete,
|
||||
Error: err,
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (pw *progressWriter) Close() error {
|
||||
pw.updates <- v1.Update{
|
||||
Total: pw.size,
|
||||
Complete: pw.complete,
|
||||
Error: io.EOF,
|
||||
}
|
||||
return io.EOF
|
||||
}
|
||||
|
||||
// calculateSingleFileInTarSize calculate the size a file will take up in a tar archive,
|
||||
// given the input data. Provided by rounding up to nearest whole block (512)
|
||||
// and adding header 512
|
||||
func calculateSingleFileInTarSize(in int64) (out int64) {
|
||||
// doing this manually, because math.Round() works with float64
|
||||
out += in
|
||||
if remainder := out % 512; remainder != 0 {
|
||||
out += (512 - remainder)
|
||||
}
|
||||
out += 512
|
||||
return out
|
||||
}
|
||||
-98
@@ -1,98 +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 types holds common OCI media types.
|
||||
package types
|
||||
|
||||
// MediaType is an enumeration of the supported mime types that an element of an image might have.
|
||||
type MediaType string
|
||||
|
||||
// The collection of known MediaType values.
|
||||
const (
|
||||
OCIContentDescriptor MediaType = "application/vnd.oci.descriptor.v1+json"
|
||||
OCIImageIndex MediaType = "application/vnd.oci.image.index.v1+json"
|
||||
OCIManifestSchema1 MediaType = "application/vnd.oci.image.manifest.v1+json"
|
||||
OCIConfigJSON MediaType = "application/vnd.oci.image.config.v1+json"
|
||||
OCILayer MediaType = "application/vnd.oci.image.layer.v1.tar+gzip"
|
||||
OCILayerZStd MediaType = "application/vnd.oci.image.layer.v1.tar+zstd"
|
||||
OCIRestrictedLayer MediaType = "application/vnd.oci.image.layer.nondistributable.v1.tar+gzip"
|
||||
OCIUncompressedLayer MediaType = "application/vnd.oci.image.layer.v1.tar"
|
||||
OCIUncompressedRestrictedLayer MediaType = "application/vnd.oci.image.layer.nondistributable.v1.tar"
|
||||
|
||||
DockerManifestSchema1 MediaType = "application/vnd.docker.distribution.manifest.v1+json"
|
||||
DockerManifestSchema1Signed MediaType = "application/vnd.docker.distribution.manifest.v1+prettyjws"
|
||||
DockerManifestSchema2 MediaType = "application/vnd.docker.distribution.manifest.v2+json"
|
||||
DockerManifestList MediaType = "application/vnd.docker.distribution.manifest.list.v2+json"
|
||||
DockerLayer MediaType = "application/vnd.docker.image.rootfs.diff.tar.gzip"
|
||||
DockerConfigJSON MediaType = "application/vnd.docker.container.image.v1+json"
|
||||
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
|
||||
}
|
||||
|
||||
// IsImage returns true if the mediaType represents an image manifest, as opposed to something else, like an index.
|
||||
func (m MediaType) IsImage() bool {
|
||||
switch m {
|
||||
case OCIManifestSchema1, DockerManifestSchema2:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsIndex returns true if the mediaType represents an index, as opposed to something else, like an image.
|
||||
func (m MediaType) IsIndex() bool {
|
||||
switch m {
|
||||
case OCIImageIndex, DockerManifestList:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsConfig returns true if the mediaType represents a config, as opposed to something else, like an image.
|
||||
func (m MediaType) IsConfig() bool {
|
||||
switch m {
|
||||
case OCIConfigJSON, DockerConfigJSON:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (m MediaType) IsSchema1() bool {
|
||||
switch m {
|
||||
case DockerManifestSchema1, DockerManifestSchema1Signed:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (m MediaType) IsLayer() bool {
|
||||
switch m {
|
||||
case DockerLayer, DockerUncompressedLayer, OCILayer, OCILayerZStd, OCIUncompressedLayer, DockerForeignLayer, OCIRestrictedLayer, OCIUncompressedRestrictedLayer:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
-338
@@ -1,338 +0,0 @@
|
||||
//go:build !ignore_autogenerated
|
||||
|
||||
// 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.
|
||||
|
||||
// Code generated by deepcopy-gen. DO NOT EDIT.
|
||||
|
||||
package v1
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Config) DeepCopyInto(out *Config) {
|
||||
*out = *in
|
||||
if in.Cmd != nil {
|
||||
in, out := &in.Cmd, &out.Cmd
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.Healthcheck != nil {
|
||||
in, out := &in.Healthcheck, &out.Healthcheck
|
||||
*out = new(HealthConfig)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.Entrypoint != nil {
|
||||
in, out := &in.Entrypoint, &out.Entrypoint
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.Env != nil {
|
||||
in, out := &in.Env, &out.Env
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.Labels != nil {
|
||||
in, out := &in.Labels, &out.Labels
|
||||
*out = make(map[string]string, len(*in))
|
||||
for key, val := range *in {
|
||||
(*out)[key] = val
|
||||
}
|
||||
}
|
||||
if in.OnBuild != nil {
|
||||
in, out := &in.OnBuild, &out.OnBuild
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.Volumes != nil {
|
||||
in, out := &in.Volumes, &out.Volumes
|
||||
*out = make(map[string]struct{}, len(*in))
|
||||
for key, val := range *in {
|
||||
(*out)[key] = val
|
||||
}
|
||||
}
|
||||
if in.ExposedPorts != nil {
|
||||
in, out := &in.ExposedPorts, &out.ExposedPorts
|
||||
*out = make(map[string]struct{}, len(*in))
|
||||
for key, val := range *in {
|
||||
(*out)[key] = val
|
||||
}
|
||||
}
|
||||
if in.Shell != nil {
|
||||
in, out := &in.Shell, &out.Shell
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Config.
|
||||
func (in *Config) DeepCopy() *Config {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Config)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ConfigFile) DeepCopyInto(out *ConfigFile) {
|
||||
*out = *in
|
||||
in.Created.DeepCopyInto(&out.Created)
|
||||
if in.History != nil {
|
||||
in, out := &in.History, &out.History
|
||||
*out = make([]History, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
in.RootFS.DeepCopyInto(&out.RootFS)
|
||||
in.Config.DeepCopyInto(&out.Config)
|
||||
if in.OSFeatures != nil {
|
||||
in, out := &in.OSFeatures, &out.OSFeatures
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConfigFile.
|
||||
func (in *ConfigFile) DeepCopy() *ConfigFile {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ConfigFile)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Descriptor) DeepCopyInto(out *Descriptor) {
|
||||
*out = *in
|
||||
out.Digest = in.Digest
|
||||
if in.Data != nil {
|
||||
in, out := &in.Data, &out.Data
|
||||
*out = make([]byte, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.URLs != nil {
|
||||
in, out := &in.URLs, &out.URLs
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.Annotations != nil {
|
||||
in, out := &in.Annotations, &out.Annotations
|
||||
*out = make(map[string]string, len(*in))
|
||||
for key, val := range *in {
|
||||
(*out)[key] = val
|
||||
}
|
||||
}
|
||||
if in.Platform != nil {
|
||||
in, out := &in.Platform, &out.Platform
|
||||
*out = new(Platform)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Descriptor.
|
||||
func (in *Descriptor) DeepCopy() *Descriptor {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Descriptor)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Hash) DeepCopyInto(out *Hash) {
|
||||
*out = *in
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Hash.
|
||||
func (in *Hash) DeepCopy() *Hash {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Hash)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *HealthConfig) DeepCopyInto(out *HealthConfig) {
|
||||
*out = *in
|
||||
if in.Test != nil {
|
||||
in, out := &in.Test, &out.Test
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HealthConfig.
|
||||
func (in *HealthConfig) DeepCopy() *HealthConfig {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(HealthConfig)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *History) DeepCopyInto(out *History) {
|
||||
*out = *in
|
||||
in.Created.DeepCopyInto(&out.Created)
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new History.
|
||||
func (in *History) DeepCopy() *History {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(History)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *IndexManifest) DeepCopyInto(out *IndexManifest) {
|
||||
*out = *in
|
||||
if in.Manifests != nil {
|
||||
in, out := &in.Manifests, &out.Manifests
|
||||
*out = make([]Descriptor, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
if in.Annotations != nil {
|
||||
in, out := &in.Annotations, &out.Annotations
|
||||
*out = make(map[string]string, len(*in))
|
||||
for key, val := range *in {
|
||||
(*out)[key] = val
|
||||
}
|
||||
}
|
||||
if in.Subject != nil {
|
||||
in, out := &in.Subject, &out.Subject
|
||||
*out = new(Descriptor)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IndexManifest.
|
||||
func (in *IndexManifest) DeepCopy() *IndexManifest {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(IndexManifest)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Manifest) DeepCopyInto(out *Manifest) {
|
||||
*out = *in
|
||||
in.Config.DeepCopyInto(&out.Config)
|
||||
if in.Layers != nil {
|
||||
in, out := &in.Layers, &out.Layers
|
||||
*out = make([]Descriptor, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
if in.Annotations != nil {
|
||||
in, out := &in.Annotations, &out.Annotations
|
||||
*out = make(map[string]string, len(*in))
|
||||
for key, val := range *in {
|
||||
(*out)[key] = val
|
||||
}
|
||||
}
|
||||
if in.Subject != nil {
|
||||
in, out := &in.Subject, &out.Subject
|
||||
*out = new(Descriptor)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Manifest.
|
||||
func (in *Manifest) DeepCopy() *Manifest {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Manifest)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Platform) DeepCopyInto(out *Platform) {
|
||||
*out = *in
|
||||
if in.OSFeatures != nil {
|
||||
in, out := &in.OSFeatures, &out.OSFeatures
|
||||
*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
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Platform.
|
||||
func (in *Platform) DeepCopy() *Platform {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Platform)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *RootFS) DeepCopyInto(out *RootFS) {
|
||||
*out = *in
|
||||
if in.DiffIDs != nil {
|
||||
in, out := &in.DiffIDs, &out.DiffIDs
|
||||
*out = make([]Hash, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RootFS.
|
||||
func (in *RootFS) DeepCopy() *RootFS {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(RootFS)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Time.
|
||||
func (in *Time) DeepCopy() *Time {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Time)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user