From fec2afc93fee50e4980d52912707d0e692d65291 Mon Sep 17 00:00:00 2001
From: Stephen J Day <stephen.day@docker.com>
Date: Fri, 7 Nov 2014 16:08:14 -0800
Subject: [PATCH] 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.
---
 routes.go      |  72 +++++++++++++++++++++++++++++
 routes_test.go | 122 +++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 194 insertions(+)
 create mode 100644 routes.go
 create mode 100644 routes_test.go

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/<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
+}
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)
+		}
+	}
+
+}