fix(executor): make pax tar builds reproducible again (#2384)

In v1.8.0 (commit 7410007) kaniko switched to using the pax tar header
format for compressing image layers, since this format allows for greater
precision in recording timestamps, however this inadvertendly broke the
"--reproducible" functionality, due to an bug in the underlying
go-containerregistry dependency which did not set the additional
timestamps in the pax header when canonicalizing image layers. This
oversight has since been fixed in the dependency.

This commit bumps the google/go-containerregistry dependency to the
first commit which has fixed the bug
(v0.13.1-0.20230201183932-824efc7772b0). It also bumps the version of
cloud.google.com/go/storage to v1.29.0 to be compatible with the higher
transitive dependency.
This commit is contained in:
Joël Pepper
2023-03-21 18:08:29 -04:00
committed by GitHub
parent 14ea7c4071
commit 0cbb7664de
873 changed files with 78917 additions and 29139 deletions
+35 -31
View File
@@ -13,21 +13,21 @@
//
// The primary features of cmp are:
//
// • When the default behavior of equality does not suit the needs of the test,
// custom equality functions can override the equality operation.
// For example, an equality function may report floats as equal so long as they
// are within some tolerance of each other.
// - When the default behavior of equality does not suit the test's needs,
// custom equality functions can override the equality operation.
// For example, an equality function may report floats as equal so long as
// they are within some tolerance of each other.
//
// • Types that have an Equal method may use that method to determine equality.
// This allows package authors to determine the equality operation for the types
// that they define.
// - Types with an Equal method may use that method to determine equality.
// This allows package authors to determine the equality operation
// for the types that they define.
//
// • If no custom equality functions are used and no Equal method is defined,
// equality is determined by recursively comparing the primitive kinds on both
// values, much like reflect.DeepEqual. Unlike reflect.DeepEqual, unexported
// fields are not compared by default; they result in panics unless suppressed
// by using an Ignore option (see cmpopts.IgnoreUnexported) or explicitly
// compared using the Exporter option.
// - If no custom equality functions are used and no Equal method is defined,
// equality is determined by recursively comparing the primitive kinds on
// both values, much like reflect.DeepEqual. Unlike reflect.DeepEqual,
// unexported fields are not compared by default; they result in panics
// unless suppressed by using an Ignore option (see cmpopts.IgnoreUnexported)
// or explicitly compared using the Exporter option.
package cmp
import (
@@ -40,28 +40,30 @@ import (
"github.com/google/go-cmp/cmp/internal/value"
)
// TODO(≥go1.18): Use any instead of interface{}.
// Equal reports whether x and y are equal by recursively applying the
// following rules in the given order to x and y and all of their sub-values:
//
// • Let S be the set of all Ignore, Transformer, and Comparer options that
// remain after applying all path filters, value filters, and type filters.
// If at least one Ignore exists in S, then the comparison is ignored.
// If the number of Transformer and Comparer options in S is greater than one,
// then Equal panics because it is ambiguous which option to use.
// If S contains a single Transformer, then use that to transform the current
// values and recursively call Equal on the output values.
// If S contains a single Comparer, then use that to compare the current values.
// Otherwise, evaluation proceeds to the next rule.
// - Let S be the set of all Ignore, Transformer, and Comparer options that
// remain after applying all path filters, value filters, and type filters.
// If at least one Ignore exists in S, then the comparison is ignored.
// If the number of Transformer and Comparer options in S is non-zero,
// then Equal panics because it is ambiguous which option to use.
// If S contains a single Transformer, then use that to transform
// the current values and recursively call Equal on the output values.
// If S contains a single Comparer, then use that to compare the current values.
// Otherwise, evaluation proceeds to the next rule.
//
// • If the values have an Equal method of the form "(T) Equal(T) bool" or
// "(T) Equal(I) bool" where T is assignable to I, then use the result of
// x.Equal(y) even if x or y is nil. Otherwise, no such method exists and
// evaluation proceeds to the next rule.
// - If the values have an Equal method of the form "(T) Equal(T) bool" or
// "(T) Equal(I) bool" where T is assignable to I, then use the result of
// x.Equal(y) even if x or y is nil. Otherwise, no such method exists and
// evaluation proceeds to the next rule.
//
// • Lastly, try to compare x and y based on their basic kinds.
// Simple kinds like booleans, integers, floats, complex numbers, strings, and
// channels are compared using the equivalent of the == operator in Go.
// Functions are only equal if they are both nil, otherwise they are unequal.
// - Lastly, try to compare x and y based on their basic kinds.
// Simple kinds like booleans, integers, floats, complex numbers, strings,
// and channels are compared using the equivalent of the == operator in Go.
// Functions are only equal if they are both nil, otherwise they are unequal.
//
// Structs are equal if recursively calling Equal on all fields report equal.
// If a struct contains unexported fields, Equal panics unless an Ignore option
@@ -142,7 +144,7 @@ func rootStep(x, y interface{}) PathStep {
// so that they have the same parent type.
var t reflect.Type
if !vx.IsValid() || !vy.IsValid() || vx.Type() != vy.Type() {
t = reflect.TypeOf((*interface{})(nil)).Elem()
t = anyType
if vx.IsValid() {
vvx := reflect.New(t).Elem()
vvx.Set(vx)
@@ -637,7 +639,9 @@ type dynChecker struct{ curr, next int }
// Next increments the state and reports whether a check should be performed.
//
// Checks occur every Nth function call, where N is a triangular number:
//
// 0 1 3 6 10 15 21 28 36 45 55 66 78 91 105 120 136 153 171 190 ...
//
// See https://en.wikipedia.org/wiki/Triangular_number
//
// This sequence ensures that the cost of checks drops significantly as
+24 -20
View File
@@ -127,9 +127,9 @@ var randBool = rand.New(rand.NewSource(time.Now().Unix())).Intn(2) == 0
// This function returns an edit-script, which is a sequence of operations
// needed to convert one list into the other. The following invariants for
// the edit-script are maintained:
// • eq == (es.Dist()==0)
// • nx == es.LenX()
// • ny == es.LenY()
// - eq == (es.Dist()==0)
// - nx == es.LenX()
// - ny == es.LenY()
//
// This algorithm is not guaranteed to be an optimal solution (i.e., one that
// produces an edit-script with a minimal Levenshtein distance). This algorithm
@@ -169,12 +169,13 @@ func Difference(nx, ny int, f EqualFunc) (es EditScript) {
// A diagonal edge is equivalent to a matching symbol between both X and Y.
// Invariants:
// • 0 ≤ fwdPath.X ≤ (fwdFrontier.X, revFrontier.X) ≤ revPath.X ≤ nx
// • 0 ≤ fwdPath.Y ≤ (fwdFrontier.Y, revFrontier.Y) ≤ revPath.Y ≤ ny
// - 0 ≤ fwdPath.X ≤ (fwdFrontier.X, revFrontier.X) ≤ revPath.X ≤ nx
// - 0 ≤ fwdPath.Y ≤ (fwdFrontier.Y, revFrontier.Y) ≤ revPath.Y ≤ ny
//
// In general:
// • fwdFrontier.X < revFrontier.X
// • fwdFrontier.Y < revFrontier.Y
// - fwdFrontier.X < revFrontier.X
// - fwdFrontier.Y < revFrontier.Y
//
// Unless, it is time for the algorithm to terminate.
fwdPath := path{+1, point{0, 0}, make(EditScript, 0, (nx+ny)/2)}
revPath := path{-1, point{nx, ny}, make(EditScript, 0)}
@@ -195,19 +196,21 @@ func Difference(nx, ny int, f EqualFunc) (es EditScript) {
// computing sub-optimal edit-scripts between two lists.
//
// The algorithm is approximately as follows:
// • Searching for differences switches back-and-forth between
// a search that starts at the beginning (the top-left corner), and
// a search that starts at the end (the bottom-right corner). The goal of
// the search is connect with the search from the opposite corner.
// • As we search, we build a path in a greedy manner, where the first
// match seen is added to the path (this is sub-optimal, but provides a
// decent result in practice). When matches are found, we try the next pair
// of symbols in the lists and follow all matches as far as possible.
// • When searching for matches, we search along a diagonal going through
// through the "frontier" point. If no matches are found, we advance the
// frontier towards the opposite corner.
// • This algorithm terminates when either the X coordinates or the
// Y coordinates of the forward and reverse frontier points ever intersect.
// - Searching for differences switches back-and-forth between
// a search that starts at the beginning (the top-left corner), and
// a search that starts at the end (the bottom-right corner).
// The goal of the search is connect with the search
// from the opposite corner.
// - As we search, we build a path in a greedy manner,
// where the first match seen is added to the path (this is sub-optimal,
// but provides a decent result in practice). When matches are found,
// we try the next pair of symbols in the lists and follow all matches
// as far as possible.
// - When searching for matches, we search along a diagonal going through
// through the "frontier" point. If no matches are found,
// we advance the frontier towards the opposite corner.
// - This algorithm terminates when either the X coordinates or the
// Y coordinates of the forward and reverse frontier points ever intersect.
// This algorithm is correct even if searching only in the forward direction
// or in the reverse direction. We do both because it is commonly observed
@@ -389,6 +392,7 @@ type point struct{ X, Y int }
func (p *point) add(dx, dy int) { p.X += dx; p.Y += dy }
// zigzag maps a consecutive sequence of integers to a zig-zag sequence.
//
// [0 1 2 3 4 5 ...] => [0 -1 +1 -2 +2 ...]
func zigzag(x int) int {
if x&1 != 0 {
-48
View File
@@ -1,48 +0,0 @@
// Copyright 2017, The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package value
import (
"math"
"reflect"
)
// IsZero reports whether v is the zero value.
// This does not rely on Interface and so can be used on unexported fields.
func IsZero(v reflect.Value) bool {
switch v.Kind() {
case reflect.Bool:
return v.Bool() == false
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return v.Int() == 0
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return v.Uint() == 0
case reflect.Float32, reflect.Float64:
return math.Float64bits(v.Float()) == 0
case reflect.Complex64, reflect.Complex128:
return math.Float64bits(real(v.Complex())) == 0 && math.Float64bits(imag(v.Complex())) == 0
case reflect.String:
return v.String() == ""
case reflect.UnsafePointer:
return v.Pointer() == 0
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Ptr, reflect.Map, reflect.Slice:
return v.IsNil()
case reflect.Array:
for i := 0; i < v.Len(); i++ {
if !IsZero(v.Index(i)) {
return false
}
}
return true
case reflect.Struct:
for i := 0; i < v.NumField(); i++ {
if !IsZero(v.Field(i)) {
return false
}
}
return true
}
return false
}
+6 -4
View File
@@ -33,6 +33,7 @@ type Option interface {
}
// applicableOption represents the following types:
//
// Fundamental: ignore | validator | *comparer | *transformer
// Grouping: Options
type applicableOption interface {
@@ -43,6 +44,7 @@ type applicableOption interface {
}
// coreOption represents the following types:
//
// Fundamental: ignore | validator | *comparer | *transformer
// Filters: *pathFilter | *valuesFilter
type coreOption interface {
@@ -336,9 +338,9 @@ func (tr transformer) String() string {
// both implement T.
//
// The equality function must be:
// • Symmetric: equal(x, y) == equal(y, x)
// • Deterministic: equal(x, y) == equal(x, y)
// • Pure: equal(x, y) does not modify x or y
// - Symmetric: equal(x, y) == equal(y, x)
// - Deterministic: equal(x, y) == equal(x, y)
// - Pure: equal(x, y) does not modify x or y
func Comparer(f interface{}) Option {
v := reflect.ValueOf(f)
if !function.IsType(v.Type(), function.Equal) || v.IsNil() {
@@ -430,7 +432,7 @@ func AllowUnexported(types ...interface{}) Option {
}
// Result represents the comparison result for a single node and
// is provided by cmp when calling Result (see Reporter).
// is provided by cmp when calling Report (see Reporter).
type Result struct {
_ [0]func() // Make Result incomparable
flags resultFlags
+11 -9
View File
@@ -41,13 +41,13 @@ type PathStep interface {
// The type of each valid value is guaranteed to be identical to Type.
//
// In some cases, one or both may be invalid or have restrictions:
// • For StructField, both are not interface-able if the current field
// is unexported and the struct type is not explicitly permitted by
// an Exporter to traverse unexported fields.
// • For SliceIndex, one may be invalid if an element is missing from
// either the x or y slice.
// • For MapIndex, one may be invalid if an entry is missing from
// either the x or y map.
// - For StructField, both are not interface-able if the current field
// is unexported and the struct type is not explicitly permitted by
// an Exporter to traverse unexported fields.
// - For SliceIndex, one may be invalid if an element is missing from
// either the x or y slice.
// - For MapIndex, one may be invalid if an entry is missing from
// either the x or y map.
//
// The provided values must not be mutated.
Values() (vx, vy reflect.Value)
@@ -94,6 +94,7 @@ func (pa Path) Index(i int) PathStep {
// The simplified path only contains struct field accesses.
//
// For example:
//
// MyMap.MySlices.MyField
func (pa Path) String() string {
var ss []string
@@ -108,6 +109,7 @@ func (pa Path) String() string {
// GoString returns the path to a specific node using Go syntax.
//
// For example:
//
// (*root.MyMap["key"].(*mypkg.MyStruct).MySlices)[2][3].MyField
func (pa Path) GoString() string {
var ssPre, ssPost []string
@@ -159,7 +161,7 @@ func (ps pathStep) String() string {
if ps.typ == nil {
return "<nil>"
}
s := ps.typ.String()
s := value.TypeString(ps.typ, false)
if s == "" || strings.ContainsAny(s, "{}\n") {
return "root" // Type too simple or complex to print
}
@@ -282,7 +284,7 @@ type typeAssertion struct {
func (ta TypeAssertion) Type() reflect.Type { return ta.typ }
func (ta TypeAssertion) Values() (vx, vy reflect.Value) { return ta.vx, ta.vy }
func (ta TypeAssertion) String() string { return fmt.Sprintf(".(%v)", ta.typ) }
func (ta TypeAssertion) String() string { return fmt.Sprintf(".(%v)", value.TypeString(ta.typ, false)) }
// Transform is a transformation from the parent type to the current type.
type Transform struct{ *transform }
+7 -6
View File
@@ -7,8 +7,6 @@ package cmp
import (
"fmt"
"reflect"
"github.com/google/go-cmp/cmp/internal/value"
)
// numContextRecords is the number of surrounding equal records to print.
@@ -116,7 +114,10 @@ func (opts formatOptions) FormatDiff(v *valueNode, ptrs *pointerReferences) (out
}
// For leaf nodes, format the value based on the reflect.Values alone.
if v.MaxDepth == 0 {
// As a special case, treat equal []byte as a leaf nodes.
isBytes := v.Type.Kind() == reflect.Slice && v.Type.Elem() == byteType
isEqualBytes := isBytes && v.NumDiff+v.NumIgnored+v.NumTransformed == 0
if v.MaxDepth == 0 || isEqualBytes {
switch opts.DiffMode {
case diffUnknown, diffIdentical:
// Format Equal.
@@ -245,11 +246,11 @@ func (opts formatOptions) formatDiffList(recs []reportRecord, k reflect.Kind, pt
var isZero bool
switch opts.DiffMode {
case diffIdentical:
isZero = value.IsZero(r.Value.ValueX) || value.IsZero(r.Value.ValueY)
isZero = r.Value.ValueX.IsZero() || r.Value.ValueY.IsZero()
case diffRemoved:
isZero = value.IsZero(r.Value.ValueX)
isZero = r.Value.ValueX.IsZero()
case diffInserted:
isZero = value.IsZero(r.Value.ValueY)
isZero = r.Value.ValueY.IsZero()
}
if isZero {
continue
+16 -5
View File
@@ -16,6 +16,13 @@ import (
"github.com/google/go-cmp/cmp/internal/value"
)
var (
anyType = reflect.TypeOf((*interface{})(nil)).Elem()
stringType = reflect.TypeOf((*string)(nil)).Elem()
bytesType = reflect.TypeOf((*[]byte)(nil)).Elem()
byteType = reflect.TypeOf((*byte)(nil)).Elem()
)
type formatValueOptions struct {
// AvoidStringer controls whether to avoid calling custom stringer
// methods like error.Error or fmt.Stringer.String.
@@ -184,7 +191,7 @@ func (opts formatOptions) FormatValue(v reflect.Value, parentKind reflect.Kind,
}
for i := 0; i < v.NumField(); i++ {
vv := v.Field(i)
if value.IsZero(vv) {
if vv.IsZero() {
continue // Elide fields with zero values
}
if len(list) == maxLen {
@@ -205,13 +212,13 @@ func (opts formatOptions) FormatValue(v reflect.Value, parentKind reflect.Kind,
}
// Check whether this is a []byte of text data.
if t.Elem() == reflect.TypeOf(byte(0)) {
if t.Elem() == byteType {
b := v.Bytes()
isPrintSpace := func(r rune) bool { return unicode.IsPrint(r) || unicode.IsSpace(r) }
if len(b) > 0 && utf8.Valid(b) && len(bytes.TrimFunc(b, isPrintSpace)) == 0 {
out = opts.formatString("", string(b))
skipType = true
return opts.WithTypeMode(emitType).FormatType(t, out)
return opts.FormatType(t, out)
}
}
@@ -282,7 +289,12 @@ func (opts formatOptions) FormatValue(v reflect.Value, parentKind reflect.Kind,
}
defer ptrs.Pop()
skipType = true // Let the underlying value print the type instead
// Skip the name only if this is an unnamed pointer type.
// Otherwise taking the address of a value does not reproduce
// the named pointer type.
if v.Type().Name() == "" {
skipType = true // Let the underlying value print the type instead
}
out = opts.FormatValue(v.Elem(), t.Kind(), ptrs)
out = wrapTrunkReference(ptrRef, opts.PrintAddresses, out)
out = &textWrap{Prefix: "&", Value: out}
@@ -293,7 +305,6 @@ func (opts formatOptions) FormatValue(v reflect.Value, parentKind reflect.Kind,
}
// Interfaces accept different concrete types,
// so configure the underlying value to explicitly print the type.
skipType = true // Print the concrete type instead
return opts.WithTypeMode(emitType).FormatValue(v.Elem(), t.Kind(), ptrs)
default:
panic(fmt.Sprintf("%v kind not handled", v.Kind()))
+13 -12
View File
@@ -104,7 +104,7 @@ func (opts formatOptions) FormatDiffSlice(v *valueNode) textNode {
case t.Kind() == reflect.String:
sx, sy = vx.String(), vy.String()
isString = true
case t.Kind() == reflect.Slice && t.Elem() == reflect.TypeOf(byte(0)):
case t.Kind() == reflect.Slice && t.Elem() == byteType:
sx, sy = string(vx.Bytes()), string(vy.Bytes())
isString = true
case t.Kind() == reflect.Array:
@@ -147,7 +147,10 @@ func (opts formatOptions) FormatDiffSlice(v *valueNode) textNode {
})
efficiencyLines := float64(esLines.Dist()) / float64(len(esLines))
efficiencyBytes := float64(esBytes.Dist()) / float64(len(esBytes))
isPureLinedText = efficiencyLines < 4*efficiencyBytes
quotedLength := len(strconv.Quote(sx + sy))
unquotedLength := len(sx) + len(sy)
escapeExpansionRatio := float64(quotedLength) / float64(unquotedLength)
isPureLinedText = efficiencyLines < 4*efficiencyBytes || escapeExpansionRatio > 1.1
}
}
@@ -171,12 +174,13 @@ func (opts formatOptions) FormatDiffSlice(v *valueNode) textNode {
// differences in a string literal. This format is more readable,
// but has edge-cases where differences are visually indistinguishable.
// This format is avoided under the following conditions:
// • A line starts with `"""`
// • A line starts with "..."
// • A line contains non-printable characters
// • Adjacent different lines differ only by whitespace
// - A line starts with `"""`
// - A line starts with "..."
// - A line contains non-printable characters
// - Adjacent different lines differ only by whitespace
//
// For example:
//
// """
// ... // 3 identical lines
// foo
@@ -231,7 +235,7 @@ func (opts formatOptions) FormatDiffSlice(v *valueNode) textNode {
var out textNode = &textWrap{Prefix: "(", Value: list2, Suffix: ")"}
switch t.Kind() {
case reflect.String:
if t != reflect.TypeOf(string("")) {
if t != stringType {
out = opts.FormatType(t, out)
}
case reflect.Slice:
@@ -326,12 +330,12 @@ func (opts formatOptions) FormatDiffSlice(v *valueNode) textNode {
switch t.Kind() {
case reflect.String:
out = &textWrap{Prefix: "strings.Join(", Value: out, Suffix: fmt.Sprintf(", %q)", delim)}
if t != reflect.TypeOf(string("")) {
if t != stringType {
out = opts.FormatType(t, out)
}
case reflect.Slice:
out = &textWrap{Prefix: "bytes.Join(", Value: out, Suffix: fmt.Sprintf(", %q)", delim)}
if t != reflect.TypeOf([]byte(nil)) {
if t != bytesType {
out = opts.FormatType(t, out)
}
}
@@ -446,7 +450,6 @@ func (opts formatOptions) formatDiffSlice(
// {NumIdentical: 3},
// {NumInserted: 1},
// ]
//
func coalesceAdjacentEdits(name string, es diff.EditScript) (groups []diffStats) {
var prevMode byte
lastStats := func(mode byte) *diffStats {
@@ -503,7 +506,6 @@ func coalesceAdjacentEdits(name string, es diff.EditScript) (groups []diffStats)
// {NumIdentical: 8, NumRemoved: 12, NumInserted: 3},
// {NumIdentical: 63},
// ]
//
func coalesceInterveningIdentical(groups []diffStats, windowSize int) []diffStats {
groups, groupsOrig := groups[:0], groups
for i, ds := range groupsOrig {
@@ -548,7 +550,6 @@ func coalesceInterveningIdentical(groups []diffStats, windowSize int) []diffStat
// {NumRemoved: 9},
// {NumIdentical: 64}, // incremented by 10
// ]
//
func cleanupSurroundingIdentical(groups []diffStats, eq func(i, j int) bool) []diffStats {
var ix, iy int // indexes into sequence x and y
for i, ds := range groups {
+1
View File
@@ -393,6 +393,7 @@ func (s diffStats) Append(ds diffStats) diffStats {
// String prints a humanly-readable summary of coalesced records.
//
// Example:
//
// diffStats{Name: "Field", NumIgnored: 5}.String() => "5 ignored fields"
func (s diffStats) String() string {
var ss []string
@@ -0,0 +1,97 @@
// 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 compression abstracts over gzip and zstd.
package compression
import (
"bufio"
"bytes"
"io"
"github.com/google/go-containerregistry/internal/gzip"
"github.com/google/go-containerregistry/internal/zstd"
"github.com/google/go-containerregistry/pkg/compression"
)
// Opener represents e.g. opening a file.
type Opener = func() (io.ReadCloser, error)
// GetCompression detects whether an Opener is compressed and which algorithm is used.
func GetCompression(opener Opener) (compression.Compression, error) {
rc, err := opener()
if err != nil {
return compression.None, err
}
defer rc.Close()
cp, _, err := PeekCompression(rc)
if err != nil {
return compression.None, err
}
return cp, nil
}
// PeekCompression detects whether the input stream is compressed and which algorithm is used.
//
// If r implements Peek, we will use that directly, otherwise a small number
// of bytes are buffered to Peek at the gzip/zstd header, and the returned
// PeekReader can be used as a replacement for the consumed input io.Reader.
func PeekCompression(r io.Reader) (compression.Compression, PeekReader, error) {
pr := intoPeekReader(r)
if isGZip, _, err := checkHeader(pr, gzip.MagicHeader); err != nil {
return compression.None, pr, err
} else if isGZip {
return compression.GZip, pr, nil
}
if isZStd, _, err := checkHeader(pr, zstd.MagicHeader); err != nil {
return compression.None, pr, err
} else if isZStd {
return compression.ZStd, pr, nil
}
return compression.None, pr, nil
}
// PeekReader is an io.Reader that also implements Peek a la bufio.Reader.
type PeekReader interface {
io.Reader
Peek(n int) ([]byte, error)
}
// IntoPeekReader creates a PeekReader from an io.Reader.
// If the reader already has a Peek method, it will just return the passed reader.
func intoPeekReader(r io.Reader) PeekReader {
if p, ok := r.(PeekReader); ok {
return p
}
return bufio.NewReader(r)
}
// CheckHeader checks whether the first bytes from a PeekReader match an expected header
func checkHeader(pr PeekReader, expectedHeader []byte) (bool, PeekReader, error) {
header, err := pr.Peek(len(expectedHeader))
if err != nil {
// https://github.com/google/go-containerregistry/issues/367
if err == io.EOF {
return false, pr, nil
}
return false, pr, err
}
return bytes.Equal(header, expectedHeader), pr, nil
}
+4 -5
View File
@@ -18,7 +18,6 @@ package estargz
import (
"bytes"
"io"
"io/ioutil"
"github.com/containerd/stargz-snapshotter/estargz"
v1 "github.com/google/go-containerregistry/pkg/v1"
@@ -29,9 +28,9 @@ var _ io.ReadCloser = (*estargz.Blob)(nil)
// ReadCloser reads uncompressed tarball input from the io.ReadCloser and
// returns:
// * An io.ReadCloser from which compressed data may be read, and
// * A v1.Hash with the hash of the estargz table of contents, or
// * An error if the estargz processing encountered a problem.
// - An io.ReadCloser from which compressed data may be read, and
// - A v1.Hash with the hash of the estargz table of contents, or
// - An error if the estargz processing encountered a problem.
//
// Refer to estargz for the options:
// https://pkg.go.dev/github.com/containerd/stargz-snapshotter/estargz@v0.4.1#Option
@@ -39,7 +38,7 @@ func ReadCloser(r io.ReadCloser, opts ...estargz.Option) (*estargz.Blob, v1.Hash
defer r.Close()
// TODO(#876): Avoid buffering into memory.
bs, err := ioutil.ReadAll(r)
bs, err := io.ReadAll(r)
if err != nil {
return nil, v1.Hash{}, err
}
+4 -32
View File
@@ -24,7 +24,8 @@ import (
"github.com/google/go-containerregistry/internal/and"
)
var gzipMagicHeader = []byte{'\x1f', '\x8b'}
// MagicHeader is the start of gzip files.
var MagicHeader = []byte{'\x1f', '\x8b'}
// ReadCloser reads uncompressed input data from the io.ReadCloser and
// returns an io.ReadCloser from which compressed data may be read.
@@ -84,7 +85,7 @@ func ReadCloserLevel(r io.ReadCloser, level int) io.ReadCloser {
}
// UnzipReadCloser reads compressed input data from the io.ReadCloser and
// returns an io.ReadCloser from which uncompessed data may be read.
// returns an io.ReadCloser from which uncompressed data may be read.
func UnzipReadCloser(r io.ReadCloser) (io.ReadCloser, error) {
gr, err := gzip.NewReader(r)
if err != nil {
@@ -113,34 +114,5 @@ func Is(r io.Reader) (bool, error) {
if err != nil {
return false, err
}
return bytes.Equal(magicHeader, gzipMagicHeader), nil
}
// PeekReader is an io.Reader that also implements Peek a la bufio.Reader.
type PeekReader interface {
io.Reader
Peek(n int) ([]byte, error)
}
// Peek detects whether the input stream is gzip compressed.
//
// If r implements Peek, we will use that directly, otherwise a small number
// of bytes are buffered to Peek at the gzip header, and the returned
// PeekReader can be used as a replacement for the consumed input io.Reader.
func Peek(r io.Reader) (bool, PeekReader, error) {
var pr PeekReader
if p, ok := r.(PeekReader); ok {
pr = p
} else {
pr = bufio.NewReader(r)
}
header, err := pr.Peek(2)
if err != nil {
// https://github.com/google/go-containerregistry/issues/367
if err == io.EOF {
return false, pr, nil
}
return false, pr, err
}
return bytes.Equal(header, gzipMagicHeader), pr, nil
return bytes.Equal(magicHeader, MagicHeader), nil
}
@@ -17,6 +17,8 @@ package redact
import (
"context"
"errors"
"net/url"
)
type contextKey string
@@ -33,3 +35,55 @@ func FromContext(ctx context.Context) (bool, string) {
reason, ok := ctx.Value(redactKey).(string)
return ok, reason
}
// Error redacts potentially sensitive query parameter values in the URL from the error's message.
//
// If the error is a *url.Error, this returns a *url.Error with the URL redacted.
// Any other error type, or nil, is returned unchanged.
func Error(err error) error {
// If the error is a url.Error, we can redact the URL.
// Otherwise (including if err is nil), we can't redact.
var uerr *url.Error
if ok := errors.As(err, &uerr); !ok {
return err
}
u, perr := url.Parse(uerr.URL)
if perr != nil {
return err // If the URL can't be parsed, just return the original error.
}
uerr.URL = URL(u).String() // Update the URL to the redacted URL.
return uerr
}
// The set of query string keys that we expect to send as part of the registry
// protocol. Anything else is potentially dangerous to leak, as it's probably
// from a redirect. These redirects often included tokens or signed URLs.
var paramAllowlist = map[string]struct{}{
// Token exchange
"scope": {},
"service": {},
// Cross-repo mounting
"mount": {},
"from": {},
// Layer PUT
"digest": {},
// Listing tags and catalog
"n": {},
"last": {},
}
// URL redacts potentially sensitive query parameter values from the URL's query string.
func URL(u *url.URL) *url.URL {
qs := u.Query()
for k, v := range qs {
for i := range v {
if _, ok := paramAllowlist[k]; !ok {
// key is not in the Allowlist
v[i] = "REDACTED"
}
}
}
r := *u
r.RawQuery = qs.Encode()
return &r
}
+18 -1
View File
@@ -18,6 +18,7 @@ package retry
import (
"context"
"errors"
"fmt"
"github.com/google/go-containerregistry/internal/retry/wait"
@@ -36,7 +37,7 @@ type temporary interface {
// IsTemporary returns true if err implements Temporary() and it returns true.
func IsTemporary(err error) bool {
if err == context.DeadlineExceeded {
if errors.Is(err, context.DeadlineExceeded) {
return false
}
if te, ok := err.(temporary); ok && te.Temporary() {
@@ -75,3 +76,19 @@ func Retry(f func() error, p Predicate, backoff wait.Backoff) (err error) {
wait.ExponentialBackoff(backoff, condition)
return
}
type contextKey string
var key = contextKey("never")
// Never returns a context that signals something should not be retried.
// This is a hack and can be used to communicate across package boundaries
// to avoid retry amplification.
func Never(ctx context.Context) context.Context {
return context.WithValue(ctx, key, true)
}
// Ever returns true if the context was wrapped by Never.
func Ever(ctx context.Context) bool {
return ctx.Value(key) == nil
}
+116
View File
@@ -0,0 +1,116 @@
// 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 zstd provides helper functions for interacting with zstd streams.
package zstd
import (
"bufio"
"bytes"
"io"
"github.com/google/go-containerregistry/internal/and"
"github.com/klauspost/compress/zstd"
)
// MagicHeader is the start of zstd files.
var MagicHeader = []byte{'\x28', '\xb5', '\x2f', '\xfd'}
// ReadCloser reads uncompressed input data from the io.ReadCloser and
// returns an io.ReadCloser from which compressed data may be read.
// This uses zstd level 1 for the compression.
func ReadCloser(r io.ReadCloser) io.ReadCloser {
return ReadCloserLevel(r, 1)
}
// ReadCloserLevel reads uncompressed input data from the io.ReadCloser and
// returns an io.ReadCloser from which compressed data may be read.
func ReadCloserLevel(r io.ReadCloser, level int) io.ReadCloser {
pr, pw := io.Pipe()
// For highly compressible layers, zstd.Writer will output a very small
// number of bytes per Write(). This is normally fine, but when pushing
// to a registry, we want to ensure that we're taking full advantage of
// the available bandwidth instead of sending tons of tiny writes over
// the wire.
// 64K ought to be small enough for anybody.
bw := bufio.NewWriterSize(pw, 2<<16)
// Returns err so we can pw.CloseWithError(err)
go func() error {
// TODO(go1.14): Just defer {pw,zw,r}.Close like you'd expect.
// Context: https://golang.org/issue/24283
zw, err := zstd.NewWriter(bw, zstd.WithEncoderLevel(zstd.EncoderLevelFromZstd(level)))
if err != nil {
return pw.CloseWithError(err)
}
if _, err := io.Copy(zw, r); err != nil {
defer r.Close()
defer zw.Close()
return pw.CloseWithError(err)
}
// Close zstd writer to Flush it and write zstd trailers.
if err := zw.Close(); err != nil {
return pw.CloseWithError(err)
}
// Flush bufio writer to ensure we write out everything.
if err := bw.Flush(); err != nil {
return pw.CloseWithError(err)
}
// We don't really care if these fail.
defer pw.Close()
defer r.Close()
return nil
}()
return pr
}
// UnzipReadCloser reads compressed input data from the io.ReadCloser and
// returns an io.ReadCloser from which uncompressed data may be read.
func UnzipReadCloser(r io.ReadCloser) (io.ReadCloser, error) {
gr, err := zstd.NewReader(r)
if err != nil {
return nil, err
}
return &and.ReadCloser{
Reader: gr,
CloseFunc: func() error {
// If the unzip fails, then this seems to return the same
// error as the read. We don't want this to interfere with
// us closing the main ReadCloser, since this could leave
// an open file descriptor (fails on Windows).
gr.Close()
return r.Close()
},
}, nil
}
// Is detects whether the input stream is compressed.
func Is(r io.Reader) (bool, error) {
magicHeader := make([]byte, 4)
n, err := r.Read(magicHeader)
if n == 0 && err == io.EOF {
return false, nil
}
if err != nil {
return false, err
}
return bytes.Equal(magicHeader, MagicHeader), nil
}
+2 -2
View File
@@ -110,8 +110,8 @@ For example:
kc := authn.NewMultiKeychain(
authn.DefaultKeychain,
google.Keychain,
authn.NewFromHelper(ecr.ECRHelper{ClientFactory: api.DefaultClientFactory{}}),
authn.NewFromHelper(acr.ACRCredHelper{}),
authn.NewKeychainFromHelper(ecr.ECRHelper{ClientFactory: api.DefaultClientFactory{}}),
authn.NewKeychainFromHelper(acr.ACRCredHelper{}),
)
```
+12 -6
View File
@@ -75,15 +75,11 @@ func (dk *defaultKeychain) Resolve(target Resource) (Authenticator, error) {
foundDockerConfig := false
home, err := homedir.Dir()
if err == nil {
if _, err := os.Stat(filepath.Join(home, ".docker/config.json")); err == nil {
foundDockerConfig = true
}
foundDockerConfig = fileExists(filepath.Join(home, ".docker/config.json"))
}
// If $HOME/.docker/config.json isn't found, check $DOCKER_CONFIG (if set)
if !foundDockerConfig && os.Getenv("DOCKER_CONFIG") != "" {
if _, err := os.Stat(filepath.Join(os.Getenv("DOCKER_CONFIG"), "config.json")); err == nil {
foundDockerConfig = true
}
foundDockerConfig = fileExists(filepath.Join(os.Getenv("DOCKER_CONFIG"), "config.json"))
}
// If either of those locations are found, load it using Docker's
// config.Load, which may fail if the config can't be parsed.
@@ -127,6 +123,10 @@ func (dk *defaultKeychain) Resolve(target Resource) (Authenticator, error) {
if err != nil {
return nil, err
}
// cf.GetAuthConfig automatically sets the ServerAddress attribute. Since
// we don't make use of it, clear the value for a proper "is-empty" test.
// See: https://github.com/google/go-containerregistry/issues/1510
cfg.ServerAddress = ""
if cfg != empty {
break
}
@@ -144,6 +144,12 @@ func (dk *defaultKeychain) Resolve(target Resource) (Authenticator, error) {
}), nil
}
// fileExists returns true if the given path exists and is not a directory.
func fileExists(path string) bool {
fi, err := os.Stat(path)
return err == nil && !fi.IsDir()
}
// Helper is a subset of the Docker credential helper credentials.Helper
// interface used by NewKeychainFromHelper.
//
@@ -0,0 +1,26 @@
// 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 compression abstracts over gzip and zstd.
package compression
// Compression is an enumeration of the supported compression algorithms
type Compression string
// The collection of known MediaType values.
const (
None Compression = "none"
GZip Compression = "gzip"
ZStd Compression = "zstd"
)
+6 -6
View File
@@ -16,24 +16,24 @@
package logs
import (
"io/ioutil"
"io"
"log"
)
var (
// Warn is used to log non-fatal errors.
Warn = log.New(ioutil.Discard, "", log.LstdFlags)
Warn = log.New(io.Discard, "", log.LstdFlags)
// Progress is used to log notable, successful events.
Progress = log.New(ioutil.Discard, "", log.LstdFlags)
Progress = log.New(io.Discard, "", log.LstdFlags)
// Debug is used to log information that is useful for debugging.
Debug = log.New(ioutil.Discard, "", log.LstdFlags)
Debug = log.New(io.Discard, "", log.LstdFlags)
)
// Enabled checks to see if the logger's writer is set to something other
// than ioutil.Discard. This allows callers to avoid expensive operations
// than io.Discard. This allows callers to avoid expensive operations
// that will end up in /dev/null anyway.
func Enabled(l *log.Logger) bool {
return l.Writer() != ioutil.Discard
return l.Writer() != io.Discard
}
+13 -15
View File
@@ -15,15 +15,14 @@
package name
import (
// nolint: depguard
_ "crypto/sha256" // Recommended by go-digest.
"strings"
"github.com/opencontainers/go-digest"
)
const (
// These have the form: sha256:<hex string>
// TODO(dekkagaijin): replace with opencontainers/go-digest or docker/distribution's validation.
digestChars = "sh:0123456789abcdef"
digestDelim = "@"
)
const digestDelim = "@"
// Digest stores a digest name in a structured form.
type Digest struct {
@@ -60,10 +59,6 @@ func (d Digest) String() string {
return d.original
}
func checkDigest(name string) error {
return checkElement("digest", name, digestChars, 7+64, 7+64)
}
// NewDigest returns a new Digest representing the given name.
func NewDigest(name string, opts ...Option) (Digest, error) {
// Split on "@"
@@ -72,10 +67,13 @@ func NewDigest(name string, opts ...Option) (Digest, error) {
return Digest{}, newErrBadName("a digest must contain exactly one '@' separator (e.g. registry/repository@digest) saw: %s", name)
}
base := parts[0]
digest := parts[1]
// Always check that the digest is valid.
if err := checkDigest(digest); err != nil {
dig := parts[1]
prefix := digest.Canonical.String() + ":"
if !strings.HasPrefix(dig, prefix) {
return Digest{}, newErrBadName("unsupported digest algorithm: %s", dig)
}
hex := strings.TrimPrefix(dig, prefix)
if err := digest.Canonical.Validate(hex); err != nil {
return Digest{}, err
}
@@ -90,7 +88,7 @@ func NewDigest(name string, opts ...Option) (Digest, error) {
}
return Digest{
Repository: repo,
digest: digest,
digest: dig,
original: name,
}, nil
}
+7 -1
View File
@@ -28,8 +28,14 @@ func (e *ErrBadName) Error() string {
return e.info
}
// Is reports whether target is an error of type ErrBadName
func (e *ErrBadName) Is(target error) bool {
var berr *ErrBadName
return errors.As(target, &berr)
}
// newErrBadName returns a ErrBadName which returns the given formatted string from Error().
func newErrBadName(fmtStr string, args ...interface{}) *ErrBadName {
func newErrBadName(fmtStr string, args ...any) *ErrBadName {
return &ErrBadName{fmt.Sprintf(fmtStr, args...)}
}
+7 -7
View File
@@ -56,16 +56,16 @@ type stringConst string
// To discourage its use in scenarios where the value is not known at code
// authoring time, it must be passed a string constant:
//
// const str = "valid/string"
// MustParseReference(str)
// MustParseReference("another/valid/string")
// MustParseReference(str + "/and/more")
// const str = "valid/string"
// MustParseReference(str)
// MustParseReference("another/valid/string")
// MustParseReference(str + "/and/more")
//
// These will not compile:
//
// var str = "valid/string"
// MustParseReference(str)
// MustParseReference(strings.Join([]string{"valid", "string"}, "/"))
// var str = "valid/string"
// MustParseReference(str)
// MustParseReference(strings.Join([]string{"valid", "string"}, "/"))
func MustParseReference(s stringConst, opts ...Option) Reference {
ref, err := ParseReference(string(s), opts...)
if err != nil {
+5 -2
View File
@@ -37,6 +37,7 @@ type ConfigFile struct {
RootFS RootFS `json:"rootfs"`
Config Config `json:"config"`
OSVersion string `json:"os.version,omitempty"`
Variant string `json:"variant,omitempty"`
}
// History is one entry of a list recording how this container image was built.
@@ -89,8 +90,10 @@ type HealthConfig struct {
}
// 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 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
+2 -3
View File
@@ -18,7 +18,6 @@ import (
"bytes"
"context"
"io"
"io/ioutil"
"sync"
"github.com/google/go-containerregistry/pkg/name"
@@ -63,12 +62,12 @@ func (i *imageOpener) bufferedOpener() (io.ReadCloser, error) {
}
defer rc.Close()
return ioutil.ReadAll(rc)
return io.ReadAll(rc)
}()
})
// Wrap the bytes in a ReadCloser so it looks like an opened file.
return ioutil.NopCloser(bytes.NewReader(i.bytes)), i.err
return io.NopCloser(bytes.NewReader(i.bytes)), i.err
}
func (i *imageOpener) opener() tarball.Opener {
+1 -2
View File
@@ -17,7 +17,6 @@ package daemon
import (
"fmt"
"io"
"io/ioutil"
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
@@ -52,7 +51,7 @@ func Write(tag name.Tag, img v1.Image, options ...Option) (string, error) {
return "", fmt.Errorf("error loading image: %w", err)
}
defer resp.Body.Close()
b, err := ioutil.ReadAll(resp.Body)
b, err := io.ReadAll(resp.Body)
response := string(b)
if err != nil {
return response, fmt.Errorf("error reading load response body: %w", err)
+6 -6
View File
@@ -129,12 +129,12 @@ func (tsa *tokenSourceAuth) Authorization() (*authn.AuthConfig, error) {
//
// `gcloud config config-helper --format=json(credential)` looks something like:
//
// {
// "credential": {
// "access_token": "ya29.abunchofnonsense",
// "token_expiry": "2018-12-02T04:08:13Z"
// }
// }
// {
// "credential": {
// "access_token": "supersecretaccesstoken",
// "token_expiry": "2018-12-02T04:08:13Z"
// }
// }
type gcloudOutput struct {
Credential struct {
AccessToken string `json:"access_token"`
@@ -71,11 +71,13 @@ func (gk *googleKeychain) Resolve(target authn.Resource) (authn.Authenticator, e
func resolve() authn.Authenticator {
auth, envErr := NewEnvAuthenticator()
if envErr == nil && auth != authn.Anonymous {
logs.Debug.Println("google.Keychain: using Application Default Credentials")
return auth
}
auth, gErr := NewGcloudAuthenticator()
if gErr == nil && auth != authn.Anonymous {
logs.Debug.Println("google.Keychain: using gcloud fallback")
return auth
}
+3 -3
View File
@@ -15,7 +15,7 @@
package v1
import (
"crypto/sha256"
"crypto"
"encoding/hex"
"encoding/json"
"fmt"
@@ -78,7 +78,7 @@ func (h *Hash) UnmarshalText(text []byte) error {
func Hasher(name string) (hash.Hash, error) {
switch name {
case "sha256":
return sha256.New(), nil
return crypto.SHA256.New(), nil
default:
return nil, fmt.Errorf("unsupported hash: %q", name)
}
@@ -111,7 +111,7 @@ func (h *Hash) parse(unquoted string) error {
// SHA256 computes the Hash of the provided io.Reader's content.
func SHA256(r io.Reader) (Hash, int64, error) {
hasher := sha256.New()
hasher := crypto.SHA256.New()
n, err := io.Copy(hasher, r)
if err != nil {
return Hash{}, 0, err
+1 -2
View File
@@ -16,7 +16,6 @@ package layout
import (
"io"
"io/ioutil"
"os"
v1 "github.com/google/go-containerregistry/pkg/v1"
@@ -30,7 +29,7 @@ func (l Path) Blob(h v1.Hash) (io.ReadCloser, error) {
// 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 ioutil.ReadFile(l.blobPath(h))
return os.ReadFile(l.blobPath(h))
}
func (l Path) blobPath(h v1.Hash) string {
+2 -2
View File
@@ -19,7 +19,7 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"os"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/partial"
@@ -45,7 +45,7 @@ func ImageIndexFromPath(path string) (v1.ImageIndex, error) {
// ImageIndex returns a v1.ImageIndex for the Path.
func (l Path) ImageIndex() (v1.ImageIndex, error) {
rawIndex, err := ioutil.ReadFile(l.path("index.json"))
rawIndex, err := os.ReadFile(l.path("index.json"))
if err != nil {
return nil, err
}
+14
View File
@@ -1,3 +1,17 @@
// 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"
+19 -49
View File
@@ -20,7 +20,6 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
@@ -45,33 +44,17 @@ func (l Path) AppendImage(img v1.Image, options ...Option) error {
return err
}
mt, err := img.MediaType()
desc, err := partial.Descriptor(img)
if err != nil {
return err
}
d, err := img.Digest()
if err != nil {
return err
}
manifest, err := img.RawManifest()
if err != nil {
return err
}
desc := v1.Descriptor{
MediaType: mt,
Size: int64(len(manifest)),
Digest: d,
}
o := makeOptions(options...)
for _, opt := range o.descOpts {
opt(&desc)
opt(desc)
}
return l.AppendDescriptor(desc)
return l.AppendDescriptor(*desc)
}
// AppendIndex writes a v1.ImageIndex to the Path and updates
@@ -81,33 +64,17 @@ func (l Path) AppendIndex(ii v1.ImageIndex, options ...Option) error {
return err
}
mt, err := ii.MediaType()
desc, err := partial.Descriptor(ii)
if err != nil {
return err
}
d, err := ii.Digest()
if err != nil {
return err
}
manifest, err := ii.RawManifest()
if err != nil {
return err
}
desc := v1.Descriptor{
MediaType: mt,
Size: int64(len(manifest)),
Digest: d,
}
o := makeOptions(options...)
for _, opt := range o.descOpts {
opt(&desc)
opt(desc)
}
return l.AppendDescriptor(desc)
return l.AppendDescriptor(*desc)
}
// AppendDescriptor adds a descriptor to the index.json of the Path.
@@ -219,7 +186,7 @@ func (l Path) WriteFile(name string, data []byte, perm os.FileMode) error {
return err
}
return ioutil.WriteFile(l.path(name), data, perm)
return os.WriteFile(l.path(name), data, perm)
}
// WriteBlob copies a file to the blobs/ directory in the Path from the given ReadCloser at
@@ -247,7 +214,7 @@ func (l Path) writeBlob(hash v1.Hash, size int64, rc io.ReadCloser, renamer func
// If a renamer func was provided write to a temporary file
open := func() (*os.File, error) { return os.Create(file) }
if renamer != nil {
open = func() (*os.File, error) { return ioutil.TempFile(dir, hash.Hex) }
open = func() (*os.File, error) { return os.CreateTemp(dir, hash.Hex) }
}
w, err := open()
if err != nil {
@@ -305,7 +272,7 @@ func (l Path) writeLayer(layer v1.Layer) error {
if errors.Is(err, stream.ErrNotComputed) {
// Allow digest errors, since streams may not have calculated the hash
// yet. Instead, use an empty value, which will be transformed into a
// random file name with `ioutil.TempFile` and the final digest will be
// 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: ""}
@@ -383,7 +350,7 @@ func (l Path) WriteImage(img v1.Image) error {
if err != nil {
return err
}
if err := l.WriteBlob(cfgName, ioutil.NopCloser(bytes.NewReader(cfgBlob))); err != nil {
if err := l.WriteBlob(cfgName, io.NopCloser(bytes.NewReader(cfgBlob))); err != nil {
return err
}
@@ -397,7 +364,7 @@ func (l Path) WriteImage(img v1.Image) error {
return err
}
return l.WriteBlob(d, ioutil.NopCloser(bytes.NewReader(manifest)))
return l.WriteBlob(d, io.NopCloser(bytes.NewReader(manifest)))
}
type withLayer interface {
@@ -492,12 +459,15 @@ func (l Path) WriteIndex(ii v1.ImageIndex) error {
//
// 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.
//
// 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.
//
// 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.
+10 -7
View File
@@ -28,6 +28,7 @@ type Manifest struct {
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.
@@ -36,17 +37,19 @@ type IndexManifest struct {
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"`
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.
+3 -1
View File
@@ -25,7 +25,9 @@ import (
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:
//
// "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)
+2
View File
@@ -37,6 +37,7 @@ type image struct {
configMediaType *types.MediaType
diffIDMap map[v1.Hash]v1.Layer
digestMap map[v1.Hash]v1.Layer
subject *v1.Descriptor
}
var _ v1.Image = (*image)(nil)
@@ -153,6 +154,7 @@ func (i *image) compute() error {
manifest.Annotations[k] = v
}
}
manifest.Subject = i.subject
i.configFile = configFile
i.manifest = manifest
+2
View File
@@ -70,6 +70,7 @@ type index struct {
imageMap map[v1.Hash]v1.Image
indexMap map[v1.Hash]v1.ImageIndex
layerMap map[v1.Hash]v1.Layer
subject *v1.Descriptor
}
var _ v1.ImageIndex = (*index)(nil)
@@ -142,6 +143,7 @@ func (i *index) compute() error {
manifest.Annotations[k] = v
}
}
manifest.Subject = i.subject
i.manifest = manifest
i.computed = true
+102 -32
View File
@@ -21,7 +21,6 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"path/filepath"
"strings"
"time"
@@ -116,9 +115,33 @@ func Config(base v1.Image, cfg v1.Config) (v1.Image, error) {
return ConfigFile(base, cf)
}
// Annotatable represents a manifest that can carry annotations.
type Annotatable interface {
partial.WithRawManifest
// 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.
@@ -126,19 +149,19 @@ type Annotatable interface {
// 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)
// 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)
// idx := Annotations(empty.Index, map[string]string{
// "foo": "bar",
// }).(v1.ImageIndex)
//
// If the input Annotatable is not an Image or ImageIndex, the result will
// attempt to lazily annotate the raw manifest.
func Annotations(f Annotatable, anns map[string]string) Annotatable {
func Annotations(f partial.WithRawManifest, anns map[string]string) partial.WithRawManifest {
if img, ok := f.(v1.Image); ok {
return &image{
base: img,
@@ -151,12 +174,13 @@ func Annotations(f Annotatable, anns map[string]string) Annotatable {
annotations: anns,
}
}
return arbitraryRawManifest{f, anns}
return arbitraryRawManifest{a: f, anns: anns}
}
type arbitraryRawManifest struct {
a Annotatable
anns map[string]string
a partial.WithRawManifest
anns map[string]string
subject *v1.Descriptor
}
func (a arbitraryRawManifest) RawManifest() ([]byte, error) {
@@ -164,7 +188,7 @@ func (a arbitraryRawManifest) RawManifest() ([]byte, error) {
if err != nil {
return nil, err
}
var m map[string]interface{}
var m map[string]any
if err := json.Unmarshal(b, &m); err != nil {
return nil, err
}
@@ -179,6 +203,9 @@ func (a arbitraryRawManifest) RawManifest() ([]byte, error) {
} else {
m["annotations"] = a.anns
}
if a.subject != nil {
m["subject"] = a.subject
}
return json.Marshal(m)
}
@@ -244,6 +271,7 @@ func extract(img v1.Image, w io.Writer) error {
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.
@@ -267,6 +295,10 @@ func extract(img v1.Image, w io.Writer) error {
// 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)
@@ -297,7 +329,9 @@ func extract(img v1.Image, w io.Writer) error {
// any entries with a matching (or child) name
fileMap[name] = tombstone || !(header.Typeflag == tar.TypeDir)
if !tombstone {
tarWriter.WriteHeader(header)
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
@@ -326,6 +360,13 @@ func inWhiteoutDir(fileMap map[string]bool, file string) bool {
return false
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
// 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
@@ -335,26 +376,45 @@ func Time(img v1.Image, t time.Time) (v1.Image, error) {
return nil, fmt.Errorf("getting image layers: %w", err)
}
// Strip away all timestamps from layers
newLayers := make([]v1.Layer, len(layers))
for idx, layer := range layers {
newLayer, err := layerTime(layer, t)
if err != nil {
return nil, fmt.Errorf("setting layer times: %w", err)
}
newLayers[idx] = newLayer
}
newImage, err = AppendLayers(newImage, newLayers...)
if err != nil {
return nil, fmt.Errorf("appending 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
}
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)
@@ -377,6 +437,7 @@ func Time(img v1.Image, t time.Time) (v1.Image, error) {
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
}
@@ -404,6 +465,13 @@ func layerTime(layer v1.Layer, t time.Time) (v1.Layer, error) {
}
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)
}
@@ -423,7 +491,7 @@ func layerTime(layer v1.Layer, t time.Time) (v1.Layer, error) {
b := w.Bytes()
// gzip the contents, then create the layer
opener := func() (io.ReadCloser, error) {
return gzip.ReadCloser(ioutil.NopCloser(bytes.NewReader(b))), nil
return gzip.ReadCloser(io.NopCloser(bytes.NewReader(b))), nil
}
layer, err = tarball.LayerFromOpener(opener)
if err != nil {
@@ -467,6 +535,8 @@ func MediaType(img v1.Image, mt types.MediaType) v1.Image {
}
// 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,
+14 -7
View File
@@ -18,7 +18,10 @@ 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"
)
@@ -51,23 +54,27 @@ func (cle *compressedLayerExtender) Uncompressed() (io.ReadCloser, error) {
return nil, err
}
// Often, the "compressed" bytes are not actually gzip-compressed.
// Peek at the first two bytes to determine whether or not it's correct to
// wrap this with gzip.UnzipReadCloser.
gzipped, pr, err := gzip.Peek(rc)
// 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,
}
if !gzipped {
switch cp {
case comp.GZip:
return gzip.UnzipReadCloser(prc)
case comp.ZStd:
return zstd.UnzipReadCloser(prc)
default:
return prc, nil
}
return gzip.UnzipReadCloser(prc)
}
// DiffID implements v1.Layer
+52 -5
View File
@@ -19,7 +19,6 @@ import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/types"
@@ -67,12 +66,12 @@ func (cl *configLayer) DiffID() (v1.Hash, error) {
// Uncompressed implements v1.Layer
func (cl *configLayer) Uncompressed() (io.ReadCloser, error) {
return ioutil.NopCloser(bytes.NewBuffer(cl.content)), nil
return io.NopCloser(bytes.NewBuffer(cl.content)), nil
}
// Compressed implements v1.Layer
func (cl *configLayer) Compressed() (io.ReadCloser, error) {
return ioutil.NopCloser(bytes.NewBuffer(cl.content)), nil
return io.NopCloser(bytes.NewBuffer(cl.content)), nil
}
// Size implements v1.Layer
@@ -88,9 +87,22 @@ func (cl *configLayer) MediaType() (types.MediaType, error) {
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
@@ -316,10 +328,28 @@ func Descriptor(d Describable) (*v1.Descriptor, error) {
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)
}
@@ -342,7 +372,7 @@ func UncompressedSize(l v1.Layer) (int64, error) {
}
defer rc.Close()
return io.Copy(ioutil.Discard, rc)
return io.Copy(io.Discard, rc)
}
type withExists interface {
@@ -372,7 +402,7 @@ func Exists(l v1.Layer) (bool, error) {
// Recursively unwrap our wrappers so that we can check for the original implementation.
// We might want to expose this?
func unwrap(i interface{}) interface{} {
func unwrap(i any) any {
if ule, ok := i.(*uncompressedLayerExtender); ok {
return unwrap(ule.UncompressedLayer)
}
@@ -387,3 +417,20 @@ func unwrap(i interface{}) interface{} {
}
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
}
+3 -4
View File
@@ -17,12 +17,11 @@ package random
import (
"archive/tar"
"bytes"
"crypto"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"io/ioutil"
mrand "math/rand"
"time"
@@ -47,7 +46,7 @@ func (ul *uncompressedLayer) DiffID() (v1.Hash, error) {
// Uncompressed implements partial.UncompressedLayer
func (ul *uncompressedLayer) Uncompressed() (io.ReadCloser, error) {
return ioutil.NopCloser(bytes.NewBuffer(ul.content)), nil
return io.NopCloser(bytes.NewBuffer(ul.content)), nil
}
// MediaType returns the media type of the layer
@@ -85,7 +84,7 @@ func Layer(byteSize int64, mt types.MediaType) (v1.Layer, error) {
// Hash the contents as we write it out to the buffer.
var b bytes.Buffer
hasher := sha256.New()
hasher := crypto.SHA256.New()
mw := io.MultiWriter(&b, hasher)
// Write a single file with a random name and random contents.
+18 -5
View File
@@ -1,3 +1,17 @@
// 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 (
@@ -24,7 +38,7 @@ func CheckPushPermission(ref name.Reference, kc authn.Keychain, t http.RoundTrip
}
scopes := []string{ref.Scope(transport.PushScope)}
tr, err := transport.New(ref.Context().Registry, auth, t, scopes)
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)
}
@@ -35,11 +49,10 @@ func CheckPushPermission(ref name.Reference, kc authn.Keychain, t http.RoundTrip
// authorize a push. Figure out how to return early here when we can,
// to avoid a roundtrip for spec-compliant registries.
w := writer{
repo: ref.Context(),
client: &http.Client{Transport: tr},
context: context.Background(),
repo: ref.Context(),
client: &http.Client{Transport: tr},
}
loc, _, err := w.initiateUpload("", "")
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
@@ -54,4 +54,8 @@ func Delete(ref name.Reference, options ...Option) error {
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.
}
+60 -9
View File
@@ -17,13 +17,15 @@ package remote
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"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/logs"
"github.com/google/go-containerregistry/pkg/name"
@@ -59,7 +61,7 @@ type Descriptor struct {
v1.Descriptor
Manifest []byte
// So we can share this implementation with Image..
// So we can share this implementation with Image.
platform v1.Platform
}
@@ -237,6 +239,45 @@ func (f *fetcher) url(resource, identifier string) url.URL {
}
}
// 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.IndexManifest, error) {
// Assume the registry doesn't support the Referrers API endpoint, so we'll use the fallback tag scheme.
b, _, err := f.fetchManifest(fallbackTag(d), []types.MediaType{types.OCIImageIndex})
if err != nil {
return nil, err
}
var terr *transport.Error
if ok := errors.As(err, &terr); ok && terr.StatusCode == http.StatusNotFound {
// Not found just means there are no attachments yet. Start with an empty manifest.
return &v1.IndexManifest{MediaType: types.OCIImageIndex}, nil
}
var im v1.IndexManifest
if err := json.Unmarshal(b, &im); err != nil {
return nil, err
}
// If filter applied, filter out by artifactType and add annotation
// See https://github.com/opencontainers/distribution-spec/blob/main/spec.md#listing-referrers
if filter != nil {
if v, ok := filter["artifactType"]; ok {
tmp := []v1.Descriptor{}
for _, desc := range im.Manifests {
if desc.ArtifactType == v {
tmp = append(tmp, desc)
}
}
im.Manifests = tmp
}
}
return &im, nil
}
func (f *fetcher) fetchManifest(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)
@@ -259,7 +300,7 @@ func (f *fetcher) fetchManifest(ref name.Reference, acceptable []types.MediaType
return nil, nil, err
}
manifest, err := ioutil.ReadAll(resp.Body)
manifest, err := io.ReadAll(resp.Body)
if err != nil {
return nil, nil, err
}
@@ -283,6 +324,15 @@ func (f *fetcher) fetchManifest(ref name.Reference, acceptable []types.MediaType
return nil, nil, fmt.Errorf("manifest digest: %q does not match requested digest: %q for %q", digest, dgst.DigestStr(), f.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,
@@ -293,9 +343,10 @@ func (f *fetcher) fetchManifest(ref name.Reference, acceptable []types.MediaType
// Return all this info since we have to calculate it anyway.
desc := v1.Descriptor{
Digest: digest,
Size: size,
MediaType: mediaType,
Digest: digest,
Size: size,
MediaType: mediaType,
ArtifactType: artifactType,
}
return manifest, &desc, nil
@@ -367,7 +418,7 @@ func (f *fetcher) fetchBlob(ctx context.Context, size int64, h v1.Hash) (io.Read
resp, err := f.Client.Do(req.WithContext(ctx))
if err != nil {
return nil, err
return nil, redact.Error(err)
}
if err := transport.CheckError(resp, http.StatusOK); err != nil {
@@ -398,7 +449,7 @@ func (f *fetcher) headBlob(h v1.Hash) (*http.Response, error) {
resp, err := f.Client.Do(req.WithContext(f.context))
if err != nil {
return nil, err
return nil, redact.Error(err)
}
if err := transport.CheckError(resp, http.StatusOK); err != nil {
@@ -418,7 +469,7 @@ func (f *fetcher) blobExists(h v1.Hash) (bool, error) {
resp, err := f.Client.Do(req.WithContext(f.context))
if err != nil {
return false, err
return false, redact.Error(err)
}
defer resp.Body.Close()
+11 -3
View File
@@ -17,7 +17,6 @@ package remote
import (
"bytes"
"io"
"io/ioutil"
"net/http"
"net/url"
"sync"
@@ -47,6 +46,15 @@ type remoteImage struct {
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.
@@ -115,7 +123,7 @@ func (r *remoteImage) RawConfigFile() ([]byte, error) {
}
defer body.Close()
r.config, err = ioutil.ReadAll(body)
r.config, err = io.ReadAll(body)
if err != nil {
return nil, err
}
@@ -153,7 +161,7 @@ func (rl *remoteImageLayer) Compressed() (io.ReadCloser, error) {
}
if d.Data != nil {
return verify.ReadCloser(ioutil.NopCloser(bytes.NewReader(d.Data)), d.Size, d.Digest)
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.
+14 -2
View File
@@ -194,10 +194,12 @@ func (r *remoteIndex) imageByPlatform(platform v1.Platform) (v1.Image, error) {
// This naively matches the first manifest with matching platform attributes.
//
// We should probably use this instead:
// github.com/containerd/containerd/platforms
//
// github.com/containerd/containerd/platforms
//
// But first we'd need to migrate to:
// github.com/opencontainers/image-spec/specs-go/v1
//
// github.com/opencontainers/image-spec/specs-go/v1
func (r *remoteIndex) childByPlatform(platform v1.Platform) (*Descriptor, error) {
index, err := r.IndexManifest()
if err != nil {
@@ -248,6 +250,16 @@ func (r *remoteIndex) childDescriptor(child v1.Descriptor, platform v1.Platform)
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{
fetcher: fetcher{
Ref: ref,
+13
View File
@@ -93,3 +93,16 @@ func (mi *mountableImage) LayerByDiffID(d v1.Hash) (v1.Layer, error) {
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
}
+9 -10
View File
@@ -87,32 +87,31 @@ func MultiWrite(m map[name.Reference]Taggable, options ...Option) (rerr error) {
return err
}
w := writer{
repo: repo,
client: &http.Client{Transport: tr},
context: o.context,
updates: o.updates,
lastUpdate: &v1.Update{},
backoff: o.retryBackoff,
predicate: o.retryPredicate,
repo: repo,
client: &http.Client{Transport: tr},
backoff: o.retryBackoff,
predicate: o.retryPredicate,
}
// Collect the total size of blobs and manifests we're about to write.
if o.updates != nil {
w.progress = &progress{updates: o.updates}
w.progress.lastUpdate = &v1.Update{}
defer close(o.updates)
defer func() { _ = sendError(o.updates, rerr) }()
defer func() { _ = w.progress.err(rerr) }()
for _, b := range blobs {
size, err := b.Size()
if err != nil {
return err
}
w.lastUpdate.Total += size
w.progress.total(size)
}
countManifest := func(t Taggable) error {
b, err := t.RawManifest()
if err != nil {
return err
}
w.lastUpdate.Total += int64(len(b))
w.progress.total(int64(len(b)))
return nil
}
for _, i := range images {
+32 -7
View File
@@ -46,6 +46,7 @@ type options struct {
pageSize int
retryBackoff Backoff
retryPredicate retry.Predicate
filter map[string]string
}
var defaultPlatform = v1.Platform{
@@ -59,7 +60,7 @@ 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) {
if retry.IsTemporary(err) || errors.Is(err, io.ErrUnexpectedEOF) || errors.Is(err, io.EOF) || errors.Is(err, syscall.EPIPE) || errors.Is(err, syscall.ECONNRESET) {
logs.Warn.Printf("retrying %v", err)
return true
}
@@ -74,6 +75,22 @@ var defaultRetryBackoff = Backoff{
Steps: 3,
}
// Useful for tests
var fastBackoff = Backoff{
Duration: 1.0 * time.Millisecond,
Factor: 3.0,
Jitter: 0.1,
Steps: 3,
}
var retryableStatusCodes = []int{
http.StatusRequestTimeout,
http.StatusInternalServerError,
http.StatusBadGateway,
http.StatusServiceUnavailable,
http.StatusGatewayTimeout,
}
const (
defaultJobs = 4
@@ -84,13 +101,10 @@ const (
// DefaultTransport is based on http.DefaultTransport with modifications
// documented inline below.
var DefaultTransport = &http.Transport{
var DefaultTransport http.RoundTripper = &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
// By default we wrap the transport in retries, so reduce the
// default dial timeout to 5s to avoid 5x 30s of connection
// timeouts when doing the "ping" on certain http registries.
Timeout: 5 * time.Second,
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
ForceAttemptHTTP2: true,
@@ -143,7 +157,7 @@ func makeOptions(target authn.Resource, opts ...Option) (*options, error) {
}
// Wrap the transport in something that can retry network flakes.
o.transport = transport.NewRetry(o.transport)
o.transport = transport.NewRetry(o.transport, transport.WithRetryPredicate(defaultRetryPredicate), transport.WithRetryStatusCodes(retryableStatusCodes...))
// Wrap this last to prevent transport.New from double-wrapping.
if o.userAgent != "" {
@@ -290,3 +304,14 @@ func WithRetryPredicate(predicate retry.Predicate) Option {
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
}
}
@@ -0,0 +1,69 @@
// 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) {
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
}
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() }
@@ -0,0 +1,35 @@
// 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 (
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
)
// 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.IndexManifest, error) {
o, err := makeOptions(d.Context(), options...)
if err != nil {
return nil, err
}
f, err := makeFetcher(d, o)
if err != nil {
return nil, err
}
return f.fetchReferrers(o.context, o.filter, d)
}
@@ -19,7 +19,7 @@ import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"io"
"net"
"net/http"
"net/url"
@@ -87,6 +87,9 @@ func (bt *bearerTransport) RoundTrip(in *http.Request) (*http.Response, error) {
// 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{}
for _, wac := range challenges {
// TODO(jonjohnsonjr): Should we also update "realm" or "service"?
@@ -265,11 +268,13 @@ func (bt *bearerTransport) refreshOauth(ctx context.Context) ([]byte, error) {
defer resp.Body.Close()
if err := CheckError(resp, http.StatusOK); err != nil {
logs.Warn.Printf("No matching credentials were found for %q", bt.registry)
if bt.basic == authn.Anonymous {
logs.Warn.Printf("No matching credentials were found for %q", bt.registry)
}
return nil, err
}
return ioutil.ReadAll(resp.Body)
return io.ReadAll(resp.Body)
}
// https://docs.docker.com/registry/spec/auth/token/
@@ -305,9 +310,11 @@ func (bt *bearerTransport) refreshBasic(ctx context.Context) ([]byte, error) {
defer resp.Body.Close()
if err := CheckError(resp, http.StatusOK); err != nil {
logs.Warn.Printf("No matching credentials were found for %q", bt.registry)
if bt.basic == authn.Anonymous {
logs.Warn.Printf("No matching credentials were found for %q", bt.registry)
}
return nil, err
}
return ioutil.ReadAll(resp.Body)
return io.ReadAll(resp.Body)
}
@@ -17,28 +17,12 @@ package transport
import (
"encoding/json"
"fmt"
"io/ioutil"
"io"
"net/http"
"net/url"
"strings"
)
// The set of query string keys that we expect to send as part of the registry
// protocol. Anything else is potentially dangerous to leak, as it's probably
// from a redirect. These redirects often included tokens or signed URLs.
var paramAllowlist = map[string]struct{}{
// Token exchange
"scope": {},
"service": {},
// Cross-repo mounting
"mount": {},
"from": {},
// Layer PUT
"digest": {},
// Listing tags and catalog
"n": {},
"last": {},
}
"github.com/google/go-containerregistry/internal/redact"
)
// Error implements error to support the following error specification:
// https://github.com/docker/distribution/blob/master/docs/spec/api.md#errors
@@ -59,7 +43,7 @@ var _ error = (*Error)(nil)
func (e *Error) Error() string {
prefix := ""
if e.Request != nil {
prefix = fmt.Sprintf("%s %s: ", e.Request.Method, redactURL(e.Request.URL))
prefix = fmt.Sprintf("%s %s: ", e.Request.Method, redact.URL(e.Request.URL))
}
return prefix + e.responseErr()
}
@@ -100,27 +84,11 @@ func (e *Error) Temporary() bool {
return true
}
// TODO(jonjohnsonjr): Consider moving to internal/redact.
func redactURL(original *url.URL) *url.URL {
qs := original.Query()
for k, v := range qs {
for i := range v {
if _, ok := paramAllowlist[k]; !ok {
// key is not in the Allowlist
v[i] = "REDACTED"
}
}
}
redacted := *original
redacted.RawQuery = qs.Encode()
return &redacted
}
// Diagnostic represents a single error returned by a Docker registry interaction.
type Diagnostic struct {
Code ErrorCode `json:"code"`
Message string `json:"message,omitempty"`
Detail interface{} `json:"detail,omitempty"`
Code ErrorCode `json:"code"`
Message string `json:"message,omitempty"`
Detail any `json:"detail,omitempty"`
}
// String stringifies the Diagnostic in the form: $Code: $Message[; $Detail]
@@ -185,7 +153,7 @@ func CheckError(resp *http.Response, codes ...int) error {
return nil
}
}
b, err := ioutil.ReadAll(resp.Body)
b, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
+110 -63
View File
@@ -19,11 +19,12 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"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"
)
@@ -35,6 +36,9 @@ const (
bearer challenge = "bearer"
)
// 300ms is the default fallback period for go's DNS dialer but we could make this configurable.
var fallbackDelay = 300 * time.Millisecond
type pingResp struct {
challenge challenge
@@ -50,27 +54,7 @@ func (c challenge) Canonical() challenge {
return challenge(strings.ToLower(string(c)))
}
func parseChallenge(suffix string) map[string]string {
kv := make(map[string]string)
for _, token := range strings.Split(suffix, ",") {
// Trim any whitespace around each token.
token = strings.Trim(token, " ")
// Break the token into a key/value pair
if parts := strings.SplitN(token, "=", 2); len(parts) == 2 {
// Unquote the value, if it is quoted.
kv[parts[0]] = strings.Trim(parts[1], `"`)
} else {
// If there was only one part, treat is as a key with an empty value
kv[token] = ""
}
}
return kv
}
func ping(ctx context.Context, reg name.Registry, t http.RoundTripper) (*pingResp, error) {
client := http.Client{Transport: t}
// 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.
@@ -78,54 +62,117 @@ func ping(ctx context.Context, reg name.Registry, t http.RoundTripper) (*pingRes
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)
}
var errs []error
for _, scheme := range schemes {
url := fmt.Sprintf("%s://%s/v2/", scheme, reg.Name())
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
}
resp, err := client.Do(req.WithContext(ctx))
if err != nil {
errs = append(errs, err)
// Potentially retry with http.
continue
}
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(ioutil.Discard, resp.Body)
resp.Body.Close()
}()
func pingSingle(ctx context.Context, reg name.Registry, t http.RoundTripper, scheme string) (*pingResp, error) {
client := http.Client{Transport: t}
url := fmt.Sprintf("%s://%s/v2/", scheme, reg.Name())
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()
}()
switch resp.StatusCode {
case http.StatusOK:
// If we get a 200, then no authentication is needed.
switch resp.StatusCode {
case http.StatusOK:
// If we get a 200, then no authentication is needed.
return &pingResp{
challenge: anonymous,
scheme: scheme,
}, 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 &pingResp{
challenge: anonymous,
scheme: scheme,
challenge: challenge(wac.Scheme).Canonical(),
parameters: wac.Parameters,
scheme: scheme,
}, 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 &pingResp{
challenge: challenge(wac.Scheme).Canonical(),
parameters: wac.Parameters,
scheme: scheme,
}, nil
}
// Otherwise, just return the challenge without parameters.
return &pingResp{
challenge: challenge(resp.Header.Get("WWW-Authenticate")).Canonical(),
scheme: scheme,
}, 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) (*pingResp, error) {
returned := make(chan struct{})
defer close(returned)
type pingResult struct {
*pingResp
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{pingResp: 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.pingResp, nil
}
if res.primary {
primary = res
} else {
fallback = res
}
if primary.done && fallback.done {
return nil, multierrs([]error{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)
}
// Otherwise, just return the challenge without parameters.
return &pingResp{
challenge: challenge(resp.Header.Get("WWW-Authenticate")).Canonical(),
scheme: scheme,
}, nil
default:
return nil, CheckError(resp, http.StatusOK, http.StatusUnauthorized)
}
}
return nil, multierrs(errs)
}
func pickFromMultipleChallenges(challenges []authchallenge.Challenge) authchallenge.Challenge {
@@ -161,7 +208,7 @@ func (m multierrs) Error() string {
return b.String()
}
func (m multierrs) As(target interface{}) bool {
func (m multierrs) As(target any) bool {
for _, err := range m {
if errors.As(err, target) {
return true
@@ -21,12 +21,12 @@ import (
"github.com/google/go-containerregistry/internal/retry"
)
// Sleep for 0.1, 0.3, 0.9, 2.7 seconds. This should cover networking blips.
// 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: 5,
Steps: 3,
}
var _ http.RoundTripper = (*retryTransport)(nil)
@@ -36,6 +36,7 @@ type retryTransport struct {
inner http.RoundTripper
backoff retry.Backoff
predicate retry.Predicate
codes []int
}
// Option is a functional option for retryTransport.
@@ -44,6 +45,7 @@ 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
@@ -63,6 +65,13 @@ func WithRetryPredicate(predicate func(error) bool) Option {
}
}
// 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{
@@ -78,12 +87,23 @@ func NewRetry(inner http.RoundTripper, opts ...Option) http.RoundTripper {
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 CheckError(out)
}
}
}
return err
}
retry.Retry(roundtrip, t.predicate, t.backoff)
@@ -27,15 +27,24 @@ import (
// setup to authenticate with the remote registry "reg", in the capacity
// laid out by the specified scopes.
//
// TODO(jonjohnsonjr): Deprecate this.
// 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
// setup to authenticate with the remote registry "reg", in the capacity
// 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.
//
+187 -97
View File
@@ -17,13 +17,14 @@ package remote
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"sort"
"strings"
"sync/atomic"
"github.com/google/go-containerregistry/internal/redact"
"github.com/google/go-containerregistry/internal/retry"
@@ -49,20 +50,21 @@ func Write(ref name.Reference, img v1.Image, options ...Option) (rerr error) {
return err
}
var lastUpdate *v1.Update
var p *progress
if o.updates != nil {
lastUpdate = &v1.Update{}
lastUpdate.Total, err = countImage(img, o.allowNondistributableArtifacts)
p = &progress{updates: o.updates}
p.lastUpdate = &v1.Update{}
p.lastUpdate.Total, err = countImage(img, o.allowNondistributableArtifacts)
if err != nil {
return err
}
defer close(o.updates)
defer func() { _ = sendError(o.updates, rerr) }()
defer func() { _ = p.err(rerr) }()
}
return writeImage(o.context, ref, img, o, lastUpdate)
return writeImage(o.context, ref, img, o, p)
}
func writeImage(ctx context.Context, ref name.Reference, img v1.Image, o *options, lastUpdate *v1.Update) error {
func writeImage(ctx context.Context, ref name.Reference, img v1.Image, o *options, progress *progress) error {
ls, err := img.Layers()
if err != nil {
return err
@@ -73,13 +75,11 @@ func writeImage(ctx context.Context, ref name.Reference, img v1.Image, o *option
return err
}
w := writer{
repo: ref.Context(),
client: &http.Client{Transport: tr},
context: ctx,
updates: o.updates,
lastUpdate: lastUpdate,
backoff: o.retryBackoff,
predicate: o.retryPredicate,
repo: ref.Context(),
client: &http.Client{Transport: tr},
progress: progress,
backoff: o.retryBackoff,
predicate: o.retryPredicate,
}
// Upload individual blobs and collect any errors.
@@ -170,21 +170,12 @@ func writeImage(ctx context.Context, ref name.Reference, img v1.Image, o *option
// writer writes the elements of an image to a remote image reference.
type writer struct {
repo name.Repository
client *http.Client
context context.Context
repo name.Repository
client *http.Client
updates chan<- v1.Update
lastUpdate *v1.Update
backoff Backoff
predicate retry.Predicate
}
func sendError(ch chan<- v1.Update, err error) error {
if err != nil && ch != nil {
ch <- v1.Update{Error: err}
}
return err
progress *progress
backoff Backoff
predicate retry.Predicate
}
// url returns a url.Url for the specified path in the context of this remote image reference.
@@ -216,7 +207,7 @@ func (w *writer) nextLocation(resp *http.Response) (string, error) {
// 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(h v1.Hash) (bool, error) {
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)
@@ -224,7 +215,7 @@ func (w *writer) checkExistingBlob(h v1.Hash) (bool, error) {
return false, err
}
resp, err := w.client.Do(req.WithContext(w.context))
resp, err := w.client.Do(req.WithContext(ctx))
if err != nil {
return false, err
}
@@ -239,7 +230,7 @@ func (w *writer) checkExistingBlob(h v1.Hash) (bool, error) {
// checkExistingManifest checks if a manifest exists already in the repository
// by making a HEAD request to the manifest API.
func (w *writer) checkExistingManifest(h v1.Hash, mt types.MediaType) (bool, error) {
func (w *writer) checkExistingManifest(ctx context.Context, h v1.Hash, mt types.MediaType) (bool, error) {
u := w.url(fmt.Sprintf("/v2/%s/manifests/%s", w.repo.RepositoryStr(), h.String()))
req, err := http.NewRequest(http.MethodHead, u.String(), nil)
@@ -248,7 +239,7 @@ func (w *writer) checkExistingManifest(h v1.Hash, mt types.MediaType) (bool, err
}
req.Header.Set("Accept", string(mt))
resp, err := w.client.Do(req.WithContext(w.context))
resp, err := w.client.Do(req.WithContext(ctx))
if err != nil {
return false, err
}
@@ -267,13 +258,16 @@ func (w *writer) checkExistingManifest(h v1.Hash, mt types.MediaType) (bool, err
// 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(from, mount string) (location string, mounted bool, err error) {
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["mount"] = []string{mount}
uv["from"] = []string{from}
uv.Set("mount", mount)
uv.Set("from", from)
if origin != "" {
uv.Set("origin", origin)
}
}
u.RawQuery = uv.Encode()
@@ -283,13 +277,18 @@ func (w *writer) initiateUpload(from, mount string) (location string, mounted bo
return "", false, err
}
req.Header.Set("Content-Type", "application/json")
resp, err := w.client.Do(req.WithContext(w.context))
resp, err := w.client.Do(req.WithContext(ctx))
if err != nil {
return "", false, err
}
defer resp.Body.Close()
if err := transport.CheckError(resp, http.StatusCreated, http.StatusAccepted); err != nil {
if origin != "" && origin != w.repo.RegistryStr() {
// https://github.com/google/go-containerregistry/issues/1404
logs.Warn.Printf("retrying without mount: %v", err)
return w.initiateUpload(ctx, "", "", "")
}
return "", false, err
}
@@ -307,46 +306,34 @@ func (w *writer) initiateUpload(from, mount string) (location string, mounted bo
}
}
type progressReader struct {
rc io.ReadCloser
count *int64 // number of bytes this reader has read, to support resetting on retry.
updates chan<- v1.Update
lastUpdate *v1.Update
}
func (r *progressReader) Read(b []byte) (int, error) {
n, err := r.rc.Read(b)
if err != nil {
return n, err
}
atomic.AddInt64(r.count, int64(n))
// TODO: warn/debug log if sending takes too long, or if sending is blocked while context is cancelled.
r.updates <- v1.Update{
Total: r.lastUpdate.Total,
Complete: atomic.AddInt64(&r.lastUpdate.Complete, int64(n)),
}
return n, nil
}
func (r *progressReader) Close() error { return r.rc.Close() }
// streamBlob streams the contents of the blob to the specified location.
// On failure, this will return an error. On success, this will return the location
// header indicating how to commit the streamed blob.
func (w *writer) streamBlob(ctx context.Context, blob io.ReadCloser, streamLocation string) (commitLocation string, rerr error) {
func (w *writer) streamBlob(ctx context.Context, layer v1.Layer, streamLocation string) (commitLocation string, rerr error) {
reset := func() {}
defer func() {
if rerr != nil {
reset()
}
}()
if w.updates != nil {
blob, err := layer.Compressed()
if err != nil {
return "", err
}
getBody := layer.Compressed
if w.progress != nil {
var count int64
blob = &progressReader{rc: blob, updates: w.updates, lastUpdate: w.lastUpdate, count: &count}
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() {
atomic.AddInt64(&w.lastUpdate.Complete, -count)
w.updates <- *w.lastUpdate
w.progress.complete(-count)
}
}
@@ -354,6 +341,10 @@ func (w *writer) streamBlob(ctx context.Context, blob io.ReadCloser, streamLocat
if err != nil {
return "", err
}
if _, ok := layer.(*stream.Layer); !ok {
// We can't retry streaming layers.
req.GetBody = getBody
}
req.Header.Set("Content-Type", "application/octet-stream")
resp, err := w.client.Do(req.WithContext(ctx))
@@ -373,7 +364,7 @@ func (w *writer) streamBlob(ctx context.Context, blob io.ReadCloser, streamLocat
// commitBlob commits this blob by sending a PUT to the location returned from
// streaming the blob.
func (w *writer) commitBlob(location, digest string) error {
func (w *writer) commitBlob(ctx context.Context, location, digest string) error {
u, err := url.Parse(location)
if err != nil {
return err
@@ -388,7 +379,7 @@ func (w *writer) commitBlob(location, digest string) error {
}
req.Header.Set("Content-Type", "application/octet-stream")
resp, err := w.client.Do(req.WithContext(w.context))
resp, err := w.client.Do(req.WithContext(ctx))
if err != nil {
return err
}
@@ -399,23 +390,21 @@ func (w *writer) commitBlob(location, digest string) error {
// incrProgress increments and sends a progress update, if WithProgress is used.
func (w *writer) incrProgress(written int64) {
if w.updates == nil {
if w.progress == nil {
return
}
w.updates <- v1.Update{
Total: w.lastUpdate.Total,
Complete: atomic.AddInt64(&w.lastUpdate.Complete, written),
}
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 {
var from, mount string
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(h)
existing, err := w.checkExistingBlob(ctx, h)
if err != nil {
return err
}
@@ -432,12 +421,11 @@ func (w *writer) uploadOne(ctx context.Context, l v1.Layer) error {
mount = h.String()
}
if ml, ok := l.(*MountableLayer); ok {
if w.repo.RegistryStr() == ml.Reference.Context().RegistryStr() {
from = ml.Reference.Context().RepositoryStr()
}
from = ml.Reference.Context().RepositoryStr()
origin = ml.Reference.Context().RegistryStr()
}
location, mounted, err := w.initiateUpload(from, mount)
location, mounted, err := w.initiateUpload(ctx, from, mount, origin)
if err != nil {
return err
} else if mounted {
@@ -465,11 +453,7 @@ func (w *writer) uploadOne(ctx context.Context, l v1.Layer) error {
ctx = redact.NewContext(ctx, "omitting binary blobs from logs")
}
blob, err := l.Compressed()
if err != nil {
return err
}
location, err = w.streamBlob(ctx, blob, location)
location, err = w.streamBlob(ctx, l, location)
if err != nil {
return err
}
@@ -480,7 +464,7 @@ func (w *writer) uploadOne(ctx context.Context, l v1.Layer) error {
}
digest := h.String()
if err := w.commitBlob(location, digest); err != nil {
if err := w.commitBlob(ctx, location, digest); err != nil {
return err
}
logs.Progress.Printf("pushed blob: %s", digest)
@@ -508,7 +492,7 @@ func (w *writer) writeIndex(ctx context.Context, ref name.Reference, ii v1.Image
// TODO(#803): Pipe through remote.WithJobs and upload these in parallel.
for _, desc := range index.Manifests {
ref := ref.Context().Digest(desc.Digest.String())
exists, err := w.checkExistingManifest(desc.Digest, desc.MediaType)
exists, err := w.checkExistingManifest(ctx, desc.Digest, desc.MediaType)
if err != nil {
return err
}
@@ -531,7 +515,7 @@ func (w *writer) writeIndex(ctx context.Context, ref name.Reference, ii v1.Image
if err != nil {
return err
}
if err := writeImage(ctx, ref, img, o, w.lastUpdate); err != nil {
if err := writeImage(ctx, ref, img, o, w.progress); err != nil {
return err
}
default:
@@ -595,9 +579,94 @@ func unpackTaggable(t Taggable) ([]byte, *v1.Descriptor, error) {
}, 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: this method only uses the fallback tag for now
// TODO: use conditional requests to avoid race conditions
func (w *writer) commitSubjectReferrers(ctx context.Context, sub name.Digest, add v1.Descriptor) error {
// 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())
if err := w.commitManifest(ctx, fallbackTaggable{im}, t); err != nil {
return err
}
return nil
}
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
@@ -622,6 +691,26 @@ func (w *writer) commitManifest(ctx context.Context, t Taggable, ref name.Refere
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)))
@@ -673,20 +762,21 @@ func WriteIndex(ref name.Reference, ii v1.ImageIndex, options ...Option) (rerr e
w := writer{
repo: ref.Context(),
client: &http.Client{Transport: tr},
context: o.context,
updates: o.updates,
backoff: o.retryBackoff,
predicate: o.retryPredicate,
}
if o.updates != nil {
w.lastUpdate = &v1.Update{}
w.lastUpdate.Total, err = countIndex(ii, o.allowNondistributableArtifacts)
w.progress = &progress{updates: o.updates}
w.progress.lastUpdate = &v1.Update{}
defer close(o.updates)
defer func() { w.progress.err(rerr) }()
w.progress.lastUpdate.Total, err = countIndex(ii, o.allowNondistributableArtifacts)
if err != nil {
return err
}
defer close(o.updates)
defer func() { sendError(o.updates, rerr) }()
}
return w.writeIndex(o.context, ref, ii, options...)
@@ -814,15 +904,16 @@ func WriteLayer(repo name.Repository, layer v1.Layer, options ...Option) (rerr e
w := writer{
repo: repo,
client: &http.Client{Transport: tr},
context: o.context,
updates: o.updates,
backoff: o.retryBackoff,
predicate: o.retryPredicate,
}
if o.updates != nil {
w.progress = &progress{updates: o.updates}
w.progress.lastUpdate = &v1.Update{}
defer close(o.updates)
defer func() { sendError(o.updates, rerr) }()
defer func() { w.progress.err(rerr) }()
// TODO: support streaming layers which update the total count as they write.
if _, ok := layer.(*stream.Layer); ok {
@@ -832,7 +923,7 @@ func WriteLayer(repo name.Repository, layer v1.Layer, options ...Option) (rerr e
if err != nil {
return err
}
w.lastUpdate = &v1.Update{Total: size}
w.progress.total(size)
}
return w.uploadOne(o.context, layer)
}
@@ -883,7 +974,6 @@ func Put(ref name.Reference, t Taggable, options ...Option) error {
w := writer{
repo: ref.Context(),
client: &http.Client{Transport: tr},
context: o.context,
backoff: o.retryBackoff,
predicate: o.retryPredicate,
}
+16 -6
View File
@@ -12,12 +12,13 @@
// 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/sha256"
"crypto"
"encoding/hex"
"errors"
"hash"
@@ -48,6 +49,7 @@ type Layer struct {
mu sync.Mutex
digest, diffID *v1.Hash
size int64
mediaType types.MediaType
}
var _ v1.Layer = (*Layer)(nil)
@@ -62,11 +64,21 @@ func WithCompressionLevel(level int) LayerOption {
}
}
// 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 {
@@ -108,9 +120,7 @@ func (l *Layer) Size() (int64, error) {
// MediaType implements v1.Layer
func (l *Layer) MediaType() (types.MediaType, error) {
// We return DockerLayer for now as uncompressed layers
// are unimplemented
return types.DockerLayer, nil
return l.mediaType, nil
}
// Uncompressed implements v1.Layer.
@@ -156,8 +166,8 @@ type compressedReader struct {
func newCompressedReader(l *Layer) (*compressedReader, error) {
// Collect digests of compressed and uncompressed stream and size of
// compressed stream.
h := sha256.New()
zh := sha256.New()
h := crypto.SHA256.New()
zh := crypto.SHA256.New()
count := &countWriter{}
// gzip.Writer writes to the output stream via pipe, a hasher to
+10 -4
View File
@@ -21,13 +21,13 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"path"
"path/filepath"
"sync"
"github.com/google/go-containerregistry/internal/gzip"
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"
@@ -166,7 +166,13 @@ func (i *image) areLayersCompressed() (bool, error) {
return false, err
}
defer blob.Close()
return gzip.Is(blob)
cp, _, err := comp.PeekCompression(blob)
if err != nil {
return false, err
}
return cp != compression.None, nil
}
func (i *image) loadTarDescriptorAndConfig() error {
@@ -195,7 +201,7 @@ func (i *image) loadTarDescriptorAndConfig() error {
}
defer cfg.Close()
i.config, err = ioutil.ReadAll(cfg)
i.config, err = io.ReadAll(cfg)
if err != nil {
return err
}
+72 -19
View File
@@ -19,14 +19,17 @@ import (
"compress/gzip"
"fmt"
"io"
"io/ioutil"
"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"
)
@@ -37,7 +40,8 @@ type layer struct {
size int64
compressedopener Opener
uncompressedopener Opener
compression int
compression compression.Compression
compressionLevel int
annotations map[string]string
estgzopts []estargz.Option
mediaType types.MediaType
@@ -90,11 +94,32 @@ func (l *layer) MediaType() (types.MediaType, error) {
// 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.compression = level
l.compressionLevel = level
}
}
@@ -128,7 +153,7 @@ func WithCompressedCaching(l *layer) {
return nil, err
}
return ioutil.NopCloser(bytes.NewBuffer(buf.Bytes())), nil
return io.NopCloser(bytes.NewBuffer(buf.Bytes())), nil
}
}
@@ -149,7 +174,7 @@ func WithEstargz(l *layer) {
if err != nil {
return nil, err
}
eopts := append(l.estgzopts, estargz.WithCompressionLevel(l.compression))
eopts := append(l.estgzopts, estargz.WithCompressionLevel(l.compressionLevel))
rc, h, err := gestargz.ReadCloser(crc, eopts...)
if err != nil {
return nil, err
@@ -196,31 +221,28 @@ func LayerFromFile(path string, opts ...LayerOption) (v1.Layer, error) {
// 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) {
rc, err := opener()
if err != nil {
return nil, err
}
defer rc.Close()
compressed, err := ggzip.Is(rc)
comp, err := comp.GetCompression(opener)
if err != nil {
return nil, err
}
layer := &layer{
compression: gzip.BestSpeed,
annotations: make(map[string]string, 1),
mediaType: types.DockerLayer,
compression: compression.GZip,
compressionLevel: gzip.BestSpeed,
annotations: make(map[string]string, 1),
mediaType: types.DockerLayer,
}
if estgz := os.Getenv("GGCR_EXPERIMENT_ESTARGZ"); estgz == "1" {
opts = append([]LayerOption{WithEstargz}, opts...)
}
if compressed {
switch comp {
case compression.GZip:
layer.compressedopener = opener
layer.uncompressedopener = func() (io.ReadCloser, error) {
urc, err := opener()
@@ -229,14 +251,28 @@ func LayerFromOpener(opener Opener, opts ...LayerOption) (v1.Layer, error) {
}
return ggzip.UnzipReadCloser(urc)
}
} else {
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
}
return ggzip.ReadCloserLevel(crc, layer.compression), nil
if layer.compression == compression.ZStd {
return zstd.ReadCloserLevel(crc, layer.compressionLevel), nil
}
return ggzip.ReadCloserLevel(crc, layer.compressionLevel), nil
}
}
@@ -244,6 +280,23 @@ func LayerFromOpener(opener Opener, opts ...LayerOption) (v1.Layer, error) {
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
}
@@ -264,7 +317,7 @@ func LayerFromOpener(opener Opener, opts ...LayerOption) (v1.Layer, error) {
//
// Deprecated: Use LayerFromOpener or stream.NewLayer instead, if possible.
func LayerFromReader(reader io.Reader, opts ...LayerOption) (v1.Layer, error) {
tmp, err := ioutil.TempFile("", "")
tmp, err := os.CreateTemp("", "")
if err != nil {
return nil, fmt.Errorf("creating temp file to buffer reader: %w", err)
}
@@ -315,6 +315,7 @@ func getSizeAndManifest(refToImage map[name.Reference]v1.Image) (int64, []byte,
func calculateTarballSize(refToImage map[name.Reference]v1.Image, mBytes []byte) (size int64, err error) {
imageToTags := dedupRefToImage(refToImage)
seenLayerDigests := make(map[string]struct{})
for img, name := range imageToTags {
manifest, err := img.Manifest()
if err != nil {
@@ -322,6 +323,11 @@ func calculateTarballSize(refToImage map[name.Reference]v1.Image, mBytes []byte)
}
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)
}
}
+11
View File
@@ -12,6 +12,7 @@
// 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.
@@ -24,6 +25,7 @@ const (
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"
@@ -69,3 +71,12 @@ func (m MediaType) IsIndex() bool {
}
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
}
+4 -5
View File
@@ -17,12 +17,11 @@ package validate
import (
"archive/tar"
"compress/gzip"
"crypto/sha256"
"crypto"
"encoding/hex"
"errors"
"fmt"
"io"
"io/ioutil"
"strings"
v1 "github.com/google/go-containerregistry/pkg/v1"
@@ -105,7 +104,7 @@ func computeLayer(layer v1.Layer) (*computedLayer, error) {
}
// Keep track of compressed digest.
digester := sha256.New()
digester := crypto.SHA256.New()
// Everything read from compressed is written to digester to compute digest.
hashCompressed := io.TeeReader(compressed, digester)
@@ -133,7 +132,7 @@ func computeLayer(layer v1.Layer) (*computedLayer, error) {
if err != nil {
return nil, err
}
diffider := sha256.New()
diffider := crypto.SHA256.New()
hashUncompressed := io.TeeReader(uncompressed, diffider)
// Ensure there aren't duplicate file paths.
@@ -154,7 +153,7 @@ func computeLayer(layer v1.Layer) (*computedLayer, error) {
}
// Discard any trailing padding that the tar.Reader doesn't consume.
if _, err := io.Copy(ioutil.Discard, hashUncompressed); err != nil {
if _, err := io.Copy(io.Discard, hashUncompressed); err != nil {
return nil, err
}