Browse code

Registry V2 HTTP route and error code definitions

This package, ported from next-generation docker regsitry, includes route and
error definitions. These facilitate compliant V2 client implementation. The
portions of the HTTP API that are included in this package are considered to be
locked down and should only be changed through a careful change proposal.
Descriptor definitions package layout may change without affecting API behavior
until the exported Go API is ready to be locked down.

When the new registry stabilizes and becomes the master branch, this package
can be vendored from the registry.

Signed-off-by: Stephen J Day <stephen.day@docker.com>

Stephen J Day authored on 2014/12/13 04:27:22
Showing 8 changed files
1 1
new file mode 100644
... ...
@@ -0,0 +1,144 @@
0
+package v2
1
+
2
+import "net/http"
3
+
4
+// TODO(stevvooe): Add route descriptors for each named route, along with
5
+// accepted methods, parameters, returned status codes and error codes.
6
+
7
+// ErrorDescriptor provides relevant information about a given error code.
8
+type ErrorDescriptor struct {
9
+	// Code is the error code that this descriptor describes.
10
+	Code ErrorCode
11
+
12
+	// Value provides a unique, string key, often captilized with
13
+	// underscores, to identify the error code. This value is used as the
14
+	// keyed value when serializing api errors.
15
+	Value string
16
+
17
+	// Message is a short, human readable decription of the error condition
18
+	// included in API responses.
19
+	Message string
20
+
21
+	// Description provides a complete account of the errors purpose, suitable
22
+	// for use in documentation.
23
+	Description string
24
+
25
+	// HTTPStatusCodes provides a list of status under which this error
26
+	// condition may arise. If it is empty, the error condition may be seen
27
+	// for any status code.
28
+	HTTPStatusCodes []int
29
+}
30
+
31
+// ErrorDescriptors provides a list of HTTP API Error codes that may be
32
+// encountered when interacting with the registry API.
33
+var ErrorDescriptors = []ErrorDescriptor{
34
+	{
35
+		Code:    ErrorCodeUnknown,
36
+		Value:   "UNKNOWN",
37
+		Message: "unknown error",
38
+		Description: `Generic error returned when the error does not have an
39
+		API classification.`,
40
+	},
41
+	{
42
+		Code:    ErrorCodeDigestInvalid,
43
+		Value:   "DIGEST_INVALID",
44
+		Message: "provided digest did not match uploaded content",
45
+		Description: `When a blob is uploaded, the registry will check that
46
+		the content matches the digest provided by the client. The error may
47
+		include a detail structure with the key "digest", including the
48
+		invalid digest string. This error may also be returned when a manifest
49
+		includes an invalid layer digest.`,
50
+		HTTPStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
51
+	},
52
+	{
53
+		Code:    ErrorCodeSizeInvalid,
54
+		Value:   "SIZE_INVALID",
55
+		Message: "provided length did not match content length",
56
+		Description: `When a layer is uploaded, the provided size will be
57
+		checked against the uploaded content. If they do not match, this error
58
+		will be returned.`,
59
+		HTTPStatusCodes: []int{http.StatusBadRequest},
60
+	},
61
+	{
62
+		Code:    ErrorCodeNameInvalid,
63
+		Value:   "NAME_INVALID",
64
+		Message: "manifest name did not match URI",
65
+		Description: `During a manifest upload, if the name in the manifest
66
+		does not match the uri name, this error will be returned.`,
67
+		HTTPStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
68
+	},
69
+	{
70
+		Code:    ErrorCodeTagInvalid,
71
+		Value:   "TAG_INVALID",
72
+		Message: "manifest tag did not match URI",
73
+		Description: `During a manifest upload, if the tag in the manifest
74
+		does not match the uri tag, this error will be returned.`,
75
+		HTTPStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
76
+	},
77
+	{
78
+		Code:    ErrorCodeNameUnknown,
79
+		Value:   "NAME_UNKNOWN",
80
+		Message: "repository name not known to registry",
81
+		Description: `This is returned if the name used during an operation is
82
+		unknown to the registry.`,
83
+		HTTPStatusCodes: []int{http.StatusNotFound},
84
+	},
85
+	{
86
+		Code:    ErrorCodeManifestUnknown,
87
+		Value:   "MANIFEST_UNKNOWN",
88
+		Message: "manifest unknown",
89
+		Description: `This error is returned when the manifest, identified by
90
+		name and tag is unknown to the repository.`,
91
+		HTTPStatusCodes: []int{http.StatusNotFound},
92
+	},
93
+	{
94
+		Code:    ErrorCodeManifestInvalid,
95
+		Value:   "MANIFEST_INVALID",
96
+		Message: "manifest invalid",
97
+		Description: `During upload, manifests undergo several checks ensuring
98
+		validity. If those checks fail, this error may be returned, unless a
99
+		more specific error is included. The detail will contain information
100
+		the failed validation.`,
101
+		HTTPStatusCodes: []int{http.StatusBadRequest},
102
+	},
103
+	{
104
+		Code:    ErrorCodeManifestUnverified,
105
+		Value:   "MANIFEST_UNVERIFIED",
106
+		Message: "manifest failed signature verification",
107
+		Description: `During manifest upload, if the manifest fails signature
108
+		verification, this error will be returned.`,
109
+		HTTPStatusCodes: []int{http.StatusBadRequest},
110
+	},
111
+	{
112
+		Code:    ErrorCodeBlobUnknown,
113
+		Value:   "BLOB_UNKNOWN",
114
+		Message: "blob unknown to registry",
115
+		Description: `This error may be returned when a blob is unknown to the
116
+		registry in a specified repository. This can be returned with a
117
+		standard get or if a manifest references an unknown layer during
118
+		upload.`,
119
+		HTTPStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
120
+	},
121
+
122
+	{
123
+		Code:    ErrorCodeBlobUploadUnknown,
124
+		Value:   "BLOB_UPLOAD_UNKNOWN",
125
+		Message: "blob upload unknown to registry",
126
+		Description: `If a blob upload has been cancelled or was never
127
+		started, this error code may be returned.`,
128
+		HTTPStatusCodes: []int{http.StatusNotFound},
129
+	},
130
+}
131
+
132
+var errorCodeToDescriptors map[ErrorCode]ErrorDescriptor
133
+var idToDescriptors map[string]ErrorDescriptor
134
+
135
+func init() {
136
+	errorCodeToDescriptors = make(map[ErrorCode]ErrorDescriptor, len(ErrorDescriptors))
137
+	idToDescriptors = make(map[string]ErrorDescriptor, len(ErrorDescriptors))
138
+
139
+	for _, descriptor := range ErrorDescriptors {
140
+		errorCodeToDescriptors[descriptor.Code] = descriptor
141
+		idToDescriptors[descriptor.Value] = descriptor
142
+	}
143
+}
0 144
new file mode 100644
... ...
@@ -0,0 +1,13 @@
0
+// Package v2 describes routes, urls and the error codes used in the Docker
1
+// Registry JSON HTTP API V2. In addition to declarations, descriptors are
2
+// provided for routes and error codes that can be used for implementation and
3
+// automatically generating documentation.
4
+//
5
+// Definitions here are considered to be locked down for the V2 registry api.
6
+// Any changes must be considered carefully and should not proceed without a
7
+// change proposal.
8
+//
9
+// Currently, while the HTTP API definitions are considered stable, the Go API
10
+// exports are considered unstable. Go API consumers should take care when
11
+// relying on these definitions until this message is deleted.
12
+package v2
0 13
new file mode 100644
... ...
@@ -0,0 +1,185 @@
0
+package v2
1
+
2
+import (
3
+	"fmt"
4
+	"strings"
5
+)
6
+
7
+// ErrorCode represents the error type. The errors are serialized via strings
8
+// and the integer format may change and should *never* be exported.
9
+type ErrorCode int
10
+
11
+const (
12
+	// ErrorCodeUnknown is a catch-all for errors not defined below.
13
+	ErrorCodeUnknown ErrorCode = iota
14
+
15
+	// ErrorCodeDigestInvalid is returned when uploading a blob if the
16
+	// provided digest does not match the blob contents.
17
+	ErrorCodeDigestInvalid
18
+
19
+	// ErrorCodeSizeInvalid is returned when uploading a blob if the provided
20
+	// size does not match the content length.
21
+	ErrorCodeSizeInvalid
22
+
23
+	// ErrorCodeNameInvalid is returned when the name in the manifest does not
24
+	// match the provided name.
25
+	ErrorCodeNameInvalid
26
+
27
+	// ErrorCodeTagInvalid is returned when the tag in the manifest does not
28
+	// match the provided tag.
29
+	ErrorCodeTagInvalid
30
+
31
+	// ErrorCodeNameUnknown when the repository name is not known.
32
+	ErrorCodeNameUnknown
33
+
34
+	// ErrorCodeManifestUnknown returned when image manifest is unknown.
35
+	ErrorCodeManifestUnknown
36
+
37
+	// ErrorCodeManifestInvalid returned when an image manifest is invalid,
38
+	// typically during a PUT operation. This error encompasses all errors
39
+	// encountered during manifest validation that aren't signature errors.
40
+	ErrorCodeManifestInvalid
41
+
42
+	// ErrorCodeManifestUnverified is returned when the manifest fails
43
+	// signature verfication.
44
+	ErrorCodeManifestUnverified
45
+
46
+	// ErrorCodeBlobUnknown is returned when a blob is unknown to the
47
+	// registry. This can happen when the manifest references a nonexistent
48
+	// layer or the result is not found by a blob fetch.
49
+	ErrorCodeBlobUnknown
50
+
51
+	// ErrorCodeBlobUploadUnknown is returned when an upload is unknown.
52
+	ErrorCodeBlobUploadUnknown
53
+)
54
+
55
+// ParseErrorCode attempts to parse the error code string, returning
56
+// ErrorCodeUnknown if the error is not known.
57
+func ParseErrorCode(s string) ErrorCode {
58
+	desc, ok := idToDescriptors[s]
59
+
60
+	if !ok {
61
+		return ErrorCodeUnknown
62
+	}
63
+
64
+	return desc.Code
65
+}
66
+
67
+// Descriptor returns the descriptor for the error code.
68
+func (ec ErrorCode) Descriptor() ErrorDescriptor {
69
+	d, ok := errorCodeToDescriptors[ec]
70
+
71
+	if !ok {
72
+		return ErrorCodeUnknown.Descriptor()
73
+	}
74
+
75
+	return d
76
+}
77
+
78
+// String returns the canonical identifier for this error code.
79
+func (ec ErrorCode) String() string {
80
+	return ec.Descriptor().Value
81
+}
82
+
83
+// Message returned the human-readable error message for this error code.
84
+func (ec ErrorCode) Message() string {
85
+	return ec.Descriptor().Message
86
+}
87
+
88
+// MarshalText encodes the receiver into UTF-8-encoded text and returns the
89
+// result.
90
+func (ec ErrorCode) MarshalText() (text []byte, err error) {
91
+	return []byte(ec.String()), nil
92
+}
93
+
94
+// UnmarshalText decodes the form generated by MarshalText.
95
+func (ec *ErrorCode) UnmarshalText(text []byte) error {
96
+	desc, ok := idToDescriptors[string(text)]
97
+
98
+	if !ok {
99
+		desc = ErrorCodeUnknown.Descriptor()
100
+	}
101
+
102
+	*ec = desc.Code
103
+
104
+	return nil
105
+}
106
+
107
+// Error provides a wrapper around ErrorCode with extra Details provided.
108
+type Error struct {
109
+	Code    ErrorCode   `json:"code"`
110
+	Message string      `json:"message,omitempty"`
111
+	Detail  interface{} `json:"detail,omitempty"`
112
+}
113
+
114
+// Error returns a human readable representation of the error.
115
+func (e Error) Error() string {
116
+	return fmt.Sprintf("%s: %s",
117
+		strings.ToLower(strings.Replace(e.Code.String(), "_", " ", -1)),
118
+		e.Message)
119
+}
120
+
121
+// Errors provides the envelope for multiple errors and a few sugar methods
122
+// for use within the application.
123
+type Errors struct {
124
+	Errors []Error `json:"errors,omitempty"`
125
+}
126
+
127
+// Push pushes an error on to the error stack, with the optional detail
128
+// argument. It is a programming error (ie panic) to push more than one
129
+// detail at a time.
130
+func (errs *Errors) Push(code ErrorCode, details ...interface{}) {
131
+	if len(details) > 1 {
132
+		panic("please specify zero or one detail items for this error")
133
+	}
134
+
135
+	var detail interface{}
136
+	if len(details) > 0 {
137
+		detail = details[0]
138
+	}
139
+
140
+	if err, ok := detail.(error); ok {
141
+		detail = err.Error()
142
+	}
143
+
144
+	errs.PushErr(Error{
145
+		Code:    code,
146
+		Message: code.Message(),
147
+		Detail:  detail,
148
+	})
149
+}
150
+
151
+// PushErr pushes an error interface onto the error stack.
152
+func (errs *Errors) PushErr(err error) {
153
+	switch err.(type) {
154
+	case Error:
155
+		errs.Errors = append(errs.Errors, err.(Error))
156
+	default:
157
+		errs.Errors = append(errs.Errors, Error{Message: err.Error()})
158
+	}
159
+}
160
+
161
+func (errs *Errors) Error() string {
162
+	switch errs.Len() {
163
+	case 0:
164
+		return "<nil>"
165
+	case 1:
166
+		return errs.Errors[0].Error()
167
+	default:
168
+		msg := "errors:\n"
169
+		for _, err := range errs.Errors {
170
+			msg += err.Error() + "\n"
171
+		}
172
+		return msg
173
+	}
174
+}
175
+
176
+// Clear clears the errors.
177
+func (errs *Errors) Clear() {
178
+	errs.Errors = errs.Errors[:0]
179
+}
180
+
181
+// Len returns the current number of errors.
182
+func (errs *Errors) Len() int {
183
+	return len(errs.Errors)
184
+}
0 185
new file mode 100644
... ...
@@ -0,0 +1,165 @@
0
+package v2
1
+
2
+import (
3
+	"encoding/json"
4
+	"reflect"
5
+	"testing"
6
+
7
+	"github.com/docker/docker-registry/digest"
8
+)
9
+
10
+// TestErrorCodes ensures that error code format, mappings and
11
+// marshaling/unmarshaling. round trips are stable.
12
+func TestErrorCodes(t *testing.T) {
13
+	for _, desc := range ErrorDescriptors {
14
+		if desc.Code.String() != desc.Value {
15
+			t.Fatalf("error code string incorrect: %q != %q", desc.Code.String(), desc.Value)
16
+		}
17
+
18
+		if desc.Code.Message() != desc.Message {
19
+			t.Fatalf("incorrect message for error code %v: %q != %q", desc.Code, desc.Code.Message(), desc.Message)
20
+		}
21
+
22
+		// Serialize the error code using the json library to ensure that we
23
+		// get a string and it works round trip.
24
+		p, err := json.Marshal(desc.Code)
25
+
26
+		if err != nil {
27
+			t.Fatalf("error marshaling error code %v: %v", desc.Code, err)
28
+		}
29
+
30
+		if len(p) <= 0 {
31
+			t.Fatalf("expected content in marshaled before for error code %v", desc.Code)
32
+		}
33
+
34
+		// First, unmarshal to interface and ensure we have a string.
35
+		var ecUnspecified interface{}
36
+		if err := json.Unmarshal(p, &ecUnspecified); err != nil {
37
+			t.Fatalf("error unmarshaling error code %v: %v", desc.Code, err)
38
+		}
39
+
40
+		if _, ok := ecUnspecified.(string); !ok {
41
+			t.Fatalf("expected a string for error code %v on unmarshal got a %T", desc.Code, ecUnspecified)
42
+		}
43
+
44
+		// Now, unmarshal with the error code type and ensure they are equal
45
+		var ecUnmarshaled ErrorCode
46
+		if err := json.Unmarshal(p, &ecUnmarshaled); err != nil {
47
+			t.Fatalf("error unmarshaling error code %v: %v", desc.Code, err)
48
+		}
49
+
50
+		if ecUnmarshaled != desc.Code {
51
+			t.Fatalf("unexpected error code during error code marshal/unmarshal: %v != %v", ecUnmarshaled, desc.Code)
52
+		}
53
+	}
54
+}
55
+
56
+// TestErrorsManagement does a quick check of the Errors type to ensure that
57
+// members are properly pushed and marshaled.
58
+func TestErrorsManagement(t *testing.T) {
59
+	var errs Errors
60
+
61
+	errs.Push(ErrorCodeDigestInvalid)
62
+	errs.Push(ErrorCodeBlobUnknown,
63
+		map[string]digest.Digest{"digest": "sometestblobsumdoesntmatter"})
64
+
65
+	p, err := json.Marshal(errs)
66
+
67
+	if err != nil {
68
+		t.Fatalf("error marashaling errors: %v", err)
69
+	}
70
+
71
+	expectedJSON := "{\"errors\":[{\"code\":\"DIGEST_INVALID\",\"message\":\"provided digest did not match uploaded content\"},{\"code\":\"BLOB_UNKNOWN\",\"message\":\"blob unknown to registry\",\"detail\":{\"digest\":\"sometestblobsumdoesntmatter\"}}]}"
72
+
73
+	if string(p) != expectedJSON {
74
+		t.Fatalf("unexpected json: %q != %q", string(p), expectedJSON)
75
+	}
76
+
77
+	errs.Clear()
78
+	errs.Push(ErrorCodeUnknown)
79
+	expectedJSON = "{\"errors\":[{\"code\":\"UNKNOWN\",\"message\":\"unknown error\"}]}"
80
+	p, err = json.Marshal(errs)
81
+
82
+	if err != nil {
83
+		t.Fatalf("error marashaling errors: %v", err)
84
+	}
85
+
86
+	if string(p) != expectedJSON {
87
+		t.Fatalf("unexpected json: %q != %q", string(p), expectedJSON)
88
+	}
89
+}
90
+
91
+// TestMarshalUnmarshal ensures that api errors can round trip through json
92
+// without losing information.
93
+func TestMarshalUnmarshal(t *testing.T) {
94
+
95
+	var errors Errors
96
+
97
+	for _, testcase := range []struct {
98
+		description string
99
+		err         Error
100
+	}{
101
+		{
102
+			description: "unknown error",
103
+			err: Error{
104
+
105
+				Code:    ErrorCodeUnknown,
106
+				Message: ErrorCodeUnknown.Descriptor().Message,
107
+			},
108
+		},
109
+		{
110
+			description: "unknown manifest",
111
+			err: Error{
112
+				Code:    ErrorCodeManifestUnknown,
113
+				Message: ErrorCodeManifestUnknown.Descriptor().Message,
114
+			},
115
+		},
116
+		{
117
+			description: "unknown manifest",
118
+			err: Error{
119
+				Code:    ErrorCodeBlobUnknown,
120
+				Message: ErrorCodeBlobUnknown.Descriptor().Message,
121
+				Detail:  map[string]interface{}{"digest": "asdfqwerqwerqwerqwer"},
122
+			},
123
+		},
124
+	} {
125
+		fatalf := func(format string, args ...interface{}) {
126
+			t.Fatalf(testcase.description+": "+format, args...)
127
+		}
128
+
129
+		unexpectedErr := func(err error) {
130
+			fatalf("unexpected error: %v", err)
131
+		}
132
+
133
+		p, err := json.Marshal(testcase.err)
134
+		if err != nil {
135
+			unexpectedErr(err)
136
+		}
137
+
138
+		var unmarshaled Error
139
+		if err := json.Unmarshal(p, &unmarshaled); err != nil {
140
+			unexpectedErr(err)
141
+		}
142
+
143
+		if !reflect.DeepEqual(unmarshaled, testcase.err) {
144
+			fatalf("errors not equal after round trip: %#v != %#v", unmarshaled, testcase.err)
145
+		}
146
+
147
+		// Roll everything up into an error response envelope.
148
+		errors.PushErr(testcase.err)
149
+	}
150
+
151
+	p, err := json.Marshal(errors)
152
+	if err != nil {
153
+		t.Fatalf("unexpected error marshaling error envelope: %v", err)
154
+	}
155
+
156
+	var unmarshaled Errors
157
+	if err := json.Unmarshal(p, &unmarshaled); err != nil {
158
+		t.Fatalf("unexpected error unmarshaling error envelope: %v", err)
159
+	}
160
+
161
+	if !reflect.DeepEqual(unmarshaled, errors) {
162
+		t.Fatalf("errors not equal after round trip: %#v != %#v", unmarshaled, errors)
163
+	}
164
+}
0 165
new file mode 100644
... ...
@@ -0,0 +1,69 @@
0
+package v2
1
+
2
+import (
3
+	"github.com/docker/docker-registry/common"
4
+	"github.com/gorilla/mux"
5
+)
6
+
7
+// The following are definitions of the name under which all V2 routes are
8
+// registered. These symbols can be used to look up a route based on the name.
9
+const (
10
+	RouteNameBase            = "base"
11
+	RouteNameManifest        = "manifest"
12
+	RouteNameTags            = "tags"
13
+	RouteNameBlob            = "blob"
14
+	RouteNameBlobUpload      = "blob-upload"
15
+	RouteNameBlobUploadChunk = "blob-upload-chunk"
16
+)
17
+
18
+var allEndpoints = []string{
19
+	RouteNameManifest,
20
+	RouteNameTags,
21
+	RouteNameBlob,
22
+	RouteNameBlobUpload,
23
+	RouteNameBlobUploadChunk,
24
+}
25
+
26
+// Router builds a gorilla router with named routes for the various API
27
+// methods. This can be used directly by both server implementations and
28
+// clients.
29
+func Router() *mux.Router {
30
+	router := mux.NewRouter().
31
+		StrictSlash(true)
32
+
33
+	// GET /v2/	Check	Check that the registry implements API version 2(.1)
34
+	router.
35
+		Path("/v2/").
36
+		Name(RouteNameBase)
37
+
38
+	// GET      /v2/<name>/manifest/<tag>	Image Manifest	Fetch the image manifest identified by name and tag.
39
+	// PUT      /v2/<name>/manifest/<tag>	Image Manifest	Upload the image manifest identified by name and tag.
40
+	// DELETE   /v2/<name>/manifest/<tag>	Image Manifest	Delete the image identified by name and tag.
41
+	router.
42
+		Path("/v2/{name:" + common.RepositoryNameRegexp.String() + "}/manifests/{tag:" + common.TagNameRegexp.String() + "}").
43
+		Name(RouteNameManifest)
44
+
45
+	// GET	/v2/<name>/tags/list	Tags	Fetch the tags under the repository identified by name.
46
+	router.
47
+		Path("/v2/{name:" + common.RepositoryNameRegexp.String() + "}/tags/list").
48
+		Name(RouteNameTags)
49
+
50
+	// GET	/v2/<name>/blob/<digest>	Layer	Fetch the blob identified by digest.
51
+	router.
52
+		Path("/v2/{name:" + common.RepositoryNameRegexp.String() + "}/blobs/{digest:[a-zA-Z0-9-_+.]+:[a-zA-Z0-9-_+.=]+}").
53
+		Name(RouteNameBlob)
54
+
55
+	// POST	/v2/<name>/blob/upload/	Layer Upload	Initiate an upload of the layer identified by tarsum.
56
+	router.
57
+		Path("/v2/{name:" + common.RepositoryNameRegexp.String() + "}/blobs/uploads/").
58
+		Name(RouteNameBlobUpload)
59
+
60
+	// GET	/v2/<name>/blob/upload/<uuid>	Layer Upload	Get the status of the upload identified by tarsum and uuid.
61
+	// PUT	/v2/<name>/blob/upload/<uuid>	Layer Upload	Upload all or a chunk of the upload identified by tarsum and uuid.
62
+	// DELETE	/v2/<name>/blob/upload/<uuid>	Layer Upload	Cancel the upload identified by layer and uuid
63
+	router.
64
+		Path("/v2/{name:" + common.RepositoryNameRegexp.String() + "}/blobs/uploads/{uuid}").
65
+		Name(RouteNameBlobUploadChunk)
66
+
67
+	return router
68
+}
0 69
new file mode 100644
... ...
@@ -0,0 +1,184 @@
0
+package v2
1
+
2
+import (
3
+	"encoding/json"
4
+	"net/http"
5
+	"net/http/httptest"
6
+	"reflect"
7
+	"testing"
8
+
9
+	"github.com/gorilla/mux"
10
+)
11
+
12
+type routeTestCase struct {
13
+	RequestURI string
14
+	Vars       map[string]string
15
+	RouteName  string
16
+	StatusCode int
17
+}
18
+
19
+// TestRouter registers a test handler with all the routes and ensures that
20
+// each route returns the expected path variables. Not method verification is
21
+// present. This not meant to be exhaustive but as check to ensure that the
22
+// expected variables are extracted.
23
+//
24
+// This may go away as the application structure comes together.
25
+func TestRouter(t *testing.T) {
26
+
27
+	router := Router()
28
+
29
+	testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
30
+		testCase := routeTestCase{
31
+			RequestURI: r.RequestURI,
32
+			Vars:       mux.Vars(r),
33
+			RouteName:  mux.CurrentRoute(r).GetName(),
34
+		}
35
+
36
+		enc := json.NewEncoder(w)
37
+
38
+		if err := enc.Encode(testCase); err != nil {
39
+			http.Error(w, err.Error(), http.StatusInternalServerError)
40
+			return
41
+		}
42
+	})
43
+
44
+	// Startup test server
45
+	server := httptest.NewServer(router)
46
+
47
+	for _, testcase := range []routeTestCase{
48
+		{
49
+			RouteName:  RouteNameBase,
50
+			RequestURI: "/v2/",
51
+			Vars:       map[string]string{},
52
+		},
53
+		{
54
+			RouteName:  RouteNameManifest,
55
+			RequestURI: "/v2/foo/bar/manifests/tag",
56
+			Vars: map[string]string{
57
+				"name": "foo/bar",
58
+				"tag":  "tag",
59
+			},
60
+		},
61
+		{
62
+			RouteName:  RouteNameTags,
63
+			RequestURI: "/v2/foo/bar/tags/list",
64
+			Vars: map[string]string{
65
+				"name": "foo/bar",
66
+			},
67
+		},
68
+		{
69
+			RouteName:  RouteNameBlob,
70
+			RequestURI: "/v2/foo/bar/blobs/tarsum.dev+foo:abcdef0919234",
71
+			Vars: map[string]string{
72
+				"name":   "foo/bar",
73
+				"digest": "tarsum.dev+foo:abcdef0919234",
74
+			},
75
+		},
76
+		{
77
+			RouteName:  RouteNameBlob,
78
+			RequestURI: "/v2/foo/bar/blobs/sha256:abcdef0919234",
79
+			Vars: map[string]string{
80
+				"name":   "foo/bar",
81
+				"digest": "sha256:abcdef0919234",
82
+			},
83
+		},
84
+		{
85
+			RouteName:  RouteNameBlobUpload,
86
+			RequestURI: "/v2/foo/bar/blobs/uploads/",
87
+			Vars: map[string]string{
88
+				"name": "foo/bar",
89
+			},
90
+		},
91
+		{
92
+			RouteName:  RouteNameBlobUploadChunk,
93
+			RequestURI: "/v2/foo/bar/blobs/uploads/uuid",
94
+			Vars: map[string]string{
95
+				"name": "foo/bar",
96
+				"uuid": "uuid",
97
+			},
98
+		},
99
+		{
100
+			RouteName:  RouteNameBlobUploadChunk,
101
+			RequestURI: "/v2/foo/bar/blobs/uploads/D95306FA-FAD3-4E36-8D41-CF1C93EF8286",
102
+			Vars: map[string]string{
103
+				"name": "foo/bar",
104
+				"uuid": "D95306FA-FAD3-4E36-8D41-CF1C93EF8286",
105
+			},
106
+		},
107
+		{
108
+			RouteName:  RouteNameBlobUploadChunk,
109
+			RequestURI: "/v2/foo/bar/blobs/uploads/RDk1MzA2RkEtRkFEMy00RTM2LThENDEtQ0YxQzkzRUY4Mjg2IA==",
110
+			Vars: map[string]string{
111
+				"name": "foo/bar",
112
+				"uuid": "RDk1MzA2RkEtRkFEMy00RTM2LThENDEtQ0YxQzkzRUY4Mjg2IA==",
113
+			},
114
+		},
115
+		{
116
+			// Check ambiguity: ensure we can distinguish between tags for
117
+			// "foo/bar/image/image" and image for "foo/bar/image" with tag
118
+			// "tags"
119
+			RouteName:  RouteNameManifest,
120
+			RequestURI: "/v2/foo/bar/manifests/manifests/tags",
121
+			Vars: map[string]string{
122
+				"name": "foo/bar/manifests",
123
+				"tag":  "tags",
124
+			},
125
+		},
126
+		{
127
+			// This case presents an ambiguity between foo/bar with tag="tags"
128
+			// and list tags for "foo/bar/manifest"
129
+			RouteName:  RouteNameTags,
130
+			RequestURI: "/v2/foo/bar/manifests/tags/list",
131
+			Vars: map[string]string{
132
+				"name": "foo/bar/manifests",
133
+			},
134
+		},
135
+		{
136
+			RouteName:  RouteNameBlobUploadChunk,
137
+			RequestURI: "/v2/foo/../../blob/uploads/D95306FA-FAD3-4E36-8D41-CF1C93EF8286",
138
+			StatusCode: http.StatusNotFound,
139
+		},
140
+	} {
141
+		// Register the endpoint
142
+		router.GetRoute(testcase.RouteName).Handler(testHandler)
143
+		u := server.URL + testcase.RequestURI
144
+
145
+		resp, err := http.Get(u)
146
+
147
+		if err != nil {
148
+			t.Fatalf("error issuing get request: %v", err)
149
+		}
150
+
151
+		if testcase.StatusCode == 0 {
152
+			// Override default, zero-value
153
+			testcase.StatusCode = http.StatusOK
154
+		}
155
+
156
+		if resp.StatusCode != testcase.StatusCode {
157
+			t.Fatalf("unexpected status for %s: %v %v", u, resp.Status, resp.StatusCode)
158
+		}
159
+
160
+		if testcase.StatusCode != http.StatusOK {
161
+			// We don't care about json response.
162
+			continue
163
+		}
164
+
165
+		dec := json.NewDecoder(resp.Body)
166
+
167
+		var actualRouteInfo routeTestCase
168
+		if err := dec.Decode(&actualRouteInfo); err != nil {
169
+			t.Fatalf("error reading json response: %v", err)
170
+		}
171
+		// Needs to be set out of band
172
+		actualRouteInfo.StatusCode = resp.StatusCode
173
+
174
+		if actualRouteInfo.RouteName != testcase.RouteName {
175
+			t.Fatalf("incorrect route %q matched, expected %q", actualRouteInfo.RouteName, testcase.RouteName)
176
+		}
177
+
178
+		if !reflect.DeepEqual(actualRouteInfo, testcase) {
179
+			t.Fatalf("actual does not equal expected: %#v != %#v", actualRouteInfo, testcase)
180
+		}
181
+	}
182
+
183
+}
0 184
new file mode 100644
... ...
@@ -0,0 +1,165 @@
0
+package v2
1
+
2
+import (
3
+	"net/http"
4
+	"net/url"
5
+
6
+	"github.com/docker/docker-registry/digest"
7
+	"github.com/gorilla/mux"
8
+)
9
+
10
+// URLBuilder creates registry API urls from a single base endpoint. It can be
11
+// used to create urls for use in a registry client or server.
12
+//
13
+// All urls will be created from the given base, including the api version.
14
+// For example, if a root of "/foo/" is provided, urls generated will be fall
15
+// under "/foo/v2/...". Most application will only provide a schema, host and
16
+// port, such as "https://localhost:5000/".
17
+type URLBuilder struct {
18
+	root   *url.URL // url root (ie http://localhost/)
19
+	router *mux.Router
20
+}
21
+
22
+// NewURLBuilder creates a URLBuilder with provided root url object.
23
+func NewURLBuilder(root *url.URL) *URLBuilder {
24
+	return &URLBuilder{
25
+		root:   root,
26
+		router: Router(),
27
+	}
28
+}
29
+
30
+// NewURLBuilderFromString workes identically to NewURLBuilder except it takes
31
+// a string argument for the root, returning an error if it is not a valid
32
+// url.
33
+func NewURLBuilderFromString(root string) (*URLBuilder, error) {
34
+	u, err := url.Parse(root)
35
+	if err != nil {
36
+		return nil, err
37
+	}
38
+
39
+	return NewURLBuilder(u), nil
40
+}
41
+
42
+// NewURLBuilderFromRequest uses information from an *http.Request to
43
+// construct the root url.
44
+func NewURLBuilderFromRequest(r *http.Request) *URLBuilder {
45
+	u := &url.URL{
46
+		Scheme: r.URL.Scheme,
47
+		Host:   r.Host,
48
+	}
49
+
50
+	return NewURLBuilder(u)
51
+}
52
+
53
+// BuildBaseURL constructs a base url for the API, typically just "/v2/".
54
+func (ub *URLBuilder) BuildBaseURL() (string, error) {
55
+	route := ub.cloneRoute(RouteNameBase)
56
+
57
+	baseURL, err := route.URL()
58
+	if err != nil {
59
+		return "", err
60
+	}
61
+
62
+	return baseURL.String(), nil
63
+}
64
+
65
+// BuildTagsURL constructs a url to list the tags in the named repository.
66
+func (ub *URLBuilder) BuildTagsURL(name string) (string, error) {
67
+	route := ub.cloneRoute(RouteNameTags)
68
+
69
+	tagsURL, err := route.URL("name", name)
70
+	if err != nil {
71
+		return "", err
72
+	}
73
+
74
+	return tagsURL.String(), nil
75
+}
76
+
77
+// BuildManifestURL constructs a url for the manifest identified by name and tag.
78
+func (ub *URLBuilder) BuildManifestURL(name, tag string) (string, error) {
79
+	route := ub.cloneRoute(RouteNameManifest)
80
+
81
+	manifestURL, err := route.URL("name", name, "tag", tag)
82
+	if err != nil {
83
+		return "", err
84
+	}
85
+
86
+	return manifestURL.String(), nil
87
+}
88
+
89
+// BuildBlobURL constructs the url for the blob identified by name and dgst.
90
+func (ub *URLBuilder) BuildBlobURL(name string, dgst digest.Digest) (string, error) {
91
+	route := ub.cloneRoute(RouteNameBlob)
92
+
93
+	layerURL, err := route.URL("name", name, "digest", dgst.String())
94
+	if err != nil {
95
+		return "", err
96
+	}
97
+
98
+	return layerURL.String(), nil
99
+}
100
+
101
+// BuildBlobUploadURL constructs a url to begin a blob upload in the
102
+// repository identified by name.
103
+func (ub *URLBuilder) BuildBlobUploadURL(name string, values ...url.Values) (string, error) {
104
+	route := ub.cloneRoute(RouteNameBlobUpload)
105
+
106
+	uploadURL, err := route.URL("name", name)
107
+	if err != nil {
108
+		return "", err
109
+	}
110
+
111
+	return appendValuesURL(uploadURL, values...).String(), nil
112
+}
113
+
114
+// BuildBlobUploadChunkURL constructs a url for the upload identified by uuid,
115
+// including any url values. This should generally not be used by clients, as
116
+// this url is provided by server implementations during the blob upload
117
+// process.
118
+func (ub *URLBuilder) BuildBlobUploadChunkURL(name, uuid string, values ...url.Values) (string, error) {
119
+	route := ub.cloneRoute(RouteNameBlobUploadChunk)
120
+
121
+	uploadURL, err := route.URL("name", name, "uuid", uuid)
122
+	if err != nil {
123
+		return "", err
124
+	}
125
+
126
+	return appendValuesURL(uploadURL, values...).String(), nil
127
+}
128
+
129
+// clondedRoute returns a clone of the named route from the router. Routes
130
+// must be cloned to avoid modifying them during url generation.
131
+func (ub *URLBuilder) cloneRoute(name string) *mux.Route {
132
+	route := new(mux.Route)
133
+	*route = *ub.router.GetRoute(name) // clone the route
134
+
135
+	return route.
136
+		Schemes(ub.root.Scheme).
137
+		Host(ub.root.Host)
138
+}
139
+
140
+// appendValuesURL appends the parameters to the url.
141
+func appendValuesURL(u *url.URL, values ...url.Values) *url.URL {
142
+	merged := u.Query()
143
+
144
+	for _, v := range values {
145
+		for k, vv := range v {
146
+			merged[k] = append(merged[k], vv...)
147
+		}
148
+	}
149
+
150
+	u.RawQuery = merged.Encode()
151
+	return u
152
+}
153
+
154
+// appendValues appends the parameters to the url. Panics if the string is not
155
+// a url.
156
+func appendValues(u string, values ...url.Values) string {
157
+	up, err := url.Parse(u)
158
+
159
+	if err != nil {
160
+		panic(err) // should never happen
161
+	}
162
+
163
+	return appendValuesURL(up, values...).String()
164
+}
0 165
new file mode 100644
... ...
@@ -0,0 +1,100 @@
0
+package v2
1
+
2
+import (
3
+	"net/url"
4
+	"testing"
5
+)
6
+
7
+type urlBuilderTestCase struct {
8
+	description string
9
+	expected    string
10
+	build       func() (string, error)
11
+}
12
+
13
+// TestURLBuilder tests the various url building functions, ensuring they are
14
+// returning the expected values.
15
+func TestURLBuilder(t *testing.T) {
16
+
17
+	root := "http://localhost:5000/"
18
+	urlBuilder, err := NewURLBuilderFromString(root)
19
+	if err != nil {
20
+		t.Fatalf("unexpected error creating urlbuilder: %v", err)
21
+	}
22
+
23
+	for _, testcase := range []struct {
24
+		description string
25
+		expected    string
26
+		build       func() (string, error)
27
+	}{
28
+		{
29
+			description: "test base url",
30
+			expected:    "http://localhost:5000/v2/",
31
+			build:       urlBuilder.BuildBaseURL,
32
+		},
33
+		{
34
+			description: "test tags url",
35
+			expected:    "http://localhost:5000/v2/foo/bar/tags/list",
36
+			build: func() (string, error) {
37
+				return urlBuilder.BuildTagsURL("foo/bar")
38
+			},
39
+		},
40
+		{
41
+			description: "test manifest url",
42
+			expected:    "http://localhost:5000/v2/foo/bar/manifests/tag",
43
+			build: func() (string, error) {
44
+				return urlBuilder.BuildManifestURL("foo/bar", "tag")
45
+			},
46
+		},
47
+		{
48
+			description: "build blob url",
49
+			expected:    "http://localhost:5000/v2/foo/bar/blobs/tarsum.v1+sha256:abcdef0123456789",
50
+			build: func() (string, error) {
51
+				return urlBuilder.BuildBlobURL("foo/bar", "tarsum.v1+sha256:abcdef0123456789")
52
+			},
53
+		},
54
+		{
55
+			description: "build blob upload url",
56
+			expected:    "http://localhost:5000/v2/foo/bar/blobs/uploads/",
57
+			build: func() (string, error) {
58
+				return urlBuilder.BuildBlobUploadURL("foo/bar")
59
+			},
60
+		},
61
+		{
62
+			description: "build blob upload url with digest and size",
63
+			expected:    "http://localhost:5000/v2/foo/bar/blobs/uploads/?digest=tarsum.v1%2Bsha256%3Aabcdef0123456789&size=10000",
64
+			build: func() (string, error) {
65
+				return urlBuilder.BuildBlobUploadURL("foo/bar", url.Values{
66
+					"size":   []string{"10000"},
67
+					"digest": []string{"tarsum.v1+sha256:abcdef0123456789"},
68
+				})
69
+			},
70
+		},
71
+		{
72
+			description: "build blob upload chunk url",
73
+			expected:    "http://localhost:5000/v2/foo/bar/blobs/uploads/uuid-part",
74
+			build: func() (string, error) {
75
+				return urlBuilder.BuildBlobUploadChunkURL("foo/bar", "uuid-part")
76
+			},
77
+		},
78
+		{
79
+			description: "build blob upload chunk url with digest and size",
80
+			expected:    "http://localhost:5000/v2/foo/bar/blobs/uploads/uuid-part?digest=tarsum.v1%2Bsha256%3Aabcdef0123456789&size=10000",
81
+			build: func() (string, error) {
82
+				return urlBuilder.BuildBlobUploadChunkURL("foo/bar", "uuid-part", url.Values{
83
+					"size":   []string{"10000"},
84
+					"digest": []string{"tarsum.v1+sha256:abcdef0123456789"},
85
+				})
86
+			},
87
+		},
88
+	} {
89
+		u, err := testcase.build()
90
+		if err != nil {
91
+			t.Fatalf("%s: error building url: %v", testcase.description, err)
92
+		}
93
+
94
+		if u != testcase.expected {
95
+			t.Fatalf("%s: %q != %q", testcase.description, u, testcase.expected)
96
+		}
97
+	}
98
+
99
+}