From 2a16a2ff6a650db4165464d2e28de1428a069707 Mon Sep 17 00:00:00 2001 From: Stephen J Day Date: Wed, 10 Dec 2014 22:29:58 -0800 Subject: [PATCH 1/2] Pluralize route API paths During the specification period, it was suggested that pluralized object names are more idiomatic in APIs than singular. This changeset simply adopts that preference for the API going forward. The client has been updated to remain compatible. --- client/client.go | 8 ++++---- client/client_test.go | 16 ++++++++-------- routes.go | 8 ++++---- routes_test.go | 24 ++++++++++++------------ 4 files changed, 28 insertions(+), 28 deletions(-) diff --git a/client/client.go b/client/client.go index 0d3432097..8f31cb4e0 100644 --- a/client/client.go +++ b/client/client.go @@ -222,7 +222,7 @@ func (r *clientImpl) ListImageTags(name string) ([]string, error) { } func (r *clientImpl) BlobLength(name string, dgst digest.Digest) (int, error) { - response, err := http.Head(fmt.Sprintf("%s/v2/%s/blob/%s", r.Endpoint, name, dgst)) + response, err := http.Head(fmt.Sprintf("%s/v2/%s/blobs/%s", r.Endpoint, name, dgst)) if err != nil { return -1, err } @@ -255,7 +255,7 @@ func (r *clientImpl) BlobLength(name string, dgst digest.Digest) (int, error) { func (r *clientImpl) GetBlob(name string, dgst digest.Digest, byteOffset int) (io.ReadCloser, int, error) { getRequest, err := http.NewRequest("GET", - fmt.Sprintf("%s/v2/%s/blob/%s", r.Endpoint, name, dgst), nil) + fmt.Sprintf("%s/v2/%s/blobs/%s", r.Endpoint, name, dgst), nil) if err != nil { return nil, 0, err } @@ -294,7 +294,7 @@ func (r *clientImpl) GetBlob(name string, dgst digest.Digest, byteOffset int) (i func (r *clientImpl) InitiateBlobUpload(name string) (string, error) { postRequest, err := http.NewRequest("POST", - fmt.Sprintf("%s/v2/%s/blob/upload/", r.Endpoint, name), nil) + fmt.Sprintf("%s/v2/%s/blobs/uploads/", r.Endpoint, name), nil) if err != nil { return "", err } @@ -519,7 +519,7 @@ func (r *clientImpl) CancelBlobUpload(location string) error { // imageManifestURL is a helper method for returning the full url to an image // manifest func (r *clientImpl) imageManifestURL(name, tag string) string { - return fmt.Sprintf("%s/v2/%s/manifest/%s", r.Endpoint, name, tag) + return fmt.Sprintf("%s/v2/%s/manifests/%s", r.Endpoint, name, tag) } // parseRangeHeader parses out the offset and length from a returned Range diff --git a/client/client_test.go b/client/client_test.go index 979f1313a..f3082141e 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -41,7 +41,7 @@ func TestPush(t *testing.T) { // because we can't know which blob will get which location. // It's sort of okay because we're using unique digests, but this needs // to change at some point. - uploadLocations[i] = fmt.Sprintf("/v2/%s/blob/test-uuid", name) + uploadLocations[i] = fmt.Sprintf("/v2/%s/blobs/test-uuid", name) blobs[i] = storage.FSLayer{BlobSum: blob.digest} history[i] = storage.ManifestHistory{V1Compatibility: blob.digest.String()} } @@ -66,7 +66,7 @@ func TestPush(t *testing.T) { blobRequestResponseMappings[2*i] = testutil.RequestResponseMapping{ Request: testutil.Request{ Method: "POST", - Route: "/v2/" + name + "/blob/upload/", + Route: "/v2/" + name + "/blobs/uploads/", }, Response: testutil.Response{ StatusCode: http.StatusAccepted, @@ -94,7 +94,7 @@ func TestPush(t *testing.T) { handler := testutil.NewHandler(append(blobRequestResponseMappings, testutil.RequestResponseMapping{ Request: testutil.Request{ Method: "PUT", - Route: "/v2/" + name + "/manifest/" + tag, + Route: "/v2/" + name + "/manifests/" + tag, Body: manifest.Raw, }, Response: testutil.Response{ @@ -185,7 +185,7 @@ func TestPull(t *testing.T) { blobRequestResponseMappings[i] = testutil.RequestResponseMapping{ Request: testutil.Request{ Method: "GET", - Route: "/v2/" + name + "/blob/" + blob.digest.String(), + Route: "/v2/" + name + "/blobs/" + blob.digest.String(), }, Response: testutil.Response{ StatusCode: http.StatusOK, @@ -197,7 +197,7 @@ func TestPull(t *testing.T) { handler := testutil.NewHandler(append(blobRequestResponseMappings, testutil.RequestResponseMapping{ Request: testutil.Request{ Method: "GET", - Route: "/v2/" + name + "/manifest/" + tag, + Route: "/v2/" + name + "/manifests/" + tag, }, Response: testutil.Response{ StatusCode: http.StatusOK, @@ -292,7 +292,7 @@ func TestPullResume(t *testing.T) { layerRequestResponseMappings[2*i] = testutil.RequestResponseMapping{ Request: testutil.Request{ Method: "GET", - Route: "/v2/" + name + "/blob/" + blob.digest.String(), + Route: "/v2/" + name + "/blobs/" + blob.digest.String(), }, Response: testutil.Response{ StatusCode: http.StatusOK, @@ -305,7 +305,7 @@ func TestPullResume(t *testing.T) { layerRequestResponseMappings[2*i+1] = testutil.RequestResponseMapping{ Request: testutil.Request{ Method: "GET", - Route: "/v2/" + name + "/blob/" + blob.digest.String(), + Route: "/v2/" + name + "/blobs/" + blob.digest.String(), }, Response: testutil.Response{ StatusCode: http.StatusOK, @@ -318,7 +318,7 @@ func TestPullResume(t *testing.T) { layerRequestResponseMappings = append(layerRequestResponseMappings, testutil.RequestResponseMapping{ Request: testutil.Request{ Method: "GET", - Route: "/v2/" + name + "/manifest/" + tag, + Route: "/v2/" + name + "/manifests/" + tag, }, Response: testutil.Response{ StatusCode: http.StatusOK, diff --git a/routes.go b/routes.go index 4aa0097f6..440473e90 100644 --- a/routes.go +++ b/routes.go @@ -31,7 +31,7 @@ func v2APIRouter() *mux.Router { // PUT /v2//manifest/ Image Manifest Upload the image manifest identified by name and tag. // DELETE /v2//manifest/ Image Manifest Delete the image identified by name and tag. router. - Path("/v2/{name:" + common.RepositoryNameRegexp.String() + "}/manifest/{tag:" + common.TagNameRegexp.String() + "}"). + Path("/v2/{name:" + common.RepositoryNameRegexp.String() + "}/manifests/{tag:" + common.TagNameRegexp.String() + "}"). Name(routeNameImageManifest) // GET /v2//tags/list Tags Fetch the tags under the repository identified by name. @@ -41,19 +41,19 @@ func v2APIRouter() *mux.Router { // GET /v2//blob/ Layer Fetch the blob identified by digest. router. - Path("/v2/{name:" + common.RepositoryNameRegexp.String() + "}/blob/{digest:[a-zA-Z0-9-_+.]+:[a-zA-Z0-9-_+.=]+}"). + Path("/v2/{name:" + common.RepositoryNameRegexp.String() + "}/blobs/{digest:[a-zA-Z0-9-_+.]+:[a-zA-Z0-9-_+.=]+}"). Name(routeNameBlob) // POST /v2//blob/upload/ Layer Upload Initiate an upload of the layer identified by tarsum. router. - Path("/v2/{name:" + common.RepositoryNameRegexp.String() + "}/blob/upload/"). + Path("/v2/{name:" + common.RepositoryNameRegexp.String() + "}/blobs/uploads/"). Name(routeNameBlobUpload) // GET /v2//blob/upload/ Layer Upload Get the status of the upload identified by tarsum and uuid. // PUT /v2//blob/upload/ Layer Upload Upload all or a chunk of the upload identified by tarsum and uuid. // DELETE /v2//blob/upload/ Layer Upload Cancel the upload identified by layer and uuid router. - Path("/v2/{name:" + common.RepositoryNameRegexp.String() + "}/blob/upload/{uuid}"). + Path("/v2/{name:" + common.RepositoryNameRegexp.String() + "}/blobs/uploads/{uuid}"). Name(routeNameBlobUploadResume) return router diff --git a/routes_test.go b/routes_test.go index 9085d3029..8c5149432 100644 --- a/routes_test.go +++ b/routes_test.go @@ -48,7 +48,7 @@ func TestRouter(t *testing.T) { for _, testcase := range []routeTestCase{ { RouteName: routeNameImageManifest, - RequestURI: "/v2/foo/bar/manifest/tag", + RequestURI: "/v2/foo/bar/manifests/tag", Vars: map[string]string{ "name": "foo/bar", "tag": "tag", @@ -63,7 +63,7 @@ func TestRouter(t *testing.T) { }, { RouteName: routeNameBlob, - RequestURI: "/v2/foo/bar/blob/tarsum.dev+foo:abcdef0919234", + RequestURI: "/v2/foo/bar/blobs/tarsum.dev+foo:abcdef0919234", Vars: map[string]string{ "name": "foo/bar", "digest": "tarsum.dev+foo:abcdef0919234", @@ -71,7 +71,7 @@ func TestRouter(t *testing.T) { }, { RouteName: routeNameBlob, - RequestURI: "/v2/foo/bar/blob/sha256:abcdef0919234", + RequestURI: "/v2/foo/bar/blobs/sha256:abcdef0919234", Vars: map[string]string{ "name": "foo/bar", "digest": "sha256:abcdef0919234", @@ -79,14 +79,14 @@ func TestRouter(t *testing.T) { }, { RouteName: routeNameBlobUpload, - RequestURI: "/v2/foo/bar/blob/upload/", + RequestURI: "/v2/foo/bar/blobs/uploads/", Vars: map[string]string{ "name": "foo/bar", }, }, { RouteName: routeNameBlobUploadResume, - RequestURI: "/v2/foo/bar/blob/upload/uuid", + RequestURI: "/v2/foo/bar/blobs/uploads/uuid", Vars: map[string]string{ "name": "foo/bar", "uuid": "uuid", @@ -94,7 +94,7 @@ func TestRouter(t *testing.T) { }, { RouteName: routeNameBlobUploadResume, - RequestURI: "/v2/foo/bar/blob/upload/D95306FA-FAD3-4E36-8D41-CF1C93EF8286", + RequestURI: "/v2/foo/bar/blobs/uploads/D95306FA-FAD3-4E36-8D41-CF1C93EF8286", Vars: map[string]string{ "name": "foo/bar", "uuid": "D95306FA-FAD3-4E36-8D41-CF1C93EF8286", @@ -102,7 +102,7 @@ func TestRouter(t *testing.T) { }, { RouteName: routeNameBlobUploadResume, - RequestURI: "/v2/foo/bar/blob/upload/RDk1MzA2RkEtRkFEMy00RTM2LThENDEtQ0YxQzkzRUY4Mjg2IA==", + RequestURI: "/v2/foo/bar/blobs/uploads/RDk1MzA2RkEtRkFEMy00RTM2LThENDEtQ0YxQzkzRUY4Mjg2IA==", Vars: map[string]string{ "name": "foo/bar", "uuid": "RDk1MzA2RkEtRkFEMy00RTM2LThENDEtQ0YxQzkzRUY4Mjg2IA==", @@ -113,9 +113,9 @@ func TestRouter(t *testing.T) { // "foo/bar/image/image" and image for "foo/bar/image" with tag // "tags" RouteName: routeNameImageManifest, - RequestURI: "/v2/foo/bar/manifest/manifest/tags", + RequestURI: "/v2/foo/bar/manifests/manifests/tags", Vars: map[string]string{ - "name": "foo/bar/manifest", + "name": "foo/bar/manifests", "tag": "tags", }, }, @@ -123,14 +123,14 @@ func TestRouter(t *testing.T) { // This case presents an ambiguity between foo/bar with tag="tags" // and list tags for "foo/bar/manifest" RouteName: routeNameTags, - RequestURI: "/v2/foo/bar/manifest/tags/list", + RequestURI: "/v2/foo/bar/manifests/tags/list", Vars: map[string]string{ - "name": "foo/bar/manifest", + "name": "foo/bar/manifests", }, }, { RouteName: routeNameBlobUploadResume, - RequestURI: "/v2/foo/../../layer/upload/D95306FA-FAD3-4E36-8D41-CF1C93EF8286", + RequestURI: "/v2/foo/../../blob/uploads/D95306FA-FAD3-4E36-8D41-CF1C93EF8286", StatusCode: http.StatusNotFound, }, } { From 76929fb63f39b5d760bd50e9febc93dab8b06c15 Mon Sep 17 00:00:00 2001 From: Stephen J Day Date: Wed, 10 Dec 2014 22:33:36 -0800 Subject: [PATCH 2/2] Implement V2 API base endpoint This implements a base endpoint that will respond with a 200 OK and an empty json response. Such an endpoint can be used as to ping the v2 service or as an endpoint to check authorization status. --- api_test.go | 45 +++++++++++++++++++++++++++++++++++++++++++++ app.go | 15 +++++++++++++++ routes.go | 6 ++++++ routes_test.go | 5 +++++ urls.go | 14 ++++++++++++++ 5 files changed, 85 insertions(+) diff --git a/api_test.go b/api_test.go index 2c832a178..d6cf34ddd 100644 --- a/api_test.go +++ b/api_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "io" + "io/ioutil" "net/http" "net/http/httptest" "net/http/httputil" @@ -22,6 +23,50 @@ import ( "github.com/gorilla/handlers" ) +// TestCheckAPI hits the base endpoint (/v2/) ensures we return the specified +// 200 OK response. +func TestCheckAPI(t *testing.T) { + config := configuration.Configuration{ + Storage: configuration.Storage{ + "inmemory": configuration.Parameters{}, + }, + } + + app := NewApp(config) + server := httptest.NewServer(handlers.CombinedLoggingHandler(os.Stderr, app)) + builder, err := newURLBuilderFromString(server.URL) + + if err != nil { + t.Fatalf("error creating url builder: %v", err) + } + + baseURL, err := builder.buildBaseURL() + if err != nil { + t.Fatalf("unexpected error building base url: %v", err) + } + + resp, err := http.Get(baseURL) + if err != nil { + t.Fatalf("unexpected error issuing request: %v", err) + } + defer resp.Body.Close() + + checkResponse(t, "issuing api base check", resp, http.StatusOK) + checkHeaders(t, resp, http.Header{ + "Content-Type": []string{"application/json"}, + "Content-Length": []string{"2"}, + }) + + p, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Fatalf("unexpected error reading response body: %v", err) + } + + if string(p) != "{}" { + t.Fatalf("unexpected response body: %v", string(p)) + } +} + // TestLayerAPI conducts a full of the of the layer api. func TestLayerAPI(t *testing.T) { // TODO(stevvooe): This test code is complete junk but it should cover the diff --git a/app.go b/app.go index 324cad295..76605f1bf 100644 --- a/app.go +++ b/app.go @@ -1,6 +1,7 @@ package registry import ( + "fmt" "net/http" "github.com/docker/docker-registry/storagedriver" @@ -38,6 +39,9 @@ func NewApp(configuration configuration.Configuration) *App { } // Register the handler dispatchers. + app.register(routeNameBase, func(ctx *Context, r *http.Request) http.Handler { + return http.HandlerFunc(apiBase) + }) app.register(routeNameImageManifest, imageManifestDispatcher) app.register(routeNameTags, tagsDispatcher) app.register(routeNameBlob, layerDispatcher) @@ -134,3 +138,14 @@ func (app *App) dispatcher(dispatch dispatchFunc) http.Handler { } }) } + +// apiBase implements a simple yes-man for doing overall checks against the +// api. This can support auth roundtrips to support docker login. +func apiBase(w http.ResponseWriter, r *http.Request) { + const emptyJSON = "{}" + // Provide a simple /v2/ 200 OK response with empty json response. + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Content-Length", fmt.Sprint(len(emptyJSON))) + + fmt.Fprint(w, emptyJSON) +} diff --git a/routes.go b/routes.go index 440473e90..b291ee4bb 100644 --- a/routes.go +++ b/routes.go @@ -6,6 +6,7 @@ import ( ) const ( + routeNameBase = "base" routeNameImageManifest = "image-manifest" routeNameTags = "tags" routeNameBlob = "blob" @@ -27,6 +28,11 @@ func v2APIRouter() *mux.Router { router := mux.NewRouter(). StrictSlash(true) + // GET /v2/ Check Check that the registry implements API version 2(.1) + router. + Path("/v2/"). + Name(routeNameBase) + // GET /v2//manifest/ Image Manifest Fetch the image manifest identified by name and tag. // PUT /v2//manifest/ Image Manifest Upload the image manifest identified by name and tag. // DELETE /v2//manifest/ Image Manifest Delete the image identified by name and tag. diff --git a/routes_test.go b/routes_test.go index 8c5149432..6d684a614 100644 --- a/routes_test.go +++ b/routes_test.go @@ -46,6 +46,11 @@ func TestRouter(t *testing.T) { server := httptest.NewServer(router) for _, testcase := range []routeTestCase{ + { + RouteName: routeNameBase, + RequestURI: "/v2/", + Vars: map[string]string{}, + }, { RouteName: routeNameImageManifest, RequestURI: "/v2/foo/bar/manifests/tag", diff --git a/urls.go b/urls.go index 8f34a5b1e..92233da4d 100644 --- a/urls.go +++ b/urls.go @@ -39,6 +39,20 @@ func newURLBuilderFromString(root string) (*urlBuilder, error) { return newURLBuilder(u), nil } +func (ub *urlBuilder) buildBaseURL() (string, error) { + route := clonedRoute(ub.router, routeNameBase) + + baseURL, err := route. + Schemes(ub.url.Scheme). + Host(ub.url.Host). + URL() + if err != nil { + return "", err + } + + return baseURL.String(), nil +} + func (ub *urlBuilder) buildTagsURL(name string) (string, error) { route := clonedRoute(ub.router, routeNameTags)