diff --git a/api/server/router/distribution/backend.go b/api/server/router/distribution/backend.go new file mode 100644 index 0000000000..fc3a80e59f --- /dev/null +++ b/api/server/router/distribution/backend.go @@ -0,0 +1,14 @@ +package distribution + +import ( + "github.com/docker/distribution" + "github.com/docker/distribution/reference" + "github.com/docker/docker/api/types" + "golang.org/x/net/context" +) + +// Backend is all the methods that need to be implemented +// to provide image specific functionality. +type Backend interface { + GetRepository(context.Context, reference.Named, *types.AuthConfig) (distribution.Repository, bool, error) +} diff --git a/api/server/router/distribution/distribution.go b/api/server/router/distribution/distribution.go new file mode 100644 index 0000000000..c1fb7bc1e9 --- /dev/null +++ b/api/server/router/distribution/distribution.go @@ -0,0 +1,31 @@ +package distribution + +import "github.com/docker/docker/api/server/router" + +// distributionRouter is a router to talk with the registry +type distributionRouter struct { + backend Backend + routes []router.Route +} + +// NewRouter initializes a new distribution router +func NewRouter(backend Backend) router.Router { + r := &distributionRouter{ + backend: backend, + } + r.initRoutes() + return r +} + +// Routes returns the available routes +func (r *distributionRouter) Routes() []router.Route { + return r.routes +} + +// initRoutes initializes the routes in the distribution router +func (r *distributionRouter) initRoutes() { + r.routes = []router.Route{ + // GET + router.NewGetRoute("/distribution/{name:.*}/json", r.getDistributionInfo), + } +} diff --git a/api/server/router/distribution/distribution_routes.go b/api/server/router/distribution/distribution_routes.go new file mode 100644 index 0000000000..3a0e13e3ac --- /dev/null +++ b/api/server/router/distribution/distribution_routes.go @@ -0,0 +1,114 @@ +package distribution + +import ( + "encoding/base64" + "encoding/json" + "net/http" + "strings" + + "github.com/docker/distribution/manifest/manifestlist" + "github.com/docker/distribution/manifest/schema1" + "github.com/docker/distribution/manifest/schema2" + "github.com/docker/distribution/reference" + "github.com/docker/docker/api/server/httputils" + "github.com/docker/docker/api/types" + registrytypes "github.com/docker/docker/api/types/registry" + "github.com/pkg/errors" + "golang.org/x/net/context" +) + +func (s *distributionRouter) getDistributionInfo(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + if err := httputils.ParseForm(r); err != nil { + return err + } + + w.Header().Set("Content-Type", "application/json") + + var ( + config = &types.AuthConfig{} + authEncoded = r.Header.Get("X-Registry-Auth") + distributionInspect registrytypes.DistributionInspect + ) + + if authEncoded != "" { + authJSON := base64.NewDecoder(base64.URLEncoding, strings.NewReader(authEncoded)) + if err := json.NewDecoder(authJSON).Decode(&config); err != nil { + // for a search it is not an error if no auth was given + // to increase compatibility with the existing api it is defaulting to be empty + config = &types.AuthConfig{} + } + } + + image := vars["name"] + + ref, err := reference.ParseAnyReference(image) + if err != nil { + return err + } + namedRef, ok := ref.(reference.Named) + if !ok { + if _, ok := ref.(reference.Digested); ok { + // full image ID + return errors.Errorf("no manifest found for full image ID") + } + return errors.Errorf("unknown image reference format: %s", image) + } + + distrepo, _, err := s.backend.GetRepository(ctx, namedRef, config) + if err != nil { + return err + } + + if canonicalRef, ok := namedRef.(reference.Canonical); !ok { + namedRef = reference.TagNameOnly(namedRef) + + taggedRef, ok := namedRef.(reference.NamedTagged) + if !ok { + return errors.Errorf("image reference not tagged: %s", image) + } + + dscrptr, err := distrepo.Tags(ctx).Get(ctx, taggedRef.Tag()) + if err != nil { + return err + } + distributionInspect.Digest = dscrptr.Digest + } else { + distributionInspect.Digest = canonicalRef.Digest() + } + // at this point, we have a digest, so we can retrieve the manifest + + mnfstsrvc, err := distrepo.Manifests(ctx) + if err != nil { + return err + } + mnfst, err := mnfstsrvc.Get(ctx, distributionInspect.Digest) + if err != nil { + return err + } + + // retrieve platform information depending on the type of manifest + switch mnfstObj := mnfst.(type) { + case *manifestlist.DeserializedManifestList: + for _, m := range mnfstObj.Manifests { + distributionInspect.Platforms = append(distributionInspect.Platforms, m.Platform) + } + case *schema2.DeserializedManifest: + blobsrvc := distrepo.Blobs(ctx) + configJSON, err := blobsrvc.Get(ctx, mnfstObj.Config.Digest) + var platform manifestlist.PlatformSpec + if err == nil { + err := json.Unmarshal(configJSON, &platform) + if err == nil { + distributionInspect.Platforms = append(distributionInspect.Platforms, platform) + } + } + case *schema1.SignedManifest: + platform := manifestlist.PlatformSpec{ + Architecture: mnfstObj.Architecture, + OS: "linux", + } + distributionInspect.Platforms = append(distributionInspect.Platforms, platform) + } + + return httputils.WriteJSON(w, http.StatusOK, distributionInspect) +} diff --git a/api/swagger.yaml b/api/swagger.yaml index 411c8c6b30..69b1ec9a9f 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -8274,3 +8274,60 @@ paths: format: "int64" required: true tags: ["Secret"] + /distribution/{name}/json: + get: + summary: "Get image information from the registry" + description: "Return image digest and platform information by contacting the registry." + operationId: "DistributionInspect" + produces: + - "application/json" + responses: + 200: + description: "digest and platform information" + schema: + type: "object" + x-go-name: DistributionInspect + required: [Digest, ID, Platforms] + properties: + Digest: + type: "string" + x-nullable: false + Platforms: + type: "array" + items: + type: "object" + properties: + Architecture: + type: "string" + OS: + type: "string" + OSVersion: + type: "string" + OSFeatures: + type: "array" + items: + type: "string" + Variant: + type: "string" + Features: + type: "array" + items: + type: "string" + 401: + description: "Failed authentication or no image found" + schema: + $ref: "#/definitions/ErrorResponse" + examples: + application/json: + message: "No such image: someimage (tag: latest)" + 500: + description: "Server error" + schema: + $ref: "#/definitions/ErrorResponse" + parameters: + - name: "name" + in: "path" + description: "Image name or id" + type: "string" + required: true + tags: ["Distribution"] diff --git a/api/types/registry/registry.go b/api/types/registry/registry.go index 28fafab901..911dbc5838 100644 --- a/api/types/registry/registry.go +++ b/api/types/registry/registry.go @@ -3,6 +3,9 @@ package registry import ( "encoding/json" "net" + + "github.com/docker/distribution/manifest/manifestlist" + digest "github.com/opencontainers/go-digest" ) // ServiceConfig stores daemon registry services configuration. @@ -102,3 +105,13 @@ type SearchResults struct { // Results is a slice containing the actual results for the search Results []SearchResult `json:"results"` } + +// DistributionInspect describes the result obtained from contacting the +// registry to retrieve image metadata +type DistributionInspect struct { + // Digest is the content addressable digest for the image on the registry + Digest digest.Digest + // Platforms contains the list of platforms supported by the image, + // obtained by parsing the manifest + Platforms []manifestlist.PlatformSpec +} diff --git a/cmd/dockerd/daemon.go b/cmd/dockerd/daemon.go index 80638a3ee4..2e3f11f84e 100644 --- a/cmd/dockerd/daemon.go +++ b/cmd/dockerd/daemon.go @@ -20,6 +20,7 @@ import ( "github.com/docker/docker/api/server/router/build" checkpointrouter "github.com/docker/docker/api/server/router/checkpoint" "github.com/docker/docker/api/server/router/container" + distributionrouter "github.com/docker/docker/api/server/router/distribution" "github.com/docker/docker/api/server/router/image" "github.com/docker/docker/api/server/router/network" pluginrouter "github.com/docker/docker/api/server/router/plugin" @@ -487,6 +488,7 @@ func initRouter(s *apiserver.Server, d *daemon.Daemon, c *cluster.Cluster) { build.NewRouter(buildbackend.NewBackend(d, d), d), swarmrouter.NewRouter(c), pluginrouter.NewRouter(d.PluginManager()), + distributionrouter.NewRouter(d), } if d.NetworkControllerEnabled() {