Merge pull request #842 from stevvooe/next-generation
Pluralize API methods and add the base endpoint to support API checks
This commit is contained in:
commit
18ace1f454
7 changed files with 113 additions and 28 deletions
45
api_test.go
45
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
|
||||
|
|
15
app.go
15
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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
14
routes.go
14
routes.go
|
@ -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
|
||||
|
|
|
@ -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
14
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)
|
||||
|
||||
|
|
Loading…
Reference in a new issue