Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/registry-facade/pkg/registry/manifest.go
2499 views
1
// Copyright (c) 2020 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 registry
6
7
import (
8
"bytes"
9
"context"
10
"encoding/json"
11
"fmt"
12
"io"
13
"mime"
14
"net/http"
15
"strconv"
16
"strings"
17
"time"
18
19
"github.com/containerd/containerd/content"
20
"github.com/containerd/containerd/errdefs"
21
"github.com/containerd/containerd/images"
22
"github.com/containerd/containerd/remotes"
23
distv2 "github.com/docker/distribution/registry/api/v2"
24
"github.com/gorilla/handlers"
25
"github.com/opencontainers/go-digest"
26
ociv1 "github.com/opencontainers/image-spec/specs-go/v1"
27
"github.com/opentracing/opentracing-go"
28
"github.com/pkg/errors"
29
"golang.org/x/xerrors"
30
"k8s.io/apimachinery/pkg/util/wait"
31
32
"github.com/gitpod-io/gitpod/common-go/log"
33
"github.com/gitpod-io/gitpod/common-go/tracing"
34
"github.com/gitpod-io/gitpod/registry-facade/api"
35
)
36
37
func (reg *Registry) handleManifest(ctx context.Context, r *http.Request) http.Handler {
38
t0 := time.Now()
39
40
spname, name := getSpecProviderName(ctx)
41
sp, ok := reg.SpecProvider[spname]
42
if !ok {
43
log.WithField("specProvName", spname).Error("unknown spec provider")
44
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
45
respondWithError(w, distv2.ErrorCodeManifestUnknown)
46
})
47
}
48
log.Infof("provider %s will handle request for %s", spname, name)
49
spec, err := sp.GetSpec(ctx, name)
50
if err != nil {
51
// treat invalid names from node-labeler as debug, not errors
52
// ref: https://github.com/gitpod-io/gitpod/blob/1a3c4b0bb6f13fe38481d21ddd146747c1a1935f/components/node-labeler/cmd/run.go#L291
53
var isNodeLabeler bool
54
if name == "not-a-valid-image" {
55
isNodeLabeler = true
56
}
57
if isNodeLabeler {
58
log.WithError(err).WithField("specProvName", spname).WithField("name", name).Info("this was node-labeler, we expected no spec")
59
} else {
60
log.WithError(err).WithField("specProvName", spname).WithField("name", name).Error("cannot get spec")
61
}
62
63
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
64
respondWithError(w, distv2.ErrorCodeManifestUnknown)
65
})
66
}
67
68
manifestHandler := &manifestHandler{
69
Context: ctx,
70
Name: name,
71
Spec: spec,
72
Resolver: reg.Resolver(),
73
Store: reg.Store,
74
ConfigModifier: reg.ConfigModifier,
75
}
76
reference := getReference(ctx)
77
dgst, err := digest.Parse(reference)
78
if err != nil {
79
manifestHandler.Tag = reference
80
} else {
81
manifestHandler.Digest = dgst
82
}
83
84
mhandler := handlers.MethodHandler{
85
"GET": http.HandlerFunc(manifestHandler.getManifest),
86
"HEAD": http.HandlerFunc(manifestHandler.getManifest),
87
"PUT": http.HandlerFunc(manifestHandler.putManifest),
88
"DELETE": http.HandlerFunc(manifestHandler.deleteManifest),
89
}
90
91
res := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
92
mhandler.ServeHTTP(w, r)
93
dt := time.Since(t0)
94
reg.metrics.ManifestHist.Observe(dt.Seconds())
95
})
96
97
return res
98
}
99
100
// fetcherBackoffParams defines the backoff parameters for blob retrieval.
101
// Aiming at ~10 seconds total time for retries
102
var fetcherBackoffParams = wait.Backoff{
103
Duration: 1 * time.Second,
104
Factor: 1.2,
105
Jitter: 0.2,
106
Steps: 5,
107
}
108
109
type manifestHandler struct {
110
Context context.Context
111
112
Spec *api.ImageSpec
113
Resolver remotes.Resolver
114
Store BlobStore
115
ConfigModifier ConfigModifier
116
117
Name string
118
Tag string
119
Digest digest.Digest
120
}
121
122
func (mh *manifestHandler) getManifest(w http.ResponseWriter, r *http.Request) {
123
//nolint:staticcheck,ineffassign
124
span, ctx := opentracing.StartSpanFromContext(r.Context(), "getManifest")
125
logFields := log.OWI("", "", mh.Name)
126
logFields["tag"] = mh.Tag
127
logFields["spec"] = log.TrustedValueWrap{Value: mh.Spec}
128
err := func() error {
129
log.WithFields(logFields).Debug("get manifest")
130
tracing.LogMessageSafe(span, "spec", mh.Spec)
131
132
var (
133
acceptType string
134
err error
135
)
136
for _, acceptHeader := range r.Header["Accept"] {
137
for _, mediaType := range strings.Split(acceptHeader, ",") {
138
if mediaType, _, err = mime.ParseMediaType(strings.TrimSpace(mediaType)); err != nil {
139
continue
140
}
141
142
if mediaType == ociv1.MediaTypeImageManifest ||
143
mediaType == images.MediaTypeDockerSchema2Manifest ||
144
mediaType == "*" {
145
146
acceptType = ociv1.MediaTypeImageManifest
147
break
148
}
149
}
150
if acceptType != "" {
151
break
152
}
153
}
154
if acceptType == "" {
155
return distv2.ErrorCodeManifestUnknown.WithMessage("Accept header does not include OCIv1 or v2 manifests")
156
}
157
158
// Note: we ignore the mh.Digest for now because we always return a manifest, never a manifest index.
159
ref := mh.Spec.BaseRef
160
161
_, desc, err := mh.Resolver.Resolve(ctx, ref)
162
if err != nil {
163
log.WithError(err).WithField("ref", ref).WithFields(logFields).Error("cannot resolve")
164
// ErrInvalidAuthorization
165
return err
166
}
167
168
var fcache remotes.Fetcher
169
fetch := func() (remotes.Fetcher, error) {
170
if fcache != nil {
171
return fcache, nil
172
}
173
174
fetcher, err := mh.Resolver.Fetcher(ctx, ref)
175
if err != nil {
176
return nil, err
177
}
178
fcache = fetcher
179
return fcache, nil
180
}
181
182
manifest, ndesc, err := DownloadManifest(ctx, fetch, desc, WithStore(mh.Store))
183
if err != nil {
184
log.WithError(err).WithField("desc", desc).WithFields(logFields).WithField("ref", ref).Error("cannot download manifest")
185
return distv2.ErrorCodeManifestUnknown.WithDetail(err)
186
}
187
desc = *ndesc
188
189
var p []byte
190
switch desc.MediaType {
191
case images.MediaTypeDockerSchema2Manifest, ociv1.MediaTypeImageManifest:
192
// download config
193
cfg, err := DownloadConfig(ctx, fetch, ref, manifest.Config, WithStore(mh.Store))
194
if err != nil {
195
log.WithError(err).WithFields(logFields).Error("cannot download config")
196
return err
197
}
198
199
originImageSize := 0
200
for _, layer := range manifest.Layers {
201
originImageSize += int(layer.Size)
202
}
203
204
// modify config
205
addonLayer, err := mh.ConfigModifier(ctx, mh.Spec, cfg)
206
if err != nil {
207
log.WithError(err).WithFields(logFields).Error("cannot modify config")
208
return err
209
}
210
manifest.Layers = append(manifest.Layers, addonLayer...)
211
if manifest.Annotations == nil {
212
manifest.Annotations = make(map[string]string)
213
}
214
manifest.Annotations["io.gitpod.workspace-image.size"] = strconv.Itoa(originImageSize)
215
manifest.Annotations["io.gitpod.workspace-image.ref"] = mh.Spec.BaseRef
216
217
// place config in store
218
rawCfg, err := json.Marshal(cfg)
219
if err != nil {
220
log.WithError(err).WithFields(logFields).Error("cannot marshal config")
221
return err
222
}
223
cfgDgst := digest.FromBytes(rawCfg)
224
225
// update config digest in manifest
226
manifest.Config.Digest = cfgDgst
227
manifest.Config.URLs = nil
228
manifest.Config.Size = int64(len(rawCfg))
229
230
// optimization: we store the config in the store just in case the client attempts to download the config blob
231
// from us. If they download it from a registry facade from which the manifest hasn't been downloaded
232
// we'll re-create the config on the fly.
233
if w, err := mh.Store.Writer(ctx, content.WithRef(ref), content.WithDescriptor(manifest.Config)); err == nil {
234
defer w.Close()
235
236
_, err = w.Write(rawCfg)
237
if err != nil {
238
log.WithError(err).WithFields(logFields).Warn("cannot write config to store - we'll regenerate it on demand")
239
}
240
err = w.Commit(ctx, 0, cfgDgst, content.WithLabels(contentTypeLabel(manifest.Config.MediaType)))
241
if err != nil {
242
log.WithError(err).WithFields(logFields).Warn("cannot commit config to store - we'll regenerate it on demand")
243
}
244
}
245
246
// When serving images.MediaTypeDockerSchema2Manifest we have to set the mediaType in the manifest itself.
247
// Although somewhat compatible with the OCI manifest spec (see https://github.com/opencontainers/image-spec/blob/master/manifest.md),
248
// this field is not part of the OCI Go structs. In this particular case, we'll go ahead and add it ourselves.
249
//
250
// fixes https://github.com/gitpod-io/gitpod/pull/3397
251
if desc.MediaType == images.MediaTypeDockerSchema2Manifest {
252
type ManifestWithMediaType struct {
253
ociv1.Manifest
254
MediaType string `json:"mediaType"`
255
}
256
p, _ = json.Marshal(ManifestWithMediaType{
257
Manifest: *manifest,
258
MediaType: images.MediaTypeDockerSchema2Manifest,
259
})
260
} else {
261
p, _ = json.Marshal(manifest)
262
}
263
}
264
265
dgst := digest.FromBytes(p).String()
266
267
w.Header().Set("Content-Type", desc.MediaType)
268
w.Header().Set("Content-Length", fmt.Sprint(len(p)))
269
w.Header().Set("Etag", fmt.Sprintf(`"%s"`, dgst))
270
w.Header().Set("Docker-Content-Digest", dgst)
271
_, _ = w.Write(p)
272
273
log.WithFields(logFields).Debug("get manifest (end)")
274
return nil
275
}()
276
277
if err != nil {
278
log.WithError(err).WithField("spec", log.TrustedValueWrap{Value: mh.Spec}).Error("cannot get manifest")
279
respondWithError(w, err)
280
}
281
tracing.FinishSpan(span, &err)
282
}
283
284
// DownloadConfig downloads and unmarshales OCIv2 image config, referred to by an OCI descriptor.
285
func DownloadConfig(ctx context.Context, fetch FetcherFunc, ref string, desc ociv1.Descriptor, options ...ManifestDownloadOption) (cfg *ociv1.Image, err error) {
286
if desc.MediaType != images.MediaTypeDockerSchema2Config &&
287
desc.MediaType != ociv1.MediaTypeImageConfig {
288
289
return nil, xerrors.Errorf("unsupported media type: %s", desc.MediaType)
290
}
291
log := log.WithField("desc", desc)
292
293
var opts manifestDownloadOptions
294
for _, o := range options {
295
o(&opts)
296
}
297
298
var buf []byte
299
err = wait.ExponentialBackoffWithContext(ctx, fetcherBackoffParams, func(ctx context.Context) (done bool, err error) {
300
var rc io.ReadCloser
301
if opts.Store != nil {
302
r, err := opts.Store.ReaderAt(ctx, desc)
303
if errors.Is(err, errdefs.ErrNotFound) {
304
// not cached yet
305
} else if err != nil {
306
log.WithError(err).Warn("cannot read config from store - fetching again")
307
} else {
308
defer r.Close()
309
rc = io.NopCloser(content.NewReader(r))
310
}
311
}
312
if rc == nil {
313
fetcher, err := fetch()
314
if err != nil {
315
log.WithError(err).Warn("cannot create fetcher")
316
return false, nil // retry
317
}
318
rc, err = fetcher.Fetch(ctx, desc)
319
if err != nil {
320
log.WithError(err).Warn("cannot fetch config")
321
if retryableError(err) {
322
return false, nil // retry
323
}
324
return false, err
325
}
326
defer rc.Close()
327
}
328
329
buf, err = io.ReadAll(rc)
330
if err != nil {
331
log.WithError(err).Warn("cannot read config")
332
return false, nil // retry
333
}
334
335
return true, nil
336
})
337
if err != nil {
338
return nil, xerrors.Errorf("failed to fetch config: %w", err)
339
}
340
341
var res ociv1.Image
342
err = json.Unmarshal(buf, &res)
343
if err != nil {
344
return nil, xerrors.Errorf("cannot decode config: %w", err)
345
}
346
347
if opts.Store != nil && ref != "" {
348
// ref can be empty for some users of DownloadConfig. However, some store implementations
349
// (e.g. the default containerd store) expect ref to be set. This would lead to stray errors.
350
351
err := func() error {
352
w, err := opts.Store.Writer(ctx, content.WithDescriptor(desc), content.WithRef(ref))
353
if err != nil {
354
return err
355
}
356
defer w.Close()
357
358
n, err := w.Write(buf)
359
if err != nil {
360
return err
361
}
362
if n != len(buf) {
363
return io.ErrShortWrite
364
}
365
366
return w.Commit(ctx, int64(len(buf)), digest.FromBytes(buf), content.WithLabels(contentTypeLabel(desc.MediaType)))
367
}()
368
if err != nil && !strings.Contains(err.Error(), "already exists") {
369
log.WithError(err).WithField("ref", ref).WithField("desc", desc).Warn("cannot cache config")
370
}
371
}
372
373
return &res, nil
374
}
375
376
func contentTypeLabel(mt string) map[string]string {
377
return map[string]string{"Content-Type": mt}
378
}
379
380
type manifestDownloadOptions struct {
381
Store BlobStore
382
}
383
384
// ManifestDownloadOption alters the default manifest download behaviour
385
type ManifestDownloadOption func(*manifestDownloadOptions)
386
387
// WithStore caches a downloaded manifest in a store
388
func WithStore(store BlobStore) ManifestDownloadOption {
389
return func(o *manifestDownloadOptions) {
390
o.Store = store
391
}
392
}
393
394
type BlobStore interface {
395
ReaderAt(ctx context.Context, desc ociv1.Descriptor) (content.ReaderAt, error)
396
397
// Some implementations require WithRef to be included in opts.
398
Writer(ctx context.Context, opts ...content.WriterOpt) (content.Writer, error)
399
400
// Info will return metadata about content available in the content store.
401
//
402
// If the content is not present, ErrNotFound will be returned.
403
Info(ctx context.Context, dgst digest.Digest) (content.Info, error)
404
}
405
406
type FetcherFunc func() (remotes.Fetcher, error)
407
408
func AsFetcherFunc(f remotes.Fetcher) FetcherFunc {
409
return func() (remotes.Fetcher, error) { return f, nil }
410
}
411
412
// DownloadManifest downloads and unmarshals the manifest of the given desc. If the desc points to manifest list
413
// we choose the first manifest in that list.
414
func DownloadManifest(ctx context.Context, fetch FetcherFunc, desc ociv1.Descriptor, options ...ManifestDownloadOption) (cfg *ociv1.Manifest, rdesc *ociv1.Descriptor, err error) {
415
log := log.WithField("desc", desc)
416
417
var opts manifestDownloadOptions
418
for _, o := range options {
419
o(&opts)
420
}
421
422
var (
423
placeInStore bool
424
mediaType = desc.MediaType
425
inpt []byte
426
)
427
err = wait.ExponentialBackoffWithContext(ctx, fetcherBackoffParams, func(ctx context.Context) (done bool, err error) {
428
var rc io.ReadCloser
429
if opts.Store != nil {
430
func() {
431
nfo, err := opts.Store.Info(ctx, desc.Digest)
432
if errors.Is(err, errdefs.ErrNotFound) {
433
// not in store yet
434
return
435
}
436
if err != nil {
437
log.WithError(err).Warn("cannot get manifest from store")
438
return
439
}
440
if nfo.Labels["Content-Type"] == "" {
441
// we have broken data in the store - ignore it and overwrite
442
return
443
}
444
445
r, err := opts.Store.ReaderAt(ctx, desc)
446
if errors.Is(err, errdefs.ErrNotFound) {
447
// not in store yet
448
return
449
}
450
if err != nil {
451
log.WithError(err).Warn("cannot get manifest from store")
452
return
453
}
454
455
mediaType, rc = nfo.Labels["Content-Type"], &reader{ReaderAt: r}
456
}()
457
}
458
if rc == nil {
459
// did not find in store, or there was no store. Either way, let's fetch this
460
// thing from the remote.
461
placeInStore = true
462
463
var fetcher remotes.Fetcher
464
fetcher, err = fetch()
465
if err != nil {
466
log.WithError(err).Warn("cannot create fetcher")
467
return false, nil // retry
468
}
469
470
rc, err = fetcher.Fetch(ctx, desc)
471
if err != nil {
472
log.WithError(err).Warn("cannot fetch manifest")
473
if retryableError(err) {
474
return false, nil // retry
475
}
476
return false, err
477
}
478
mediaType = desc.MediaType
479
}
480
481
inpt, err = io.ReadAll(rc)
482
rc.Close()
483
if err != nil {
484
log.WithError(err).Warn("cannot read manifest")
485
return false, nil // retry
486
}
487
488
return true, nil
489
})
490
if err != nil {
491
err = xerrors.Errorf("failed to fetch manifest: %w", err)
492
return
493
}
494
495
rdesc = &desc
496
rdesc.MediaType = mediaType
497
498
switch rdesc.MediaType {
499
case images.MediaTypeDockerSchema2ManifestList, ociv1.MediaTypeImageIndex:
500
log := log.WithField("desc", rdesc)
501
log.Debug("resolving image index")
502
503
// we received a manifest list which means we'll pick the default platform
504
// and fetch that manifest
505
var list ociv1.Index
506
err = json.Unmarshal(inpt, &list)
507
if err != nil {
508
err = xerrors.Errorf("cannot unmarshal index: %w", err)
509
return
510
}
511
if len(list.Manifests) == 0 {
512
err = xerrors.Errorf("empty manifest")
513
return
514
}
515
516
err = wait.ExponentialBackoffWithContext(ctx, fetcherBackoffParams, func(ctx context.Context) (done bool, err error) {
517
var fetcher remotes.Fetcher
518
fetcher, err = fetch()
519
if err != nil {
520
log.WithError(err).Warn("cannot create fetcher")
521
return false, nil // retry
522
}
523
524
// TODO(cw): choose by platform, not just the first manifest
525
var rc io.ReadCloser
526
md := list.Manifests[0]
527
rc, err = fetcher.Fetch(ctx, md)
528
if err != nil {
529
log.WithError(err).Warn("cannot download config")
530
if retryableError(err) {
531
return false, nil // retry
532
}
533
return false, err
534
}
535
rdesc = &md
536
inpt, err = io.ReadAll(rc)
537
rc.Close()
538
if err != nil {
539
log.WithError(err).Warn("cannot download manifest")
540
return false, nil // retry
541
}
542
543
return true, nil
544
})
545
if err != nil {
546
err = xerrors.Errorf("failed to download config: %w", err)
547
return
548
}
549
}
550
551
switch rdesc.MediaType {
552
case images.MediaTypeDockerSchema2Manifest, ociv1.MediaTypeImageManifest:
553
default:
554
err = xerrors.Errorf("unsupported media type: %s", rdesc.MediaType)
555
return
556
}
557
558
var res ociv1.Manifest
559
err = json.Unmarshal(inpt, &res)
560
if err != nil {
561
err = xerrors.Errorf("cannot decode config: %w", err)
562
return
563
}
564
565
if opts.Store != nil && placeInStore {
566
// We're cheating here and store the actual image manifest under the desc of what's
567
// possibly an image index. This way we don't have to resolve the image index the next
568
// time one wishes to resolve desc.
569
w, err := opts.Store.Writer(ctx, content.WithDescriptor(desc), content.WithRef(desc.Digest.String()))
570
if err != nil {
571
if err != nil && !strings.Contains(err.Error(), "already exists") {
572
log.WithError(err).WithField("desc", *rdesc).Warn("cannot create store writer")
573
}
574
} else {
575
_, err = io.Copy(w, bytes.NewReader(inpt))
576
if err != nil {
577
log.WithError(err).WithField("desc", *rdesc).Warn("cannot copy manifest")
578
}
579
580
err = w.Commit(ctx, 0, digest.FromBytes(inpt), content.WithLabels(map[string]string{"Content-Type": rdesc.MediaType}))
581
if err != nil {
582
log.WithError(err).WithField("desc", *rdesc).Warn("cannot store manifest")
583
}
584
w.Close()
585
}
586
}
587
588
cfg = &res
589
return
590
}
591
592
func (mh *manifestHandler) putManifest(w http.ResponseWriter, r *http.Request) {
593
respondWithError(w, distv2.ErrorCodeManifestInvalid)
594
}
595
596
func (mh *manifestHandler) deleteManifest(w http.ResponseWriter, r *http.Request) {
597
respondWithError(w, distv2.ErrorCodeManifestUnknown)
598
}
599
600
func retryableError(err error) bool {
601
if err == nil {
602
return false
603
}
604
if errors.Is(err, errdefs.ErrNotFound) || errors.Is(err, errdefs.ErrInvalidArgument) {
605
return false
606
}
607
if strings.Contains(err.Error(), "not found") ||
608
strings.Contains(err.Error(), "invalid argument") ||
609
strings.Contains(err.Error(), "not implemented") ||
610
strings.Contains(err.Error(), "unsupported media type") {
611
return false
612
}
613
return true
614
}
615
616