From 095d2a29a3840815704c9c8f3c29fc755e36a40b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Gronowski?= Date: Wed, 20 Sep 2023 11:47:39 +0200 Subject: [PATCH] distribution: Add Tags MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a function to return tags for the given repository reference. This is needed to implement the `pull -a` (pull all tags) for containerd which doesn't directly use distribution, but we need to somehow make an API call to the registry to obtain the available tags. Signed-off-by: Paweł Gronowski --- distribution/pull.go | 163 +++++++++++++++++++++++++--------------- distribution/pull_v2.go | 13 +--- 2 files changed, 102 insertions(+), 74 deletions(-) diff --git a/distribution/pull.go b/distribution/pull.go index 32910b9694..0131824e60 100644 --- a/distribution/pull.go +++ b/distribution/pull.go @@ -9,6 +9,7 @@ import ( "github.com/docker/docker/api" "github.com/docker/docker/api/types/events" refstore "github.com/docker/docker/reference" + "github.com/docker/docker/registry" "github.com/opencontainers/go-digest" "github.com/pkg/errors" ) @@ -16,73 +17,33 @@ import ( // Pull initiates a pull operation. image is the repository name to pull, and // tag may be either empty, or indicate a specific tag to pull. func Pull(ctx context.Context, ref reference.Named, config *ImagePullConfig, local ContentStore) error { - // Resolve the Repository name from fqn to RepositoryInfo - repoInfo, err := config.RegistryService.ResolveRepository(ref) - if err != nil { - return err - } - - // makes sure name is not `scratch` - if err := validateRepoName(repoInfo.Name); err != nil { - return err - } - - endpoints, err := config.RegistryService.LookupPullEndpoints(reference.Domain(repoInfo.Name)) - if err != nil { - return err - } - - var ( - lastErr error - - // confirmedTLSRegistries is a map indicating which registries - // are known to be using TLS. There should never be a plaintext - // retry for any of these. - confirmedTLSRegistries = make(map[string]struct{}) - ) - for _, endpoint := range endpoints { - if endpoint.URL.Scheme != "https" { - if _, confirmedTLS := confirmedTLSRegistries[endpoint.URL.Host]; confirmedTLS { - log.G(ctx).Debugf("Skipping non-TLS endpoint %s for host/port that appears to use TLS", endpoint.URL) - continue - } - } - + repoInfo, err := pullEndpoints(ctx, config.RegistryService, ref, func(ctx context.Context, repoInfo registry.RepositoryInfo, endpoint registry.APIEndpoint) error { log.G(ctx).Debugf("Trying to pull %s from %s", reference.FamiliarName(repoInfo.Name), endpoint.URL) + puller := newPuller(endpoint, &repoInfo, config, local) + return puller.pull(ctx, ref) + }) - if err := newPuller(endpoint, repoInfo, config, local).pull(ctx, ref); err != nil { - // Was this pull cancelled? If so, don't try to fall - // back. - fallback := false - select { - case <-ctx.Done(): - default: - if fallbackErr, ok := err.(fallbackError); ok { - fallback = true - if fallbackErr.transportOK && endpoint.URL.Scheme == "https" { - confirmedTLSRegistries[endpoint.URL.Host] = struct{}{} - } - err = fallbackErr.err - } - } - if fallback { - lastErr = err - log.G(ctx).Infof("Attempting next endpoint for pull after error: %v", err) - continue - } - log.G(ctx).Errorf("Not continuing with pull after error: %v", err) - return translatePullError(err, ref) + if err == nil { + config.ImageEventLogger(reference.FamiliarString(ref), reference.FamiliarName(repoInfo.Name), events.ActionPull) + } + + return err +} + +// Tags returns available tags for the given image in the remote repository. +func Tags(ctx context.Context, ref reference.Named, config *Config) ([]string, error) { + var tags []string + _, err := pullEndpoints(ctx, config.RegistryService, ref, func(ctx context.Context, repoInfo registry.RepositoryInfo, endpoint registry.APIEndpoint) error { + repo, err := newRepository(ctx, &repoInfo, endpoint, config.MetaHeaders, config.AuthConfig, "pull") + if err != nil { + return err } - config.ImageEventLogger(reference.FamiliarString(ref), reference.FamiliarName(repoInfo.Name), events.ActionPull) - return nil - } + tags, err = repo.Tags(ctx).All(ctx) + return err + }) - if lastErr == nil { - lastErr = fmt.Errorf("no endpoints found for %s", reference.FamiliarString(ref)) - } - - return translatePullError(lastErr, ref) + return tags, err } // validateRepoName validates the name of a repository. @@ -111,3 +72,81 @@ func addDigestReference(store refstore.Store, ref reference.Named, dgst digest.D return store.AddDigest(dgstRef, id, true) } + +func pullEndpoints(ctx context.Context, registryService RegistryResolver, ref reference.Named, + f func(context.Context, registry.RepositoryInfo, registry.APIEndpoint) error, +) (*registry.RepositoryInfo, error) { + // Resolve the Repository name from fqn to RepositoryInfo + repoInfo, err := registryService.ResolveRepository(ref) + if err != nil { + return nil, err + } + + // makes sure name is not `scratch` + if err := validateRepoName(repoInfo.Name); err != nil { + return repoInfo, err + } + + endpoints, err := registryService.LookupPullEndpoints(reference.Domain(repoInfo.Name)) + if err != nil { + return repoInfo, err + } + + var ( + lastErr error + + // confirmedTLSRegistries is a map indicating which registries + // are known to be using TLS. There should never be a plaintext + // retry for any of these. + confirmedTLSRegistries = make(map[string]struct{}) + ) + for _, endpoint := range endpoints { + if endpoint.URL.Scheme != "https" { + if _, confirmedTLS := confirmedTLSRegistries[endpoint.URL.Host]; confirmedTLS { + log.G(ctx).Debugf("Skipping non-TLS endpoint %s for host/port that appears to use TLS", endpoint.URL) + continue + } + } + + log.G(ctx).Debugf("Trying to pull %s from %s", reference.FamiliarName(repoInfo.Name), endpoint.URL) + + if err := f(ctx, *repoInfo, endpoint); err != nil { + if _, ok := err.(fallbackError); !ok && continueOnError(err, endpoint.Mirror) { + err = fallbackError{ + err: err, + transportOK: true, + } + } + + // Was this pull cancelled? If so, don't try to fall + // back. + fallback := false + select { + case <-ctx.Done(): + default: + if fallbackErr, ok := err.(fallbackError); ok { + fallback = true + if fallbackErr.transportOK && endpoint.URL.Scheme == "https" { + confirmedTLSRegistries[endpoint.URL.Host] = struct{}{} + } + err = fallbackErr.err + } + } + if fallback { + lastErr = err + log.G(ctx).Infof("Attempting next endpoint for pull after error: %v", err) + continue + } + log.G(ctx).Errorf("Not continuing with pull after error: %v", err) + return repoInfo, translatePullError(err, ref) + } + + return repoInfo, nil + } + + if lastErr == nil { + lastErr = fmt.Errorf("no endpoints found for %s", reference.FamiliarString(ref)) + } + + return repoInfo, translatePullError(lastErr, ref) +} diff --git a/distribution/pull_v2.go b/distribution/pull_v2.go index e7e1abda7b..da3d8a8edf 100644 --- a/distribution/pull_v2.go +++ b/distribution/pull_v2.go @@ -86,18 +86,7 @@ func (p *puller) pull(ctx context.Context, ref reference.Named) (err error) { return err } - if err = p.pullRepository(ctx, ref); err != nil { - if _, ok := err.(fallbackError); ok { - return err - } - if continueOnError(err, p.endpoint.Mirror) { - return fallbackError{ - err: err, - transportOK: true, - } - } - } - return err + return p.pullRepository(ctx, ref) } func (p *puller) pullRepository(ctx context.Context, ref reference.Named) (err error) {