mirror of
https://github.com/gitpod-io/gitpod.git
synced 2025-12-08 17:36:30 +00:00
290 lines
7.9 KiB
Go
290 lines
7.9 KiB
Go
// Copyright (c) 2021 Gitpod GmbH. All rights reserved.
|
|
// Licensed under the GNU Affero General Public License (AGPL).
|
|
// See License-AGPL.txt in the project root for license information.
|
|
|
|
package resolve
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/containerd/containerd/remotes"
|
|
dockerremote "github.com/containerd/containerd/remotes/docker"
|
|
"github.com/docker/distribution/reference"
|
|
"github.com/opencontainers/go-digest"
|
|
"github.com/opentracing/opentracing-go"
|
|
"golang.org/x/xerrors"
|
|
|
|
"github.com/gitpod-io/gitpod/common-go/log"
|
|
"github.com/gitpod-io/gitpod/common-go/tracing"
|
|
"github.com/gitpod-io/gitpod/image-builder/pkg/auth"
|
|
ociv1 "github.com/opencontainers/image-spec/specs-go/v1"
|
|
)
|
|
|
|
var (
|
|
// ErrNotFound is returned when the reference was not found
|
|
ErrNotFound = xerrors.Errorf("not found")
|
|
|
|
// ErrNotFound is returned when we're not authorized to return the reference
|
|
ErrUnauthorized = xerrors.Errorf("not authorized")
|
|
)
|
|
|
|
// StandaloneRefResolver can resolve image references without a Docker daemon
|
|
type StandaloneRefResolver struct {
|
|
ResolverFactory func() remotes.Resolver
|
|
}
|
|
|
|
// Resolve resolves a mutable Docker tag to its absolute digest form by asking the corresponding Docker registry
|
|
func (sr *StandaloneRefResolver) Resolve(ctx context.Context, ref string, opts ...DockerRefResolverOption) (res string, err error) {
|
|
span, ctx := opentracing.StartSpanFromContext(ctx, "StandaloneRefResolver.Resolve")
|
|
defer func() {
|
|
var rerr error
|
|
if err != ErrNotFound {
|
|
rerr = err
|
|
}
|
|
tracing.FinishSpan(span, &rerr)
|
|
}()
|
|
|
|
options := getOptions(opts)
|
|
|
|
var r remotes.Resolver
|
|
if sr.ResolverFactory == nil {
|
|
r = dockerremote.NewResolver(dockerremote.ResolverOptions{
|
|
Authorizer: dockerremote.NewDockerAuthorizer(dockerremote.WithAuthCreds(func(host string) (username, password string, err error) {
|
|
if options.Auth == nil {
|
|
return
|
|
}
|
|
|
|
return options.Auth.Username, options.Auth.Password, nil
|
|
})),
|
|
})
|
|
} else {
|
|
r = sr.ResolverFactory()
|
|
}
|
|
|
|
// The ref may be what Docker calls a "familiar" name, e.g. ubuntu:latest instead of docker.io/library/ubuntu:latest.
|
|
// To make this a valid digested form we first need to normalize that familiar name.
|
|
pref, err := reference.ParseNormalizedNamed(ref)
|
|
if err != nil {
|
|
return "", xerrors.Errorf("cannt resolve image ref: %w", err)
|
|
}
|
|
|
|
// The reference is already in digest form we don't have to do anything
|
|
if cref, ok := pref.(reference.Canonical); ok {
|
|
// if reference contain tag, we should remove it to avoid tag and digest conflict
|
|
if _, ok := pref.(reference.Tagged); ok {
|
|
dref, err := reference.WithDigest(reference.TrimNamed(pref), cref.Digest())
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
ref = dref.String()
|
|
}
|
|
span.LogKV("result", ref)
|
|
return ref, nil
|
|
}
|
|
|
|
// Some users don't specify a tag, and Docker assumes "latest" - we follow the same convention
|
|
if _, ok := pref.(reference.Tagged); !ok {
|
|
pref, err = reference.WithTag(pref, "latest")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
|
|
nref := pref.String()
|
|
pref = reference.TrimNamed(pref)
|
|
span.LogKV("normalized-ref", nref)
|
|
|
|
res, desc, err := r.Resolve(ctx, nref)
|
|
if err != nil {
|
|
if strings.Contains(err.Error(), "not found") {
|
|
err = ErrNotFound
|
|
} else if strings.Contains(err.Error(), "Unauthorized") {
|
|
err = ErrUnauthorized
|
|
}
|
|
return
|
|
}
|
|
fetcher, err := r.Fetcher(ctx, res)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
in, err := fetcher.Fetch(ctx, desc)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer in.Close()
|
|
buf, err := ioutil.ReadAll(in)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
var mf ociv1.Manifest
|
|
err = json.Unmarshal(buf, &mf)
|
|
if err != nil {
|
|
return "", fmt.Errorf("cannot unmarshal manifest: %w", err)
|
|
}
|
|
|
|
if mf.Config.Size != 0 {
|
|
pref, err = reference.WithDigest(pref, desc.Digest)
|
|
if err != nil {
|
|
return
|
|
}
|
|
return pref.String(), nil
|
|
}
|
|
|
|
var mfl ociv1.Index
|
|
err = json.Unmarshal(buf, &mfl)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
var dgst digest.Digest
|
|
for _, mf := range mfl.Manifests {
|
|
if mf.Platform == nil {
|
|
continue
|
|
}
|
|
if fmt.Sprintf("%s-%s", mf.Platform.OS, mf.Platform.Architecture) == "linux-amd64" {
|
|
dgst = mf.Digest
|
|
break
|
|
}
|
|
}
|
|
if dgst == "" {
|
|
return "", fmt.Errorf("no manifest for platform linux-amd64 found")
|
|
}
|
|
|
|
pref, err = reference.WithDigest(pref, dgst)
|
|
if err != nil {
|
|
return
|
|
}
|
|
return pref.String(), nil
|
|
}
|
|
|
|
type opts struct {
|
|
Auth *auth.Authentication
|
|
}
|
|
|
|
// DockerRefResolverOption configures reference resolution
|
|
type DockerRefResolverOption func(o *opts)
|
|
|
|
// WithAuthentication sets a base64 encoded authentication for accessing a Docker registry
|
|
func WithAuthentication(auth *auth.Authentication) DockerRefResolverOption {
|
|
return func(o *opts) {
|
|
o.Auth = auth
|
|
}
|
|
}
|
|
|
|
func getOptions(o []DockerRefResolverOption) *opts {
|
|
var res opts
|
|
for _, opt := range o {
|
|
opt(&res)
|
|
}
|
|
return &res
|
|
}
|
|
|
|
// DockerRefResolver resolves a mutable Docker tag to its absolute digest form.
|
|
// For example: gitpod/workspace-full:latest becomes docker.io/gitpod/workspace-full@sha256:sha-hash-goes-here
|
|
type DockerRefResolver interface {
|
|
// Resolve resolves a mutable Docker tag to its absolute digest form.
|
|
Resolve(ctx context.Context, ref string, opts ...DockerRefResolverOption) (res string, err error)
|
|
}
|
|
|
|
// PrecachingRefResolver regularly resolves a set of references and returns the cached value when asked to resolve that reference.
|
|
type PrecachingRefResolver struct {
|
|
Resolver DockerRefResolver
|
|
Candidates []string
|
|
Auth auth.RegistryAuthenticator
|
|
|
|
mu sync.RWMutex
|
|
cache map[string]string
|
|
}
|
|
|
|
var _ DockerRefResolver = &PrecachingRefResolver{}
|
|
|
|
// StartCaching starts the precaching of resolved references at the given interval. This function blocks until the context is canceled
|
|
// and is intended to run as a Go routine.
|
|
func (pr *PrecachingRefResolver) StartCaching(ctx context.Context, interval time.Duration) {
|
|
span, ctx := opentracing.StartSpanFromContext(ctx, "PrecachingRefResolver.StartCaching")
|
|
defer tracing.FinishSpan(span, nil)
|
|
|
|
t := time.NewTicker(interval)
|
|
|
|
log.WithField("interval", interval.String()).WithField("refs", pr.Candidates).Info("starting Docker ref pre-cache")
|
|
|
|
pr.cache = make(map[string]string)
|
|
for {
|
|
for _, c := range pr.Candidates {
|
|
var opts []DockerRefResolverOption
|
|
if pr.Auth != nil {
|
|
ref, err := reference.ParseNormalizedNamed(c)
|
|
if err != nil {
|
|
log.WithError(err).WithField("ref", c).Warn("unable to precache reference: cannot parse")
|
|
continue
|
|
}
|
|
|
|
auth, err := pr.Auth.Authenticate(reference.Domain(ref))
|
|
if err != nil {
|
|
log.WithError(err).WithField("ref", c).Warn("unable to precache reference: cannot authenticate")
|
|
continue
|
|
}
|
|
|
|
opts = append(opts, WithAuthentication(auth))
|
|
}
|
|
|
|
res, err := pr.Resolver.Resolve(ctx, c, opts...)
|
|
if err != nil {
|
|
log.WithError(err).WithField("ref", c).Warn("unable to precache reference")
|
|
continue
|
|
}
|
|
|
|
pr.mu.Lock()
|
|
pr.cache[c] = res
|
|
pr.mu.Unlock()
|
|
|
|
log.WithField("ref", c).WithField("resolved-to", res).Debug("pre-cached Docker ref")
|
|
}
|
|
|
|
select {
|
|
case <-t.C:
|
|
case <-ctx.Done():
|
|
log.Debug("context cancelled - shutting down Docker ref pre-caching")
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// Resolve aims to resolve a ref using its own cache first and asks the underlying resolver otherwise
|
|
func (pr *PrecachingRefResolver) Resolve(ctx context.Context, ref string, opts ...DockerRefResolverOption) (res string, err error) {
|
|
span, ctx := opentracing.StartSpanFromContext(ctx, "PrecachingRefResolver.Resolve")
|
|
defer tracing.FinishSpan(span, &err)
|
|
|
|
pr.mu.RLock()
|
|
defer pr.mu.RUnlock()
|
|
|
|
if pr.cache == nil {
|
|
return pr.Resolver.Resolve(ctx, ref, opts...)
|
|
}
|
|
|
|
res, ok := pr.cache[ref]
|
|
if !ok {
|
|
return pr.Resolver.Resolve(ctx, ref, opts...)
|
|
}
|
|
|
|
return res, nil
|
|
}
|
|
|
|
type MockRefResolver map[string]string
|
|
|
|
func (m MockRefResolver) Resolve(ctx context.Context, ref string, opts ...DockerRefResolverOption) (res string, err error) {
|
|
res, ok := m[ref]
|
|
if !ok {
|
|
return "", ErrNotFound
|
|
}
|
|
return res, nil
|
|
}
|