diff --git a/routes.go b/routes.go new file mode 100644 index 00000000..10c9e398 --- /dev/null +++ b/routes.go @@ -0,0 +1,72 @@ +package registry + +import ( + "github.com/gorilla/mux" +) + +const ( + routeNameRoot = "root" + routeNameName = "name" + routeNameImageManifest = "image-manifest" + routeNameTags = "tags" + routeNameLayer = "layer" + routeNameStartLayerUpload = "start-layer-upload" + routeNameLayerUpload = "layer-upload" +) + +var allEndpoints = []string{ + routeNameImageManifest, + routeNameTags, + routeNameLayer, + routeNameStartLayerUpload, + routeNameLayerUpload, +} + +// v2APIRouter builds a gorilla router with named routes for the various API +// methods. We may export this for use by the client. +func v2APIRouter() *mux.Router { + router := mux.NewRouter() + + rootRouter := router. + PathPrefix("/v2"). + Name(routeNameRoot). + Subrouter() + + // All routes are subordinate to named routes + namedRouter := rootRouter. + PathPrefix("/{name:[A-Za-z0-9-_]+/[A-Za-z0-9-_]+}"). // TODO(stevvooe): Verify this format with core + Name(routeNameName). + Subrouter(). + StrictSlash(true) + + // GET /v2//image/ Image Manifest Fetch the image manifest identified by name and tag. + // PUT /v2//image/ Image Manifest Upload the image manifest identified by name and tag. + // DELETE /v2//image/ Image Manifest Delete the image identified by name and tag. + namedRouter. + Path("/image/{tag:[A-Za-z0-9-_]+}"). + Name(routeNameImageManifest) + + // GET /v2//tags Tags Fetch the tags under the repository identified by name. + namedRouter. + Path("/tags"). + Name(routeNameTags) + + // GET /v2//layer/ Layer Fetch the layer identified by tarsum. + namedRouter. + Path("/layer/{tarsum}"). + Name(routeNameLayer) + + // POST /v2//layer//upload/ Layer Upload Initiate an upload of the layer identified by tarsum. Requires length and a checksum parameter. + namedRouter. + Path("/layer/{tarsum}/upload/"). + Name(routeNameStartLayerUpload) + + // GET /v2//layer//upload/ Layer Upload Get the status of the upload identified by tarsum and uuid. + // PUT /v2//layer//upload/ Layer Upload Upload all or a chunk of the upload identified by tarsum and uuid. + // DELETE /v2//layer//upload/ Layer Upload Cancel the upload identified by layer and uuid + namedRouter. + Path("/layer/{tarsum}/upload/{uuid}"). + Name(routeNameLayerUpload) + + return router +} diff --git a/routes_test.go b/routes_test.go new file mode 100644 index 00000000..6b1daf80 --- /dev/null +++ b/routes_test.go @@ -0,0 +1,122 @@ +package registry + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "reflect" + "testing" + + "github.com/gorilla/mux" +) + +type routeInfo struct { + RequestURI string + Vars map[string]string +} + +// TestRouter registers a test handler with all the routes and ensures that +// each route returns the expected path variables. Not method verification is +// present. This not meant to be exhaustive but as check to ensure that the +// expected variables are extracted. +// +// This may go away as the application structure comes together. +func TestRouter(t *testing.T) { + + router := v2APIRouter() + + testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + routeInfo := routeInfo{ + RequestURI: r.RequestURI, + Vars: mux.Vars(r), + } + + enc := json.NewEncoder(w) + + if err := enc.Encode(routeInfo); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + }) + + // Startup test server + server := httptest.NewServer(router) + + for _, testcase := range []struct { + routeName string + expectedRouteInfo routeInfo + }{ + { + routeName: routeNameImageManifest, + expectedRouteInfo: routeInfo{ + RequestURI: "/v2/foo/bar/image/tag", + Vars: map[string]string{ + "name": "foo/bar", + "tag": "tag", + }, + }, + }, + { + routeName: routeNameTags, + expectedRouteInfo: routeInfo{ + RequestURI: "/v2/foo/bar/tags", + Vars: map[string]string{ + "name": "foo/bar", + }, + }, + }, + { + routeName: routeNameLayer, + expectedRouteInfo: routeInfo{ + RequestURI: "/v2/foo/bar/layer/tarsum", + Vars: map[string]string{ + "name": "foo/bar", + "tarsum": "tarsum", + }, + }, + }, + { + routeName: routeNameStartLayerUpload, + expectedRouteInfo: routeInfo{ + RequestURI: "/v2/foo/bar/layer/tarsum/upload/", + Vars: map[string]string{ + "name": "foo/bar", + "tarsum": "tarsum", + }, + }, + }, + { + routeName: routeNameLayerUpload, + expectedRouteInfo: routeInfo{ + RequestURI: "/v2/foo/bar/layer/tarsum/upload/uuid", + Vars: map[string]string{ + "name": "foo/bar", + "tarsum": "tarsum", + "uuid": "uuid", + }, + }, + }, + } { + // Register the endpoint + router.GetRoute(testcase.routeName).Handler(testHandler) + u := server.URL + testcase.expectedRouteInfo.RequestURI + + resp, err := http.Get(u) + + if err != nil { + t.Fatalf("error issuing get request: %v", err) + } + + dec := json.NewDecoder(resp.Body) + + var actualRouteInfo routeInfo + if err := dec.Decode(&actualRouteInfo); err != nil { + t.Fatalf("error reading json response: %v", err) + } + + if !reflect.DeepEqual(actualRouteInfo, testcase.expectedRouteInfo) { + t.Fatalf("actual does not equal expected: %v != %v", actualRouteInfo, testcase.expectedRouteInfo) + } + } + +}