Initial V2 API Router Implementation

This commit includes the initial API router, based on gorilla mux and a test
suite ensuring the expected variables are extracted. Currently unexported, the
structure here will likely change as this definition will be shared with the
API client.
This commit is contained in:
Stephen J Day 2014-11-07 16:08:14 -08:00
parent da205085f3
commit fec2afc93f
2 changed files with 194 additions and 0 deletions

72
routes.go Normal file
View file

@ -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/<name>/image/<tag> Image Manifest Fetch the image manifest identified by name and tag.
// PUT /v2/<name>/image/<tag> Image Manifest Upload the image manifest identified by name and tag.
// DELETE /v2/<name>/image/<tag> Image Manifest Delete the image identified by name and tag.
namedRouter.
Path("/image/{tag:[A-Za-z0-9-_]+}").
Name(routeNameImageManifest)
// GET /v2/<name>/tags Tags Fetch the tags under the repository identified by name.
namedRouter.
Path("/tags").
Name(routeNameTags)
// GET /v2/<name>/layer/<tarsum> Layer Fetch the layer identified by tarsum.
namedRouter.
Path("/layer/{tarsum}").
Name(routeNameLayer)
// POST /v2/<name>/layer/<tarsum>/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/<name>/layer/<tarsum>/upload/<uuid> Layer Upload Get the status of the upload identified by tarsum and uuid.
// PUT /v2/<name>/layer/<tarsum>/upload/<uuid> Layer Upload Upload all or a chunk of the upload identified by tarsum and uuid.
// DELETE /v2/<name>/layer/<tarsum>/upload/<uuid> Layer Upload Cancel the upload identified by layer and uuid
namedRouter.
Path("/layer/{tarsum}/upload/{uuid}").
Name(routeNameLayerUpload)
return router
}

122
routes_test.go Normal file
View file

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