Path: blob/main/components/image-builder-mk3/pkg/resolve/resolve.go
2500 views
// Copyright (c) 2021 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 resolve56import (7"context"8"encoding/json"9"fmt"10"io"11"net/http"12"strings"13"sync"14"time"1516"github.com/containerd/containerd/remotes"17dockerremote "github.com/containerd/containerd/remotes/docker"18"github.com/distribution/reference"19"github.com/opencontainers/go-digest"20"github.com/opentracing/opentracing-go"21"golang.org/x/xerrors"2223"github.com/gitpod-io/gitpod/common-go/log"24"github.com/gitpod-io/gitpod/common-go/tracing"25"github.com/gitpod-io/gitpod/image-builder/pkg/auth"26ociv1 "github.com/opencontainers/image-spec/specs-go/v1"27)2829var (30// ErrNotFound is returned when the reference was not found31ErrNotFound = xerrors.Errorf("not found")3233// ErrNotFound is returned when we're not authorized to return the reference34ErrUnauthorized = xerrors.Errorf("not authorized")3536// TooManyRequestsMatcher returns true if an error is a code 429 "Too Many Requests" error37TooManyRequestsMatcher = func(err error) bool {38if err == nil {39return false40}41return strings.Contains(err.Error(), "429 Too Many Requests")42}43)4445// StandaloneRefResolver can resolve image references without a Docker daemon46type StandaloneRefResolver struct {47ResolverFactory func() remotes.Resolver48}4950// Resolve resolves a mutable Docker tag to its absolute digest form by asking the corresponding Docker registry51func (sr *StandaloneRefResolver) Resolve(ctx context.Context, ref string, opts ...DockerRefResolverOption) (res string, err error) {52span, ctx := opentracing.StartSpanFromContext(ctx, "StandaloneRefResolver.Resolve")53defer func() {54var rerr error55if err != ErrNotFound {56rerr = err57}58tracing.FinishSpan(span, &rerr)59}()6061options := getOptions(opts)6263var r remotes.Resolver64if sr.ResolverFactory == nil {65registryOpts := []dockerremote.RegistryOpt{66dockerremote.WithAuthorizer(dockerremote.NewDockerAuthorizer(dockerremote.WithAuthCreds(func(host string) (username, password string, err error) {67if options.Auth == nil {68return69}7071return options.Auth.Username, options.Auth.Password, nil72}))),73}7475if options.Client != nil {76registryOpts = append(registryOpts, dockerremote.WithClient(options.Client))77}7879r = dockerremote.NewResolver(dockerremote.ResolverOptions{80Hosts: dockerremote.ConfigureDefaultRegistries(81registryOpts...,82),83})84} else {85r = sr.ResolverFactory()86}8788// The ref may be what Docker calls a "familiar" name, e.g. ubuntu:latest instead of docker.io/library/ubuntu:latest.89// To make this a valid digested form we first need to normalize that familiar name.90pref, err := reference.ParseDockerRef(ref)91if err != nil {92return "", xerrors.Errorf("cannt resolve image ref: %w", err)93}9495nref := pref.String()96pref = reference.TrimNamed(pref)97span.LogKV("normalized-ref", nref)9899res, desc, err := r.Resolve(ctx, nref)100if err != nil {101if strings.Contains(err.Error(), "not found") {102err = ErrNotFound103} else if strings.Contains(err.Error(), "Unauthorized") {104err = ErrUnauthorized105}106return107}108fetcher, err := r.Fetcher(ctx, res)109if err != nil {110return111}112113in, err := fetcher.Fetch(ctx, desc)114if err != nil {115return116}117defer in.Close()118buf, err := io.ReadAll(in)119if err != nil {120return121}122123var mf ociv1.Manifest124err = json.Unmarshal(buf, &mf)125if err != nil {126return "", fmt.Errorf("cannot unmarshal manifest: %w", err)127}128129if mf.Config.Size != 0 {130pref, err = reference.WithDigest(pref, desc.Digest)131if err != nil {132return133}134return pref.String(), nil135}136137var mfl ociv1.Index138err = json.Unmarshal(buf, &mfl)139if err != nil {140return141}142143var dgst digest.Digest144for _, mf := range mfl.Manifests {145if mf.Platform == nil {146continue147}148if fmt.Sprintf("%s-%s", mf.Platform.OS, mf.Platform.Architecture) == "linux-amd64" {149dgst = mf.Digest150break151}152}153if dgst == "" {154return "", fmt.Errorf("no manifest for platform linux-amd64 found")155}156157pref, err = reference.WithDigest(pref, dgst)158if err != nil {159return160}161return pref.String(), nil162}163164type opts struct {165Auth *auth.Authentication166Client *http.Client167}168169// DockerRefResolverOption configures reference resolution170type DockerRefResolverOption func(o *opts)171172// WithAuthentication sets a base64 encoded authentication for accessing a Docker registry173func WithAuthentication(auth *auth.Authentication) DockerRefResolverOption {174if auth == nil {175log.Debug("WithAuthentication - auth was nil")176}177178return func(o *opts) {179o.Auth = auth180}181}182183// WithHttpClient sets the HTTP client to use for making requests to the Docker registry.184func WithHttpClient(client *http.Client) DockerRefResolverOption {185return func(o *opts) {186if client == nil {187log.Debug("WithHttpClient - client was nil")188}189o.Client = client190}191}192193func getOptions(o []DockerRefResolverOption) *opts {194var res opts195for _, opt := range o {196opt(&res)197}198return &res199}200201// DockerRefResolver resolves a mutable Docker tag to its absolute digest form.202// For example: gitpod/workspace-full:latest becomes docker.io/gitpod/workspace-full@sha256:sha-hash-goes-here203type DockerRefResolver interface {204// Resolve resolves a mutable Docker tag to its absolute digest form.205Resolve(ctx context.Context, ref string, opts ...DockerRefResolverOption) (res string, err error)206}207208// PrecachingRefResolver regularly resolves a set of references and returns the cached value when asked to resolve that reference.209type PrecachingRefResolver struct {210Resolver DockerRefResolver211Candidates []string212Auth auth.RegistryAuthenticator213214mu sync.RWMutex215cache map[string]string216}217218var _ DockerRefResolver = &PrecachingRefResolver{}219220// StartCaching starts the precaching of resolved references at the given interval. This function blocks until the context is canceled221// and is intended to run as a Go routine.222func (pr *PrecachingRefResolver) StartCaching(ctx context.Context, interval time.Duration) {223span, ctx := opentracing.StartSpanFromContext(ctx, "PrecachingRefResolver.StartCaching")224defer tracing.FinishSpan(span, nil)225226t := time.NewTicker(interval)227228log.WithField("interval", interval.String()).WithField("refs", pr.Candidates).Info("starting Docker ref pre-cache")229230pr.cache = make(map[string]string)231for {232for _, c := range pr.Candidates {233var opts []DockerRefResolverOption234if pr.Auth != ((auth.RegistryAuthenticator)(nil)) {235ref, err := reference.ParseNormalizedNamed(c)236if err != nil {237log.WithError(err).WithField("ref", c).Warn("unable to precache reference: cannot parse")238continue239}240241auth, err := pr.Auth.Authenticate(ctx, reference.Domain(ref))242if err != nil {243log.WithError(err).WithField("ref", c).Warn("unable to precache reference: cannot authenticate")244continue245}246247opts = append(opts, WithAuthentication(auth))248}249250res, err := pr.Resolver.Resolve(ctx, c, opts...)251if err != nil {252log.WithError(err).WithField("ref", c).Warn("unable to precache reference")253continue254}255256pr.mu.Lock()257pr.cache[c] = res258pr.mu.Unlock()259260log.WithField("ref", c).WithField("resolved-to", res).Debug("pre-cached Docker ref")261}262263select {264case <-t.C:265case <-ctx.Done():266log.Debug("context cancelled - shutting down Docker ref pre-caching")267return268}269}270}271272// Resolve aims to resolve a ref using its own cache first and asks the underlying resolver otherwise273func (pr *PrecachingRefResolver) Resolve(ctx context.Context, ref string, opts ...DockerRefResolverOption) (res string, err error) {274span, ctx := opentracing.StartSpanFromContext(ctx, "PrecachingRefResolver.Resolve")275defer tracing.FinishSpan(span, &err)276277pr.mu.RLock()278defer pr.mu.RUnlock()279280if pr.cache == nil {281return pr.Resolver.Resolve(ctx, ref, opts...)282}283284res, ok := pr.cache[ref]285if !ok {286return pr.Resolver.Resolve(ctx, ref, opts...)287}288289return res, nil290}291292type MockRefResolver map[string]string293294func (m MockRefResolver) Resolve(ctx context.Context, ref string, opts ...DockerRefResolverOption) (res string, err error) {295res, ok := m[ref]296if !ok {297return "", ErrNotFound298}299return res, nil300}301302303