summaryrefslogtreecommitdiffstats
path: root/pkg/v1/remote/options.go
blob: 54a0af227b32a8eca57f3a88a75bff4a9ba2eb58 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
// Copyright 2018 Google LLC All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package remote

import (
	"context"
	"errors"
	"io"
	"net"
	"net/http"
	"syscall"
	"time"

	"github.com/google/go-containerregistry/internal/retry"
	"github.com/google/go-containerregistry/pkg/authn"
	"github.com/google/go-containerregistry/pkg/logs"
	v1 "github.com/google/go-containerregistry/pkg/v1"
	"github.com/google/go-containerregistry/pkg/v1/remote/transport"
)

// Option is a functional option for remote operations.
type Option func(*options) error

type options struct {
	auth                           authn.Authenticator
	keychain                       authn.Keychain
	transport                      http.RoundTripper
	platform                       v1.Platform
	context                        context.Context
	jobs                           int
	userAgent                      string
	allowNondistributableArtifacts bool
	updates                        chan<- v1.Update
	pageSize                       int
	retryBackoff                   Backoff
	retryPredicate                 retry.Predicate
	filter                         map[string]string
}

var defaultPlatform = v1.Platform{
	Architecture: "amd64",
	OS:           "linux",
}

// Backoff is an alias of retry.Backoff to expose this configuration option to consumers of this lib
type Backoff = retry.Backoff

var defaultRetryPredicate retry.Predicate = func(err error) bool {
	// Various failure modes here, as we're often reading from and writing to
	// the network.
	if retry.IsTemporary(err) || errors.Is(err, io.ErrUnexpectedEOF) || errors.Is(err, io.EOF) || errors.Is(err, syscall.EPIPE) || errors.Is(err, syscall.ECONNRESET) {
		logs.Warn.Printf("retrying %v", err)
		return true
	}
	return false
}

// Try this three times, waiting 1s after first failure, 3s after second.
var defaultRetryBackoff = Backoff{
	Duration: 1.0 * time.Second,
	Factor:   3.0,
	Jitter:   0.1,
	Steps:    3,
}

// Useful for tests
var fastBackoff = Backoff{
	Duration: 1.0 * time.Millisecond,
	Factor:   3.0,
	Jitter:   0.1,
	Steps:    3,
}

var retryableStatusCodes = []int{
	http.StatusRequestTimeout,
	http.StatusInternalServerError,
	http.StatusBadGateway,
	http.StatusServiceUnavailable,
	http.StatusGatewayTimeout,
}

const (
	defaultJobs = 4

	// ECR returns an error if n > 1000:
	// https://github.com/google/go-containerregistry/issues/1091
	defaultPageSize = 1000
)

// DefaultTransport is based on http.DefaultTransport with modifications
// documented inline below.
var DefaultTransport http.RoundTripper = &http.Transport{
	Proxy: http.ProxyFromEnvironment,
	DialContext: (&net.Dialer{
		Timeout:   30 * time.Second,
		KeepAlive: 30 * time.Second,
	}).DialContext,
	ForceAttemptHTTP2:     true,
	MaxIdleConns:          100,
	IdleConnTimeout:       90 * time.Second,
	TLSHandshakeTimeout:   10 * time.Second,
	ExpectContinueTimeout: 1 * time.Second,
}

func makeOptions(target authn.Resource, opts ...Option) (*options, error) {
	o := &options{
		transport:      DefaultTransport,
		platform:       defaultPlatform,
		context:        context.Background(),
		jobs:           defaultJobs,
		pageSize:       defaultPageSize,
		retryPredicate: defaultRetryPredicate,
		retryBackoff:   defaultRetryBackoff,
	}

	for _, option := range opts {
		if err := option(o); err != nil {
			return nil, err
		}
	}

	switch {
	case o.auth != nil && o.keychain != nil:
		// It is a better experience to explicitly tell a caller their auth is misconfigured
		// than potentially fail silently when the correct auth is overridden by option misuse.
		return nil, errors.New("provide an option for either authn.Authenticator or authn.Keychain, not both")
	case o.keychain != nil:
		auth, err := o.keychain.Resolve(target)
		if err != nil {
			return nil, err
		}
		o.auth = auth
	case o.auth == nil:
		o.auth = authn.Anonymous
	}

	// transport.Wrapper is a signal that consumers are opt-ing into providing their own transport without any additional wrapping.
	// This is to allow consumers full control over the transports logic, such as providing retry logic.
	if _, ok := o.transport.(*transport.Wrapper); !ok {
		// Wrap the transport in something that logs requests and responses.
		// It's expensive to generate the dumps, so skip it if we're writing
		// to nothing.
		if logs.Enabled(logs.Debug) {
			o.transport = transport.NewLogger(o.transport)
		}

		// Wrap the transport in something that can retry network flakes.
		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 != "" {
			o.transport = transport.NewUserAgent(o.transport, o.userAgent)
		}
	}

	return o, nil
}

