From fae2fb0e646f3eec40bedf558c1e57ba60b39058 Mon Sep 17 00:00:00 2001 From: Nishant Totla Date: Mon, 1 May 2017 16:14:59 -0700 Subject: [PATCH 1/4] Change GetRepository to take Named arguments Signed-off-by: Nishant Totla Upstream-commit: e842c653a0de4fa7073d2402f4817de308a82bd0 Component: engine --- components/engine/daemon/cluster/executor/backend.go | 2 +- components/engine/daemon/image_pull.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/components/engine/daemon/cluster/executor/backend.go b/components/engine/daemon/cluster/executor/backend.go index 13b643c4b5..1e1d83655d 100644 --- a/components/engine/daemon/cluster/executor/backend.go +++ b/components/engine/daemon/cluster/executor/backend.go @@ -55,7 +55,7 @@ type Backend interface { UnsubscribeFromEvents(listener chan interface{}) UpdateAttachment(string, string, string, *network.NetworkingConfig) error WaitForDetachment(context.Context, string, string, string, string) error - GetRepository(context.Context, reference.NamedTagged, *types.AuthConfig) (distribution.Repository, bool, error) + GetRepository(context.Context, reference.Named, *types.AuthConfig) (distribution.Repository, bool, error) LookupImage(name string) (*types.ImageInspect, error) PluginManager() *plugin.Manager PluginGetter() *plugin.Store diff --git a/components/engine/daemon/image_pull.go b/components/engine/daemon/image_pull.go index 5cbd7ba42a..e0ac92ce44 100644 --- a/components/engine/daemon/image_pull.go +++ b/components/engine/daemon/image_pull.go @@ -111,7 +111,7 @@ func (daemon *Daemon) pullImageWithReference(ctx context.Context, ref reference. } // GetRepository returns a repository from the registry. -func (daemon *Daemon) GetRepository(ctx context.Context, ref reference.NamedTagged, authConfig *types.AuthConfig) (dist.Repository, bool, error) { +func (daemon *Daemon) GetRepository(ctx context.Context, ref reference.Named, authConfig *types.AuthConfig) (dist.Repository, bool, error) { // get repository info repoInfo, err := daemon.RegistryService.ResolveRepository(ref) if err != nil { From 9657e321b14a2b17ec74475fb2e1fd1082db8666 Mon Sep 17 00:00:00 2001 From: Nishant Totla Date: Mon, 1 May 2017 16:17:35 -0700 Subject: [PATCH 2/4] Adding /distribution/{name}/json endpoint to contact registry Signed-off-by: Nishant Totla Upstream-commit: 41b27de41b8b0e034bfe2a3c73b2bc6ff98c7ceb Component: engine --- .../api/server/router/distribution/backend.go | 14 +++ .../router/distribution/distribution.go | 31 +++++ .../distribution/distribution_routes.go | 114 ++++++++++++++++++ components/engine/api/swagger.yaml | 57 +++++++++ .../engine/api/types/registry/registry.go | 13 ++ components/engine/cmd/dockerd/daemon.go | 2 + 6 files changed, 231 insertions(+) create mode 100644 components/engine/api/server/router/distribution/backend.go create mode 100644 components/engine/api/server/router/distribution/distribution.go create mode 100644 components/engine/api/server/router/distribution/distribution_routes.go diff --git a/components/engine/api/server/router/distribution/backend.go b/components/engine/api/server/router/distribution/backend.go new file mode 100644 index 0000000000..fc3a80e59f --- /dev/null +++ b/components/engine/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/components/engine/api/server/router/distribution/distribution.go b/components/engine/api/server/router/distribution/distribution.go new file mode 100644 index 0000000000..c1fb7bc1e9 --- /dev/null +++ b/components/engine/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/components/engine/api/server/router/distribution/distribution_routes.go b/components/engine/api/server/router/distribution/distribution_routes.go new file mode 100644 index 0000000000..3a0e13e3ac --- /dev/null +++ b/components/engine/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/components/engine/api/swagger.yaml b/components/engine/api/swagger.yaml index 411c8c6b30..69b1ec9a9f 100644 --- a/components/engine/api/swagger.yaml +++ b/components/engine/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/components/engine/api/types/registry/registry.go b/components/engine/api/types/registry/registry.go index 28fafab901..911dbc5838 100644 --- a/components/engine/api/types/registry/registry.go +++ b/components/engine/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/components/engine/cmd/dockerd/daemon.go b/components/engine/cmd/dockerd/daemon.go index 80638a3ee4..2e3f11f84e 100644 --- a/components/engine/cmd/dockerd/daemon.go +++ b/components/engine/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() { From 34899cbe9ca281ff4da40d7ee504045ffca4e70d Mon Sep 17 00:00:00 2001 From: Nishant Totla Date: Mon, 8 May 2017 16:29:31 -0700 Subject: [PATCH 3/4] /distribution/{name}/json returns full Descriptor object Signed-off-by: Nishant Totla Upstream-commit: 12e232ee35b56cb2954c48d83ec9febb40cdeb90 Component: engine --- .../distribution/distribution_routes.go | 24 ++++++++++++++----- components/engine/api/swagger.yaml | 21 ++++++++++++---- .../engine/api/types/registry/registry.go | 7 +++--- 3 files changed, 38 insertions(+), 14 deletions(-) diff --git a/components/engine/api/server/router/distribution/distribution_routes.go b/components/engine/api/server/router/distribution/distribution_routes.go index 3a0e13e3ac..fd57114495 100644 --- a/components/engine/api/server/router/distribution/distribution_routes.go +++ b/components/engine/api/server/router/distribution/distribution_routes.go @@ -58,6 +58,7 @@ func (s *distributionRouter) getDistributionInfo(ctx context.Context, w http.Res if err != nil { return err } + blobsrvc := distrepo.Blobs(ctx) if canonicalRef, ok := namedRef.(reference.Canonical); !ok { namedRef = reference.TagNameOnly(namedRef) @@ -67,25 +68,37 @@ func (s *distributionRouter) getDistributionInfo(ctx context.Context, w http.Res return errors.Errorf("image reference not tagged: %s", image) } - dscrptr, err := distrepo.Tags(ctx).Get(ctx, taggedRef.Tag()) + distributionInspect.Descriptor, err = distrepo.Tags(ctx).Get(ctx, taggedRef.Tag()) if err != nil { return err } - distributionInspect.Digest = dscrptr.Digest } else { - distributionInspect.Digest = canonicalRef.Digest() + // TODO(nishanttotla): Once manifests can be looked up as a blob, the + // descriptor should be set using blobsrvc.Stat(ctx, canonicalRef.Digest()) + // instead of having to manually fill in the fields + distributionInspect.Descriptor.Digest = canonicalRef.Digest() } - // at this point, we have a digest, so we can retrieve the manifest + // 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) + mnfst, err := mnfstsrvc.Get(ctx, distributionInspect.Descriptor.Digest) if err != nil { return err } + mediaType, payload, err := mnfst.Payload() + if err != nil { + return err + } + // update MediaType because registry might return something incorrect + distributionInspect.Descriptor.MediaType = mediaType + if distributionInspect.Descriptor.Size == 0 { + distributionInspect.Descriptor.Size = int64(len(payload)) + } + // retrieve platform information depending on the type of manifest switch mnfstObj := mnfst.(type) { case *manifestlist.DeserializedManifestList: @@ -93,7 +106,6 @@ func (s *distributionRouter) getDistributionInfo(ctx context.Context, w http.Res 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 { diff --git a/components/engine/api/swagger.yaml b/components/engine/api/swagger.yaml index 69b1ec9a9f..7d8adeaff4 100644 --- a/components/engine/api/swagger.yaml +++ b/components/engine/api/swagger.yaml @@ -8283,15 +8283,26 @@ paths: - "application/json" responses: 200: - description: "digest and platform information" + description: "descriptor and platform information" schema: type: "object" x-go-name: DistributionInspect - required: [Digest, ID, Platforms] + required: [Descriptor, Platforms] properties: - Digest: - type: "string" - x-nullable: false + Descriptor: + type: "object" + properties: + MediaType: + type: "string" + Size: + type: "integer" + format: "int64" + Digest: + type: "string" + URLs: + type: "array" + items: + type: "string" Platforms: type: "array" items: diff --git a/components/engine/api/types/registry/registry.go b/components/engine/api/types/registry/registry.go index 911dbc5838..94f594ae69 100644 --- a/components/engine/api/types/registry/registry.go +++ b/components/engine/api/types/registry/registry.go @@ -4,8 +4,8 @@ import ( "encoding/json" "net" + "github.com/docker/distribution" "github.com/docker/distribution/manifest/manifestlist" - digest "github.com/opencontainers/go-digest" ) // ServiceConfig stores daemon registry services configuration. @@ -109,8 +109,9 @@ type SearchResults struct { // 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 + // Descriptor contains information about the manifest, including + // the content addressable digest + Descriptor distribution.Descriptor // Platforms contains the list of platforms supported by the image, // obtained by parsing the manifest Platforms []manifestlist.PlatformSpec From 817f36020aa281f81d0a27eeff14ac3522096b36 Mon Sep 17 00:00:00 2001 From: Nishant Totla Date: Wed, 10 May 2017 11:00:29 -0700 Subject: [PATCH 4/4] Adding example to /distribution/{name}/json endpoint swagger spec Signed-off-by: Nishant Totla Upstream-commit: 4a812040eb14a8e2a6aeeb7686d19e8ef81f29cd Component: engine --- components/engine/api/swagger.yaml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/components/engine/api/swagger.yaml b/components/engine/api/swagger.yaml index 7d8adeaff4..6cdc37997c 100644 --- a/components/engine/api/swagger.yaml +++ b/components/engine/api/swagger.yaml @@ -8291,6 +8291,7 @@ paths: properties: Descriptor: type: "object" + description: "A descriptor struct containing digest, media type, and size" properties: MediaType: type: "string" @@ -8305,6 +8306,7 @@ paths: type: "string" Platforms: type: "array" + description: "An array containing all platforms supported by the image" items: type: "object" properties: @@ -8324,6 +8326,23 @@ paths: type: "array" items: type: "string" + examples: + application/json: + Descriptor: + MediaType: "application/vnd.docker.distribution.manifest.v2+json" + Digest: "sha256:c0537ff6a5218ef531ece93d4984efc99bbf3f7497c0a7726c88e2bb7584dc96" + Size: 3987495 + URLs: + - "" + Platforms: + - Architecture: "amd64" + OS: "linux" + OSVersion: "" + OSFeatures: + - "" + Variant: "" + Features: + - "" 401: description: "Failed authentication or no image found" schema: