Path: blob/main/components/registry-facade/pkg/registry/manifest.go
2499 views
// Copyright (c) 2020 Gitpod GmbH. All rights reserved.1// Licensed under the GNU Affero General Public License (AGPL).2// See License.AGPL.txt in the project root for license information.34package registry56import (7"bytes"8"context"9"encoding/json"10"fmt"11"io"12"mime"13"net/http"14"strconv"15"strings"16"time"1718"github.com/containerd/containerd/content"19"github.com/containerd/containerd/errdefs"20"github.com/containerd/containerd/images"21"github.com/containerd/containerd/remotes"22distv2 "github.com/docker/distribution/registry/api/v2"23"github.com/gorilla/handlers"24"github.com/opencontainers/go-digest"25ociv1 "github.com/opencontainers/image-spec/specs-go/v1"26"github.com/opentracing/opentracing-go"27"github.com/pkg/errors"28"golang.org/x/xerrors"29"k8s.io/apimachinery/pkg/util/wait"3031"github.com/gitpod-io/gitpod/common-go/log"32"github.com/gitpod-io/gitpod/common-go/tracing"33"github.com/gitpod-io/gitpod/registry-facade/api"34)3536func (reg *Registry) handleManifest(ctx context.Context, r *http.Request) http.Handler {37t0 := time.Now()3839spname, name := getSpecProviderName(ctx)40sp, ok := reg.SpecProvider[spname]41if !ok {42log.WithField("specProvName", spname).Error("unknown spec provider")43return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {44respondWithError(w, distv2.ErrorCodeManifestUnknown)45})46}47log.Infof("provider %s will handle request for %s", spname, name)48spec, err := sp.GetSpec(ctx, name)49if err != nil {50// treat invalid names from node-labeler as debug, not errors51// ref: https://github.com/gitpod-io/gitpod/blob/1a3c4b0bb6f13fe38481d21ddd146747c1a1935f/components/node-labeler/cmd/run.go#L29152var isNodeLabeler bool53if name == "not-a-valid-image" {54isNodeLabeler = true55}56if isNodeLabeler {57log.WithError(err).WithField("specProvName", spname).WithField("name", name).Info("this was node-labeler, we expected no spec")58} else {59log.WithError(err).WithField("specProvName", spname).WithField("name", name).Error("cannot get spec")60}6162return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {63respondWithError(w, distv2.ErrorCodeManifestUnknown)64})65}6667manifestHandler := &manifestHandler{68Context: ctx,69Name: name,70Spec: spec,71Resolver: reg.Resolver(),72Store: reg.Store,73ConfigModifier: reg.ConfigModifier,74}75reference := getReference(ctx)76dgst, err := digest.Parse(reference)77if err != nil {78manifestHandler.Tag = reference79} else {80manifestHandler.Digest = dgst81}8283mhandler := handlers.MethodHandler{84"GET": http.HandlerFunc(manifestHandler.getManifest),85"HEAD": http.HandlerFunc(manifestHandler.getManifest),86"PUT": http.HandlerFunc(manifestHandler.putManifest),87"DELETE": http.HandlerFunc(manifestHandler.deleteManifest),88}8990res := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {91mhandler.ServeHTTP(w, r)92dt := time.Since(t0)93reg.metrics.ManifestHist.Observe(dt.Seconds())94})9596return res97}9899// fetcherBackoffParams defines the backoff parameters for blob retrieval.100// Aiming at ~10 seconds total time for retries101var fetcherBackoffParams = wait.Backoff{102Duration: 1 * time.Second,103Factor: 1.2,104Jitter: 0.2,105Steps: 5,106}107108type manifestHandler struct {109Context context.Context110111Spec *api.ImageSpec112Resolver remotes.Resolver113Store BlobStore114ConfigModifier ConfigModifier115116Name string117Tag string118Digest digest.Digest119}120121func (mh *manifestHandler) getManifest(w http.ResponseWriter, r *http.Request) {122//nolint:staticcheck,ineffassign123span, ctx := opentracing.StartSpanFromContext(r.Context(), "getManifest")124logFields := log.OWI("", "", mh.Name)125logFields["tag"] = mh.Tag126logFields["spec"] = log.TrustedValueWrap{Value: mh.Spec}127err := func() error {128log.WithFields(logFields).Debug("get manifest")129tracing.LogMessageSafe(span, "spec", mh.Spec)130131var (132acceptType string133err error134)135for _, acceptHeader := range r.Header["Accept"] {136for _, mediaType := range strings.Split(acceptHeader, ",") {137if mediaType, _, err = mime.ParseMediaType(strings.TrimSpace(mediaType)); err != nil {138continue139}140141if mediaType == ociv1.MediaTypeImageManifest ||142mediaType == images.MediaTypeDockerSchema2Manifest ||143mediaType == "*" {144145acceptType = ociv1.MediaTypeImageManifest146break147}148}149if acceptType != "" {150break151}152}153if acceptType == "" {154return distv2.ErrorCodeManifestUnknown.WithMessage("Accept header does not include OCIv1 or v2 manifests")155}156157// Note: we ignore the mh.Digest for now because we always return a manifest, never a manifest index.158ref := mh.Spec.BaseRef159160_, desc, err := mh.Resolver.Resolve(ctx, ref)161if err != nil {162log.WithError(err).WithField("ref", ref).WithFields(logFields).Error("cannot resolve")163// ErrInvalidAuthorization164return err165}166167var fcache remotes.Fetcher168fetch := func() (remotes.Fetcher, error) {169if fcache != nil {170return fcache, nil171}172173fetcher, err := mh.Resolver.Fetcher(ctx, ref)174if err != nil {175return nil, err176}177fcache = fetcher178return fcache, nil179}180181manifest, ndesc, err := DownloadManifest(ctx, fetch, desc, WithStore(mh.Store))182if err != nil {183log.WithError(err).WithField("desc", desc).WithFields(logFields).WithField("ref", ref).Error("cannot download manifest")184return distv2.ErrorCodeManifestUnknown.WithDetail(err)185}186desc = *ndesc187188var p []byte189switch desc.MediaType {190case images.MediaTypeDockerSchema2Manifest, ociv1.MediaTypeImageManifest:191// download config192cfg, err := DownloadConfig(ctx, fetch, ref, manifest.Config, WithStore(mh.Store))193if err != nil {194log.WithError(err).WithFields(logFields).Error("cannot download config")195return err196}197198originImageSize := 0199for _, layer := range manifest.Layers {200originImageSize += int(layer.Size)201}202203// modify config204addonLayer, err := mh.ConfigModifier(ctx, mh.Spec, cfg)205if err != nil {206log.WithError(err).WithFields(logFields).Error("cannot modify config")207return err208}209manifest.Layers = append(manifest.Layers, addonLayer...)210if manifest.Annotations == nil {211manifest.Annotations = make(map[string]string)212}213manifest.Annotations["io.gitpod.workspace-image.size"] = strconv.Itoa(originImageSize)214manifest.Annotations["io.gitpod.workspace-image.ref"] = mh.Spec.BaseRef215216// place config in store217rawCfg, err := json.Marshal(cfg)218if err != nil {219log.WithError(err).WithFields(logFields).Error("cannot marshal config")220return err221}222cfgDgst := digest.FromBytes(rawCfg)223224// update config digest in manifest225manifest.Config.Digest = cfgDgst226manifest.Config.URLs = nil227manifest.Config.Size = int64(len(rawCfg))228229// optimization: we store the config in the store just in case the client attempts to download the config blob230// from us. If they download it from a registry facade from which the manifest hasn't been downloaded231// we'll re-create the config on the fly.232if w, err := mh.Store.Writer(ctx, content.WithRef(ref), content.WithDescriptor(manifest.Config)); err == nil {233defer w.Close()234235_, err = w.Write(rawCfg)236if err != nil {237log.WithError(err).WithFields(logFields).Warn("cannot write config to store - we'll regenerate it on demand")238}239err = w.Commit(ctx, 0, cfgDgst, content.WithLabels(contentTypeLabel(manifest.Config.MediaType)))240if err != nil {241log.WithError(err).WithFields(logFields).Warn("cannot commit config to store - we'll regenerate it on demand")242}243}244245// When serving images.MediaTypeDockerSchema2Manifest we have to set the mediaType in the manifest itself.246// Although somewhat compatible with the OCI manifest spec (see https://github.com/opencontainers/image-spec/blob/master/manifest.md),247// this field is not part of the OCI Go structs. In this particular case, we'll go ahead and add it ourselves.248//249// fixes https://github.com/gitpod-io/gitpod/pull/3397250if desc.MediaType == images.MediaTypeDockerSchema2Manifest {251type ManifestWithMediaType struct {252ociv1.Manifest253MediaType string `json:"mediaType"`254}255p, _ = json.Marshal(ManifestWithMediaType{256Manifest: *manifest,257MediaType: images.MediaTypeDockerSchema2Manifest,258})259} else {260p, _ = json.Marshal(manifest)261}262}263264dgst := digest.FromBytes(p).String()265266w.Header().Set("Content-Type", desc.MediaType)267w.Header().Set("Content-Length", fmt.Sprint(len(p)))268w.Header().Set("Etag", fmt.Sprintf(`"%s"`, dgst))269w.Header().Set("Docker-Content-Digest", dgst)270_, _ = w.Write(p)271272log.WithFields(logFields).Debug("get manifest (end)")273return nil274}()275276if err != nil {277log.WithError(err).WithField("spec", log.TrustedValueWrap{Value: mh.Spec}).Error("cannot get manifest")278respondWithError(w, err)279}280tracing.FinishSpan(span, &err)281}282283// DownloadConfig downloads and unmarshales OCIv2 image config, referred to by an OCI descriptor.284func DownloadConfig(ctx context.Context, fetch FetcherFunc, ref string, desc ociv1.Descriptor, options ...ManifestDownloadOption) (cfg *ociv1.Image, err error) {285if desc.MediaType != images.MediaTypeDockerSchema2Config &&286desc.MediaType != ociv1.MediaTypeImageConfig {287288return nil, xerrors.Errorf("unsupported media type: %s", desc.MediaType)289}290log := log.WithField("desc", desc)291292var opts manifestDownloadOptions293for _, o := range options {294o(&opts)295}296297var buf []byte298err = wait.ExponentialBackoffWithContext(ctx, fetcherBackoffParams, func(ctx context.Context) (done bool, err error) {299var rc io.ReadCloser300if opts.Store != nil {301r, err := opts.Store.ReaderAt(ctx, desc)302if errors.Is(err, errdefs.ErrNotFound) {303// not cached yet304} else if err != nil {305log.WithError(err).Warn("cannot read config from store - fetching again")306} else {307defer r.Close()308rc = io.NopCloser(content.NewReader(r))309}310}311if rc == nil {312fetcher, err := fetch()313if err != nil {314log.WithError(err).Warn("cannot create fetcher")315return false, nil // retry316}317rc, err = fetcher.Fetch(ctx, desc)318if err != nil {319log.WithError(err).Warn("cannot fetch config")320if retryableError(err) {321return false, nil // retry322}323return false, err324}325defer rc.Close()326}327328buf, err = io.ReadAll(rc)329if err != nil {330log.WithError(err).Warn("cannot read config")331return false, nil // retry332}333334return true, nil335})336if err != nil {337return nil, xerrors.Errorf("failed to fetch config: %w", err)338}339340var res ociv1.Image341err = json.Unmarshal(buf, &res)342if err != nil {343return nil, xerrors.Errorf("cannot decode config: %w", err)344}345346if opts.Store != nil && ref != "" {347// ref can be empty for some users of DownloadConfig. However, some store implementations348// (e.g. the default containerd store) expect ref to be set. This would lead to stray errors.349350err := func() error {351w, err := opts.Store.Writer(ctx, content.WithDescriptor(desc), content.WithRef(ref))352if err != nil {353return err354}355defer w.Close()356357n, err := w.Write(buf)358if err != nil {359return err360}361if n != len(buf) {362return io.ErrShortWrite363}364365return w.Commit(ctx, int64(len(buf)), digest.FromBytes(buf), content.WithLabels(contentTypeLabel(desc.MediaType)))366}()367if err != nil && !strings.Contains(err.Error(), "already exists") {368log.WithError(err).WithField("ref", ref).WithField("desc", desc).Warn("cannot cache config")369}370}371372return &res, nil373}374375func contentTypeLabel(mt string) map[string]string {376return map[string]string{"Content-Type": mt}377}378379type manifestDownloadOptions struct {380Store BlobStore381}382383// ManifestDownloadOption alters the default manifest download behaviour384type ManifestDownloadOption func(*manifestDownloadOptions)385386// WithStore caches a downloaded manifest in a store387func WithStore(store BlobStore) ManifestDownloadOption {388return func(o *manifestDownloadOptions) {389o.Store = store390}391}392393type BlobStore interface {394ReaderAt(ctx context.Context, desc ociv1.Descriptor) (content.ReaderAt, error)395396// Some implementations require WithRef to be included in opts.397Writer(ctx context.Context, opts ...content.WriterOpt) (content.Writer, error)398399// Info will return metadata about content available in the content store.400//401// If the content is not present, ErrNotFound will be returned.402Info(ctx context.Context, dgst digest.Digest) (content.Info, error)403}404405type FetcherFunc func() (remotes.Fetcher, error)406407func AsFetcherFunc(f remotes.Fetcher) FetcherFunc {408return func() (remotes.Fetcher, error) { return f, nil }409}410411// DownloadManifest downloads and unmarshals the manifest of the given desc. If the desc points to manifest list412// we choose the first manifest in that list.413func DownloadManifest(ctx context.Context, fetch FetcherFunc, desc ociv1.Descriptor, options ...ManifestDownloadOption) (cfg *ociv1.Manifest, rdesc *ociv1.Descriptor, err error) {414log := log.WithField("desc", desc)415416var opts manifestDownloadOptions417for _, o := range options {418o(&opts)419}420421var (422placeInStore bool423mediaType = desc.MediaType424inpt []byte425)426err = wait.ExponentialBackoffWithContext(ctx, fetcherBackoffParams, func(ctx context.Context) (done bool, err error) {427var rc io.ReadCloser428if opts.Store != nil {429func() {430nfo, err := opts.Store.Info(ctx, desc.Digest)431if errors.Is(err, errdefs.ErrNotFound) {432// not in store yet433return434}435if err != nil {436log.WithError(err).Warn("cannot get manifest from store")437return438}439if nfo.Labels["Content-Type"] == "" {440// we have broken data in the store - ignore it and overwrite441return442}443444r, err := opts.Store.ReaderAt(ctx, desc)445if errors.Is(err, errdefs.ErrNotFound) {446// not in store yet447return448}449if err != nil {450log.WithError(err).Warn("cannot get manifest from store")451return452}453454mediaType, rc = nfo.Labels["Content-Type"], &reader{ReaderAt: r}455}()456}457if rc == nil {458// did not find in store, or there was no store. Either way, let's fetch this459// thing from the remote.460placeInStore = true461462var fetcher remotes.Fetcher463fetcher, err = fetch()464if err != nil {465log.WithError(err).Warn("cannot create fetcher")466return false, nil // retry467}468469rc, err = fetcher.Fetch(ctx, desc)470if err != nil {471log.WithError(err).Warn("cannot fetch manifest")472if retryableError(err) {473return false, nil // retry474}475return false, err476}477mediaType = desc.MediaType478}479480inpt, err = io.ReadAll(rc)481rc.Close()482if err != nil {483log.WithError(err).Warn("cannot read manifest")484return false, nil // retry485}486487return true, nil488})489if err != nil {490err = xerrors.Errorf("failed to fetch manifest: %w", err)491return492}493494rdesc = &desc495rdesc.MediaType = mediaType496497switch rdesc.MediaType {498case images.MediaTypeDockerSchema2ManifestList, ociv1.MediaTypeImageIndex:499log := log.WithField("desc", rdesc)500log.Debug("resolving image index")501502// we received a manifest list which means we'll pick the default platform503// and fetch that manifest504var list ociv1.Index505err = json.Unmarshal(inpt, &list)506if err != nil {507err = xerrors.Errorf("cannot unmarshal index: %w", err)508return509}510if len(list.Manifests) == 0 {511err = xerrors.Errorf("empty manifest")512return513}514515err = wait.ExponentialBackoffWithContext(ctx, fetcherBackoffParams, func(ctx context.Context) (done bool, err error) {516var fetcher remotes.Fetcher517fetcher, err = fetch()518if err != nil {519log.WithError(err).Warn("cannot create fetcher")520return false, nil // retry521}522523// TODO(cw): choose by platform, not just the first manifest524var rc io.ReadCloser525md := list.Manifests[0]526rc, err = fetcher.Fetch(ctx, md)527if err != nil {528log.WithError(err).Warn("cannot download config")529if retryableError(err) {530return false, nil // retry531}532return false, err533}534rdesc = &md535inpt, err = io.ReadAll(rc)536rc.Close()537if err != nil {538log.WithError(err).Warn("cannot download manifest")539return false, nil // retry540}541542return true, nil543})544if err != nil {545err = xerrors.Errorf("failed to download config: %w", err)546return547}548}549550switch rdesc.MediaType {551case images.MediaTypeDockerSchema2Manifest, ociv1.MediaTypeImageManifest:552default:553err = xerrors.Errorf("unsupported media type: %s", rdesc.MediaType)554return555}556557var res ociv1.Manifest558err = json.Unmarshal(inpt, &res)559if err != nil {560err = xerrors.Errorf("cannot decode config: %w", err)561return562}563564if opts.Store != nil && placeInStore {565// We're cheating here and store the actual image manifest under the desc of what's566// possibly an image index. This way we don't have to resolve the image index the next567// time one wishes to resolve desc.568w, err := opts.Store.Writer(ctx, content.WithDescriptor(desc), content.WithRef(desc.Digest.String()))569if err != nil {570if err != nil && !strings.Contains(err.Error(), "already exists") {571log.WithError(err).WithField("desc", *rdesc).Warn("cannot create store writer")572}573} else {574_, err = io.Copy(w, bytes.NewReader(inpt))575if err != nil {576log.WithError(err).WithField("desc", *rdesc).Warn("cannot copy manifest")577}578579err = w.Commit(ctx, 0, digest.FromBytes(inpt), content.WithLabels(map[string]string{"Content-Type": rdesc.MediaType}))580if err != nil {581log.WithError(err).WithField("desc", *rdesc).Warn("cannot store manifest")582}583w.Close()584}585}586587cfg = &res588return589}590591func (mh *manifestHandler) putManifest(w http.ResponseWriter, r *http.Request) {592respondWithError(w, distv2.ErrorCodeManifestInvalid)593}594595func (mh *manifestHandler) deleteManifest(w http.ResponseWriter, r *http.Request) {596respondWithError(w, distv2.ErrorCodeManifestUnknown)597}598599func retryableError(err error) bool {600if err == nil {601return false602}603if errors.Is(err, errdefs.ErrNotFound) || errors.Is(err, errdefs.ErrInvalidArgument) {604return false605}606if strings.Contains(err.Error(), "not found") ||607strings.Contains(err.Error(), "invalid argument") ||608strings.Contains(err.Error(), "not implemented") ||609strings.Contains(err.Error(), "unsupported media type") {610return false611}612return true613}614615616