package registryclient import ( "context" "fmt" "net/http" "strings" "github.com/distribution/reference" manifesttypes "github.com/docker/cli/cli/manifest/types" "github.com/docker/distribution" distributionclient "github.com/docker/distribution/registry/client" registrytypes "github.com/moby/moby/api/types/registry" "github.com/opencontainers/go-digest" "github.com/sirupsen/logrus" ) // RegistryClient is a client used to communicate with a Docker distribution // registry type RegistryClient interface { GetManifest(ctx context.Context, ref reference.Named) (manifesttypes.ImageManifest, error) GetManifestList(ctx context.Context, ref reference.Named) ([]manifesttypes.ImageManifest, error) MountBlob(ctx context.Context, source reference.Canonical, target reference.Named) error PutManifest(ctx context.Context, ref reference.Named, manifest distribution.Manifest) (digest.Digest, error) } // NewRegistryClient returns a new RegistryClient with a resolver func NewRegistryClient(resolver AuthConfigResolver, userAgent string, insecure bool) RegistryClient { return &client{ authConfigResolver: resolver, insecureRegistry: insecure, userAgent: userAgent, } } // AuthConfigResolver returns Auth Configuration for an index type AuthConfigResolver func(ctx context.Context, hostName string) registrytypes.AuthConfig type client struct { authConfigResolver AuthConfigResolver insecureRegistry bool userAgent string } // ErrBlobCreated returned when a blob mount request was created type ErrBlobCreated struct { From reference.Named Target reference.Named } func (err ErrBlobCreated) Error() string { return fmt.Sprintf("blob mounted from: %v to: %v", err.From, err.Target) } // httpProtoError returned if attempting to use TLS with a non-TLS registry type httpProtoError struct { cause error } func (e httpProtoError) Error() string { return e.cause.Error() } var _ RegistryClient = &client{} // MountBlob into the registry, so it can be referenced by a manifest func (c *client) MountBlob(ctx context.Context, sourceRef reference.Canonical, targetRef reference.Named) error { repoEndpoint, err := newDefaultRepositoryEndpoint(targetRef, c.insecureRegistry) if err != nil { return err } repoEndpoint.actions = []string{"pull", "push"} repo, err := c.getRepositoryForReference(ctx, targetRef, repoEndpoint) if err != nil { return err } lu, err := repo.Blobs(ctx).Create(ctx, distributionclient.WithMountFrom(sourceRef)) switch err.(type) { case distribution.ErrBlobMounted: logrus.Debugf("mount of blob %s succeeded", sourceRef) return nil case nil: default: return fmt.Errorf("failed to mount blob %s to %s: %w", sourceRef, targetRef, err) } _ = lu.Cancel(ctx) logrus.Debugf("mount of blob %s created", sourceRef) return ErrBlobCreated{From: sourceRef, Target: targetRef} } // PutManifest sends the manifest to a registry and returns the new digest func (c *client) PutManifest(ctx context.Context, ref reference.Named, manifest distribution.Manifest) (digest.Digest, error) { repoEndpoint, err := newDefaultRepositoryEndpoint(ref, c.insecureRegistry) if err != nil { return "", err } repoEndpoint.actions = []string{"pull", "push"} repo, err := c.getRepositoryForReference(ctx, ref, repoEndpoint) if err != nil { return "", err } manifestService, err := repo.Manifests(ctx) if err != nil { return "", err } _, opts, err := getManifestOptionsFromReference(ref) if err != nil { return "", err } dgst, err := manifestService.Put(ctx, manifest, opts...) if err != nil { return dgst, fmt.Errorf("failed to put manifest %s: %w", ref, err) } return dgst, nil } func (c *client) getRepositoryForReference(ctx context.Context, ref reference.Named, repoEndpoint repositoryEndpoint) (distribution.Repository, error) { repoName, err := reference.WithName(repoEndpoint.repoName) if err != nil { return nil, fmt.Errorf("failed to parse repo name from %s: %w", ref, err) } httpTransport, err := c.getHTTPTransportForRepoEndpoint(ctx, repoEndpoint) if err != nil { if !strings.Contains(err.Error(), "server gave HTTP response to HTTPS client") { return nil, err } if !repoEndpoint.endpoint.TLSConfig.InsecureSkipVerify { return nil, httpProtoError{cause: err} } // --insecure was set; fall back to plain HTTP if url := repoEndpoint.endpoint.URL; url != nil && url.Scheme == "https" { url.Scheme = "http" httpTransport, err = c.getHTTPTransportForRepoEndpoint(ctx, repoEndpoint) if err != nil { return nil, err } } } return distributionclient.NewRepository(repoName, repoEndpoint.BaseURL(), httpTransport) } func (c *client) getHTTPTransportForRepoEndpoint(ctx context.Context, repoEndpoint repositoryEndpoint) (http.RoundTripper, error) { httpTransport, err := getHTTPTransport( c.authConfigResolver(ctx, repoEndpoint.indexInfo.Name), repoEndpoint.endpoint, repoEndpoint.repoName, c.userAgent, repoEndpoint.actions, ) if err != nil { return nil, fmt.Errorf("failed to configure transport: %w", err) } return httpTransport, nil } // GetManifest returns an ImageManifest for the reference func (c *client) GetManifest(ctx context.Context, ref reference.Named) (manifesttypes.ImageManifest, error) { var result manifesttypes.ImageManifest fetch := func(ctx context.Context, repo distribution.Repository, ref reference.Named) (bool, error) { var err error result, err = fetchManifest(ctx, repo, ref) return result.Ref != nil, err } err := c.iterateEndpoints(ctx, ref, fetch) return result, err } // GetManifestList returns a list of ImageManifest for the reference func (c *client) GetManifestList(ctx context.Context, ref reference.Named) ([]manifesttypes.ImageManifest, error) { result := []manifesttypes.ImageManifest{} fetch := func(ctx context.Context, repo distribution.Repository, ref reference.Named) (bool, error) { var err error result, err = fetchList(ctx, repo, ref) return len(result) > 0, err } err := c.iterateEndpoints(ctx, ref, fetch) return result, err } func getManifestOptionsFromReference(ref reference.Named) (digest.Digest, []distribution.ManifestServiceOption, error) { if tagged, isTagged := ref.(reference.NamedTagged); isTagged { tag := tagged.Tag() return "", []distribution.ManifestServiceOption{distribution.WithTag(tag)}, nil } if digested, isDigested := ref.(reference.Canonical); isDigested { return digested.Digest(), []distribution.ManifestServiceOption{}, nil } return "", nil, fmt.Errorf("%s no tag or digest", ref) }