Path: blob/main/components/registry-facade/pkg/registry/layersource.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"compress/gzip"9"context"10"io"11"io/fs"12"os"13"strconv"14"strings"15"sync"1617"github.com/containerd/containerd/errdefs"18"github.com/containerd/containerd/remotes"19lru "github.com/hashicorp/golang-lru"20"github.com/opencontainers/go-digest"21ociv1 "github.com/opencontainers/image-spec/specs-go/v1"22"github.com/pkg/errors"23"golang.org/x/xerrors"2425"github.com/gitpod-io/gitpod/common-go/log"26"github.com/gitpod-io/gitpod/registry-facade/api"27)2829const (30// labelSkipNLayer is a label on images that tells registry-facade to discard the first N layer.31// If the value of this label32// - is not a number (cannot be parsed by strconv.ParseUint), registry-facade fails to use the image,33// - is larger than the number of layers in the image, the image is considered empty (i.e. to have no layer).34labelSkipNLayer = "skip-n.registry-facade.gitpod.io"35)3637// LayerSource provides layers for a workspace image38type LayerSource interface {39BlobSource40GetLayer(ctx context.Context, spec *api.ImageSpec) ([]AddonLayer, error)41Envs(ctx context.Context, spec *api.ImageSpec) ([]EnvModifier, error)42}4344// AddonLayer is an OCI descriptor for a layer + the layers diffID45type AddonLayer struct {46Descriptor ociv1.Descriptor47DiffID digest.Digest48}4950type filebackedLayer struct {51AddonLayer52Filename string53}5455// FileLayerSource provides the same layers independent of the workspace spec56type FileLayerSource []filebackedLayer5758func (s FileLayerSource) Name() string {59return "filelayer"60}6162// Envs returns the list of env modifiers63func (s FileLayerSource) Envs(ctx context.Context, spec *api.ImageSpec) ([]EnvModifier, error) {64return nil, nil65}6667// GetLayer return all layers of this source68func (s FileLayerSource) GetLayer(ctx context.Context, spec *api.ImageSpec) ([]AddonLayer, error) {69res := make([]AddonLayer, len(s))70for i := range s {71res[i] = s[i].AddonLayer72}73return res, nil74}7576// HasBlob checks if a digest can be served by this blob source77func (s FileLayerSource) HasBlob(ctx context.Context, spec *api.ImageSpec, dgst digest.Digest) bool {78for _, l := range s {79if l.Descriptor.Digest == dgst {80return true81}82}83return false84}8586// GetBlob provides access to a blob. If a ReadCloser is returned the receiver is expected to87// call close on it eventually.88func (s FileLayerSource) GetBlob(ctx context.Context, spec *api.ImageSpec, dgst digest.Digest) (dontCache bool, mediaType string, url string, data io.ReadCloser, err error) {89var src filebackedLayer90for _, l := range s {91if l.Descriptor.Digest == dgst {92src = l93break94}95}96if src.Filename == "" {97err = errdefs.ErrNotFound98return99}100101f, err := os.OpenFile(src.Filename, os.O_RDONLY, 0)102if errors.Is(err, fs.ErrNotExist) {103err = errdefs.ErrNotFound104return105}106if err != nil {107return108}109110return false, src.Descriptor.MediaType, "", f, nil111}112113// NewFileLayerSource produces a static layer source where each file is expected to be a gzipped layer114func NewFileLayerSource(ctx context.Context, file ...string) (FileLayerSource, error) {115var res FileLayerSource116for _, fn := range file {117fr, err := os.OpenFile(fn, os.O_RDONLY, 0)118if err != nil {119return nil, err120}121defer fr.Close()122123stat, err := fr.Stat()124if err != nil {125return nil, err126}127128dgst, err := digest.FromReader(fr)129if err != nil {130return nil, err131}132133// start again to read the diffID134_, err = fr.Seek(0, 0)135if err != nil {136return nil, err137}138diffr, err := gzip.NewReader(fr)139if err != nil {140return nil, err141}142defer diffr.Close()143diffID, err := digest.FromReader(diffr)144if err != nil {145return nil, err146}147148desc := ociv1.Descriptor{149MediaType: ociv1.MediaTypeImageLayer,150Digest: dgst,151Size: stat.Size(),152}153res = append(res, filebackedLayer{154AddonLayer: AddonLayer{155Descriptor: desc,156DiffID: diffID,157},158Filename: fn,159})160161log.WithField("diffID", diffID).WithField("fn", fn).Debug("loaded static layer")162}163164return res, nil165}166167type imagebackedLayer struct {168AddonLayer169NewFetcher func() (remotes.Fetcher, error)170}171172// ImageLayerSource provides additional layers from another image173type ImageLayerSource struct {174envs []EnvModifier175layers []imagebackedLayer176}177178func (s ImageLayerSource) Name() string {179return "imagelayer"180}181182// Envs returns the list of env modifiers183func (s ImageLayerSource) Envs(ctx context.Context, spec *api.ImageSpec) ([]EnvModifier, error) {184return s.envs, nil185}186187// GetLayer return all layers of this source188func (s ImageLayerSource) GetLayer(ctx context.Context, spec *api.ImageSpec) ([]AddonLayer, error) {189res := make([]AddonLayer, len(s.layers))190for i := range s.layers {191res[i] = s.layers[i].AddonLayer192}193return res, nil194}195196// HasBlob checks if a digest can be served by this blob source197func (s ImageLayerSource) HasBlob(ctx context.Context, spec *api.ImageSpec, dgst digest.Digest) bool {198for _, l := range s.layers {199if l.Descriptor.Digest == dgst {200return true201}202}203return false204}205206// GetBlob provides access to a blob. If a ReadCloser is returned the receiver is expected to207// call close on it eventually.208func (s ImageLayerSource) GetBlob(ctx context.Context, spec *api.ImageSpec, dgst digest.Digest) (dontCache bool, mediaType string, url string, data io.ReadCloser, err error) {209var src imagebackedLayer210for _, l := range s.layers {211if l.Descriptor.Digest == dgst {212src = l213}214}215if src.NewFetcher == nil {216err = errdefs.ErrNotFound217return218}219220fetcher, err := src.NewFetcher()221if err != nil {222return223}224rc, err := fetcher.Fetch(ctx, src.Descriptor)225if err != nil {226return227}228229return false, src.Descriptor.MediaType, "", rc, nil230}231232const (233envPrefixSet = "GITPOD_ENV_SET_"234envPrefixAppend = "GITPOD_ENV_APPEND_"235envPrefixPrepend = "GITPOD_ENV_PREPEND_"236)237238// NewStaticSourceFromImage downloads image layers into the store and uses them as static layer239func NewStaticSourceFromImage(ctx context.Context, newResolver ResolverProvider, ref string) (*ImageLayerSource, error) {240resolver := newResolver()241_, desc, err := resolver.Resolve(ctx, ref)242if err != nil {243return nil, err244}245fetcher, err := resolver.Fetcher(ctx, ref)246if err != nil {247return nil, err248}249250manifest, _, err := DownloadManifest(ctx, AsFetcherFunc(fetcher), desc)251if err != nil {252return nil, err253}254255cfg, err := DownloadConfig(ctx, AsFetcherFunc(fetcher), ref, manifest.Config)256if err != nil {257return nil, err258}259260// images can mark the first N layers as irrelevant.261// We use labels for that to ship that information with the image.262skipN, err := getSkipNLabelValue(&cfg.Config)263if err != nil {264return nil, err265}266267res := make([]imagebackedLayer, 0, len(manifest.Layers))268for i, ml := range manifest.Layers {269if i < skipN {270continue271}272273l := imagebackedLayer{274AddonLayer: AddonLayer{275Descriptor: ml,276DiffID: cfg.RootFS.DiffIDs[i],277},278NewFetcher: func() (remotes.Fetcher, error) {279// Must create a new resolver for each fetcher, otherwise this will keep using the originally280// provided pull secret which eventually expires.281resolver := newResolver()282return resolver.Fetcher(ctx, ref)283},284}285res = append(res, l)286}287288var envs []EnvModifier289parsedEnvs := parseEnvs(cfg.Config.Env)290for _, name := range parsedEnvs.keys {291value := parsedEnvs.values[name]292if strings.HasPrefix(name, envPrefixAppend) {293name = strings.TrimPrefix(name, envPrefixAppend)294if name == "" || value == "" {295continue296}297envs = append(envs, newAppendEnvModifier(name, value))298} else if strings.HasPrefix(name, envPrefixPrepend) {299name = strings.TrimPrefix(name, envPrefixPrepend)300if name == "" || value == "" {301continue302}303envs = append(envs, newPrependEnvModifier(name, value))304} else if strings.HasPrefix(name, envPrefixSet) {305name = strings.TrimPrefix(name, envPrefixSet)306if name == "" {307continue308}309envs = append(envs, newSetEnvModifier(name, value))310}311}312313return &ImageLayerSource{314layers: res,315envs: envs,316}, nil317}318319// getSkipNLabelValue returns the parsed label value of the LabelSkipNLayer label.320func getSkipNLabelValue(cfg *ociv1.ImageConfig) (skipN int, err error) {321v, ok := cfg.Labels[labelSkipNLayer]322if !ok {323return 0, nil324}325326vi, err := strconv.ParseUint(v, 10, 16)327if err != nil {328return 0, xerrors.Errorf("skipN layer label: %w", err)329}330return int(vi), nil331}332333// CompositeLayerSource appends layers from different sources334type CompositeLayerSource []LayerSource335336func (cs CompositeLayerSource) Name() string {337return "composite"338}339340// Envs returns the list of env modifiers341func (cs CompositeLayerSource) Envs(ctx context.Context, spec *api.ImageSpec) ([]EnvModifier, error) {342var res []EnvModifier343for _, s := range cs {344envs, err := s.Envs(ctx, spec)345if err != nil {346return nil, err347}348res = append(res, envs...)349}350return res, nil351}352353// GetLayer returns the list of all layers from all sources354func (cs CompositeLayerSource) GetLayer(ctx context.Context, spec *api.ImageSpec) ([]AddonLayer, error) {355var res []AddonLayer356for _, s := range cs {357ls, err := s.GetLayer(ctx, spec)358if err != nil {359return nil, err360}361res = append(res, ls...)362}363return res, nil364}365366// HasBlob checks if a digest can be served by this blob source367func (cs CompositeLayerSource) HasBlob(ctx context.Context, spec *api.ImageSpec, dgst digest.Digest) bool {368for _, s := range cs {369if s.HasBlob(ctx, spec, dgst) {370return true371}372}373return false374}375376// GetBlob provides access to a blob. If a ReadCloser is returned the receiver is expected to377// call close on it eventually.378func (cs CompositeLayerSource) GetBlob(ctx context.Context, spec *api.ImageSpec, dgst digest.Digest) (dontCache bool, mediaType string, url string, data io.ReadCloser, err error) {379for _, s := range cs {380if s.HasBlob(ctx, spec, dgst) {381return s.GetBlob(ctx, spec, dgst)382}383}384385err = errdefs.ErrNotFound386return387}388389// RefSource extracts an image reference from an image spec390type RefSource func(*api.ImageSpec) (ref []string, err error)391392// NewSpecMappedImageSource creates a new spec mapped image source393func NewSpecMappedImageSource(resolver ResolverProvider, refSource RefSource) (*SpecMappedImagedSource, error) {394cache, err := lru.New(128)395if err != nil {396return nil, err397}398return &SpecMappedImagedSource{399RefSource: refSource,400Resolver: resolver,401cache: cache,402}, nil403}404405// SpecMappedImagedSource provides layers from other images based on the image spec406type SpecMappedImagedSource struct {407RefSource RefSource408Resolver ResolverProvider409410// TODO: add ttl411cache *lru.Cache412}413414func (src *SpecMappedImagedSource) Name() string {415return "specmapped"416}417418// Envs returns the list of env modifiers419func (src *SpecMappedImagedSource) Envs(ctx context.Context, spec *api.ImageSpec) ([]EnvModifier, error) {420lsrcs, err := src.getDelegate(ctx, spec)421if err != nil {422return nil, err423}424var res []EnvModifier425for _, lsrc := range lsrcs {426if lsrc == nil {427continue428}429envs, err := lsrc.Envs(ctx, spec)430if err != nil {431return nil, err432}433res = append(res, envs...)434}435return res, nil436}437438// GetLayer returns the list of all layers from this source439func (src *SpecMappedImagedSource) GetLayer(ctx context.Context, spec *api.ImageSpec) ([]AddonLayer, error) {440lsrcs, err := src.getDelegate(ctx, spec)441if err != nil {442return nil, err443}444var res []AddonLayer445for _, lsrc := range lsrcs {446if lsrc == nil {447continue448}449ls, err := lsrc.GetLayer(ctx, spec)450if err != nil {451return nil, err452}453res = append(res, ls...)454}455return res, nil456}457458// HasBlob checks if a digest can be served by this blob source459func (src *SpecMappedImagedSource) HasBlob(ctx context.Context, spec *api.ImageSpec, dgst digest.Digest) bool {460lsrcs, err := src.getDelegate(ctx, spec)461if err != nil {462return false463}464for _, lsrc := range lsrcs {465if lsrc == nil {466continue467}468if lsrc.HasBlob(ctx, spec, dgst) {469return true470}471}472return false473}474475// GetBlob provides access to a blob. If a ReadCloser is returned the receiver is expected to476// call close on it eventually.477func (src *SpecMappedImagedSource) GetBlob(ctx context.Context, spec *api.ImageSpec, dgst digest.Digest) (dontCache bool, mediaType string, url string, data io.ReadCloser, err error) {478lsrcs, err := src.getDelegate(ctx, spec)479if err != nil {480return481}482for _, lsrc := range lsrcs {483if lsrc == nil {484continue485}486if lsrc.HasBlob(ctx, spec, dgst) {487return lsrc.GetBlob(ctx, spec, dgst)488}489}490err = errdefs.ErrNotFound491return492}493494// getDelegate returns the cached layer source delegate computed from the image spec495func (src *SpecMappedImagedSource) getDelegate(ctx context.Context, spec *api.ImageSpec) ([]LayerSource, error) {496refs, err := src.RefSource(spec)497if err != nil {498return nil, err499}500layers := make([]LayerSource, len(refs))501502for i, ref := range refs {503if ref == "" {504continue505}506if s, ok := src.cache.Get(ref); ok {507layers[i] = s.(LayerSource)508continue509}510lsrc, err := NewStaticSourceFromImage(ctx, src.Resolver, ref)511if err != nil {512return nil, err513}514src.cache.Add(ref, lsrc)515layers[i] = lsrc516}517return layers, nil518}519520// NewContentLayerSource creates a new layer source providing the content layer of an image spec521func NewContentLayerSource() (*ContentLayerSource, error) {522blobCache, err := lru.New(128)523if err != nil {524return nil, err525}526return &ContentLayerSource{527blobCache: blobCache,528}, nil529}530531// ContentLayerSource provides layers from other images based on the image spec532type ContentLayerSource struct {533blobCache *lru.Cache534}535536func (src *ContentLayerSource) Name() string {537return "contentlayer"538}539540// Envs returns the list of env modifiers541func (src *ContentLayerSource) Envs(ctx context.Context, spec *api.ImageSpec) ([]EnvModifier, error) {542return nil, nil543}544545// GetLayer returns the list of all layers from this source546func (src *ContentLayerSource) GetLayer(ctx context.Context, spec *api.ImageSpec) ([]AddonLayer, error) {547res := make([]AddonLayer, len(spec.ContentLayer))548for i, layer := range spec.ContentLayer {549if dl := layer.GetDirect(); dl != nil {550dgst := digest.FromBytes(dl.Content)551res[i] = AddonLayer{552Descriptor: ociv1.Descriptor{553MediaType: ociv1.MediaTypeImageLayer,554Digest: dgst,555Size: int64(len(dl.Content)),556},557DiffID: dgst,558}559continue560}561562if rl := layer.GetRemote(); rl != nil {563dgst, err := digest.Parse(rl.Digest)564if err != nil {565return nil, xerrors.Errorf("cannot parse layer digest %s: %w", rl.Digest, err)566}567diffID, err := digest.Parse(rl.DiffId)568if err != nil {569return nil, xerrors.Errorf("cannot parse layer diffID %s: %w", rl.DiffId, err)570}571var urls []string572if rl.Url != "" {573urls = []string{rl.Url}574}575576res[i] = AddonLayer{577Descriptor: ociv1.Descriptor{578MediaType: rl.MediaType,579Digest: dgst,580URLs: urls,581Size: rl.Size,582},583DiffID: diffID,584}585continue586}587588return nil, xerrors.Errorf("unknown layer type")589}590return res, nil591}592593// HasBlob checks if a digest can be served by this blob source594func (src *ContentLayerSource) HasBlob(ctx context.Context, spec *api.ImageSpec, dgst digest.Digest) bool {595if src.blobCache.Contains(dgst) {596return true597}598for _, layer := range spec.ContentLayer {599if dl := layer.GetDirect(); dl != nil {600if digest.FromBytes(dl.Content) == dgst {601return true602}603}604605if rl := layer.GetRemote(); rl != nil {606if rl.Digest == dgst.String() {607return true608}609}610}611return false612}613614// GetBlob provides access to a blob. If a ReadCloser is returned the receiver is expected to615// call close on it eventually.616func (src *ContentLayerSource) GetBlob(ctx context.Context, spec *api.ImageSpec, dgst digest.Digest) (dontCache bool, mediaType string, url string, data io.ReadCloser, err error) {617if blob, ok := src.blobCache.Get(dgst); ok {618return false, ociv1.MediaTypeImageLayer, "", io.NopCloser(bytes.NewReader(blob.([]byte))), nil619}620621for _, layer := range spec.ContentLayer {622if dl := layer.GetDirect(); dl != nil {623if digest.FromBytes(dl.Content) == dgst {624return false, ociv1.MediaTypeImageLayer, "", io.NopCloser(bytes.NewReader(dl.Content)), nil625}626}627628if rl := layer.GetRemote(); rl != nil {629if rl.Digest == dgst.String() {630mt := ociv1.MediaTypeImageLayerGzip631if rl.DiffId == rl.Digest || rl.DiffId == "" {632mt = ociv1.MediaTypeImageLayer633}634635return false, mt, rl.Url, nil, nil636}637}638}639640err = errdefs.ErrNotFound641return642}643644// ParsedEnvs is parsed image envs configuration645type ParsedEnvs struct {646keys []string647values map[string]string648}649650// ParseEnv parses environment variables651func parseEnvs(envs []string) *ParsedEnvs {652result := ParsedEnvs{653values: make(map[string]string),654}655for _, e := range envs {656parts := strings.SplitN(e, "=", 2)657if len(parts) == 0 {658continue659}660key := parts[0]661var value string662if len(parts) > 1 {663value = parts[1]664}665result.keys = append(result.keys, key)666result.values[key] = value667}668return &result669}670671// Set the give value as a variable's value of the given name672func (envs *ParsedEnvs) Set(name, value string) {673_, exists := envs.values[name]674if !exists {675envs.keys = append(envs.keys, name)676}677envs.values[name] = value678}679680// Append the given value to a variable's value of the given name681func (envs *ParsedEnvs) Append(name, value string) {682current, exists := envs.values[name]683if exists {684envs.values[name] = value + current685} else {686envs.keys = append(envs.keys, name)687envs.values[name] = value688}689}690691// Prepend the given value to a variable's value of the given name692func (envs *ParsedEnvs) Prepend(name, value string) {693current, exists := envs.values[name]694if exists {695envs.values[name] = current + value696} else {697envs.keys = append(envs.keys, name)698envs.values[name] = value699}700}701702func (envs *ParsedEnvs) serialize() (result []string) {703for _, key := range envs.keys {704result = append(result, key+"="+envs.values[key])705}706return707}708709// EnvModifier modifies an image envs configuration710type EnvModifier func(*ParsedEnvs)711712func newSetEnvModifier(name, value string) EnvModifier {713return func(pe *ParsedEnvs) {714pe.Set(name, value)715}716}717718func newAppendEnvModifier(name, value string) EnvModifier {719return func(pe *ParsedEnvs) {720pe.Append(name, value)721}722}723724func newPrependEnvModifier(name, value string) EnvModifier {725return func(pe *ParsedEnvs) {726pe.Prepend(name, value)727}728}729730// NewRevisioningLayerSource produces a new revisioning layer source731func NewRevisioningLayerSource(active LayerSource) *RevisioningLayerSource {732return &RevisioningLayerSource{733active: active,734}735}736737type RevisioningLayerSource struct {738mu sync.RWMutex739active LayerSource740past []LayerSource741}742743func (src *RevisioningLayerSource) Name() string {744src.mu.RLock()745defer src.mu.RUnlock()746747return src.active.Name()748}749750func (src *RevisioningLayerSource) Update(s LayerSource) {751src.mu.Lock()752defer src.mu.Unlock()753754src.past = append(src.past, src.active)755src.active = s756}757758func (src *RevisioningLayerSource) GetLayer(ctx context.Context, spec *api.ImageSpec) ([]AddonLayer, error) {759src.mu.RLock()760defer src.mu.RUnlock()761762return src.active.GetLayer(ctx, spec)763}764765func (src *RevisioningLayerSource) Envs(ctx context.Context, spec *api.ImageSpec) ([]EnvModifier, error) {766src.mu.RLock()767defer src.mu.RUnlock()768769return src.active.Envs(ctx, spec)770}771772// HasBlob checks if a digest can be served by this blob source773func (src *RevisioningLayerSource) HasBlob(ctx context.Context, details *api.ImageSpec, dgst digest.Digest) bool {774src.mu.RLock()775defer src.mu.RUnlock()776777if src.active.HasBlob(ctx, details, dgst) {778return true779}780for _, p := range src.past {781if p.HasBlob(ctx, details, dgst) {782return true783}784}785return false786}787788// GetBlob provides access to a blob. If a ReadCloser is returned the receiver is expected to789// call close on it eventually.790func (src *RevisioningLayerSource) GetBlob(ctx context.Context, details *api.ImageSpec, dgst digest.Digest) (dontCache bool, mediaType string, url string, data io.ReadCloser, err error) {791src.mu.RLock()792defer src.mu.RUnlock()793794if src.active.HasBlob(ctx, details, dgst) {795return src.active.GetBlob(ctx, details, dgst)796}797for _, p := range src.past {798if p.HasBlob(ctx, details, dgst) {799return p.GetBlob(ctx, details, dgst)800}801}802803err = errdefs.ErrNotFound804return805}806807808