Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/image-builder-mk3/pkg/resolve/resolve.go
2500 views
1
// Copyright (c) 2021 Gitpod GmbH. All rights reserved.
2
// Licensed under the GNU Affero General Public License (AGPL).
3
// See License.AGPL.txt in the project root for license information.
4
5
package resolve
6
7
import (
8
"context"
9
"encoding/json"
10
"fmt"
11
"io"
12
"net/http"
13
"strings"
14
"sync"
15
"time"
16
17
"github.com/containerd/containerd/remotes"
18
dockerremote "github.com/containerd/containerd/remotes/docker"
19
"github.com/distribution/reference"
20
"github.com/opencontainers/go-digest"
21
"github.com/opentracing/opentracing-go"
22
"golang.org/x/xerrors"
23
24
"github.com/gitpod-io/gitpod/common-go/log"
25
"github.com/gitpod-io/gitpod/common-go/tracing"
26
"github.com/gitpod-io/gitpod/image-builder/pkg/auth"
27
ociv1 "github.com/opencontainers/image-spec/specs-go/v1"
28
)
29
30
var (
31
// ErrNotFound is returned when the reference was not found
32
ErrNotFound = xerrors.Errorf("not found")
33
34
// ErrNotFound is returned when we're not authorized to return the reference
35
ErrUnauthorized = xerrors.Errorf("not authorized")
36
37
// TooManyRequestsMatcher returns true if an error is a code 429 "Too Many Requests" error
38
TooManyRequestsMatcher = func(err error) bool {
39
if err == nil {
40
return false
41
}
42
return strings.Contains(err.Error(), "429 Too Many Requests")
43
}
44
)
45
46
// StandaloneRefResolver can resolve image references without a Docker daemon
47
type StandaloneRefResolver struct {
48
ResolverFactory func() remotes.Resolver
49
}
50
51
// Resolve resolves a mutable Docker tag to its absolute digest form by asking the corresponding Docker registry
52
func (sr *StandaloneRefResolver) Resolve(ctx context.Context, ref string, opts ...DockerRefResolverOption) (res string, err error) {
53
span, ctx := opentracing.StartSpanFromContext(ctx, "StandaloneRefResolver.Resolve")
54
defer func() {
55
var rerr error
56
if err != ErrNotFound {
57
rerr = err
58
}
59
tracing.FinishSpan(span, &rerr)
60
}()
61
62
options := getOptions(opts)
63
64
var r remotes.Resolver
65
if sr.ResolverFactory == nil {
66
registryOpts := []dockerremote.RegistryOpt{
67
dockerremote.WithAuthorizer(dockerremote.NewDockerAuthorizer(dockerremote.WithAuthCreds(func(host string) (username, password string, err error) {
68
if options.Auth == nil {
69
return
70
}
71
72
return options.Auth.Username, options.Auth.Password, nil
73
}))),
74
}
75
76
if options.Client != nil {
77
registryOpts = append(registryOpts, dockerremote.WithClient(options.Client))
78
}
79
80
r = dockerremote.NewResolver(dockerremote.ResolverOptions{
81
Hosts: dockerremote.ConfigureDefaultRegistries(
82
registryOpts...,
83
),
84
})
85
} else {
86
r = sr.ResolverFactory()
87
}
88
89
// The ref may be what Docker calls a "familiar" name, e.g. ubuntu:latest instead of docker.io/library/ubuntu:latest.
90
// To make this a valid digested form we first need to normalize that familiar name.
91
pref, err := reference.ParseDockerRef(ref)
92
if err != nil {
93
return "", xerrors.Errorf("cannt resolve image ref: %w", err)
94
}
95
96
nref := pref.String()
97
pref = reference.TrimNamed(pref)
98
span.LogKV("normalized-ref", nref)
99
100
res, desc, err := r.Resolve(ctx, nref)
101
if err != nil {
102
if strings.Contains(err.Error(), "not found") {
103
err = ErrNotFound
104
} else if strings.Contains(err.Error(), "Unauthorized") {
105
err = ErrUnauthorized
106
}
107
return
108
}
109
fetcher, err := r.Fetcher(ctx, res)
110
if err != nil {
111
return
112
}
113
114
in, err := fetcher.Fetch(ctx, desc)
115
if err != nil {
116
return
117
}
118
defer in.Close()
119
buf, err := io.ReadAll(in)
120
if err != nil {
121
return
122
}
123
124
var mf ociv1.Manifest
125
err = json.Unmarshal(buf, &mf)
126
if err != nil {
127
return "", fmt.Errorf("cannot unmarshal manifest: %w", err)
128
}
129
130
if mf.Config.Size != 0 {
131
pref, err = reference.WithDigest(pref, desc.Digest)
132
if err != nil {
133
return
134
}
135
return pref.String(), nil
136
}
137
138
var mfl ociv1.Index
139
err = json.Unmarshal(buf, &mfl)
140
if err != nil {
141
return
142
}
143
144
var dgst digest.Digest
145
for _, mf := range mfl.Manifests {
146
if mf.Platform == nil {
147
continue
148
}
149
if fmt.Sprintf("%s-%s", mf.Platform.OS, mf.Platform.Architecture) == "linux-amd64" {
150
dgst = mf.Digest
151
break
152
}
153
}
154
if dgst == "" {
155
return "", fmt.Errorf("no manifest for platform linux-amd64 found")
156
}
157
158
pref, err = reference.WithDigest(pref, dgst)
159
if err != nil {
160
return
161
}
162
return pref.String(), nil
163
}
164
165
type opts struct {
166
Auth *auth.Authentication
167
Client *http.Client
168
}
169
170
// DockerRefResolverOption configures reference resolution
171
type DockerRefResolverOption func(o *opts)
172
173
// WithAuthentication sets a base64 encoded authentication for accessing a Docker registry
174
func WithAuthentication(auth *auth.Authentication) DockerRefResolverOption {
175
if auth == nil {
176
log.Debug("WithAuthentication - auth was nil")
177
}
178
179
return func(o *opts) {
180
o.Auth = auth
181
}
182
}
183
184
// WithHttpClient sets the HTTP client to use for making requests to the Docker registry.
185
func WithHttpClient(client *http.Client) DockerRefResolverOption {
186
return func(o *opts) {
187
if client == nil {
188
log.Debug("WithHttpClient - client was nil")
189
}
190
o.Client = client
191
}
192
}
193
194
func getOptions(o []DockerRefResolverOption) *opts {
195
var res opts
196
for _, opt := range o {
197
opt(&res)
198
}
199
return &res
200
}
201
202
// DockerRefResolver resolves a mutable Docker tag to its absolute digest form.
203
// For example: gitpod/workspace-full:latest becomes docker.io/gitpod/workspace-full@sha256:sha-hash-goes-here
204
type DockerRefResolver interface {
205
// Resolve resolves a mutable Docker tag to its absolute digest form.
206
Resolve(ctx context.Context, ref string, opts ...DockerRefResolverOption) (res string, err error)
207
}
208
209
// PrecachingRefResolver regularly resolves a set of references and returns the cached value when asked to resolve that reference.
210
type PrecachingRefResolver struct {
211
Resolver DockerRefResolver
212
Candidates []string
213
Auth auth.RegistryAuthenticator
214
215
mu sync.RWMutex
216
cache map[string]string
217
}
218
219
var _ DockerRefResolver = &PrecachingRefResolver{}
220
221
// StartCaching starts the precaching of resolved references at the given interval. This function blocks until the context is canceled
222
// and is intended to run as a Go routine.
223
func (pr *PrecachingRefResolver) StartCaching(ctx context.Context, interval time.Duration) {
224
span, ctx := opentracing.StartSpanFromContext(ctx, "PrecachingRefResolver.StartCaching")
225
defer tracing.FinishSpan(span, nil)
226
227
t := time.NewTicker(interval)
228
229
log.WithField("interval", interval.String()).WithField("refs", pr.Candidates).Info("starting Docker ref pre-cache")
230
231
pr.cache = make(map[string]string)
232
for {
233
for _, c := range pr.Candidates {
234
var opts []DockerRefResolverOption
235
if pr.Auth != ((auth.RegistryAuthenticator)(nil)) {
236
ref, err := reference.ParseNormalizedNamed(c)
237
if err != nil {
238
log.WithError(err).WithField("ref", c).Warn("unable to precache reference: cannot parse")
239
continue
240
}
241
242
auth, err := pr.Auth.Authenticate(ctx, reference.Domain(ref))
243
if err != nil {
244
log.WithError(err).WithField("ref", c).Warn("unable to precache reference: cannot authenticate")
245
continue
246
}
247
248
opts = append(opts, WithAuthentication(auth))
249
}
250
251
res, err := pr.Resolver.Resolve(ctx, c, opts...)
252
if err != nil {
253
log.WithError(err).WithField("ref", c).Warn("unable to precache reference")
254
continue
255
}
256
257
pr.mu.Lock()
258
pr.cache[c] = res
259
pr.mu.Unlock()
260
261
log.WithField("ref", c).WithField("resolved-to", res).Debug("pre-cached Docker ref")
262
}
263
264
select {
265
case <-t.C:
266
case <-ctx.Done():
267
log.Debug("context cancelled - shutting down Docker ref pre-caching")
268
return
269
}
270
}
271
}
272
273
// Resolve aims to resolve a ref using its own cache first and asks the underlying resolver otherwise
274
func (pr *PrecachingRefResolver) Resolve(ctx context.Context, ref string, opts ...DockerRefResolverOption) (res string, err error) {
275
span, ctx := opentracing.StartSpanFromContext(ctx, "PrecachingRefResolver.Resolve")
276
defer tracing.FinishSpan(span, &err)
277
278
pr.mu.RLock()
279
defer pr.mu.RUnlock()
280
281
if pr.cache == nil {
282
return pr.Resolver.Resolve(ctx, ref, opts...)
283
}
284
285
res, ok := pr.cache[ref]
286
if !ok {
287
return pr.Resolver.Resolve(ctx, ref, opts...)
288
}
289
290
return res, nil
291
}
292
293
type MockRefResolver map[string]string
294
295
func (m MockRefResolver) Resolve(ctx context.Context, ref string, opts ...DockerRefResolverOption) (res string, err error) {
296
res, ok := m[ref]
297
if !ok {
298
return "", ErrNotFound
299
}
300
return res, nil
301
}
302
303