Merge pull request #47023 from thaJeztah/fix_distribution_mirrors
api: fix "GET /distribution" endpoint ignoring mirrors
diff --git a/api/server/router/distribution/backend.go b/api/server/router/distribution/backend.go
index c28f548..1c58238 100644
--- a/api/server/router/distribution/backend.go
+++ b/api/server/router/distribution/backend.go
@@ -11,5 +11,5 @@
// Backend is all the methods that need to be implemented
// to provide image specific functionality.
type Backend interface {
- GetRepository(context.Context, reference.Named, *registry.AuthConfig) (distribution.Repository, error)
+ GetRepositories(context.Context, reference.Named, *registry.AuthConfig) ([]distribution.Repository, error)
}
diff --git a/api/server/router/distribution/distribution_routes.go b/api/server/router/distribution/distribution_routes.go
index 66dd053..9fb0f47 100644
--- a/api/server/router/distribution/distribution_routes.go
+++ b/api/server/router/distribution/distribution_routes.go
@@ -6,6 +6,7 @@
"net/http"
"github.com/distribution/reference"
+ "github.com/docker/distribution"
"github.com/docker/distribution/manifest/manifestlist"
"github.com/docker/distribution/manifest/schema1"
"github.com/docker/distribution/manifest/schema2"
@@ -42,24 +43,50 @@
// For a search it is not an error if no auth was given. Ignore invalid
// AuthConfig to increase compatibility with the existing API.
authConfig, _ := registry.DecodeAuthConfig(r.Header.Get(registry.AuthHeader))
- distrepo, err := s.backend.GetRepository(ctx, namedRef, authConfig)
+ repos, err := s.backend.GetRepositories(ctx, namedRef, authConfig)
if err != nil {
return err
}
- blobsrvc := distrepo.Blobs(ctx)
+ // Fetch the manifest; if a mirror is configured, try the mirror first,
+ // but continue with upstream on failure.
+ //
+ // FIXME(thaJeztah): construct "repositories" on-demand;
+ // GetRepositories() will attempt to connect to all endpoints (registries),
+ // but we may only need the first one if it contains the manifest we're
+ // looking for, or if the configured mirror is a pull-through mirror.
+ //
+ // Logic for this could be implemented similar to "distribution.Pull()",
+ // which uses the "pullEndpoints" utility to iterate over the list
+ // of endpoints;
+ //
+ // - https://github.com/moby/moby/blob/12c7411b6b7314bef130cd59f1c7384a7db06d0b/distribution/pull.go#L17-L31
+ // - https://github.com/moby/moby/blob/12c7411b6b7314bef130cd59f1c7384a7db06d0b/distribution/pull.go#L76-L152
+ var lastErr error
+ for _, repo := range repos {
+ distributionInspect, err := s.fetchManifest(ctx, repo, namedRef)
+ if err != nil {
+ lastErr = err
+ continue
+ }
+ return httputils.WriteJSON(w, http.StatusOK, distributionInspect)
+ }
+ return lastErr
+}
+
+func (s *distributionRouter) fetchManifest(ctx context.Context, distrepo distribution.Repository, namedRef reference.Named) (registry.DistributionInspect, error) {
var distributionInspect registry.DistributionInspect
if canonicalRef, ok := namedRef.(reference.Canonical); !ok {
namedRef = reference.TagNameOnly(namedRef)
taggedRef, ok := namedRef.(reference.NamedTagged)
if !ok {
- return errdefs.InvalidParameter(errors.Errorf("image reference not tagged: %s", image))
+ return registry.DistributionInspect{}, errdefs.InvalidParameter(errors.Errorf("image reference not tagged: %s", namedRef))
}
descriptor, err := distrepo.Tags(ctx).Get(ctx, taggedRef.Tag())
if err != nil {
- return err
+ return registry.DistributionInspect{}, err
}
distributionInspect.Descriptor = ocispec.Descriptor{
MediaType: descriptor.MediaType,
@@ -76,7 +103,7 @@
// we have a digest, so we can retrieve the manifest
mnfstsrvc, err := distrepo.Manifests(ctx)
if err != nil {
- return err
+ return registry.DistributionInspect{}, err
}
mnfst, err := mnfstsrvc.Get(ctx, distributionInspect.Descriptor.Digest)
if err != nil {
@@ -88,14 +115,14 @@
reference.ErrNameEmpty,
reference.ErrNameTooLong,
reference.ErrNameNotCanonical:
- return errdefs.InvalidParameter(err)
+ return registry.DistributionInspect{}, errdefs.InvalidParameter(err)
}
- return err
+ return registry.DistributionInspect{}, err
}
mediaType, payload, err := mnfst.Payload()
if err != nil {
- return err
+ return registry.DistributionInspect{}, err
}
// update MediaType because registry might return something incorrect
distributionInspect.Descriptor.MediaType = mediaType
@@ -116,7 +143,8 @@
})
}
case *schema2.DeserializedManifest:
- configJSON, err := blobsrvc.Get(ctx, mnfstObj.Config.Digest)
+ blobStore := distrepo.Blobs(ctx)
+ configJSON, err := blobStore.Get(ctx, mnfstObj.Config.Digest)
var platform ocispec.Platform
if err == nil {
err := json.Unmarshal(configJSON, &platform)
@@ -131,6 +159,5 @@
}
distributionInspect.Platforms = append(distributionInspect.Platforms, platform)
}
-
- return httputils.WriteJSON(w, http.StatusOK, distributionInspect)
+ return distributionInspect, nil
}
diff --git a/daemon/cluster/executor/backend.go b/daemon/cluster/executor/backend.go
index 4cd9a23..6026795 100644
--- a/daemon/cluster/executor/backend.go
+++ b/daemon/cluster/executor/backend.go
@@ -78,5 +78,6 @@
type ImageBackend interface {
PullImage(ctx context.Context, ref reference.Named, platform *ocispec.Platform, metaHeaders map[string][]string, authConfig *registry.AuthConfig, outStream io.Writer) error
GetRepository(context.Context, reference.Named, *registry.AuthConfig) (distribution.Repository, error)
+ GetRepositories(context.Context, reference.Named, *registry.AuthConfig) ([]distribution.Repository, error)
GetImage(ctx context.Context, refOrID string, options opts.GetImageOpts) (*image.Image, error)
}
diff --git a/daemon/daemon.go b/daemon/daemon.go
index 397d28e..73f7d0d 100644
--- a/daemon/daemon.go
+++ b/daemon/daemon.go
@@ -1595,3 +1595,19 @@
},
})
}
+
+// GetRepositories returns a list of repositories configured for the given
+// reference. Multiple repositories can be returned if the reference is for
+// the default (Docker Hub) registry and a mirror is configured, but it omits
+// registries that were not reachable (pinging the /v2/ endpoint failed).
+//
+// It returns an error if it was unable to reach any of the registries for
+// the given reference, or if the provided reference is invalid.
+func (i *imageBackend) GetRepositories(ctx context.Context, ref reference.Named, authConfig *registrytypes.AuthConfig) ([]dist.Repository, error) {
+ return distribution.GetRepositories(ctx, ref, &distribution.ImagePullConfig{
+ Config: distribution.Config{
+ AuthConfig: authConfig,
+ RegistryService: i.registryService,
+ },
+ })
+}
diff --git a/distribution/repository.go b/distribution/repository.go
index 5b83197..c5229f7 100644
--- a/distribution/repository.go
+++ b/distribution/repository.go
@@ -3,6 +3,7 @@
import (
"context"
+ "github.com/containerd/log"
"github.com/distribution/reference"
"github.com/docker/distribution"
"github.com/docker/docker/errdefs"
@@ -10,6 +11,25 @@
// GetRepository returns a repository from the registry.
func GetRepository(ctx context.Context, ref reference.Named, config *ImagePullConfig) (repository distribution.Repository, lastError error) {
+ repos, err := getRepositories(ctx, ref, config, true)
+ if len(repos) == 0 {
+ return nil, err
+ }
+ return repos[0], nil
+}
+
+// GetRepositories returns a list of repositories configured for the given
+// reference. Multiple repositories can be returned if the reference is for
+// the default (Docker Hub) registry and a mirror is configured, but it omits
+// registries that were not reachable (pinging the /v2/ endpoint failed).
+//
+// It returns an error if it was unable to reach any of the registries for
+// the given reference, or if the provided reference is invalid.
+func GetRepositories(ctx context.Context, ref reference.Named, config *ImagePullConfig) ([]distribution.Repository, error) {
+ return getRepositories(ctx, ref, config, false)
+}
+
+func getRepositories(ctx context.Context, ref reference.Named, config *ImagePullConfig, firstOnly bool) ([]distribution.Repository, error) {
repoInfo, err := config.RegistryService.ResolveRepository(ref)
if err != nil {
return nil, errdefs.InvalidParameter(err)
@@ -24,11 +44,24 @@
return nil, err
}
+ var (
+ repositories []distribution.Repository
+ lastError error
+ )
for _, endpoint := range endpoints {
- repository, lastError = newRepository(ctx, repoInfo, endpoint, nil, config.AuthConfig, "pull")
- if lastError == nil {
- break
+ repo, err := newRepository(ctx, repoInfo, endpoint, nil, config.AuthConfig, "pull")
+ if err != nil {
+ log.G(ctx).WithFields(log.Fields{"endpoint": endpoint.URL.String(), "error": err}).Info("endpoint")
+ lastError = err
+ continue
+ }
+ repositories = append(repositories, repo)
+ if firstOnly {
+ return repositories, nil
}
}
- return repository, lastError
+ if len(repositories) == 0 {
+ return nil, lastError
+ }
+ return repositories, nil
}