Merge pull request #842 from stevvooe/next-generation

Pluralize API methods and add the base endpoint to support API checks
pull/4/head
Olivier Gambier 2014-12-11 01:47:21 -08:00
commit 18ace1f454
7 changed files with 113 additions and 28 deletions

View File

@ -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

15
app.go
View File

@ -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)
}

View File

@ -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

View File

@ -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,

View File

@ -6,6 +6,7 @@ import (
)
const (
routeNameBase = "base"
routeNameImageManifest = "image-manifest"
routeNameTags = "tags"
routeNameBlob = "blob"
@ -27,11 +28,16 @@ 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/<name>/manifest/<tag> Image Manifest Fetch the image manifest identified by name and tag.
// PUT /v2/<name>/manifest/<tag> Image Manifest Upload the image manifest identified by name and tag.
// DELETE /v2/<name>/manifest/<tag> 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/<name>/tags/list Tags Fetch the tags under the repository identified by name.
@ -41,19 +47,19 @@ func v2APIRouter() *mux.Router {
// GET /v2/<name>/blob/<digest> 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/<name>/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/<name>/blob/upload/<uuid> Layer Upload Get the status of the upload identified by tarsum and uuid.
// PUT /v2/<name>/blob/upload/<uuid> Layer Upload Upload all or a chunk of the upload identified by tarsum and uuid.
// DELETE /v2/<name>/blob/upload/<uuid> 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

View File

@ -46,9 +46,14 @@ 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/manifest/tag",
RequestURI: "/v2/foo/bar/manifests/tag",
Vars: map[string]string{
"name": "foo/bar",
"tag": "tag",
@ -63,7 +68,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 +76,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 +84,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 +99,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 +107,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 +118,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 +128,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,
},
} {

14
urls.go
View File

@ -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)