// WithTransport is a functional option for overriding the default transport
// for remote operations.
// If transport.Wrapper is provided, this signals that the consumer does *not* want any further wrapping to occur.
// i.e. logging, retry and useragent
//
// The default transport is DefaultTransport.
func WithTransport(t http.RoundTripper) Option {
	return func(o *options) error {
		o.transport = t
		return nil
	}
}

// WithAuth is a functional option for overriding the default authenticator
// for remote operations.
// It is an error to use both WithAuth and WithAuthFromKeychain in the same Option set.
//
// The default authenticator is authn.Anonymous.
func WithAuth(auth authn.Authenticator) Option {
	return func(o *options) error {
		o.auth = auth
		return nil
	}
}

// WithAuthFromKeychain is a functional option for overriding the default
// authenticator for remote operations, using an authn.Keychain to find
// credentials.
// It is an error to use both WithAuth and WithAuthFromKeychain in the same Option set.
//
// The default authenticator is authn.Anonymous.
func WithAuthFromKeychain(keys authn.Keychain) Option {
	return func(o *options) error {
		o.keychain = keys
		return nil
	}
}

// WithPlatform is a functional option for overriding the default platform
// that Image and Descriptor.Image use for resolving an index to an image.
//
// The default platform is amd64/linux.
func WithPlatform(p v1.Platform) Option {
	return func(o *options) error {
		o.platform = p
		return nil
	}
}

// WithContext is a functional option for setting the context in http requests
// performed by a given function. Note that this context is used for _all_
// http requests, not just the initial volley. E.g., for remote.Image, the
// context will be set on http requests generated by subsequent calls to
// RawConfigFile() and even methods on layers returned by Layers().
//
// The default context is context.Background().
func WithContext(ctx context.Context) Option {
	return func(o *options) error {
		o.context = ctx
		return nil
	}
}

// WithJobs is a functional option for setting the parallelism of remote
// operations performed by a given function. Note that not all remote
// operations support parallelism.
//
// The default value is 4.
func WithJobs(jobs int) Option {
	return func(o *options) error {
		if jobs <= 0 {
			return errors.New("jobs must be greater than zero")
		}
		o.jobs = jobs
		return nil
	}
}

// WithUserAgent adds the given string to the User-Agent header for any HTTP
// requests. This header will also include "go-containerregistry/${version}".
//
// If you want to completely overwrite the User-Agent header, use WithTransport.
func WithUserAgent(ua string) Option {
	return func(o *options) error {
		o.userAgent = ua
		return nil
	}
}

// WithNondistributable includes non-distributable (foreign) layers
// when writing images, see:
// https://github.com/opencontainers/image-spec/blob/master/layer.md#non-distributable-layers
//
// The default behaviour is to skip these layers
func WithNondistributable(o *options) error {
	o.allowNondistributableArtifacts = true
	return nil
}

// WithProgress takes a channel that will receive progress updates as bytes are written.
//
// Sending updates to an unbuffered channel will block writes, so callers
// should provide a buffered channel to avoid potential deadlocks.
func WithProgress(updates chan<- v1.Update) Option {
	return func(o *options) error {
		o.updates = updates
		return nil
	}
}

// WithPageSize sets the given size as the value of parameter 'n' in the request.
//
// To omit the `n` parameter entirely, use WithPageSize(0).
// The default value is 1000.
func WithPageSize(size int) Option {
	return func(o *options) error {
		o.pageSize = size
		return nil
	}
}

// WithRetryBackoff sets the httpBackoff for retry HTTP operations.
func WithRetryBackoff(backoff Backoff) Option {
	return func(o *options) error {
		o.retryBackoff = backoff
		return nil
	}
}

// WithRetryPredicate sets the predicate for retry HTTP operations.
func WithRetryPredicate(predicate retry.Predicate) Option {
	return func(o *options) error {
		o.retryPredicate = predicate
		return nil
	}
}

// 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
	}
}