Browse code

Merge pull request #11478 from dmcgowan/v2-vendored-api

Use v2 api from distribution

Tibor Vass authored on 2015/04/11 06:22:38
Showing 29 changed files
... ...
@@ -495,7 +495,7 @@ func (s *TagStore) pullV2Tag(eng *engine.Engine, r *registry.Session, out io.Wri
495 495
 					return err
496 496
 				}
497 497
 
498
-				r, l, err := r.GetV2ImageBlobReader(endpoint, repoInfo.RemoteName, di.digest.Algorithm(), di.digest.Hex(), auth)
498
+				r, l, err := r.GetV2ImageBlobReader(endpoint, repoInfo.RemoteName, di.digest, auth)
499 499
 				if err != nil {
500 500
 					return err
501 501
 				}
... ...
@@ -8,10 +8,10 @@ import (
8 8
 	"io/ioutil"
9 9
 	"os"
10 10
 	"path"
11
-	"strings"
12 11
 	"sync"
13 12
 
14 13
 	"github.com/Sirupsen/logrus"
14
+	"github.com/docker/distribution/digest"
15 15
 	"github.com/docker/docker/engine"
16 16
 	"github.com/docker/docker/image"
17 17
 	"github.com/docker/docker/pkg/progressreader"
... ...
@@ -376,13 +376,13 @@ func (s *TagStore) pushV2Repository(r *registry.Session, localRepo Repository, o
376 376
 
377 377
 			var exists bool
378 378
 			if len(checksum) > 0 {
379
-				sumParts := strings.SplitN(checksum, ":", 2)
380
-				if len(sumParts) < 2 {
381
-					return fmt.Errorf("Invalid checksum: %s", checksum)
379
+				dgst, err := digest.ParseDigest(checksum)
380
+				if err != nil {
381
+					return fmt.Errorf("Invalid checksum %s: %s", checksum, err)
382 382
 				}
383 383
 
384 384
 				// Call mount blob
385
-				exists, err = r.HeadV2ImageBlob(endpoint, repoInfo.RemoteName, sumParts[0], sumParts[1], auth)
385
+				exists, err = r.HeadV2ImageBlob(endpoint, repoInfo.RemoteName, dgst, auth)
386 386
 				if err != nil {
387 387
 					out.Write(sf.FormatProgress(stringid.TruncateID(layer.ID), "Image push failed", nil))
388 388
 					return err
... ...
@@ -468,7 +468,7 @@ func (s *TagStore) pushV2Image(r *registry.Session, img *image.Image, endpoint *
468 468
 	// Send the layer
469 469
 	logrus.Debugf("rendered layer for %s of [%d] size", img.ID, size)
470 470
 
471
-	if err := r.PutV2ImageBlob(endpoint, imageName, dgst.Algorithm(), dgst.Hex(),
471
+	if err := r.PutV2ImageBlob(endpoint, imageName, dgst,
472 472
 		progressreader.New(progressreader.Config{
473 473
 			In:        tf,
474 474
 			Out:       out,
... ...
@@ -43,7 +43,7 @@ clone git github.com/kr/pty 05017fcccf
43 43
 
44 44
 clone git github.com/gorilla/context 14f550f51a
45 45
 
46
-clone git github.com/gorilla/mux 136d54f81f
46
+clone git github.com/gorilla/mux e444e69cbd
47 47
 
48 48
 clone git github.com/tchap/go-patricia v1.0.1
49 49
 
... ...
@@ -68,12 +68,15 @@ if [ "$1" = '--go' ]; then
68 68
 	mv tmp-tar src/code.google.com/p/go/src/pkg/archive/tar
69 69
 fi
70 70
 
71
-# get digest package from distribution
71
+# get distribution packages
72 72
 clone git github.com/docker/distribution d957768537c5af40e4f4cd96871f7b2bde9e2923
73 73
 mv src/github.com/docker/distribution/digest tmp-digest
74
+mv src/github.com/docker/distribution/registry/api tmp-api
74 75
 rm -rf src/github.com/docker/distribution
75 76
 mkdir -p src/github.com/docker/distribution
76 77
 mv tmp-digest src/github.com/docker/distribution/digest
78
+mkdir -p src/github.com/docker/distribution/registry
79
+mv tmp-api src/github.com/docker/distribution/registry/api
77 80
 
78 81
 clone git github.com/docker/libcontainer bd8ec36106086f72b66e1be85a81202b93503e44
79 82
 # see src/github.com/docker/libcontainer/update-vendor.sh which is the "source of truth" for libcontainer deps (just like this file)
... ...
@@ -11,8 +11,8 @@ import (
11 11
 	"strings"
12 12
 
13 13
 	"github.com/Sirupsen/logrus"
14
+	"github.com/docker/distribution/registry/api/v2"
14 15
 	"github.com/docker/docker/pkg/requestdecorator"
15
-	"github.com/docker/docker/registry/v2"
16 16
 )
17 17
 
18 18
 // for mocking in unit tests
... ...
@@ -11,7 +11,7 @@ import (
11 11
 
12 12
 	"github.com/Sirupsen/logrus"
13 13
 	"github.com/docker/distribution/digest"
14
-	"github.com/docker/docker/registry/v2"
14
+	"github.com/docker/distribution/registry/api/v2"
15 15
 	"github.com/docker/docker/utils"
16 16
 )
17 17
 
... ...
@@ -109,8 +109,8 @@ func (r *Session) GetV2ImageManifest(ep *Endpoint, imageName, tagName string, au
109 109
 // - Succeeded to head image blob (already exists)
110 110
 // - Failed with no error (continue to Push the Blob)
111 111
 // - Failed with error
112
-func (r *Session) HeadV2ImageBlob(ep *Endpoint, imageName, sumType, sum string, auth *RequestAuthorization) (bool, error) {
113
-	routeURL, err := getV2Builder(ep).BuildBlobURL(imageName, sumType+":"+sum)
112
+func (r *Session) HeadV2ImageBlob(ep *Endpoint, imageName string, dgst digest.Digest, auth *RequestAuthorization) (bool, error) {
113
+	routeURL, err := getV2Builder(ep).BuildBlobURL(imageName, dgst)
114 114
 	if err != nil {
115 115
 		return false, err
116 116
 	}
... ...
@@ -141,11 +141,11 @@ func (r *Session) HeadV2ImageBlob(ep *Endpoint, imageName, sumType, sum string,
141 141
 		return false, nil
142 142
 	}
143 143
 
144
-	return false, utils.NewHTTPRequestError(fmt.Sprintf("Server error: %d trying head request for %s - %s:%s", res.StatusCode, imageName, sumType, sum), res)
144
+	return false, utils.NewHTTPRequestError(fmt.Sprintf("Server error: %d trying head request for %s - %s", res.StatusCode, imageName, dgst), res)
145 145
 }
146 146
 
147
-func (r *Session) GetV2ImageBlob(ep *Endpoint, imageName, sumType, sum string, blobWrtr io.Writer, auth *RequestAuthorization) error {
148
-	routeURL, err := getV2Builder(ep).BuildBlobURL(imageName, sumType+":"+sum)
147
+func (r *Session) GetV2ImageBlob(ep *Endpoint, imageName string, dgst digest.Digest, blobWrtr io.Writer, auth *RequestAuthorization) error {
148
+	routeURL, err := getV2Builder(ep).BuildBlobURL(imageName, dgst)
149 149
 	if err != nil {
150 150
 		return err
151 151
 	}
... ...
@@ -175,8 +175,8 @@ func (r *Session) GetV2ImageBlob(ep *Endpoint, imageName, sumType, sum string, b
175 175
 	return err
176 176
 }
177 177
 
178
-func (r *Session) GetV2ImageBlobReader(ep *Endpoint, imageName, sumType, sum string, auth *RequestAuthorization) (io.ReadCloser, int64, error) {
179
-	routeURL, err := getV2Builder(ep).BuildBlobURL(imageName, sumType+":"+sum)
178
+func (r *Session) GetV2ImageBlobReader(ep *Endpoint, imageName string, dgst digest.Digest, auth *RequestAuthorization) (io.ReadCloser, int64, error) {
179
+	routeURL, err := getV2Builder(ep).BuildBlobURL(imageName, dgst)
180 180
 	if err != nil {
181 181
 		return nil, 0, err
182 182
 	}
... ...
@@ -198,7 +198,7 @@ func (r *Session) GetV2ImageBlobReader(ep *Endpoint, imageName, sumType, sum str
198 198
 		if res.StatusCode == 401 {
199 199
 			return nil, 0, errLoginRequired
200 200
 		}
201
-		return nil, 0, utils.NewHTTPRequestError(fmt.Sprintf("Server error: %d trying to pull %s blob - %s:%s", res.StatusCode, imageName, sumType, sum), res)
201
+		return nil, 0, utils.NewHTTPRequestError(fmt.Sprintf("Server error: %d trying to pull %s blob - %s", res.StatusCode, imageName, dgst), res)
202 202
 	}
203 203
 	lenStr := res.Header.Get("Content-Length")
204 204
 	l, err := strconv.ParseInt(lenStr, 10, 64)
... ...
@@ -212,7 +212,7 @@ func (r *Session) GetV2ImageBlobReader(ep *Endpoint, imageName, sumType, sum str
212 212
 // Push the image to the server for storage.
213 213
 // 'layer' is an uncompressed reader of the blob to be pushed.
214 214
 // The server will generate it's own checksum calculation.
215
-func (r *Session) PutV2ImageBlob(ep *Endpoint, imageName, sumType, sumStr string, blobRdr io.Reader, auth *RequestAuthorization) error {
215
+func (r *Session) PutV2ImageBlob(ep *Endpoint, imageName string, dgst digest.Digest, blobRdr io.Reader, auth *RequestAuthorization) error {
216 216
 	location, err := r.initiateBlobUpload(ep, imageName, auth)
217 217
 	if err != nil {
218 218
 		return err
... ...
@@ -225,7 +225,7 @@ func (r *Session) PutV2ImageBlob(ep *Endpoint, imageName, sumType, sumStr string
225 225
 		return err
226 226
 	}
227 227
 	queryParams := req.URL.Query()
228
-	queryParams.Add("digest", sumType+":"+sumStr)
228
+	queryParams.Add("digest", dgst.String())
229 229
 	req.URL.RawQuery = queryParams.Encode()
230 230
 	if err := auth.Authorize(req); err != nil {
231 231
 		return err
... ...
@@ -245,7 +245,7 @@ func (r *Session) PutV2ImageBlob(ep *Endpoint, imageName, sumType, sumStr string
245 245
 			return err
246 246
 		}
247 247
 		logrus.Debugf("Unexpected response from server: %q %#v", errBody, res.Header)
248
-		return utils.NewHTTPRequestError(fmt.Sprintf("Server error: %d trying to push %s blob - %s:%s", res.StatusCode, imageName, sumType, sumStr), res)
248
+		return utils.NewHTTPRequestError(fmt.Sprintf("Server error: %d trying to push %s blob - %s", res.StatusCode, imageName, dgst), res)
249 249
 	}
250 250
 
251 251
 	return nil
252 252
deleted file mode 100644
... ...
@@ -1,144 +0,0 @@
1
-package v2
2
-
3
-import "net/http"
4
-
5
-// TODO(stevvooe): Add route descriptors for each named route, along with
6
-// accepted methods, parameters, returned status codes and error codes.
7
-
8
-// ErrorDescriptor provides relevant information about a given error code.
9
-type ErrorDescriptor struct {
10
-	// Code is the error code that this descriptor describes.
11
-	Code ErrorCode
12
-
13
-	// Value provides a unique, string key, often captilized with
14
-	// underscores, to identify the error code. This value is used as the
15
-	// keyed value when serializing api errors.
16
-	Value string
17
-
18
-	// Message is a short, human readable decription of the error condition
19
-	// included in API responses.
20
-	Message string
21
-
22
-	// Description provides a complete account of the errors purpose, suitable
23
-	// for use in documentation.
24
-	Description string
25
-
26
-	// HTTPStatusCodes provides a list of status under which this error
27
-	// condition may arise. If it is empty, the error condition may be seen
28
-	// for any status code.
29
-	HTTPStatusCodes []int
30
-}
31
-
32
-// ErrorDescriptors provides a list of HTTP API Error codes that may be
33
-// encountered when interacting with the registry API.
34
-var ErrorDescriptors = []ErrorDescriptor{
35
-	{
36
-		Code:    ErrorCodeUnknown,
37
-		Value:   "UNKNOWN",
38
-		Message: "unknown error",
39
-		Description: `Generic error returned when the error does not have an
40
-		API classification.`,
41
-	},
42
-	{
43
-		Code:    ErrorCodeDigestInvalid,
44
-		Value:   "DIGEST_INVALID",
45
-		Message: "provided digest did not match uploaded content",
46
-		Description: `When a blob is uploaded, the registry will check that
47
-		the content matches the digest provided by the client. The error may
48
-		include a detail structure with the key "digest", including the
49
-		invalid digest string. This error may also be returned when a manifest
50
-		includes an invalid layer digest.`,
51
-		HTTPStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
52
-	},
53
-	{
54
-		Code:    ErrorCodeSizeInvalid,
55
-		Value:   "SIZE_INVALID",
56
-		Message: "provided length did not match content length",
57
-		Description: `When a layer is uploaded, the provided size will be
58
-		checked against the uploaded content. If they do not match, this error
59
-		will be returned.`,
60
-		HTTPStatusCodes: []int{http.StatusBadRequest},
61
-	},
62
-	{
63
-		Code:    ErrorCodeNameInvalid,
64
-		Value:   "NAME_INVALID",
65
-		Message: "manifest name did not match URI",
66
-		Description: `During a manifest upload, if the name in the manifest
67
-		does not match the uri name, this error will be returned.`,
68
-		HTTPStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
69
-	},
70
-	{
71
-		Code:    ErrorCodeTagInvalid,
72
-		Value:   "TAG_INVALID",
73
-		Message: "manifest tag did not match URI",
74
-		Description: `During a manifest upload, if the tag in the manifest
75
-		does not match the uri tag, this error will be returned.`,
76
-		HTTPStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
77
-	},
78
-	{
79
-		Code:    ErrorCodeNameUnknown,
80
-		Value:   "NAME_UNKNOWN",
81
-		Message: "repository name not known to registry",
82
-		Description: `This is returned if the name used during an operation is
83
-		unknown to the registry.`,
84
-		HTTPStatusCodes: []int{http.StatusNotFound},
85
-	},
86
-	{
87
-		Code:    ErrorCodeManifestUnknown,
88
-		Value:   "MANIFEST_UNKNOWN",
89
-		Message: "manifest unknown",
90
-		Description: `This error is returned when the manifest, identified by
91
-		name and tag is unknown to the repository.`,
92
-		HTTPStatusCodes: []int{http.StatusNotFound},
93
-	},
94
-	{
95
-		Code:    ErrorCodeManifestInvalid,
96
-		Value:   "MANIFEST_INVALID",
97
-		Message: "manifest invalid",
98
-		Description: `During upload, manifests undergo several checks ensuring
99
-		validity. If those checks fail, this error may be returned, unless a
100
-		more specific error is included. The detail will contain information
101
-		the failed validation.`,
102
-		HTTPStatusCodes: []int{http.StatusBadRequest},
103
-	},
104
-	{
105
-		Code:    ErrorCodeManifestUnverified,
106
-		Value:   "MANIFEST_UNVERIFIED",
107
-		Message: "manifest failed signature verification",
108
-		Description: `During manifest upload, if the manifest fails signature
109
-		verification, this error will be returned.`,
110
-		HTTPStatusCodes: []int{http.StatusBadRequest},
111
-	},
112
-	{
113
-		Code:    ErrorCodeBlobUnknown,
114
-		Value:   "BLOB_UNKNOWN",
115
-		Message: "blob unknown to registry",
116
-		Description: `This error may be returned when a blob is unknown to the
117
-		registry in a specified repository. This can be returned with a
118
-		standard get or if a manifest references an unknown layer during
119
-		upload.`,
120
-		HTTPStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
121
-	},
122
-
123
-	{
124
-		Code:    ErrorCodeBlobUploadUnknown,
125
-		Value:   "BLOB_UPLOAD_UNKNOWN",
126
-		Message: "blob upload unknown to registry",
127
-		Description: `If a blob upload has been cancelled or was never
128
-		started, this error code may be returned.`,
129
-		HTTPStatusCodes: []int{http.StatusNotFound},
130
-	},
131
-}
132
-
133
-var errorCodeToDescriptors map[ErrorCode]ErrorDescriptor
134
-var idToDescriptors map[string]ErrorDescriptor
135
-
136
-func init() {
137
-	errorCodeToDescriptors = make(map[ErrorCode]ErrorDescriptor, len(ErrorDescriptors))
138
-	idToDescriptors = make(map[string]ErrorDescriptor, len(ErrorDescriptors))
139
-
140
-	for _, descriptor := range ErrorDescriptors {
141
-		errorCodeToDescriptors[descriptor.Code] = descriptor
142
-		idToDescriptors[descriptor.Value] = descriptor
143
-	}
144
-}
145 1
deleted file mode 100644
... ...
@@ -1,13 +0,0 @@
1
-// Package v2 describes routes, urls and the error codes used in the Docker
2
-// Registry JSON HTTP API V2. In addition to declarations, descriptors are
3
-// provided for routes and error codes that can be used for implementation and
4
-// automatically generating documentation.
5
-//
6
-// Definitions here are considered to be locked down for the V2 registry api.
7
-// Any changes must be considered carefully and should not proceed without a
8
-// change proposal.
9
-//
10
-// Currently, while the HTTP API definitions are considered stable, the Go API
11
-// exports are considered unstable. Go API consumers should take care when
12
-// relying on these definitions until this message is deleted.
13
-package v2
14 1
deleted file mode 100644
... ...
@@ -1,185 +0,0 @@
1
-package v2
2
-
3
-import (
4
-	"fmt"
5
-	"strings"
6
-)
7
-
8
-// ErrorCode represents the error type. The errors are serialized via strings
9
-// and the integer format may change and should *never* be exported.
10
-type ErrorCode int
11
-
12
-const (
13
-	// ErrorCodeUnknown is a catch-all for errors not defined below.
14
-	ErrorCodeUnknown ErrorCode = iota
15
-
16
-	// ErrorCodeDigestInvalid is returned when uploading a blob if the
17
-	// provided digest does not match the blob contents.
18
-	ErrorCodeDigestInvalid
19
-
20
-	// ErrorCodeSizeInvalid is returned when uploading a blob if the provided
21
-	// size does not match the content length.
22
-	ErrorCodeSizeInvalid
23
-
24
-	// ErrorCodeNameInvalid is returned when the name in the manifest does not
25
-	// match the provided name.
26
-	ErrorCodeNameInvalid
27
-
28
-	// ErrorCodeTagInvalid is returned when the tag in the manifest does not
29
-	// match the provided tag.
30
-	ErrorCodeTagInvalid
31
-
32
-	// ErrorCodeNameUnknown when the repository name is not known.
33
-	ErrorCodeNameUnknown
34
-
35
-	// ErrorCodeManifestUnknown returned when image manifest is unknown.
36
-	ErrorCodeManifestUnknown
37
-
38
-	// ErrorCodeManifestInvalid returned when an image manifest is invalid,
39
-	// typically during a PUT operation. This error encompasses all errors
40
-	// encountered during manifest validation that aren't signature errors.
41
-	ErrorCodeManifestInvalid
42
-
43
-	// ErrorCodeManifestUnverified is returned when the manifest fails
44
-	// signature verfication.
45
-	ErrorCodeManifestUnverified
46
-
47
-	// ErrorCodeBlobUnknown is returned when a blob is unknown to the
48
-	// registry. This can happen when the manifest references a nonexistent
49
-	// layer or the result is not found by a blob fetch.
50
-	ErrorCodeBlobUnknown
51
-
52
-	// ErrorCodeBlobUploadUnknown is returned when an upload is unknown.
53
-	ErrorCodeBlobUploadUnknown
54
-)
55
-
56
-// ParseErrorCode attempts to parse the error code string, returning
57
-// ErrorCodeUnknown if the error is not known.
58
-func ParseErrorCode(s string) ErrorCode {
59
-	desc, ok := idToDescriptors[s]
60
-
61
-	if !ok {
62
-		return ErrorCodeUnknown
63
-	}
64
-
65
-	return desc.Code
66
-}
67
-
68
-// Descriptor returns the descriptor for the error code.
69
-func (ec ErrorCode) Descriptor() ErrorDescriptor {
70
-	d, ok := errorCodeToDescriptors[ec]
71
-
72
-	if !ok {
73
-		return ErrorCodeUnknown.Descriptor()
74
-	}
75
-
76
-	return d
77
-}
78
-
79
-// String returns the canonical identifier for this error code.
80
-func (ec ErrorCode) String() string {
81
-	return ec.Descriptor().Value
82
-}
83
-
84
-// Message returned the human-readable error message for this error code.
85
-func (ec ErrorCode) Message() string {
86
-	return ec.Descriptor().Message
87
-}
88
-
89
-// MarshalText encodes the receiver into UTF-8-encoded text and returns the
90
-// result.
91
-func (ec ErrorCode) MarshalText() (text []byte, err error) {
92
-	return []byte(ec.String()), nil
93
-}
94
-
95
-// UnmarshalText decodes the form generated by MarshalText.
96
-func (ec *ErrorCode) UnmarshalText(text []byte) error {
97
-	desc, ok := idToDescriptors[string(text)]
98
-
99
-	if !ok {
100
-		desc = ErrorCodeUnknown.Descriptor()
101
-	}
102
-
103
-	*ec = desc.Code
104
-
105
-	return nil
106
-}
107
-
108
-// Error provides a wrapper around ErrorCode with extra Details provided.
109
-type Error struct {
110
-	Code    ErrorCode   `json:"code"`
111
-	Message string      `json:"message,omitempty"`
112
-	Detail  interface{} `json:"detail,omitempty"`
113
-}
114
-
115
-// Error returns a human readable representation of the error.
116
-func (e Error) Error() string {
117
-	return fmt.Sprintf("%s: %s",
118
-		strings.ToLower(strings.Replace(e.Code.String(), "_", " ", -1)),
119
-		e.Message)
120
-}
121
-
122
-// Errors provides the envelope for multiple errors and a few sugar methods
123
-// for use within the application.
124
-type Errors struct {
125
-	Errors []Error `json:"errors,omitempty"`
126
-}
127
-
128
-// Push pushes an error on to the error stack, with the optional detail
129
-// argument. It is a programming error (ie panic) to push more than one
130
-// detail at a time.
131
-func (errs *Errors) Push(code ErrorCode, details ...interface{}) {
132
-	if len(details) > 1 {
133
-		panic("please specify zero or one detail items for this error")
134
-	}
135
-
136
-	var detail interface{}
137
-	if len(details) > 0 {
138
-		detail = details[0]
139
-	}
140
-
141
-	if err, ok := detail.(error); ok {
142
-		detail = err.Error()
143
-	}
144
-
145
-	errs.PushErr(Error{
146
-		Code:    code,
147
-		Message: code.Message(),
148
-		Detail:  detail,
149
-	})
150
-}
151
-
152
-// PushErr pushes an error interface onto the error stack.
153
-func (errs *Errors) PushErr(err error) {
154
-	switch err.(type) {
155
-	case Error:
156
-		errs.Errors = append(errs.Errors, err.(Error))
157
-	default:
158
-		errs.Errors = append(errs.Errors, Error{Message: err.Error()})
159
-	}
160
-}
161
-
162
-func (errs *Errors) Error() string {
163
-	switch errs.Len() {
164
-	case 0:
165
-		return "<nil>"
166
-	case 1:
167
-		return errs.Errors[0].Error()
168
-	default:
169
-		msg := "errors:\n"
170
-		for _, err := range errs.Errors {
171
-			msg += err.Error() + "\n"
172
-		}
173
-		return msg
174
-	}
175
-}
176
-
177
-// Clear clears the errors.
178
-func (errs *Errors) Clear() {
179
-	errs.Errors = errs.Errors[:0]
180
-}
181
-
182
-// Len returns the current number of errors.
183
-func (errs *Errors) Len() int {
184
-	return len(errs.Errors)
185
-}
186 1
deleted file mode 100644
... ...
@@ -1,163 +0,0 @@
1
-package v2
2
-
3
-import (
4
-	"encoding/json"
5
-	"reflect"
6
-	"testing"
7
-)
8
-
9
-// TestErrorCodes ensures that error code format, mappings and
10
-// marshaling/unmarshaling. round trips are stable.
11
-func TestErrorCodes(t *testing.T) {
12
-	for _, desc := range ErrorDescriptors {
13
-		if desc.Code.String() != desc.Value {
14
-			t.Fatalf("error code string incorrect: %q != %q", desc.Code.String(), desc.Value)
15
-		}
16
-
17
-		if desc.Code.Message() != desc.Message {
18
-			t.Fatalf("incorrect message for error code %v: %q != %q", desc.Code, desc.Code.Message(), desc.Message)
19
-		}
20
-
21
-		// Serialize the error code using the json library to ensure that we
22
-		// get a string and it works round trip.
23
-		p, err := json.Marshal(desc.Code)
24
-
25
-		if err != nil {
26
-			t.Fatalf("error marshaling error code %v: %v", desc.Code, err)
27
-		}
28
-
29
-		if len(p) <= 0 {
30
-			t.Fatalf("expected content in marshaled before for error code %v", desc.Code)
31
-		}
32
-
33
-		// First, unmarshal to interface and ensure we have a string.
34
-		var ecUnspecified interface{}
35
-		if err := json.Unmarshal(p, &ecUnspecified); err != nil {
36
-			t.Fatalf("error unmarshaling error code %v: %v", desc.Code, err)
37
-		}
38
-
39
-		if _, ok := ecUnspecified.(string); !ok {
40
-			t.Fatalf("expected a string for error code %v on unmarshal got a %T", desc.Code, ecUnspecified)
41
-		}
42
-
43
-		// Now, unmarshal with the error code type and ensure they are equal
44
-		var ecUnmarshaled ErrorCode
45
-		if err := json.Unmarshal(p, &ecUnmarshaled); err != nil {
46
-			t.Fatalf("error unmarshaling error code %v: %v", desc.Code, err)
47
-		}
48
-
49
-		if ecUnmarshaled != desc.Code {
50
-			t.Fatalf("unexpected error code during error code marshal/unmarshal: %v != %v", ecUnmarshaled, desc.Code)
51
-		}
52
-	}
53
-}
54
-
55
-// TestErrorsManagement does a quick check of the Errors type to ensure that
56
-// members are properly pushed and marshaled.
57
-func TestErrorsManagement(t *testing.T) {
58
-	var errs Errors
59
-
60
-	errs.Push(ErrorCodeDigestInvalid)
61
-	errs.Push(ErrorCodeBlobUnknown,
62
-		map[string]string{"digest": "sometestblobsumdoesntmatter"})
63
-
64
-	p, err := json.Marshal(errs)
65
-
66
-	if err != nil {
67
-		t.Fatalf("error marashaling errors: %v", err)
68
-	}
69
-
70
-	expectedJSON := "{\"errors\":[{\"code\":\"DIGEST_INVALID\",\"message\":\"provided digest did not match uploaded content\"},{\"code\":\"BLOB_UNKNOWN\",\"message\":\"blob unknown to registry\",\"detail\":{\"digest\":\"sometestblobsumdoesntmatter\"}}]}"
71
-
72
-	if string(p) != expectedJSON {
73
-		t.Fatalf("unexpected json: %q != %q", string(p), expectedJSON)
74
-	}
75
-
76
-	errs.Clear()
77
-	errs.Push(ErrorCodeUnknown)
78
-	expectedJSON = "{\"errors\":[{\"code\":\"UNKNOWN\",\"message\":\"unknown error\"}]}"
79
-	p, err = json.Marshal(errs)
80
-
81
-	if err != nil {
82
-		t.Fatalf("error marashaling errors: %v", err)
83
-	}
84
-
85
-	if string(p) != expectedJSON {
86
-		t.Fatalf("unexpected json: %q != %q", string(p), expectedJSON)
87
-	}
88
-}
89
-
90
-// TestMarshalUnmarshal ensures that api errors can round trip through json
91
-// without losing information.
92
-func TestMarshalUnmarshal(t *testing.T) {
93
-
94
-	var errors Errors
95
-
96
-	for _, testcase := range []struct {
97
-		description string
98
-		err         Error
99
-	}{
100
-		{
101
-			description: "unknown error",
102
-			err: Error{
103
-
104
-				Code:    ErrorCodeUnknown,
105
-				Message: ErrorCodeUnknown.Descriptor().Message,
106
-			},
107
-		},
108
-		{
109
-			description: "unknown manifest",
110
-			err: Error{
111
-				Code:    ErrorCodeManifestUnknown,
112
-				Message: ErrorCodeManifestUnknown.Descriptor().Message,
113
-			},
114
-		},
115
-		{
116
-			description: "unknown manifest",
117
-			err: Error{
118
-				Code:    ErrorCodeBlobUnknown,
119
-				Message: ErrorCodeBlobUnknown.Descriptor().Message,
120
-				Detail:  map[string]interface{}{"digest": "asdfqwerqwerqwerqwer"},
121
-			},
122
-		},
123
-	} {
124
-		fatalf := func(format string, args ...interface{}) {
125
-			t.Fatalf(testcase.description+": "+format, args...)
126
-		}
127
-
128
-		unexpectedErr := func(err error) {
129
-			fatalf("unexpected error: %v", err)
130
-		}
131
-
132
-		p, err := json.Marshal(testcase.err)
133
-		if err != nil {
134
-			unexpectedErr(err)
135
-		}
136
-
137
-		var unmarshaled Error
138
-		if err := json.Unmarshal(p, &unmarshaled); err != nil {
139
-			unexpectedErr(err)
140
-		}
141
-
142
-		if !reflect.DeepEqual(unmarshaled, testcase.err) {
143
-			fatalf("errors not equal after round trip: %#v != %#v", unmarshaled, testcase.err)
144
-		}
145
-
146
-		// Roll everything up into an error response envelope.
147
-		errors.PushErr(testcase.err)
148
-	}
149
-
150
-	p, err := json.Marshal(errors)
151
-	if err != nil {
152
-		t.Fatalf("unexpected error marshaling error envelope: %v", err)
153
-	}
154
-
155
-	var unmarshaled Errors
156
-	if err := json.Unmarshal(p, &unmarshaled); err != nil {
157
-		t.Fatalf("unexpected error unmarshaling error envelope: %v", err)
158
-	}
159
-
160
-	if !reflect.DeepEqual(unmarshaled, errors) {
161
-		t.Fatalf("errors not equal after round trip: %#v != %#v", unmarshaled, errors)
162
-	}
163
-}
164 1
deleted file mode 100644
... ...
@@ -1,22 +0,0 @@
1
-package v2
2
-
3
-import "regexp"
4
-
5
-// This file defines regular expressions for use in route definition. These
6
-// are also defined in the registry code base. Until they are in a common,
7
-// shared location, and exported, they must be repeated here.
8
-
9
-// RepositoryNameComponentRegexp restricts registtry path components names to
10
-// start with at least two letters or numbers, with following parts able to
11
-// separated by one period, dash or underscore.
12
-var RepositoryNameComponentRegexp = regexp.MustCompile(`[a-z0-9]+(?:[._-][a-z0-9]+)*`)
13
-
14
-// RepositoryNameRegexp builds on RepositoryNameComponentRegexp to allow 1 to
15
-// 5 path components, separated by a forward slash.
16
-var RepositoryNameRegexp = regexp.MustCompile(`(?:` + RepositoryNameComponentRegexp.String() + `/){0,4}` + RepositoryNameComponentRegexp.String())
17
-
18
-// TagNameRegexp matches valid tag names. From docker/docker:graph/tags.go.
19
-var TagNameRegexp = regexp.MustCompile(`[\w][\w.-]{0,127}`)
20
-
21
-// DigestRegexp matches valid digest types.
22
-var DigestRegexp = regexp.MustCompile(`[a-zA-Z0-9-_+.]+:[a-zA-Z0-9-_+.=]+`)
23 1
deleted file mode 100644
... ...
@@ -1,66 +0,0 @@
1
-package v2
2
-
3
-import "github.com/gorilla/mux"
4
-
5
-// The following are definitions of the name under which all V2 routes are
6
-// registered. These symbols can be used to look up a route based on the name.
7
-const (
8
-	RouteNameBase            = "base"
9
-	RouteNameManifest        = "manifest"
10
-	RouteNameTags            = "tags"
11
-	RouteNameBlob            = "blob"
12
-	RouteNameBlobUpload      = "blob-upload"
13
-	RouteNameBlobUploadChunk = "blob-upload-chunk"
14
-)
15
-
16
-var allEndpoints = []string{
17
-	RouteNameManifest,
18
-	RouteNameTags,
19
-	RouteNameBlob,
20
-	RouteNameBlobUpload,
21
-	RouteNameBlobUploadChunk,
22
-}
23
-
24
-// Router builds a gorilla router with named routes for the various API
25
-// methods. This can be used directly by both server implementations and
26
-// clients.
27
-func Router() *mux.Router {
28
-	router := mux.NewRouter().
29
-		StrictSlash(true)
30
-
31
-	// GET /v2/	Check	Check that the registry implements API version 2(.1)
32
-	router.
33
-		Path("/v2/").
34
-		Name(RouteNameBase)
35
-
36
-	// GET      /v2/<name>/manifest/<reference>	Image Manifest	Fetch the image manifest identified by name and reference where reference can be a tag or digest.
37
-	// PUT      /v2/<name>/manifest/<reference>	Image Manifest	Upload the image manifest identified by name and reference where reference can be a tag or digest.
38
-	// DELETE   /v2/<name>/manifest/<reference>	Image Manifest	Delete the image identified by name and reference where reference can be a tag or digest.
39
-	router.
40
-		Path("/v2/{name:" + RepositoryNameRegexp.String() + "}/manifests/{reference:" + TagNameRegexp.String() + "|" + DigestRegexp.String() + "}").
41
-		Name(RouteNameManifest)
42
-
43
-	// GET	/v2/<name>/tags/list	Tags	Fetch the tags under the repository identified by name.
44
-	router.
45
-		Path("/v2/{name:" + RepositoryNameRegexp.String() + "}/tags/list").
46
-		Name(RouteNameTags)
47
-
48
-	// GET	/v2/<name>/blob/<digest>	Layer	Fetch the blob identified by digest.
49
-	router.
50
-		Path("/v2/{name:" + RepositoryNameRegexp.String() + "}/blobs/{digest:[a-zA-Z0-9-_+.]+:[a-zA-Z0-9-_+.=]+}").
51
-		Name(RouteNameBlob)
52
-
53
-	// POST	/v2/<name>/blob/upload/	Layer Upload	Initiate an upload of the layer identified by tarsum.
54
-	router.
55
-		Path("/v2/{name:" + RepositoryNameRegexp.String() + "}/blobs/uploads/").
56
-		Name(RouteNameBlobUpload)
57
-
58
-	// GET	/v2/<name>/blob/upload/<uuid>	Layer Upload	Get the status of the upload identified by tarsum and uuid.
59
-	// PUT	/v2/<name>/blob/upload/<uuid>	Layer Upload	Upload all or a chunk of the upload identified by tarsum and uuid.
60
-	// DELETE	/v2/<name>/blob/upload/<uuid>	Layer Upload	Cancel the upload identified by layer and uuid
61
-	router.
62
-		Path("/v2/{name:" + RepositoryNameRegexp.String() + "}/blobs/uploads/{uuid}").
63
-		Name(RouteNameBlobUploadChunk)
64
-
65
-	return router
66
-}
67 1
deleted file mode 100644
... ...
@@ -1,192 +0,0 @@
1
-package v2
2
-
3
-import (
4
-	"encoding/json"
5
-	"net/http"
6
-	"net/http/httptest"
7
-	"reflect"
8
-	"testing"
9
-
10
-	"github.com/gorilla/mux"
11
-)
12
-
13
-type routeTestCase struct {
14
-	RequestURI string
15
-	Vars       map[string]string
16
-	RouteName  string
17
-	StatusCode int
18
-}
19
-
20
-// TestRouter registers a test handler with all the routes and ensures that
21
-// each route returns the expected path variables. Not method verification is
22
-// present. This not meant to be exhaustive but as check to ensure that the
23
-// expected variables are extracted.
24
-//
25
-// This may go away as the application structure comes together.
26
-func TestRouter(t *testing.T) {
27
-
28
-	router := Router()
29
-
30
-	testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
31
-		testCase := routeTestCase{
32
-			RequestURI: r.RequestURI,
33
-			Vars:       mux.Vars(r),
34
-			RouteName:  mux.CurrentRoute(r).GetName(),
35
-		}
36
-
37
-		enc := json.NewEncoder(w)
38
-
39
-		if err := enc.Encode(testCase); err != nil {
40
-			http.Error(w, err.Error(), http.StatusInternalServerError)
41
-			return
42
-		}
43
-	})
44
-
45
-	// Startup test server
46
-	server := httptest.NewServer(router)
47
-
48
-	for _, testcase := range []routeTestCase{
49
-		{
50
-			RouteName:  RouteNameBase,
51
-			RequestURI: "/v2/",
52
-			Vars:       map[string]string{},
53
-		},
54
-		{
55
-			RouteName:  RouteNameManifest,
56
-			RequestURI: "/v2/foo/manifests/bar",
57
-			Vars: map[string]string{
58
-				"name":      "foo",
59
-				"reference": "bar",
60
-			},
61
-		},
62
-		{
63
-			RouteName:  RouteNameManifest,
64
-			RequestURI: "/v2/foo/bar/manifests/tag",
65
-			Vars: map[string]string{
66
-				"name":      "foo/bar",
67
-				"reference": "tag",
68
-			},
69
-		},
70
-		{
71
-			RouteName:  RouteNameTags,
72
-			RequestURI: "/v2/foo/bar/tags/list",
73
-			Vars: map[string]string{
74
-				"name": "foo/bar",
75
-			},
76
-		},
77
-		{
78
-			RouteName:  RouteNameBlob,
79
-			RequestURI: "/v2/foo/bar/blobs/tarsum.dev+foo:abcdef0919234",
80
-			Vars: map[string]string{
81
-				"name":   "foo/bar",
82
-				"digest": "tarsum.dev+foo:abcdef0919234",
83
-			},
84
-		},
85
-		{
86
-			RouteName:  RouteNameBlob,
87
-			RequestURI: "/v2/foo/bar/blobs/sha256:abcdef0919234",
88
-			Vars: map[string]string{
89
-				"name":   "foo/bar",
90
-				"digest": "sha256:abcdef0919234",
91
-			},
92
-		},
93
-		{
94
-			RouteName:  RouteNameBlobUpload,
95
-			RequestURI: "/v2/foo/bar/blobs/uploads/",
96
-			Vars: map[string]string{
97
-				"name": "foo/bar",
98
-			},
99
-		},
100
-		{
101
-			RouteName:  RouteNameBlobUploadChunk,
102
-			RequestURI: "/v2/foo/bar/blobs/uploads/uuid",
103
-			Vars: map[string]string{
104
-				"name": "foo/bar",
105
-				"uuid": "uuid",
106
-			},
107
-		},
108
-		{
109
-			RouteName:  RouteNameBlobUploadChunk,
110
-			RequestURI: "/v2/foo/bar/blobs/uploads/D95306FA-FAD3-4E36-8D41-CF1C93EF8286",
111
-			Vars: map[string]string{
112
-				"name": "foo/bar",
113
-				"uuid": "D95306FA-FAD3-4E36-8D41-CF1C93EF8286",
114
-			},
115
-		},
116
-		{
117
-			RouteName:  RouteNameBlobUploadChunk,
118
-			RequestURI: "/v2/foo/bar/blobs/uploads/RDk1MzA2RkEtRkFEMy00RTM2LThENDEtQ0YxQzkzRUY4Mjg2IA==",
119
-			Vars: map[string]string{
120
-				"name": "foo/bar",
121
-				"uuid": "RDk1MzA2RkEtRkFEMy00RTM2LThENDEtQ0YxQzkzRUY4Mjg2IA==",
122
-			},
123
-		},
124
-		{
125
-			// Check ambiguity: ensure we can distinguish between tags for
126
-			// "foo/bar/image/image" and image for "foo/bar/image" with tag
127
-			// "tags"
128
-			RouteName:  RouteNameManifest,
129
-			RequestURI: "/v2/foo/bar/manifests/manifests/tags",
130
-			Vars: map[string]string{
131
-				"name":      "foo/bar/manifests",
132
-				"reference": "tags",
133
-			},
134
-		},
135
-		{
136
-			// This case presents an ambiguity between foo/bar with tag="tags"
137
-			// and list tags for "foo/bar/manifest"
138
-			RouteName:  RouteNameTags,
139
-			RequestURI: "/v2/foo/bar/manifests/tags/list",
140
-			Vars: map[string]string{
141
-				"name": "foo/bar/manifests",
142
-			},
143
-		},
144
-		{
145
-			RouteName:  RouteNameBlobUploadChunk,
146
-			RequestURI: "/v2/foo/../../blob/uploads/D95306FA-FAD3-4E36-8D41-CF1C93EF8286",
147
-			StatusCode: http.StatusNotFound,
148
-		},
149
-	} {
150
-		// Register the endpoint
151
-		router.GetRoute(testcase.RouteName).Handler(testHandler)
152
-		u := server.URL + testcase.RequestURI
153
-
154
-		resp, err := http.Get(u)
155
-
156
-		if err != nil {
157
-			t.Fatalf("error issuing get request: %v", err)
158
-		}
159
-
160
-		if testcase.StatusCode == 0 {
161
-			// Override default, zero-value
162
-			testcase.StatusCode = http.StatusOK
163
-		}
164
-
165
-		if resp.StatusCode != testcase.StatusCode {
166
-			t.Fatalf("unexpected status for %s: %v %v", u, resp.Status, resp.StatusCode)
167
-		}
168
-
169
-		if testcase.StatusCode != http.StatusOK {
170
-			// We don't care about json response.
171
-			continue
172
-		}
173
-
174
-		dec := json.NewDecoder(resp.Body)
175
-
176
-		var actualRouteInfo routeTestCase
177
-		if err := dec.Decode(&actualRouteInfo); err != nil {
178
-			t.Fatalf("error reading json response: %v", err)
179
-		}
180
-		// Needs to be set out of band
181
-		actualRouteInfo.StatusCode = resp.StatusCode
182
-
183
-		if actualRouteInfo.RouteName != testcase.RouteName {
184
-			t.Fatalf("incorrect route %q matched, expected %q", actualRouteInfo.RouteName, testcase.RouteName)
185
-		}
186
-
187
-		if !reflect.DeepEqual(actualRouteInfo, testcase) {
188
-			t.Fatalf("actual does not equal expected: %#v != %#v", actualRouteInfo, testcase)
189
-		}
190
-	}
191
-
192
-}
193 1
deleted file mode 100644
... ...
@@ -1,179 +0,0 @@
1
-package v2
2
-
3
-import (
4
-	"net/http"
5
-	"net/url"
6
-
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 reference.
78
-func (ub *URLBuilder) BuildManifestURL(name, reference string) (string, error) {
79
-	route := ub.cloneRoute(RouteNameManifest)
80
-
81
-	manifestURL, err := route.URL("name", name, "reference", reference)
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 string) (string, error) {
91
-	route := ub.cloneRoute(RouteNameBlob)
92
-
93
-	layerURL, err := route.URL("name", name, "digest", dgst)
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) clonedRoute {
132
-	route := new(mux.Route)
133
-	root := new(url.URL)
134
-
135
-	*route = *ub.router.GetRoute(name) // clone the route
136
-	*root = *ub.root
137
-
138
-	return clonedRoute{Route: route, root: root}
139
-}
140
-
141
-type clonedRoute struct {
142
-	*mux.Route
143
-	root *url.URL
144
-}
145
-
146
-func (cr clonedRoute) URL(pairs ...string) (*url.URL, error) {
147
-	routeURL, err := cr.Route.URL(pairs...)
148
-	if err != nil {
149
-		return nil, err
150
-	}
151
-
152
-	return cr.root.ResolveReference(routeURL), nil
153
-}
154
-
155
-// appendValuesURL appends the parameters to the url.
156
-func appendValuesURL(u *url.URL, values ...url.Values) *url.URL {
157
-	merged := u.Query()
158
-
159
-	for _, v := range values {
160
-		for k, vv := range v {
161
-			merged[k] = append(merged[k], vv...)
162
-		}
163
-	}
164
-
165
-	u.RawQuery = merged.Encode()
166
-	return u
167
-}
168
-
169
-// appendValues appends the parameters to the url. Panics if the string is not
170
-// a url.
171
-func appendValues(u string, values ...url.Values) string {
172
-	up, err := url.Parse(u)
173
-
174
-	if err != nil {
175
-		panic(err) // should never happen
176
-	}
177
-
178
-	return appendValuesURL(up, values...).String()
179
-}
180 1
deleted file mode 100644
... ...
@@ -1,113 +0,0 @@
1
-package v2
2
-
3
-import (
4
-	"net/url"
5
-	"testing"
6
-)
7
-
8
-type urlBuilderTestCase struct {
9
-	description  string
10
-	expectedPath string
11
-	build        func() (string, error)
12
-}
13
-
14
-// TestURLBuilder tests the various url building functions, ensuring they are
15
-// returning the expected values.
16
-func TestURLBuilder(t *testing.T) {
17
-	var (
18
-		urlBuilder *URLBuilder
19
-		err        error
20
-	)
21
-
22
-	testCases := []urlBuilderTestCase{
23
-		{
24
-			description:  "test base url",
25
-			expectedPath: "/v2/",
26
-			build: func() (string, error) {
27
-				return urlBuilder.BuildBaseURL()
28
-			},
29
-		},
30
-		{
31
-			description:  "test tags url",
32
-			expectedPath: "/v2/foo/bar/tags/list",
33
-			build: func() (string, error) {
34
-				return urlBuilder.BuildTagsURL("foo/bar")
35
-			},
36
-		},
37
-		{
38
-			description:  "test manifest url",
39
-			expectedPath: "/v2/foo/bar/manifests/tag",
40
-			build: func() (string, error) {
41
-				return urlBuilder.BuildManifestURL("foo/bar", "tag")
42
-			},
43
-		},
44
-		{
45
-			description:  "build blob url",
46
-			expectedPath: "/v2/foo/bar/blobs/tarsum.v1+sha256:abcdef0123456789",
47
-			build: func() (string, error) {
48
-				return urlBuilder.BuildBlobURL("foo/bar", "tarsum.v1+sha256:abcdef0123456789")
49
-			},
50
-		},
51
-		{
52
-			description:  "build blob upload url",
53
-			expectedPath: "/v2/foo/bar/blobs/uploads/",
54
-			build: func() (string, error) {
55
-				return urlBuilder.BuildBlobUploadURL("foo/bar")
56
-			},
57
-		},
58
-		{
59
-			description:  "build blob upload url with digest and size",
60
-			expectedPath: "/v2/foo/bar/blobs/uploads/?digest=tarsum.v1%2Bsha256%3Aabcdef0123456789&size=10000",
61
-			build: func() (string, error) {
62
-				return urlBuilder.BuildBlobUploadURL("foo/bar", url.Values{
63
-					"size":   []string{"10000"},
64
-					"digest": []string{"tarsum.v1+sha256:abcdef0123456789"},
65
-				})
66
-			},
67
-		},
68
-		{
69
-			description:  "build blob upload chunk url",
70
-			expectedPath: "/v2/foo/bar/blobs/uploads/uuid-part",
71
-			build: func() (string, error) {
72
-				return urlBuilder.BuildBlobUploadChunkURL("foo/bar", "uuid-part")
73
-			},
74
-		},
75
-		{
76
-			description:  "build blob upload chunk url with digest and size",
77
-			expectedPath: "/v2/foo/bar/blobs/uploads/uuid-part?digest=tarsum.v1%2Bsha256%3Aabcdef0123456789&size=10000",
78
-			build: func() (string, error) {
79
-				return urlBuilder.BuildBlobUploadChunkURL("foo/bar", "uuid-part", url.Values{
80
-					"size":   []string{"10000"},
81
-					"digest": []string{"tarsum.v1+sha256:abcdef0123456789"},
82
-				})
83
-			},
84
-		},
85
-	}
86
-
87
-	roots := []string{
88
-		"http://example.com",
89
-		"https://example.com",
90
-		"http://localhost:5000",
91
-		"https://localhost:5443",
92
-	}
93
-
94
-	for _, root := range roots {
95
-		urlBuilder, err = NewURLBuilderFromString(root)
96
-		if err != nil {
97
-			t.Fatalf("unexpected error creating urlbuilder: %v", err)
98
-		}
99
-
100
-		for _, testCase := range testCases {
101
-			url, err := testCase.build()
102
-			if err != nil {
103
-				t.Fatalf("%s: error building url: %v", testCase.description, err)
104
-			}
105
-
106
-			expectedURL := root + testCase.expectedPath
107
-
108
-			if url != expectedURL {
109
-				t.Fatalf("%s: %q != %q", testCase.description, url, expectedURL)
110
-			}
111
-		}
112
-	}
113
-}
114 1
new file mode 100644
... ...
@@ -0,0 +1,1459 @@
0
+package v2
1
+
2
+import (
3
+	"net/http"
4
+	"regexp"
5
+
6
+	"github.com/docker/distribution/digest"
7
+)
8
+
9
+var (
10
+	nameParameterDescriptor = ParameterDescriptor{
11
+		Name:        "name",
12
+		Type:        "string",
13
+		Format:      RepositoryNameRegexp.String(),
14
+		Required:    true,
15
+		Description: `Name of the target repository.`,
16
+	}
17
+
18
+	tagParameterDescriptor = ParameterDescriptor{
19
+		Name:        "tag",
20
+		Type:        "string",
21
+		Format:      TagNameRegexp.String(),
22
+		Required:    true,
23
+		Description: `Tag of the target manifiest.`,
24
+	}
25
+
26
+	uuidParameterDescriptor = ParameterDescriptor{
27
+		Name:        "uuid",
28
+		Type:        "opaque",
29
+		Required:    true,
30
+		Description: `A uuid identifying the upload. This field can accept almost anything.`,
31
+	}
32
+
33
+	digestPathParameter = ParameterDescriptor{
34
+		Name:        "digest",
35
+		Type:        "path",
36
+		Required:    true,
37
+		Format:      digest.DigestRegexp.String(),
38
+		Description: `Digest of desired blob.`,
39
+	}
40
+
41
+	hostHeader = ParameterDescriptor{
42
+		Name:        "Host",
43
+		Type:        "string",
44
+		Description: "Standard HTTP Host Header. Should be set to the registry host.",
45
+		Format:      "<registry host>",
46
+		Examples:    []string{"registry-1.docker.io"},
47
+	}
48
+
49
+	authHeader = ParameterDescriptor{
50
+		Name:        "Authorization",
51
+		Type:        "string",
52
+		Description: "An RFC7235 compliant authorization header.",
53
+		Format:      "<scheme> <token>",
54
+		Examples:    []string{"Bearer dGhpcyBpcyBhIGZha2UgYmVhcmVyIHRva2VuIQ=="},
55
+	}
56
+
57
+	authChallengeHeader = ParameterDescriptor{
58
+		Name:        "WWW-Authenticate",
59
+		Type:        "string",
60
+		Description: "An RFC7235 compliant authentication challenge header.",
61
+		Format:      `<scheme> realm="<realm>", ..."`,
62
+		Examples: []string{
63
+			`Bearer realm="https://auth.docker.com/", service="registry.docker.com", scopes="repository:library/ubuntu:pull"`,
64
+		},
65
+	}
66
+
67
+	contentLengthZeroHeader = ParameterDescriptor{
68
+		Name:        "Content-Length",
69
+		Description: "The `Content-Length` header must be zero and the body must be empty.",
70
+		Type:        "integer",
71
+		Format:      "0",
72
+	}
73
+
74
+	dockerUploadUUIDHeader = ParameterDescriptor{
75
+		Name:        "Docker-Upload-UUID",
76
+		Description: "Identifies the docker upload uuid for the current request.",
77
+		Type:        "uuid",
78
+		Format:      "<uuid>",
79
+	}
80
+
81
+	digestHeader = ParameterDescriptor{
82
+		Name:        "Docker-Content-Digest",
83
+		Description: "Digest of the targeted content for the request.",
84
+		Type:        "digest",
85
+		Format:      "<digest>",
86
+	}
87
+
88
+	unauthorizedResponse = ResponseDescriptor{
89
+		Description: "The client does not have access to the repository.",
90
+		StatusCode:  http.StatusUnauthorized,
91
+		Headers: []ParameterDescriptor{
92
+			authChallengeHeader,
93
+			{
94
+				Name:        "Content-Length",
95
+				Type:        "integer",
96
+				Description: "Length of the JSON error response body.",
97
+				Format:      "<length>",
98
+			},
99
+		},
100
+		ErrorCodes: []ErrorCode{
101
+			ErrorCodeUnauthorized,
102
+		},
103
+		Body: BodyDescriptor{
104
+			ContentType: "application/json; charset=utf-8",
105
+			Format:      unauthorizedErrorsBody,
106
+		},
107
+	}
108
+
109
+	unauthorizedResponsePush = ResponseDescriptor{
110
+		Description: "The client does not have access to push to the repository.",
111
+		StatusCode:  http.StatusUnauthorized,
112
+		Headers: []ParameterDescriptor{
113
+			authChallengeHeader,
114
+			{
115
+				Name:        "Content-Length",
116
+				Type:        "integer",
117
+				Description: "Length of the JSON error response body.",
118
+				Format:      "<length>",
119
+			},
120
+		},
121
+		ErrorCodes: []ErrorCode{
122
+			ErrorCodeUnauthorized,
123
+		},
124
+		Body: BodyDescriptor{
125
+			ContentType: "application/json; charset=utf-8",
126
+			Format:      unauthorizedErrorsBody,
127
+		},
128
+	}
129
+)
130
+
131
+const (
132
+	manifestBody = `{
133
+   "name": <name>,
134
+   "tag": <tag>,
135
+   "fsLayers": [
136
+      {
137
+         "blobSum": <tarsum>
138
+      },
139
+      ...
140
+    ]
141
+   ],
142
+   "history": <v1 images>,
143
+   "signature": <JWS>
144
+}`
145
+
146
+	errorsBody = `{
147
+	"errors:" [
148
+	    {
149
+            "code": <error code>,
150
+            "message": "<error message>",
151
+            "detail": ...
152
+        },
153
+        ...
154
+    ]
155
+}`
156
+
157
+	unauthorizedErrorsBody = `{
158
+	"errors:" [
159
+	    {
160
+            "code": "UNAUTHORIZED",
161
+            "message": "access to the requested resource is not authorized",
162
+            "detail": ...
163
+        },
164
+        ...
165
+    ]
166
+}`
167
+)
168
+
169
+// APIDescriptor exports descriptions of the layout of the v2 registry API.
170
+var APIDescriptor = struct {
171
+	// RouteDescriptors provides a list of the routes available in the API.
172
+	RouteDescriptors []RouteDescriptor
173
+
174
+	// ErrorDescriptors provides a list of the error codes and their
175
+	// associated documentation and metadata.
176
+	ErrorDescriptors []ErrorDescriptor
177
+}{
178
+	RouteDescriptors: routeDescriptors,
179
+	ErrorDescriptors: errorDescriptors,
180
+}
181
+
182
+// RouteDescriptor describes a route specified by name.
183
+type RouteDescriptor struct {
184
+	// Name is the name of the route, as specified in RouteNameXXX exports.
185
+	// These names a should be considered a unique reference for a route. If
186
+	// the route is registered with gorilla, this is the name that will be
187
+	// used.
188
+	Name string
189
+
190
+	// Path is a gorilla/mux-compatible regexp that can be used to match the
191
+	// route. For any incoming method and path, only one route descriptor
192
+	// should match.
193
+	Path string
194
+
195
+	// Entity should be a short, human-readalbe description of the object
196
+	// targeted by the endpoint.
197
+	Entity string
198
+
199
+	// Description should provide an accurate overview of the functionality
200
+	// provided by the route.
201
+	Description string
202
+
203
+	// Methods should describe the various HTTP methods that may be used on
204
+	// this route, including request and response formats.
205
+	Methods []MethodDescriptor
206
+}
207
+
208
+// MethodDescriptor provides a description of the requests that may be
209
+// conducted with the target method.
210
+type MethodDescriptor struct {
211
+
212
+	// Method is an HTTP method, such as GET, PUT or POST.
213
+	Method string
214
+
215
+	// Description should provide an overview of the functionality provided by
216
+	// the covered method, suitable for use in documentation. Use of markdown
217
+	// here is encouraged.
218
+	Description string
219
+
220
+	// Requests is a slice of request descriptors enumerating how this
221
+	// endpoint may be used.
222
+	Requests []RequestDescriptor
223
+}
224
+
225
+// RequestDescriptor covers a particular set of headers and parameters that
226
+// can be carried out with the parent method. Its most helpful to have one
227
+// RequestDescriptor per API use case.
228
+type RequestDescriptor struct {
229
+	// Name provides a short identifier for the request, usable as a title or
230
+	// to provide quick context for the particalar request.
231
+	Name string
232
+
233
+	// Description should cover the requests purpose, covering any details for
234
+	// this particular use case.
235
+	Description string
236
+
237
+	// Headers describes headers that must be used with the HTTP request.
238
+	Headers []ParameterDescriptor
239
+
240
+	// PathParameters enumerate the parameterized path components for the
241
+	// given request, as defined in the route's regular expression.
242
+	PathParameters []ParameterDescriptor
243
+
244
+	// QueryParameters provides a list of query parameters for the given
245
+	// request.
246
+	QueryParameters []ParameterDescriptor
247
+
248
+	// Body describes the format of the request body.
249
+	Body BodyDescriptor
250
+
251
+	// Successes enumerates the possible responses that are considered to be
252
+	// the result of a successful request.
253
+	Successes []ResponseDescriptor
254
+
255
+	// Failures covers the possible failures from this particular request.
256
+	Failures []ResponseDescriptor
257
+}
258
+
259
+// ResponseDescriptor describes the components of an API response.
260
+type ResponseDescriptor struct {
261
+	// Name provides a short identifier for the response, usable as a title or
262
+	// to provide quick context for the particalar response.
263
+	Name string
264
+
265
+	// Description should provide a brief overview of the role of the
266
+	// response.
267
+	Description string
268
+
269
+	// StatusCode specifies the status recieved by this particular response.
270
+	StatusCode int
271
+
272
+	// Headers covers any headers that may be returned from the response.
273
+	Headers []ParameterDescriptor
274
+
275
+	// ErrorCodes enumerates the error codes that may be returned along with
276
+	// the response.
277
+	ErrorCodes []ErrorCode
278
+
279
+	// Body describes the body of the response, if any.
280
+	Body BodyDescriptor
281
+}
282
+
283
+// BodyDescriptor describes a request body and its expected content type. For
284
+// the most  part, it should be example json or some placeholder for body
285
+// data in documentation.
286
+type BodyDescriptor struct {
287
+	ContentType string
288
+	Format      string
289
+}
290
+
291
+// ParameterDescriptor describes the format of a request parameter, which may
292
+// be a header, path parameter or query parameter.
293
+type ParameterDescriptor struct {
294
+	// Name is the name of the parameter, either of the path component or
295
+	// query parameter.
296
+	Name string
297
+
298
+	// Type specifies the type of the parameter, such as string, integer, etc.
299
+	Type string
300
+
301
+	// Description provides a human-readable description of the parameter.
302
+	Description string
303
+
304
+	// Required means the field is required when set.
305
+	Required bool
306
+
307
+	// Format is a specifying the string format accepted by this parameter.
308
+	Format string
309
+
310
+	// Regexp is a compiled regular expression that can be used to validate
311
+	// the contents of the parameter.
312
+	Regexp *regexp.Regexp
313
+
314
+	// Examples provides multiple examples for the values that might be valid
315
+	// for this parameter.
316
+	Examples []string
317
+}
318
+
319
+// ErrorDescriptor provides relevant information about a given error code.
320
+type ErrorDescriptor struct {
321
+	// Code is the error code that this descriptor describes.
322
+	Code ErrorCode
323
+
324
+	// Value provides a unique, string key, often captilized with
325
+	// underscores, to identify the error code. This value is used as the
326
+	// keyed value when serializing api errors.
327
+	Value string
328
+
329
+	// Message is a short, human readable decription of the error condition
330
+	// included in API responses.
331
+	Message string
332
+
333
+	// Description provides a complete account of the errors purpose, suitable
334
+	// for use in documentation.
335
+	Description string
336
+
337
+	// HTTPStatusCodes provides a list of status under which this error
338
+	// condition may arise. If it is empty, the error condition may be seen
339
+	// for any status code.
340
+	HTTPStatusCodes []int
341
+}
342
+
343
+var routeDescriptors = []RouteDescriptor{
344
+	{
345
+		Name:        RouteNameBase,
346
+		Path:        "/v2/",
347
+		Entity:      "Base",
348
+		Description: `Base V2 API route. Typically, this can be used for lightweight version checks and to validate registry authorization.`,
349
+		Methods: []MethodDescriptor{
350
+			{
351
+				Method:      "GET",
352
+				Description: "Check that the endpoint implements Docker Registry API V2.",
353
+				Requests: []RequestDescriptor{
354
+					{
355
+						Headers: []ParameterDescriptor{
356
+							hostHeader,
357
+							authHeader,
358
+						},
359
+						Successes: []ResponseDescriptor{
360
+							{
361
+								Description: "The API implements V2 protocol and is accessible.",
362
+								StatusCode:  http.StatusOK,
363
+							},
364
+						},
365
+						Failures: []ResponseDescriptor{
366
+							{
367
+								Description: "The client is not authorized to access the registry.",
368
+								StatusCode:  http.StatusUnauthorized,
369
+								Headers: []ParameterDescriptor{
370
+									authChallengeHeader,
371
+								},
372
+								Body: BodyDescriptor{
373
+									ContentType: "application/json; charset=utf-8",
374
+									Format:      errorsBody,
375
+								},
376
+								ErrorCodes: []ErrorCode{
377
+									ErrorCodeUnauthorized,
378
+								},
379
+							},
380
+							{
381
+								Description: "The registry does not implement the V2 API.",
382
+								StatusCode:  http.StatusNotFound,
383
+							},
384
+						},
385
+					},
386
+				},
387
+			},
388
+		},
389
+	},
390
+	{
391
+		Name:        RouteNameTags,
392
+		Path:        "/v2/{name:" + RepositoryNameRegexp.String() + "}/tags/list",
393
+		Entity:      "Tags",
394
+		Description: "Retrieve information about tags.",
395
+		Methods: []MethodDescriptor{
396
+			{
397
+				Method:      "GET",
398
+				Description: "Fetch the tags under the repository identified by `name`.",
399
+				Requests: []RequestDescriptor{
400
+					{
401
+						Headers: []ParameterDescriptor{
402
+							hostHeader,
403
+							authHeader,
404
+						},
405
+						PathParameters: []ParameterDescriptor{
406
+							nameParameterDescriptor,
407
+						},
408
+						Successes: []ResponseDescriptor{
409
+							{
410
+								StatusCode:  http.StatusOK,
411
+								Description: "A list of tags for the named repository.",
412
+								Headers: []ParameterDescriptor{
413
+									{
414
+										Name:        "Content-Length",
415
+										Type:        "integer",
416
+										Description: "Length of the JSON response body.",
417
+										Format:      "<length>",
418
+									},
419
+								},
420
+								Body: BodyDescriptor{
421
+									ContentType: "application/json; charset=utf-8",
422
+									Format: `{
423
+    "name": <name>,
424
+    "tags": [
425
+        <tag>,
426
+        ...
427
+    ]
428
+}`,
429
+								},
430
+							},
431
+						},
432
+						Failures: []ResponseDescriptor{
433
+							{
434
+								StatusCode:  http.StatusNotFound,
435
+								Description: "The repository is not known to the registry.",
436
+								Body: BodyDescriptor{
437
+									ContentType: "application/json; charset=utf-8",
438
+									Format:      errorsBody,
439
+								},
440
+								ErrorCodes: []ErrorCode{
441
+									ErrorCodeNameUnknown,
442
+								},
443
+							},
444
+							{
445
+								StatusCode:  http.StatusUnauthorized,
446
+								Description: "The client does not have access to the repository.",
447
+								Body: BodyDescriptor{
448
+									ContentType: "application/json; charset=utf-8",
449
+									Format:      errorsBody,
450
+								},
451
+								ErrorCodes: []ErrorCode{
452
+									ErrorCodeUnauthorized,
453
+								},
454
+							},
455
+						},
456
+					},
457
+				},
458
+			},
459
+		},
460
+	},
461
+	{
462
+		Name:        RouteNameManifest,
463
+		Path:        "/v2/{name:" + RepositoryNameRegexp.String() + "}/manifests/{reference:" + TagNameRegexp.String() + "|" + digest.DigestRegexp.String() + "}",
464
+		Entity:      "Manifest",
465
+		Description: "Create, update and retrieve manifests.",
466
+		Methods: []MethodDescriptor{
467
+			{
468
+				Method:      "GET",
469
+				Description: "Fetch the manifest identified by `name` and `reference` where `reference` can be a tag or digest.",
470
+				Requests: []RequestDescriptor{
471
+					{
472
+						Headers: []ParameterDescriptor{
473
+							hostHeader,
474
+							authHeader,
475
+						},
476
+						PathParameters: []ParameterDescriptor{
477
+							nameParameterDescriptor,
478
+							tagParameterDescriptor,
479
+						},
480
+						Successes: []ResponseDescriptor{
481
+							{
482
+								Description: "The manifest idenfied by `name` and `reference`. The contents can be used to identify and resolve resources required to run the specified image.",
483
+								StatusCode:  http.StatusOK,
484
+								Headers: []ParameterDescriptor{
485
+									digestHeader,
486
+								},
487
+								Body: BodyDescriptor{
488
+									ContentType: "application/json; charset=utf-8",
489
+									Format:      manifestBody,
490
+								},
491
+							},
492
+						},
493
+						Failures: []ResponseDescriptor{
494
+							{
495
+								Description: "The name or reference was invalid.",
496
+								StatusCode:  http.StatusBadRequest,
497
+								ErrorCodes: []ErrorCode{
498
+									ErrorCodeNameInvalid,
499
+									ErrorCodeTagInvalid,
500
+								},
501
+								Body: BodyDescriptor{
502
+									ContentType: "application/json; charset=utf-8",
503
+									Format:      errorsBody,
504
+								},
505
+							},
506
+							{
507
+								StatusCode:  http.StatusUnauthorized,
508
+								Description: "The client does not have access to the repository.",
509
+								Body: BodyDescriptor{
510
+									ContentType: "application/json; charset=utf-8",
511
+									Format:      errorsBody,
512
+								},
513
+								ErrorCodes: []ErrorCode{
514
+									ErrorCodeUnauthorized,
515
+								},
516
+							},
517
+							{
518
+								Description: "The named manifest is not known to the registry.",
519
+								StatusCode:  http.StatusNotFound,
520
+								ErrorCodes: []ErrorCode{
521
+									ErrorCodeNameUnknown,
522
+									ErrorCodeManifestUnknown,
523
+								},
524
+								Body: BodyDescriptor{
525
+									ContentType: "application/json; charset=utf-8",
526
+									Format:      errorsBody,
527
+								},
528
+							},
529
+						},
530
+					},
531
+				},
532
+			},
533
+			{
534
+				Method:      "PUT",
535
+				Description: "Put the manifest identified by `name` and `reference` where `reference` can be a tag or digest.",
536
+				Requests: []RequestDescriptor{
537
+					{
538
+						Headers: []ParameterDescriptor{
539
+							hostHeader,
540
+							authHeader,
541
+						},
542
+						PathParameters: []ParameterDescriptor{
543
+							nameParameterDescriptor,
544
+							tagParameterDescriptor,
545
+						},
546
+						Body: BodyDescriptor{
547
+							ContentType: "application/json; charset=utf-8",
548
+							Format:      manifestBody,
549
+						},
550
+						Successes: []ResponseDescriptor{
551
+							{
552
+								Description: "The manifest has been accepted by the registry and is stored under the specified `name` and `tag`.",
553
+								StatusCode:  http.StatusAccepted,
554
+								Headers: []ParameterDescriptor{
555
+									{
556
+										Name:        "Location",
557
+										Type:        "url",
558
+										Description: "The canonical location url of the uploaded manifest.",
559
+										Format:      "<url>",
560
+									},
561
+									contentLengthZeroHeader,
562
+									digestHeader,
563
+								},
564
+							},
565
+						},
566
+						Failures: []ResponseDescriptor{
567
+							{
568
+								Name:        "Invalid Manifest",
569
+								Description: "The received manifest was invalid in some way, as described by the error codes. The client should resolve the issue and retry the request.",
570
+								StatusCode:  http.StatusBadRequest,
571
+								Body: BodyDescriptor{
572
+									ContentType: "application/json; charset=utf-8",
573
+									Format:      errorsBody,
574
+								},
575
+								ErrorCodes: []ErrorCode{
576
+									ErrorCodeNameInvalid,
577
+									ErrorCodeTagInvalid,
578
+									ErrorCodeManifestInvalid,
579
+									ErrorCodeManifestUnverified,
580
+									ErrorCodeBlobUnknown,
581
+								},
582
+							},
583
+							{
584
+								StatusCode:  http.StatusUnauthorized,
585
+								Description: "The client does not have permission to push to the repository.",
586
+								Body: BodyDescriptor{
587
+									ContentType: "application/json; charset=utf-8",
588
+									Format:      errorsBody,
589
+								},
590
+								ErrorCodes: []ErrorCode{
591
+									ErrorCodeUnauthorized,
592
+								},
593
+							},
594
+							{
595
+								Name:        "Missing Layer(s)",
596
+								Description: "One or more layers may be missing during a manifest upload. If so, the missing layers will be enumerated in the error response.",
597
+								StatusCode:  http.StatusBadRequest,
598
+								ErrorCodes: []ErrorCode{
599
+									ErrorCodeBlobUnknown,
600
+								},
601
+								Body: BodyDescriptor{
602
+									ContentType: "application/json; charset=utf-8",
603
+									Format: `{
604
+    "errors:" [{
605
+            "code": "BLOB_UNKNOWN",
606
+            "message": "blob unknown to registry",
607
+            "detail": {
608
+                "digest": <tarsum>
609
+            }
610
+        },
611
+        ...
612
+    ]
613
+}`,
614
+								},
615
+							},
616
+							{
617
+								StatusCode: http.StatusUnauthorized,
618
+								Headers: []ParameterDescriptor{
619
+									authChallengeHeader,
620
+									{
621
+										Name:        "Content-Length",
622
+										Type:        "integer",
623
+										Description: "Length of the JSON error response body.",
624
+										Format:      "<length>",
625
+									},
626
+								},
627
+								ErrorCodes: []ErrorCode{
628
+									ErrorCodeUnauthorized,
629
+								},
630
+								Body: BodyDescriptor{
631
+									ContentType: "application/json; charset=utf-8",
632
+									Format:      errorsBody,
633
+								},
634
+							},
635
+						},
636
+					},
637
+				},
638
+			},
639
+			{
640
+				Method:      "DELETE",
641
+				Description: "Delete the manifest identified by `name` and `reference` where `reference` can be a tag or digest.",
642
+				Requests: []RequestDescriptor{
643
+					{
644
+						Headers: []ParameterDescriptor{
645
+							hostHeader,
646
+							authHeader,
647
+						},
648
+						PathParameters: []ParameterDescriptor{
649
+							nameParameterDescriptor,
650
+							tagParameterDescriptor,
651
+						},
652
+						Successes: []ResponseDescriptor{
653
+							{
654
+								StatusCode: http.StatusAccepted,
655
+							},
656
+						},
657
+						Failures: []ResponseDescriptor{
658
+							{
659
+								Name:        "Invalid Name or Tag",
660
+								Description: "The specified `name` or `tag` were invalid and the delete was unable to proceed.",
661
+								StatusCode:  http.StatusBadRequest,
662
+								ErrorCodes: []ErrorCode{
663
+									ErrorCodeNameInvalid,
664
+									ErrorCodeTagInvalid,
665
+								},
666
+								Body: BodyDescriptor{
667
+									ContentType: "application/json; charset=utf-8",
668
+									Format:      errorsBody,
669
+								},
670
+							},
671
+							{
672
+								StatusCode: http.StatusUnauthorized,
673
+								Headers: []ParameterDescriptor{
674
+									authChallengeHeader,
675
+									{
676
+										Name:        "Content-Length",
677
+										Type:        "integer",
678
+										Description: "Length of the JSON error response body.",
679
+										Format:      "<length>",
680
+									},
681
+								},
682
+								ErrorCodes: []ErrorCode{
683
+									ErrorCodeUnauthorized,
684
+								},
685
+								Body: BodyDescriptor{
686
+									ContentType: "application/json; charset=utf-8",
687
+									Format:      errorsBody,
688
+								},
689
+							},
690
+							{
691
+								Name:        "Unknown Manifest",
692
+								Description: "The specified `name` or `tag` are unknown to the registry and the delete was unable to proceed. Clients can assume the manifest was already deleted if this response is returned.",
693
+								StatusCode:  http.StatusNotFound,
694
+								ErrorCodes: []ErrorCode{
695
+									ErrorCodeNameUnknown,
696
+									ErrorCodeManifestUnknown,
697
+								},
698
+								Body: BodyDescriptor{
699
+									ContentType: "application/json; charset=utf-8",
700
+									Format:      errorsBody,
701
+								},
702
+							},
703
+						},
704
+					},
705
+				},
706
+			},
707
+		},
708
+	},
709
+
710
+	{
711
+		Name:        RouteNameBlob,
712
+		Path:        "/v2/{name:" + RepositoryNameRegexp.String() + "}/blobs/{digest:" + digest.DigestRegexp.String() + "}",
713
+		Entity:      "Blob",
714
+		Description: "Fetch the blob identified by `name` and `digest`. Used to fetch layers by tarsum digest.",
715
+		Methods: []MethodDescriptor{
716
+
717
+			{
718
+				Method:      "GET",
719
+				Description: "Retrieve the blob from the registry identified by `digest`. A `HEAD` request can also be issued to this endpoint to obtain resource information without receiving all data.",
720
+				Requests: []RequestDescriptor{
721
+					{
722
+						Name: "Fetch Blob",
723
+						Headers: []ParameterDescriptor{
724
+							hostHeader,
725
+							authHeader,
726
+						},
727
+						PathParameters: []ParameterDescriptor{
728
+							nameParameterDescriptor,
729
+							digestPathParameter,
730
+						},
731
+						Successes: []ResponseDescriptor{
732
+							{
733
+								Description: "The blob identified by `digest` is available. The blob content will be present in the body of the request.",
734
+								StatusCode:  http.StatusOK,
735
+								Headers: []ParameterDescriptor{
736
+									{
737
+										Name:        "Content-Length",
738
+										Type:        "integer",
739
+										Description: "The length of the requested blob content.",
740
+										Format:      "<length>",
741
+									},
742
+									digestHeader,
743
+								},
744
+								Body: BodyDescriptor{
745
+									ContentType: "application/octet-stream",
746
+									Format:      "<blob binary data>",
747
+								},
748
+							},
749
+							{
750
+								Description: "The blob identified by `digest` is available at the provided location.",
751
+								StatusCode:  http.StatusTemporaryRedirect,
752
+								Headers: []ParameterDescriptor{
753
+									{
754
+										Name:        "Location",
755
+										Type:        "url",
756
+										Description: "The location where the layer should be accessible.",
757
+										Format:      "<blob location>",
758
+									},
759
+									digestHeader,
760
+								},
761
+							},
762
+						},
763
+						Failures: []ResponseDescriptor{
764
+							{
765
+								Description: "There was a problem with the request that needs to be addressed by the client, such as an invalid `name` or `tag`.",
766
+								StatusCode:  http.StatusBadRequest,
767
+								ErrorCodes: []ErrorCode{
768
+									ErrorCodeNameInvalid,
769
+									ErrorCodeDigestInvalid,
770
+								},
771
+								Body: BodyDescriptor{
772
+									ContentType: "application/json; charset=utf-8",
773
+									Format:      errorsBody,
774
+								},
775
+							},
776
+							unauthorizedResponse,
777
+							{
778
+								Description: "The blob, identified by `name` and `digest`, is unknown to the registry.",
779
+								StatusCode:  http.StatusNotFound,
780
+								Body: BodyDescriptor{
781
+									ContentType: "application/json; charset=utf-8",
782
+									Format:      errorsBody,
783
+								},
784
+								ErrorCodes: []ErrorCode{
785
+									ErrorCodeNameUnknown,
786
+									ErrorCodeBlobUnknown,
787
+								},
788
+							},
789
+						},
790
+					},
791
+					{
792
+						Name:        "Fetch Blob Part",
793
+						Description: "This endpoint may also support RFC7233 compliant range requests. Support can be detected by issuing a HEAD request. If the header `Accept-Range: bytes` is returned, range requests can be used to fetch partial content.",
794
+						Headers: []ParameterDescriptor{
795
+							hostHeader,
796
+							authHeader,
797
+							{
798
+								Name:        "Range",
799
+								Type:        "string",
800
+								Description: "HTTP Range header specifying blob chunk.",
801
+								Format:      "bytes=<start>-<end>",
802
+							},
803
+						},
804
+						PathParameters: []ParameterDescriptor{
805
+							nameParameterDescriptor,
806
+							digestPathParameter,
807
+						},
808
+						Successes: []ResponseDescriptor{
809
+							{
810
+								Description: "The blob identified by `digest` is available. The specified chunk of blob content will be present in the body of the request.",
811
+								StatusCode:  http.StatusPartialContent,
812
+								Headers: []ParameterDescriptor{
813
+									{
814
+										Name:        "Content-Length",
815
+										Type:        "integer",
816
+										Description: "The length of the requested blob chunk.",
817
+										Format:      "<length>",
818
+									},
819
+									{
820
+										Name:        "Content-Range",
821
+										Type:        "byte range",
822
+										Description: "Content range of blob chunk.",
823
+										Format:      "bytes <start>-<end>/<size>",
824
+									},
825
+								},
826
+								Body: BodyDescriptor{
827
+									ContentType: "application/octet-stream",
828
+									Format:      "<blob binary data>",
829
+								},
830
+							},
831
+						},
832
+						Failures: []ResponseDescriptor{
833
+							{
834
+								Description: "There was a problem with the request that needs to be addressed by the client, such as an invalid `name` or `tag`.",
835
+								StatusCode:  http.StatusBadRequest,
836
+								ErrorCodes: []ErrorCode{
837
+									ErrorCodeNameInvalid,
838
+									ErrorCodeDigestInvalid,
839
+								},
840
+								Body: BodyDescriptor{
841
+									ContentType: "application/json; charset=utf-8",
842
+									Format:      errorsBody,
843
+								},
844
+							},
845
+							unauthorizedResponse,
846
+							{
847
+								StatusCode: http.StatusNotFound,
848
+								ErrorCodes: []ErrorCode{
849
+									ErrorCodeNameUnknown,
850
+									ErrorCodeBlobUnknown,
851
+								},
852
+								Body: BodyDescriptor{
853
+									ContentType: "application/json; charset=utf-8",
854
+									Format:      errorsBody,
855
+								},
856
+							},
857
+							{
858
+								Description: "The range specification cannot be satisfied for the requested content. This can happen when the range is not formatted correctly or if the range is outside of the valid size of the content.",
859
+								StatusCode:  http.StatusRequestedRangeNotSatisfiable,
860
+							},
861
+						},
862
+					},
863
+				},
864
+			},
865
+			// TODO(stevvooe): We may want to add a PUT request here to
866
+			// kickoff an upload of a blob, integrated with the blob upload
867
+			// API.
868
+		},
869
+	},
870
+
871
+	{
872
+		Name:        RouteNameBlobUpload,
873
+		Path:        "/v2/{name:" + RepositoryNameRegexp.String() + "}/blobs/uploads/",
874
+		Entity:      "Intiate Blob Upload",
875
+		Description: "Initiate a blob upload. This endpoint can be used to create resumable uploads or monolithic uploads.",
876
+		Methods: []MethodDescriptor{
877
+			{
878
+				Method:      "POST",
879
+				Description: "Initiate a resumable blob upload. If successful, an upload location will be provided to complete the upload. Optionally, if the `digest` parameter is present, the request body will be used to complete the upload in a single request.",
880
+				Requests: []RequestDescriptor{
881
+					{
882
+						Name:        "Initiate Monolithic Blob Upload",
883
+						Description: "Upload a blob identified by the `digest` parameter in single request. This upload will not be resumable unless a recoverable error is returned.",
884
+						Headers: []ParameterDescriptor{
885
+							hostHeader,
886
+							authHeader,
887
+							{
888
+								Name:   "Content-Length",
889
+								Type:   "integer",
890
+								Format: "<length of blob>",
891
+							},
892
+						},
893
+						PathParameters: []ParameterDescriptor{
894
+							nameParameterDescriptor,
895
+						},
896
+						QueryParameters: []ParameterDescriptor{
897
+							{
898
+								Name:        "digest",
899
+								Type:        "query",
900
+								Format:      "<tarsum>",
901
+								Regexp:      digest.DigestRegexp,
902
+								Description: `Digest of uploaded blob. If present, the upload will be completed, in a single request, with contents of the request body as the resulting blob.`,
903
+							},
904
+						},
905
+						Body: BodyDescriptor{
906
+							ContentType: "application/octect-stream",
907
+							Format:      "<binary data>",
908
+						},
909
+						Successes: []ResponseDescriptor{
910
+							{
911
+								Description: "The blob has been created in the registry and is available at the provided location.",
912
+								StatusCode:  http.StatusCreated,
913
+								Headers: []ParameterDescriptor{
914
+									{
915
+										Name:   "Location",
916
+										Type:   "url",
917
+										Format: "<blob location>",
918
+									},
919
+									contentLengthZeroHeader,
920
+									dockerUploadUUIDHeader,
921
+								},
922
+							},
923
+						},
924
+						Failures: []ResponseDescriptor{
925
+							{
926
+								Name:       "Invalid Name or Digest",
927
+								StatusCode: http.StatusBadRequest,
928
+								ErrorCodes: []ErrorCode{
929
+									ErrorCodeDigestInvalid,
930
+									ErrorCodeNameInvalid,
931
+								},
932
+							},
933
+							unauthorizedResponsePush,
934
+						},
935
+					},
936
+					{
937
+						Name:        "Initiate Resumable Blob Upload",
938
+						Description: "Initiate a resumable blob upload with an empty request body.",
939
+						Headers: []ParameterDescriptor{
940
+							hostHeader,
941
+							authHeader,
942
+							contentLengthZeroHeader,
943
+						},
944
+						PathParameters: []ParameterDescriptor{
945
+							nameParameterDescriptor,
946
+						},
947
+						Successes: []ResponseDescriptor{
948
+							{
949
+								Description: "The upload has been created. The `Location` header must be used to complete the upload. The response should be identical to a `GET` request on the contents of the returned `Location` header.",
950
+								StatusCode:  http.StatusAccepted,
951
+								Headers: []ParameterDescriptor{
952
+									contentLengthZeroHeader,
953
+									{
954
+										Name:        "Location",
955
+										Type:        "url",
956
+										Format:      "/v2/<name>/blobs/uploads/<uuid>",
957
+										Description: "The location of the created upload. Clients should use the contents verbatim to complete the upload, adding parameters where required.",
958
+									},
959
+									{
960
+										Name:        "Range",
961
+										Format:      "0-0",
962
+										Description: "Range header indicating the progress of the upload. When starting an upload, it will return an empty range, since no content has been received.",
963
+									},
964
+									dockerUploadUUIDHeader,
965
+								},
966
+							},
967
+						},
968
+						Failures: []ResponseDescriptor{
969
+							{
970
+								Name:       "Invalid Name or Digest",
971
+								StatusCode: http.StatusBadRequest,
972
+								ErrorCodes: []ErrorCode{
973
+									ErrorCodeDigestInvalid,
974
+									ErrorCodeNameInvalid,
975
+								},
976
+							},
977
+							unauthorizedResponsePush,
978
+						},
979
+					},
980
+				},
981
+			},
982
+		},
983
+	},
984
+
985
+	{
986
+		Name:        RouteNameBlobUploadChunk,
987
+		Path:        "/v2/{name:" + RepositoryNameRegexp.String() + "}/blobs/uploads/{uuid}",
988
+		Entity:      "Blob Upload",
989
+		Description: "Interact with blob uploads. Clients should never assemble URLs for this endpoint and should only take it through the `Location` header on related API requests. The `Location` header and its parameters should be preserved by clients, using the latest value returned via upload related API calls.",
990
+		Methods: []MethodDescriptor{
991
+			{
992
+				Method:      "GET",
993
+				Description: "Retrieve status of upload identified by `uuid`. The primary purpose of this endpoint is to resolve the current status of a resumable upload.",
994
+				Requests: []RequestDescriptor{
995
+					{
996
+						Description: "Retrieve the progress of the current upload, as reported by the `Range` header.",
997
+						Headers: []ParameterDescriptor{
998
+							hostHeader,
999
+							authHeader,
1000
+						},
1001
+						PathParameters: []ParameterDescriptor{
1002
+							nameParameterDescriptor,
1003
+							uuidParameterDescriptor,
1004
+						},
1005
+						Successes: []ResponseDescriptor{
1006
+							{
1007
+								Name:        "Upload Progress",
1008
+								Description: "The upload is known and in progress. The last received offset is available in the `Range` header.",
1009
+								StatusCode:  http.StatusNoContent,
1010
+								Headers: []ParameterDescriptor{
1011
+									{
1012
+										Name:        "Range",
1013
+										Type:        "header",
1014
+										Format:      "0-<offset>",
1015
+										Description: "Range indicating the current progress of the upload.",
1016
+									},
1017
+									contentLengthZeroHeader,
1018
+									dockerUploadUUIDHeader,
1019
+								},
1020
+							},
1021
+						},
1022
+						Failures: []ResponseDescriptor{
1023
+							{
1024
+								Description: "There was an error processing the upload and it must be restarted.",
1025
+								StatusCode:  http.StatusBadRequest,
1026
+								ErrorCodes: []ErrorCode{
1027
+									ErrorCodeDigestInvalid,
1028
+									ErrorCodeNameInvalid,
1029
+									ErrorCodeBlobUploadInvalid,
1030
+								},
1031
+								Body: BodyDescriptor{
1032
+									ContentType: "application/json; charset=utf-8",
1033
+									Format:      errorsBody,
1034
+								},
1035
+							},
1036
+							unauthorizedResponse,
1037
+							{
1038
+								Description: "The upload is unknown to the registry. The upload must be restarted.",
1039
+								StatusCode:  http.StatusNotFound,
1040
+								ErrorCodes: []ErrorCode{
1041
+									ErrorCodeBlobUploadUnknown,
1042
+								},
1043
+								Body: BodyDescriptor{
1044
+									ContentType: "application/json; charset=utf-8",
1045
+									Format:      errorsBody,
1046
+								},
1047
+							},
1048
+						},
1049
+					},
1050
+				},
1051
+			},
1052
+			{
1053
+				Method:      "PATCH",
1054
+				Description: "Upload a chunk of data for the specified upload.",
1055
+				Requests: []RequestDescriptor{
1056
+					{
1057
+						Description: "Upload a chunk of data to specified upload without completing the upload.",
1058
+						PathParameters: []ParameterDescriptor{
1059
+							nameParameterDescriptor,
1060
+							uuidParameterDescriptor,
1061
+						},
1062
+						Headers: []ParameterDescriptor{
1063
+							hostHeader,
1064
+							authHeader,
1065
+							{
1066
+								Name:        "Content-Range",
1067
+								Type:        "header",
1068
+								Format:      "<start of range>-<end of range, inclusive>",
1069
+								Required:    true,
1070
+								Description: "Range of bytes identifying the desired block of content represented by the body. Start must the end offset retrieved via status check plus one. Note that this is a non-standard use of the `Content-Range` header.",
1071
+							},
1072
+							{
1073
+								Name:        "Content-Length",
1074
+								Type:        "integer",
1075
+								Format:      "<length of chunk>",
1076
+								Description: "Length of the chunk being uploaded, corresponding the length of the request body.",
1077
+							},
1078
+						},
1079
+						Body: BodyDescriptor{
1080
+							ContentType: "application/octet-stream",
1081
+							Format:      "<binary chunk>",
1082
+						},
1083
+						Successes: []ResponseDescriptor{
1084
+							{
1085
+								Name:        "Chunk Accepted",
1086
+								Description: "The chunk of data has been accepted and the current progress is available in the range header. The updated upload location is available in the `Location` header.",
1087
+								StatusCode:  http.StatusNoContent,
1088
+								Headers: []ParameterDescriptor{
1089
+									{
1090
+										Name:        "Location",
1091
+										Type:        "url",
1092
+										Format:      "/v2/<name>/blobs/uploads/<uuid>",
1093
+										Description: "The location of the upload. Clients should assume this changes after each request. Clients should use the contents verbatim to complete the upload, adding parameters where required.",
1094
+									},
1095
+									{
1096
+										Name:        "Range",
1097
+										Type:        "header",
1098
+										Format:      "0-<offset>",
1099
+										Description: "Range indicating the current progress of the upload.",
1100
+									},
1101
+									contentLengthZeroHeader,
1102
+									dockerUploadUUIDHeader,
1103
+								},
1104
+							},
1105
+						},
1106
+						Failures: []ResponseDescriptor{
1107
+							{
1108
+								Description: "There was an error processing the upload and it must be restarted.",
1109
+								StatusCode:  http.StatusBadRequest,
1110
+								ErrorCodes: []ErrorCode{
1111
+									ErrorCodeDigestInvalid,
1112
+									ErrorCodeNameInvalid,
1113
+									ErrorCodeBlobUploadInvalid,
1114
+								},
1115
+								Body: BodyDescriptor{
1116
+									ContentType: "application/json; charset=utf-8",
1117
+									Format:      errorsBody,
1118
+								},
1119
+							},
1120
+							unauthorizedResponsePush,
1121
+							{
1122
+								Description: "The upload is unknown to the registry. The upload must be restarted.",
1123
+								StatusCode:  http.StatusNotFound,
1124
+								ErrorCodes: []ErrorCode{
1125
+									ErrorCodeBlobUploadUnknown,
1126
+								},
1127
+								Body: BodyDescriptor{
1128
+									ContentType: "application/json; charset=utf-8",
1129
+									Format:      errorsBody,
1130
+								},
1131
+							},
1132
+							{
1133
+								Description: "The `Content-Range` specification cannot be accepted, either because it does not overlap with the current progress or it is invalid.",
1134
+								StatusCode:  http.StatusRequestedRangeNotSatisfiable,
1135
+							},
1136
+						},
1137
+					},
1138
+				},
1139
+			},
1140
+			{
1141
+				Method:      "PUT",
1142
+				Description: "Complete the upload specified by `uuid`, optionally appending the body as the final chunk.",
1143
+				Requests: []RequestDescriptor{
1144
+					{
1145
+						// TODO(stevvooe): Break this down into three separate requests:
1146
+						// 	1. Complete an upload where all data has already been sent.
1147
+						// 	2. Complete an upload where the entire body is in the PUT.
1148
+						// 	3. Complete an upload where the final, partial chunk is the body.
1149
+
1150
+						Description: "Complete the upload, providing the _final_ chunk of data, if necessary. This method may take a body with all the data. If the `Content-Range` header is specified, it may include the final chunk. A request without a body will just complete the upload with previously uploaded content.",
1151
+						Headers: []ParameterDescriptor{
1152
+							hostHeader,
1153
+							authHeader,
1154
+							{
1155
+								Name:        "Content-Range",
1156
+								Type:        "header",
1157
+								Format:      "<start of range>-<end of range, inclusive>",
1158
+								Description: "Range of bytes identifying the block of content represented by the body. Start must the end offset retrieved via status check plus one. Note that this is a non-standard use of the `Content-Range` header. May be omitted if no data is provided.",
1159
+							},
1160
+							{
1161
+								Name:        "Content-Length",
1162
+								Type:        "integer",
1163
+								Format:      "<length of chunk>",
1164
+								Description: "Length of the chunk being uploaded, corresponding to the length of the request body. May be zero if no data is provided.",
1165
+							},
1166
+						},
1167
+						PathParameters: []ParameterDescriptor{
1168
+							nameParameterDescriptor,
1169
+							uuidParameterDescriptor,
1170
+						},
1171
+						QueryParameters: []ParameterDescriptor{
1172
+							{
1173
+								Name:        "digest",
1174
+								Type:        "string",
1175
+								Format:      "<tarsum>",
1176
+								Regexp:      digest.DigestRegexp,
1177
+								Required:    true,
1178
+								Description: `Digest of uploaded blob.`,
1179
+							},
1180
+						},
1181
+						Body: BodyDescriptor{
1182
+							ContentType: "application/octet-stream",
1183
+							Format:      "<binary chunk>",
1184
+						},
1185
+						Successes: []ResponseDescriptor{
1186
+							{
1187
+								Name:        "Upload Complete",
1188
+								Description: "The upload has been completed and accepted by the registry. The canonical location will be available in the `Location` header.",
1189
+								StatusCode:  http.StatusNoContent,
1190
+								Headers: []ParameterDescriptor{
1191
+									{
1192
+										Name:   "Location",
1193
+										Type:   "url",
1194
+										Format: "<blob location>",
1195
+									},
1196
+									{
1197
+										Name:        "Content-Range",
1198
+										Type:        "header",
1199
+										Format:      "<start of range>-<end of range, inclusive>",
1200
+										Description: "Range of bytes identifying the desired block of content represented by the body. Start must match the end of offset retrieved via status check. Note that this is a non-standard use of the `Content-Range` header.",
1201
+									},
1202
+									{
1203
+										Name:        "Content-Length",
1204
+										Type:        "integer",
1205
+										Format:      "<length of chunk>",
1206
+										Description: "Length of the chunk being uploaded, corresponding the length of the request body.",
1207
+									},
1208
+									digestHeader,
1209
+								},
1210
+							},
1211
+						},
1212
+						Failures: []ResponseDescriptor{
1213
+							{
1214
+								Description: "There was an error processing the upload and it must be restarted.",
1215
+								StatusCode:  http.StatusBadRequest,
1216
+								ErrorCodes: []ErrorCode{
1217
+									ErrorCodeDigestInvalid,
1218
+									ErrorCodeNameInvalid,
1219
+									ErrorCodeBlobUploadInvalid,
1220
+								},
1221
+								Body: BodyDescriptor{
1222
+									ContentType: "application/json; charset=utf-8",
1223
+									Format:      errorsBody,
1224
+								},
1225
+							},
1226
+							unauthorizedResponsePush,
1227
+							{
1228
+								Description: "The upload is unknown to the registry. The upload must be restarted.",
1229
+								StatusCode:  http.StatusNotFound,
1230
+								ErrorCodes: []ErrorCode{
1231
+									ErrorCodeBlobUploadUnknown,
1232
+								},
1233
+								Body: BodyDescriptor{
1234
+									ContentType: "application/json; charset=utf-8",
1235
+									Format:      errorsBody,
1236
+								},
1237
+							},
1238
+							{
1239
+								Description: "The `Content-Range` specification cannot be accepted, either because it does not overlap with the current progress or it is invalid. The contents of the `Range` header may be used to resolve the condition.",
1240
+								StatusCode:  http.StatusRequestedRangeNotSatisfiable,
1241
+								Headers: []ParameterDescriptor{
1242
+									{
1243
+										Name:        "Location",
1244
+										Type:        "url",
1245
+										Format:      "/v2/<name>/blobs/uploads/<uuid>",
1246
+										Description: "The location of the upload. Clients should assume this changes after each request. Clients should use the contents verbatim to complete the upload, adding parameters where required.",
1247
+									},
1248
+									{
1249
+										Name:        "Range",
1250
+										Type:        "header",
1251
+										Format:      "0-<offset>",
1252
+										Description: "Range indicating the current progress of the upload.",
1253
+									},
1254
+								},
1255
+							},
1256
+						},
1257
+					},
1258
+				},
1259
+			},
1260
+			{
1261
+				Method:      "DELETE",
1262
+				Description: "Cancel outstanding upload processes, releasing associated resources. If this is not called, the unfinished uploads will eventually timeout.",
1263
+				Requests: []RequestDescriptor{
1264
+					{
1265
+						Description: "Cancel the upload specified by `uuid`.",
1266
+						PathParameters: []ParameterDescriptor{
1267
+							nameParameterDescriptor,
1268
+							uuidParameterDescriptor,
1269
+						},
1270
+						Headers: []ParameterDescriptor{
1271
+							hostHeader,
1272
+							authHeader,
1273
+							contentLengthZeroHeader,
1274
+						},
1275
+						Successes: []ResponseDescriptor{
1276
+							{
1277
+								Name:        "Upload Deleted",
1278
+								Description: "The upload has been successfully deleted.",
1279
+								StatusCode:  http.StatusNoContent,
1280
+								Headers: []ParameterDescriptor{
1281
+									contentLengthZeroHeader,
1282
+								},
1283
+							},
1284
+						},
1285
+						Failures: []ResponseDescriptor{
1286
+							{
1287
+								Description: "An error was encountered processing the delete. The client may ignore this error.",
1288
+								StatusCode:  http.StatusBadRequest,
1289
+								ErrorCodes: []ErrorCode{
1290
+									ErrorCodeNameInvalid,
1291
+									ErrorCodeBlobUploadInvalid,
1292
+								},
1293
+								Body: BodyDescriptor{
1294
+									ContentType: "application/json; charset=utf-8",
1295
+									Format:      errorsBody,
1296
+								},
1297
+							},
1298
+							unauthorizedResponse,
1299
+							{
1300
+								Description: "The upload is unknown to the registry. The client may ignore this error and assume the upload has been deleted.",
1301
+								StatusCode:  http.StatusNotFound,
1302
+								ErrorCodes: []ErrorCode{
1303
+									ErrorCodeBlobUploadUnknown,
1304
+								},
1305
+								Body: BodyDescriptor{
1306
+									ContentType: "application/json; charset=utf-8",
1307
+									Format:      errorsBody,
1308
+								},
1309
+							},
1310
+						},
1311
+					},
1312
+				},
1313
+			},
1314
+		},
1315
+	},
1316
+}
1317
+
1318
+// ErrorDescriptors provides a list of HTTP API Error codes that may be
1319
+// encountered when interacting with the registry API.
1320
+var errorDescriptors = []ErrorDescriptor{
1321
+	{
1322
+		Code:    ErrorCodeUnknown,
1323
+		Value:   "UNKNOWN",
1324
+		Message: "unknown error",
1325
+		Description: `Generic error returned when the error does not have an
1326
+		API classification.`,
1327
+	},
1328
+	{
1329
+		Code:    ErrorCodeUnsupported,
1330
+		Value:   "UNSUPPORTED",
1331
+		Message: "The operation is unsupported.",
1332
+		Description: `The operation was unsupported due to a missing
1333
+		implementation or invalid set of parameters.`,
1334
+	},
1335
+	{
1336
+		Code:    ErrorCodeUnauthorized,
1337
+		Value:   "UNAUTHORIZED",
1338
+		Message: "access to the requested resource is not authorized",
1339
+		Description: `The access controller denied access for the operation on
1340
+		a resource. Often this will be accompanied by a 401 Unauthorized
1341
+		response status.`,
1342
+	},
1343
+	{
1344
+		Code:    ErrorCodeDigestInvalid,
1345
+		Value:   "DIGEST_INVALID",
1346
+		Message: "provided digest did not match uploaded content",
1347
+		Description: `When a blob is uploaded, the registry will check that
1348
+		the content matches the digest provided by the client. The error may
1349
+		include a detail structure with the key "digest", including the
1350
+		invalid digest string. This error may also be returned when a manifest
1351
+		includes an invalid layer digest.`,
1352
+		HTTPStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
1353
+	},
1354
+	{
1355
+		Code:    ErrorCodeSizeInvalid,
1356
+		Value:   "SIZE_INVALID",
1357
+		Message: "provided length did not match content length",
1358
+		Description: `When a layer is uploaded, the provided size will be
1359
+		checked against the uploaded content. If they do not match, this error
1360
+		will be returned.`,
1361
+		HTTPStatusCodes: []int{http.StatusBadRequest},
1362
+	},
1363
+	{
1364
+		Code:    ErrorCodeNameInvalid,
1365
+		Value:   "NAME_INVALID",
1366
+		Message: "invalid repository name",
1367
+		Description: `Invalid repository name encountered either during
1368
+		manifest validation or any API operation.`,
1369
+		HTTPStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
1370
+	},
1371
+	{
1372
+		Code:    ErrorCodeTagInvalid,
1373
+		Value:   "TAG_INVALID",
1374
+		Message: "manifest tag did not match URI",
1375
+		Description: `During a manifest upload, if the tag in the manifest
1376
+		does not match the uri tag, this error will be returned.`,
1377
+		HTTPStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
1378
+	},
1379
+	{
1380
+		Code:    ErrorCodeNameUnknown,
1381
+		Value:   "NAME_UNKNOWN",
1382
+		Message: "repository name not known to registry",
1383
+		Description: `This is returned if the name used during an operation is
1384
+		unknown to the registry.`,
1385
+		HTTPStatusCodes: []int{http.StatusNotFound},
1386
+	},
1387
+	{
1388
+		Code:    ErrorCodeManifestUnknown,
1389
+		Value:   "MANIFEST_UNKNOWN",
1390
+		Message: "manifest unknown",
1391
+		Description: `This error is returned when the manifest, identified by
1392
+		name and tag is unknown to the repository.`,
1393
+		HTTPStatusCodes: []int{http.StatusNotFound},
1394
+	},
1395
+	{
1396
+		Code:    ErrorCodeManifestInvalid,
1397
+		Value:   "MANIFEST_INVALID",
1398
+		Message: "manifest invalid",
1399
+		Description: `During upload, manifests undergo several checks ensuring
1400
+		validity. If those checks fail, this error may be returned, unless a
1401
+		more specific error is included. The detail will contain information
1402
+		the failed validation.`,
1403
+		HTTPStatusCodes: []int{http.StatusBadRequest},
1404
+	},
1405
+	{
1406
+		Code:    ErrorCodeManifestUnverified,
1407
+		Value:   "MANIFEST_UNVERIFIED",
1408
+		Message: "manifest failed signature verification",
1409
+		Description: `During manifest upload, if the manifest fails signature
1410
+		verification, this error will be returned.`,
1411
+		HTTPStatusCodes: []int{http.StatusBadRequest},
1412
+	},
1413
+	{
1414
+		Code:    ErrorCodeBlobUnknown,
1415
+		Value:   "BLOB_UNKNOWN",
1416
+		Message: "blob unknown to registry",
1417
+		Description: `This error may be returned when a blob is unknown to the
1418
+		registry in a specified repository. This can be returned with a
1419
+		standard get or if a manifest references an unknown layer during
1420
+		upload.`,
1421
+		HTTPStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
1422
+	},
1423
+
1424
+	{
1425
+		Code:    ErrorCodeBlobUploadUnknown,
1426
+		Value:   "BLOB_UPLOAD_UNKNOWN",
1427
+		Message: "blob upload unknown to registry",
1428
+		Description: `If a blob upload has been cancelled or was never
1429
+		started, this error code may be returned.`,
1430
+		HTTPStatusCodes: []int{http.StatusNotFound},
1431
+	},
1432
+	{
1433
+		Code:    ErrorCodeBlobUploadInvalid,
1434
+		Value:   "BLOB_UPLOAD_INVALID",
1435
+		Message: "blob upload invalid",
1436
+		Description: `The blob upload encountered an error and can no
1437
+		longer proceed.`,
1438
+		HTTPStatusCodes: []int{http.StatusNotFound},
1439
+	},
1440
+}
1441
+
1442
+var errorCodeToDescriptors map[ErrorCode]ErrorDescriptor
1443
+var idToDescriptors map[string]ErrorDescriptor
1444
+var routeDescriptorsMap map[string]RouteDescriptor
1445
+
1446
+func init() {
1447
+	errorCodeToDescriptors = make(map[ErrorCode]ErrorDescriptor, len(errorDescriptors))
1448
+	idToDescriptors = make(map[string]ErrorDescriptor, len(errorDescriptors))
1449
+	routeDescriptorsMap = make(map[string]RouteDescriptor, len(routeDescriptors))
1450
+
1451
+	for _, descriptor := range errorDescriptors {
1452
+		errorCodeToDescriptors[descriptor.Code] = descriptor
1453
+		idToDescriptors[descriptor.Value] = descriptor
1454
+	}
1455
+	for _, descriptor := range routeDescriptors {
1456
+		routeDescriptorsMap[descriptor.Name] = descriptor
1457
+	}
1458
+}
0 1459
new file mode 100644
... ...
@@ -0,0 +1,9 @@
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 in docker core.
8
+package v2
0 9
new file mode 100644
... ...
@@ -0,0 +1,194 @@
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
+	// ErrorCodeUnsupported is returned when an operation is not supported.
16
+	ErrorCodeUnsupported
17
+
18
+	// ErrorCodeUnauthorized is returned if a request is not authorized.
19
+	ErrorCodeUnauthorized
20
+
21
+	// ErrorCodeDigestInvalid is returned when uploading a blob if the
22
+	// provided digest does not match the blob contents.
23
+	ErrorCodeDigestInvalid
24
+
25
+	// ErrorCodeSizeInvalid is returned when uploading a blob if the provided
26
+	// size does not match the content length.
27
+	ErrorCodeSizeInvalid
28
+
29
+	// ErrorCodeNameInvalid is returned when the name in the manifest does not
30
+	// match the provided name.
31
+	ErrorCodeNameInvalid
32
+
33
+	// ErrorCodeTagInvalid is returned when the tag in the manifest does not
34
+	// match the provided tag.
35
+	ErrorCodeTagInvalid
36
+
37
+	// ErrorCodeNameUnknown when the repository name is not known.
38
+	ErrorCodeNameUnknown
39
+
40
+	// ErrorCodeManifestUnknown returned when image manifest is unknown.
41
+	ErrorCodeManifestUnknown
42
+
43
+	// ErrorCodeManifestInvalid returned when an image manifest is invalid,
44
+	// typically during a PUT operation. This error encompasses all errors
45
+	// encountered during manifest validation that aren't signature errors.
46
+	ErrorCodeManifestInvalid
47
+
48
+	// ErrorCodeManifestUnverified is returned when the manifest fails
49
+	// signature verfication.
50
+	ErrorCodeManifestUnverified
51
+
52
+	// ErrorCodeBlobUnknown is returned when a blob is unknown to the
53
+	// registry. This can happen when the manifest references a nonexistent
54
+	// layer or the result is not found by a blob fetch.
55
+	ErrorCodeBlobUnknown
56
+
57
+	// ErrorCodeBlobUploadUnknown is returned when an upload is unknown.
58
+	ErrorCodeBlobUploadUnknown
59
+
60
+	// ErrorCodeBlobUploadInvalid is returned when an upload is invalid.
61
+	ErrorCodeBlobUploadInvalid
62
+)
63
+
64
+// ParseErrorCode attempts to parse the error code string, returning
65
+// ErrorCodeUnknown if the error is not known.
66
+func ParseErrorCode(s string) ErrorCode {
67
+	desc, ok := idToDescriptors[s]
68
+
69
+	if !ok {
70
+		return ErrorCodeUnknown
71
+	}
72
+
73
+	return desc.Code
74
+}
75
+
76
+// Descriptor returns the descriptor for the error code.
77
+func (ec ErrorCode) Descriptor() ErrorDescriptor {
78
+	d, ok := errorCodeToDescriptors[ec]
79
+
80
+	if !ok {
81
+		return ErrorCodeUnknown.Descriptor()
82
+	}
83
+
84
+	return d
85
+}
86
+
87
+// String returns the canonical identifier for this error code.
88
+func (ec ErrorCode) String() string {
89
+	return ec.Descriptor().Value
90
+}
91
+
92
+// Message returned the human-readable error message for this error code.
93
+func (ec ErrorCode) Message() string {
94
+	return ec.Descriptor().Message
95
+}
96
+
97
+// MarshalText encodes the receiver into UTF-8-encoded text and returns the
98
+// result.
99
+func (ec ErrorCode) MarshalText() (text []byte, err error) {
100
+	return []byte(ec.String()), nil
101
+}
102
+
103
+// UnmarshalText decodes the form generated by MarshalText.
104
+func (ec *ErrorCode) UnmarshalText(text []byte) error {
105
+	desc, ok := idToDescriptors[string(text)]
106
+
107
+	if !ok {
108
+		desc = ErrorCodeUnknown.Descriptor()
109
+	}
110
+
111
+	*ec = desc.Code
112
+
113
+	return nil
114
+}
115
+
116
+// Error provides a wrapper around ErrorCode with extra Details provided.
117
+type Error struct {
118
+	Code    ErrorCode   `json:"code"`
119
+	Message string      `json:"message,omitempty"`
120
+	Detail  interface{} `json:"detail,omitempty"`
121
+}
122
+
123
+// Error returns a human readable representation of the error.
124
+func (e Error) Error() string {
125
+	return fmt.Sprintf("%s: %s",
126
+		strings.ToLower(strings.Replace(e.Code.String(), "_", " ", -1)),
127
+		e.Message)
128
+}
129
+
130
+// Errors provides the envelope for multiple errors and a few sugar methods
131
+// for use within the application.
132
+type Errors struct {
133
+	Errors []Error `json:"errors,omitempty"`
134
+}
135
+
136
+// Push pushes an error on to the error stack, with the optional detail
137
+// argument. It is a programming error (ie panic) to push more than one
138
+// detail at a time.
139
+func (errs *Errors) Push(code ErrorCode, details ...interface{}) {
140
+	if len(details) > 1 {
141
+		panic("please specify zero or one detail items for this error")
142
+	}
143
+
144
+	var detail interface{}
145
+	if len(details) > 0 {
146
+		detail = details[0]
147
+	}
148
+
149
+	if err, ok := detail.(error); ok {
150
+		detail = err.Error()
151
+	}
152
+
153
+	errs.PushErr(Error{
154
+		Code:    code,
155
+		Message: code.Message(),
156
+		Detail:  detail,
157
+	})
158
+}
159
+
160
+// PushErr pushes an error interface onto the error stack.
161
+func (errs *Errors) PushErr(err error) {
162
+	switch err.(type) {
163
+	case Error:
164
+		errs.Errors = append(errs.Errors, err.(Error))
165
+	default:
166
+		errs.Errors = append(errs.Errors, Error{Message: err.Error()})
167
+	}
168
+}
169
+
170
+func (errs *Errors) Error() string {
171
+	switch errs.Len() {
172
+	case 0:
173
+		return "<nil>"
174
+	case 1:
175
+		return errs.Errors[0].Error()
176
+	default:
177
+		msg := "errors:\n"
178
+		for _, err := range errs.Errors {
179
+			msg += err.Error() + "\n"
180
+		}
181
+		return msg
182
+	}
183
+}
184
+
185
+// Clear clears the errors.
186
+func (errs *Errors) Clear() {
187
+	errs.Errors = errs.Errors[:0]
188
+}
189
+
190
+// Len returns the current number of errors.
191
+func (errs *Errors) Len() int {
192
+	return len(errs.Errors)
193
+}
0 194
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/distribution/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,100 @@
0
+package v2
1
+
2
+import (
3
+	"fmt"
4
+	"regexp"
5
+	"strings"
6
+)
7
+
8
+// TODO(stevvooe): Move these definitions back to an exported package. While
9
+// they are used with v2 definitions, their relevance expands beyond.
10
+// "distribution/names" is a candidate package.
11
+
12
+const (
13
+	// RepositoryNameComponentMinLength is the minimum number of characters in a
14
+	// single repository name slash-delimited component
15
+	RepositoryNameComponentMinLength = 2
16
+
17
+	// RepositoryNameMinComponents is the minimum number of slash-delimited
18
+	// components that a repository name must have
19
+	RepositoryNameMinComponents = 1
20
+
21
+	// RepositoryNameTotalLengthMax is the maximum total number of characters in
22
+	// a repository name
23
+	RepositoryNameTotalLengthMax = 255
24
+)
25
+
26
+// RepositoryNameComponentRegexp restricts registry path component names to
27
+// start with at least one letter or number, with following parts able to
28
+// be separated by one period, dash or underscore.
29
+var RepositoryNameComponentRegexp = regexp.MustCompile(`[a-z0-9]+(?:[._-][a-z0-9]+)*`)
30
+
31
+// RepositoryNameComponentAnchoredRegexp is the version of
32
+// RepositoryNameComponentRegexp which must completely match the content
33
+var RepositoryNameComponentAnchoredRegexp = regexp.MustCompile(`^` + RepositoryNameComponentRegexp.String() + `$`)
34
+
35
+// RepositoryNameRegexp builds on RepositoryNameComponentRegexp to allow
36
+// multiple path components, separated by a forward slash.
37
+var RepositoryNameRegexp = regexp.MustCompile(`(?:` + RepositoryNameComponentRegexp.String() + `/)*` + RepositoryNameComponentRegexp.String())
38
+
39
+// TagNameRegexp matches valid tag names. From docker/docker:graph/tags.go.
40
+var TagNameRegexp = regexp.MustCompile(`[\w][\w.-]{0,127}`)
41
+
42
+// TODO(stevvooe): Contribute these exports back to core, so they are shared.
43
+
44
+var (
45
+	// ErrRepositoryNameComponentShort is returned when a repository name
46
+	// contains a component which is shorter than
47
+	// RepositoryNameComponentMinLength
48
+	ErrRepositoryNameComponentShort = fmt.Errorf("respository name component must be %v or more characters", RepositoryNameComponentMinLength)
49
+
50
+	// ErrRepositoryNameMissingComponents is returned when a repository name
51
+	// contains fewer than RepositoryNameMinComponents components
52
+	ErrRepositoryNameMissingComponents = fmt.Errorf("repository name must have at least %v components", RepositoryNameMinComponents)
53
+
54
+	// ErrRepositoryNameLong is returned when a repository name is longer than
55
+	// RepositoryNameTotalLengthMax
56
+	ErrRepositoryNameLong = fmt.Errorf("repository name must not be more than %v characters", RepositoryNameTotalLengthMax)
57
+
58
+	// ErrRepositoryNameComponentInvalid is returned when a repository name does
59
+	// not match RepositoryNameComponentRegexp
60
+	ErrRepositoryNameComponentInvalid = fmt.Errorf("repository name component must match %q", RepositoryNameComponentRegexp.String())
61
+)
62
+
63
+// ValidateRespositoryName ensures the repository name is valid for use in the
64
+// registry. This function accepts a superset of what might be accepted by
65
+// docker core or docker hub. If the name does not pass validation, an error,
66
+// describing the conditions, is returned.
67
+//
68
+// Effectively, the name should comply with the following grammar:
69
+//
70
+// 	alpha-numeric := /[a-z0-9]+/
71
+//	separator := /[._-]/
72
+//	component := alpha-numeric [separator alpha-numeric]*
73
+//	namespace := component ['/' component]*
74
+//
75
+// The result of the production, known as the "namespace", should be limited
76
+// to 255 characters.
77
+func ValidateRespositoryName(name string) error {
78
+	if len(name) > RepositoryNameTotalLengthMax {
79
+		return ErrRepositoryNameLong
80
+	}
81
+
82
+	components := strings.Split(name, "/")
83
+
84
+	if len(components) < RepositoryNameMinComponents {
85
+		return ErrRepositoryNameMissingComponents
86
+	}
87
+
88
+	for _, component := range components {
89
+		if len(component) < RepositoryNameComponentMinLength {
90
+			return ErrRepositoryNameComponentShort
91
+		}
92
+
93
+		if !RepositoryNameComponentAnchoredRegexp.MatchString(component) {
94
+			return ErrRepositoryNameComponentInvalid
95
+		}
96
+	}
97
+
98
+	return nil
99
+}
0 100
new file mode 100644
... ...
@@ -0,0 +1,100 @@
0
+package v2
1
+
2
+import (
3
+	"strings"
4
+	"testing"
5
+)
6
+
7
+func TestRepositoryNameRegexp(t *testing.T) {
8
+	for _, testcase := range []struct {
9
+		input string
10
+		err   error
11
+	}{
12
+		{
13
+			input: "short",
14
+		},
15
+		{
16
+			input: "simple/name",
17
+		},
18
+		{
19
+			input: "library/ubuntu",
20
+		},
21
+		{
22
+			input: "docker/stevvooe/app",
23
+		},
24
+		{
25
+			input: "aa/aa/aa/aa/aa/aa/aa/aa/aa/bb/bb/bb/bb/bb/bb",
26
+		},
27
+		{
28
+			input: "aa/aa/bb/bb/bb",
29
+		},
30
+		{
31
+			input: "a/a/a/b/b",
32
+			err:   ErrRepositoryNameComponentShort,
33
+		},
34
+		{
35
+			input: "a/a/a/a/",
36
+			err:   ErrRepositoryNameComponentShort,
37
+		},
38
+		{
39
+			input: "foo.com/bar/baz",
40
+		},
41
+		{
42
+			input: "blog.foo.com/bar/baz",
43
+		},
44
+		{
45
+			input: "asdf",
46
+		},
47
+		{
48
+			input: "asdf$$^/aa",
49
+			err:   ErrRepositoryNameComponentInvalid,
50
+		},
51
+		{
52
+			input: "aa-a/aa",
53
+		},
54
+		{
55
+			input: "aa/aa",
56
+		},
57
+		{
58
+			input: "a-a/a-a",
59
+		},
60
+		{
61
+			input: "a",
62
+			err:   ErrRepositoryNameComponentShort,
63
+		},
64
+		{
65
+			input: "a-/a/a/a",
66
+			err:   ErrRepositoryNameComponentInvalid,
67
+		},
68
+		{
69
+			input: strings.Repeat("a", 255),
70
+		},
71
+		{
72
+			input: strings.Repeat("a", 256),
73
+			err:   ErrRepositoryNameLong,
74
+		},
75
+	} {
76
+
77
+		failf := func(format string, v ...interface{}) {
78
+			t.Logf(testcase.input+": "+format, v...)
79
+			t.Fail()
80
+		}
81
+
82
+		if err := ValidateRespositoryName(testcase.input); err != testcase.err {
83
+			if testcase.err != nil {
84
+				if err != nil {
85
+					failf("unexpected error for invalid repository: got %v, expected %v", err, testcase.err)
86
+				} else {
87
+					failf("expected invalid repository: %v", testcase.err)
88
+				}
89
+			} else {
90
+				if err != nil {
91
+					// Wrong error returned.
92
+					failf("unexpected error validating repository name: %v, expected %v", err, testcase.err)
93
+				} else {
94
+					failf("unexpected error validating repository name: %v", err)
95
+				}
96
+			}
97
+		}
98
+	}
99
+}
0 100
new file mode 100644
... ...
@@ -0,0 +1,47 @@
0
+package v2
1
+
2
+import "github.com/gorilla/mux"
3
+
4
+// The following are definitions of the name under which all V2 routes are
5
+// registered. These symbols can be used to look up a route based on the name.
6
+const (
7
+	RouteNameBase            = "base"
8
+	RouteNameManifest        = "manifest"
9
+	RouteNameTags            = "tags"
10
+	RouteNameBlob            = "blob"
11
+	RouteNameBlobUpload      = "blob-upload"
12
+	RouteNameBlobUploadChunk = "blob-upload-chunk"
13
+)
14
+
15
+var allEndpoints = []string{
16
+	RouteNameManifest,
17
+	RouteNameTags,
18
+	RouteNameBlob,
19
+	RouteNameBlobUpload,
20
+	RouteNameBlobUploadChunk,
21
+}
22
+
23
+// Router builds a gorilla router with named routes for the various API
24
+// methods. This can be used directly by both server implementations and
25
+// clients.
26
+func Router() *mux.Router {
27
+	return RouterWithPrefix("")
28
+}
29
+
30
+// RouterWithPrefix builds a gorilla router with a configured prefix
31
+// on all routes.
32
+func RouterWithPrefix(prefix string) *mux.Router {
33
+	rootRouter := mux.NewRouter()
34
+	router := rootRouter
35
+	if prefix != "" {
36
+		router = router.PathPrefix(prefix).Subrouter()
37
+	}
38
+
39
+	router.StrictSlash(true)
40
+
41
+	for _, descriptor := range routeDescriptors {
42
+		router.Path(descriptor.Path).Name(descriptor.Name)
43
+	}
44
+
45
+	return rootRouter
46
+}
0 47
new file mode 100644
... ...
@@ -0,0 +1,315 @@
0
+package v2
1
+
2
+import (
3
+	"encoding/json"
4
+	"fmt"
5
+	"math/rand"
6
+	"net/http"
7
+	"net/http/httptest"
8
+	"reflect"
9
+	"strings"
10
+	"testing"
11
+	"time"
12
+
13
+	"github.com/gorilla/mux"
14
+)
15
+
16
+type routeTestCase struct {
17
+	RequestURI  string
18
+	ExpectedURI string
19
+	Vars        map[string]string
20
+	RouteName   string
21
+	StatusCode  int
22
+}
23
+
24
+// TestRouter registers a test handler with all the routes and ensures that
25
+// each route returns the expected path variables. Not method verification is
26
+// present. This not meant to be exhaustive but as check to ensure that the
27
+// expected variables are extracted.
28
+//
29
+// This may go away as the application structure comes together.
30
+func TestRouter(t *testing.T) {
31
+	testCases := []routeTestCase{
32
+		{
33
+			RouteName:  RouteNameBase,
34
+			RequestURI: "/v2/",
35
+			Vars:       map[string]string{},
36
+		},
37
+		{
38
+			RouteName:  RouteNameManifest,
39
+			RequestURI: "/v2/foo/manifests/bar",
40
+			Vars: map[string]string{
41
+				"name":      "foo",
42
+				"reference": "bar",
43
+			},
44
+		},
45
+		{
46
+			RouteName:  RouteNameManifest,
47
+			RequestURI: "/v2/foo/bar/manifests/tag",
48
+			Vars: map[string]string{
49
+				"name":      "foo/bar",
50
+				"reference": "tag",
51
+			},
52
+		},
53
+		{
54
+			RouteName:  RouteNameManifest,
55
+			RequestURI: "/v2/foo/bar/manifests/sha256:abcdef01234567890",
56
+			Vars: map[string]string{
57
+				"name":      "foo/bar",
58
+				"reference": "sha256:abcdef01234567890",
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
+				"reference": "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
+
137
+	checkTestRouter(t, testCases, "", true)
138
+	checkTestRouter(t, testCases, "/prefix/", true)
139
+}
140
+
141
+func TestRouterWithPathTraversals(t *testing.T) {
142
+	testCases := []routeTestCase{
143
+		{
144
+			RouteName:   RouteNameBlobUploadChunk,
145
+			RequestURI:  "/v2/foo/../../blob/uploads/D95306FA-FAD3-4E36-8D41-CF1C93EF8286",
146
+			ExpectedURI: "/blob/uploads/D95306FA-FAD3-4E36-8D41-CF1C93EF8286",
147
+			StatusCode:  http.StatusNotFound,
148
+		},
149
+		{
150
+			// Testing for path traversal attack handling
151
+			RouteName:   RouteNameTags,
152
+			RequestURI:  "/v2/foo/../bar/baz/tags/list",
153
+			ExpectedURI: "/v2/bar/baz/tags/list",
154
+			Vars: map[string]string{
155
+				"name": "bar/baz",
156
+			},
157
+		},
158
+	}
159
+	checkTestRouter(t, testCases, "", false)
160
+}
161
+
162
+func TestRouterWithBadCharacters(t *testing.T) {
163
+	if testing.Short() {
164
+		testCases := []routeTestCase{
165
+			{
166
+				RouteName:  RouteNameBlobUploadChunk,
167
+				RequestURI: "/v2/foo/blob/uploads/不95306FA-FAD3-4E36-8D41-CF1C93EF8286",
168
+				StatusCode: http.StatusNotFound,
169
+			},
170
+			{
171
+				// Testing for path traversal attack handling
172
+				RouteName:  RouteNameTags,
173
+				RequestURI: "/v2/foo/不bar/tags/list",
174
+				StatusCode: http.StatusNotFound,
175
+			},
176
+		}
177
+		checkTestRouter(t, testCases, "", true)
178
+	} else {
179
+		// in the long version we're going to fuzz the router
180
+		// with random UTF8 characters not in the 128 bit ASCII range.
181
+		// These are not valid characters for the router and we expect
182
+		// 404s on every test.
183
+		rand.Seed(time.Now().UTC().UnixNano())
184
+		testCases := make([]routeTestCase, 1000)
185
+		for idx := range testCases {
186
+			testCases[idx] = routeTestCase{
187
+				RouteName:  RouteNameTags,
188
+				RequestURI: fmt.Sprintf("/v2/%v/%v/tags/list", randomString(10), randomString(10)),
189
+				StatusCode: http.StatusNotFound,
190
+			}
191
+		}
192
+		checkTestRouter(t, testCases, "", true)
193
+	}
194
+}
195
+
196
+func checkTestRouter(t *testing.T, testCases []routeTestCase, prefix string, deeplyEqual bool) {
197
+	router := RouterWithPrefix(prefix)
198
+
199
+	testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
200
+		testCase := routeTestCase{
201
+			RequestURI: r.RequestURI,
202
+			Vars:       mux.Vars(r),
203
+			RouteName:  mux.CurrentRoute(r).GetName(),
204
+		}
205
+
206
+		enc := json.NewEncoder(w)
207
+
208
+		if err := enc.Encode(testCase); err != nil {
209
+			http.Error(w, err.Error(), http.StatusInternalServerError)
210
+			return
211
+		}
212
+	})
213
+
214
+	// Startup test server
215
+	server := httptest.NewServer(router)
216
+
217
+	for _, testcase := range testCases {
218
+		testcase.RequestURI = strings.TrimSuffix(prefix, "/") + testcase.RequestURI
219
+		// Register the endpoint
220
+		route := router.GetRoute(testcase.RouteName)
221
+		if route == nil {
222
+			t.Fatalf("route for name %q not found", testcase.RouteName)
223
+		}
224
+
225
+		route.Handler(testHandler)
226
+
227
+		u := server.URL + testcase.RequestURI
228
+
229
+		resp, err := http.Get(u)
230
+
231
+		if err != nil {
232
+			t.Fatalf("error issuing get request: %v", err)
233
+		}
234
+
235
+		if testcase.StatusCode == 0 {
236
+			// Override default, zero-value
237
+			testcase.StatusCode = http.StatusOK
238
+		}
239
+		if testcase.ExpectedURI == "" {
240
+			// Override default, zero-value
241
+			testcase.ExpectedURI = testcase.RequestURI
242
+		}
243
+
244
+		if resp.StatusCode != testcase.StatusCode {
245
+			t.Fatalf("unexpected status for %s: %v %v", u, resp.Status, resp.StatusCode)
246
+		}
247
+
248
+		if testcase.StatusCode != http.StatusOK {
249
+			// We don't care about json response.
250
+			continue
251
+		}
252
+
253
+		dec := json.NewDecoder(resp.Body)
254
+
255
+		var actualRouteInfo routeTestCase
256
+		if err := dec.Decode(&actualRouteInfo); err != nil {
257
+			t.Fatalf("error reading json response: %v", err)
258
+		}
259
+		// Needs to be set out of band
260
+		actualRouteInfo.StatusCode = resp.StatusCode
261
+
262
+		if actualRouteInfo.RequestURI != testcase.ExpectedURI {
263
+			t.Fatalf("URI %v incorrectly parsed, expected %v", actualRouteInfo.RequestURI, testcase.ExpectedURI)
264
+		}
265
+
266
+		if actualRouteInfo.RouteName != testcase.RouteName {
267
+			t.Fatalf("incorrect route %q matched, expected %q", actualRouteInfo.RouteName, testcase.RouteName)
268
+		}
269
+
270
+		// when testing deep equality, the actualRouteInfo has an empty ExpectedURI, we don't want
271
+		// that to make the comparison fail. We're otherwise done with the testcase so empty the
272
+		// testcase.ExpectedURI
273
+		testcase.ExpectedURI = ""
274
+		if deeplyEqual && !reflect.DeepEqual(actualRouteInfo, testcase) {
275
+			t.Fatalf("actual does not equal expected: %#v != %#v", actualRouteInfo, testcase)
276
+		}
277
+	}
278
+
279
+}
280
+
281
+// -------------- START LICENSED CODE --------------
282
+// The following code is derivative of https://github.com/google/gofuzz
283
+// gofuzz is licensed under the Apache License, Version 2.0, January 2004,
284
+// a copy of which can be found in the LICENSE file at the root of this
285
+// repository.
286
+
287
+// These functions allow us to generate strings containing only multibyte
288
+// characters that are invalid in our URLs. They are used above for fuzzing
289
+// to ensure we always get 404s on these invalid strings
290
+type charRange struct {
291
+	first, last rune
292
+}
293
+
294
+// choose returns a random unicode character from the given range, using the
295
+// given randomness source.
296
+func (r *charRange) choose() rune {
297
+	count := int64(r.last - r.first)
298
+	return r.first + rune(rand.Int63n(count))
299
+}
300
+
301
+var unicodeRanges = []charRange{
302
+	{'\u00a0', '\u02af'}, // Multi-byte encoded characters
303
+	{'\u4e00', '\u9fff'}, // Common CJK (even longer encodings)
304
+}
305
+
306
+func randomString(length int) string {
307
+	runes := make([]rune, length)
308
+	for i := range runes {
309
+		runes[i] = unicodeRanges[rand.Intn(len(unicodeRanges))].choose()
310
+	}
311
+	return string(runes)
312
+}
313
+
314
+// -------------- END LICENSED CODE --------------
0 315
new file mode 100644
... ...
@@ -0,0 +1,217 @@
0
+package v2
1
+
2
+import (
3
+	"net/http"
4
+	"net/url"
5
+	"strings"
6
+
7
+	"github.com/docker/distribution/digest"
8
+	"github.com/gorilla/mux"
9
+)
10
+
11
+// URLBuilder creates registry API urls from a single base endpoint. It can be
12
+// used to create urls for use in a registry client or server.
13
+//
14
+// All urls will be created from the given base, including the api version.
15
+// For example, if a root of "/foo/" is provided, urls generated will be fall
16
+// under "/foo/v2/...". Most application will only provide a schema, host and
17
+// port, such as "https://localhost:5000/".
18
+type URLBuilder struct {
19
+	root   *url.URL // url root (ie http://localhost/)
20
+	router *mux.Router
21
+}
22
+
23
+// NewURLBuilder creates a URLBuilder with provided root url object.
24
+func NewURLBuilder(root *url.URL) *URLBuilder {
25
+	return &URLBuilder{
26
+		root:   root,
27
+		router: Router(),
28
+	}
29
+}
30
+
31
+// NewURLBuilderFromString workes identically to NewURLBuilder except it takes
32
+// a string argument for the root, returning an error if it is not a valid
33
+// url.
34
+func NewURLBuilderFromString(root string) (*URLBuilder, error) {
35
+	u, err := url.Parse(root)
36
+	if err != nil {
37
+		return nil, err
38
+	}
39
+
40
+	return NewURLBuilder(u), nil
41
+}
42
+
43
+// NewURLBuilderFromRequest uses information from an *http.Request to
44
+// construct the root url.
45
+func NewURLBuilderFromRequest(r *http.Request) *URLBuilder {
46
+	var scheme string
47
+
48
+	forwardedProto := r.Header.Get("X-Forwarded-Proto")
49
+
50
+	switch {
51
+	case len(forwardedProto) > 0:
52
+		scheme = forwardedProto
53
+	case r.TLS != nil:
54
+		scheme = "https"
55
+	case len(r.URL.Scheme) > 0:
56
+		scheme = r.URL.Scheme
57
+	default:
58
+		scheme = "http"
59
+	}
60
+
61
+	host := r.Host
62
+	forwardedHost := r.Header.Get("X-Forwarded-Host")
63
+	if len(forwardedHost) > 0 {
64
+		host = forwardedHost
65
+	}
66
+
67
+	basePath := routeDescriptorsMap[RouteNameBase].Path
68
+
69
+	requestPath := r.URL.Path
70
+	index := strings.Index(requestPath, basePath)
71
+
72
+	u := &url.URL{
73
+		Scheme: scheme,
74
+		Host:   host,
75
+	}
76
+
77
+	if index > 0 {
78
+		// N.B. index+1 is important because we want to include the trailing /
79
+		u.Path = requestPath[0 : index+1]
80
+	}
81
+
82
+	return NewURLBuilder(u)
83
+}
84
+
85
+// BuildBaseURL constructs a base url for the API, typically just "/v2/".
86
+func (ub *URLBuilder) BuildBaseURL() (string, error) {
87
+	route := ub.cloneRoute(RouteNameBase)
88
+
89
+	baseURL, err := route.URL()
90
+	if err != nil {
91
+		return "", err
92
+	}
93
+
94
+	return baseURL.String(), nil
95
+}
96
+
97
+// BuildTagsURL constructs a url to list the tags in the named repository.
98
+func (ub *URLBuilder) BuildTagsURL(name string) (string, error) {
99
+	route := ub.cloneRoute(RouteNameTags)
100
+
101
+	tagsURL, err := route.URL("name", name)
102
+	if err != nil {
103
+		return "", err
104
+	}
105
+
106
+	return tagsURL.String(), nil
107
+}
108
+
109
+// BuildManifestURL constructs a url for the manifest identified by name and
110
+// reference. The argument reference may be either a tag or digest.
111
+func (ub *URLBuilder) BuildManifestURL(name, reference string) (string, error) {
112
+	route := ub.cloneRoute(RouteNameManifest)
113
+
114
+	manifestURL, err := route.URL("name", name, "reference", reference)
115
+	if err != nil {
116
+		return "", err
117
+	}
118
+
119
+	return manifestURL.String(), nil
120
+}
121
+
122
+// BuildBlobURL constructs the url for the blob identified by name and dgst.
123
+func (ub *URLBuilder) BuildBlobURL(name string, dgst digest.Digest) (string, error) {
124
+	route := ub.cloneRoute(RouteNameBlob)
125
+
126
+	layerURL, err := route.URL("name", name, "digest", dgst.String())
127
+	if err != nil {
128
+		return "", err
129
+	}
130
+
131
+	return layerURL.String(), nil
132
+}
133
+
134
+// BuildBlobUploadURL constructs a url to begin a blob upload in the
135
+// repository identified by name.
136
+func (ub *URLBuilder) BuildBlobUploadURL(name string, values ...url.Values) (string, error) {
137
+	route := ub.cloneRoute(RouteNameBlobUpload)
138
+
139
+	uploadURL, err := route.URL("name", name)
140
+	if err != nil {
141
+		return "", err
142
+	}
143
+
144
+	return appendValuesURL(uploadURL, values...).String(), nil
145
+}
146
+
147
+// BuildBlobUploadChunkURL constructs a url for the upload identified by uuid,
148
+// including any url values. This should generally not be used by clients, as
149
+// this url is provided by server implementations during the blob upload
150
+// process.
151
+func (ub *URLBuilder) BuildBlobUploadChunkURL(name, uuid string, values ...url.Values) (string, error) {
152
+	route := ub.cloneRoute(RouteNameBlobUploadChunk)
153
+
154
+	uploadURL, err := route.URL("name", name, "uuid", uuid)
155
+	if err != nil {
156
+		return "", err
157
+	}
158
+
159
+	return appendValuesURL(uploadURL, values...).String(), nil
160
+}
161
+
162
+// clondedRoute returns a clone of the named route from the router. Routes
163
+// must be cloned to avoid modifying them during url generation.
164
+func (ub *URLBuilder) cloneRoute(name string) clonedRoute {
165
+	route := new(mux.Route)
166
+	root := new(url.URL)
167
+
168
+	*route = *ub.router.GetRoute(name) // clone the route
169
+	*root = *ub.root
170
+
171
+	return clonedRoute{Route: route, root: root}
172
+}
173
+
174
+type clonedRoute struct {
175
+	*mux.Route
176
+	root *url.URL
177
+}
178
+
179
+func (cr clonedRoute) URL(pairs ...string) (*url.URL, error) {
180
+	routeURL, err := cr.Route.URL(pairs...)
181
+	if err != nil {
182
+		return nil, err
183
+	}
184
+
185
+	if routeURL.Scheme == "" && routeURL.User == nil && routeURL.Host == "" {
186
+		routeURL.Path = routeURL.Path[1:]
187
+	}
188
+
189
+	return cr.root.ResolveReference(routeURL), nil
190
+}
191
+
192
+// appendValuesURL appends the parameters to the url.
193
+func appendValuesURL(u *url.URL, values ...url.Values) *url.URL {
194
+	merged := u.Query()
195
+
196
+	for _, v := range values {
197
+		for k, vv := range v {
198
+			merged[k] = append(merged[k], vv...)
199
+		}
200
+	}
201
+
202
+	u.RawQuery = merged.Encode()
203
+	return u
204
+}
205
+
206
+// appendValues appends the parameters to the url. Panics if the string is not
207
+// a url.
208
+func appendValues(u string, values ...url.Values) string {
209
+	up, err := url.Parse(u)
210
+
211
+	if err != nil {
212
+		panic(err) // should never happen
213
+	}
214
+
215
+	return appendValuesURL(up, values...).String()
216
+}
0 217
new file mode 100644
... ...
@@ -0,0 +1,225 @@
0
+package v2
1
+
2
+import (
3
+	"net/http"
4
+	"net/url"
5
+	"testing"
6
+)
7
+
8
+type urlBuilderTestCase struct {
9
+	description  string
10
+	expectedPath string
11
+	build        func() (string, error)
12
+}
13
+
14
+func makeURLBuilderTestCases(urlBuilder *URLBuilder) []urlBuilderTestCase {
15
+	return []urlBuilderTestCase{
16
+		{
17
+			description:  "test base url",
18
+			expectedPath: "/v2/",
19
+			build:        urlBuilder.BuildBaseURL,
20
+		},
21
+		{
22
+			description:  "test tags url",
23
+			expectedPath: "/v2/foo/bar/tags/list",
24
+			build: func() (string, error) {
25
+				return urlBuilder.BuildTagsURL("foo/bar")
26
+			},
27
+		},
28
+		{
29
+			description:  "test manifest url",
30
+			expectedPath: "/v2/foo/bar/manifests/tag",
31
+			build: func() (string, error) {
32
+				return urlBuilder.BuildManifestURL("foo/bar", "tag")
33
+			},
34
+		},
35
+		{
36
+			description:  "build blob url",
37
+			expectedPath: "/v2/foo/bar/blobs/tarsum.v1+sha256:abcdef0123456789",
38
+			build: func() (string, error) {
39
+				return urlBuilder.BuildBlobURL("foo/bar", "tarsum.v1+sha256:abcdef0123456789")
40
+			},
41
+		},
42
+		{
43
+			description:  "build blob upload url",
44
+			expectedPath: "/v2/foo/bar/blobs/uploads/",
45
+			build: func() (string, error) {
46
+				return urlBuilder.BuildBlobUploadURL("foo/bar")
47
+			},
48
+		},
49
+		{
50
+			description:  "build blob upload url with digest and size",
51
+			expectedPath: "/v2/foo/bar/blobs/uploads/?digest=tarsum.v1%2Bsha256%3Aabcdef0123456789&size=10000",
52
+			build: func() (string, error) {
53
+				return urlBuilder.BuildBlobUploadURL("foo/bar", url.Values{
54
+					"size":   []string{"10000"},
55
+					"digest": []string{"tarsum.v1+sha256:abcdef0123456789"},
56
+				})
57
+			},
58
+		},
59
+		{
60
+			description:  "build blob upload chunk url",
61
+			expectedPath: "/v2/foo/bar/blobs/uploads/uuid-part",
62
+			build: func() (string, error) {
63
+				return urlBuilder.BuildBlobUploadChunkURL("foo/bar", "uuid-part")
64
+			},
65
+		},
66
+		{
67
+			description:  "build blob upload chunk url with digest and size",
68
+			expectedPath: "/v2/foo/bar/blobs/uploads/uuid-part?digest=tarsum.v1%2Bsha256%3Aabcdef0123456789&size=10000",
69
+			build: func() (string, error) {
70
+				return urlBuilder.BuildBlobUploadChunkURL("foo/bar", "uuid-part", url.Values{
71
+					"size":   []string{"10000"},
72
+					"digest": []string{"tarsum.v1+sha256:abcdef0123456789"},
73
+				})
74
+			},
75
+		},
76
+	}
77
+}
78
+
79
+// TestURLBuilder tests the various url building functions, ensuring they are
80
+// returning the expected values.
81
+func TestURLBuilder(t *testing.T) {
82
+	roots := []string{
83
+		"http://example.com",
84
+		"https://example.com",
85
+		"http://localhost:5000",
86
+		"https://localhost:5443",
87
+	}
88
+
89
+	for _, root := range roots {
90
+		urlBuilder, err := NewURLBuilderFromString(root)
91
+		if err != nil {
92
+			t.Fatalf("unexpected error creating urlbuilder: %v", err)
93
+		}
94
+
95
+		for _, testCase := range makeURLBuilderTestCases(urlBuilder) {
96
+			url, err := testCase.build()
97
+			if err != nil {
98
+				t.Fatalf("%s: error building url: %v", testCase.description, err)
99
+			}
100
+
101
+			expectedURL := root + testCase.expectedPath
102
+
103
+			if url != expectedURL {
104
+				t.Fatalf("%s: %q != %q", testCase.description, url, expectedURL)
105
+			}
106
+		}
107
+	}
108
+}
109
+
110
+func TestURLBuilderWithPrefix(t *testing.T) {
111
+	roots := []string{
112
+		"http://example.com/prefix/",
113
+		"https://example.com/prefix/",
114
+		"http://localhost:5000/prefix/",
115
+		"https://localhost:5443/prefix/",
116
+	}
117
+
118
+	for _, root := range roots {
119
+		urlBuilder, err := NewURLBuilderFromString(root)
120
+		if err != nil {
121
+			t.Fatalf("unexpected error creating urlbuilder: %v", err)
122
+		}
123
+
124
+		for _, testCase := range makeURLBuilderTestCases(urlBuilder) {
125
+			url, err := testCase.build()
126
+			if err != nil {
127
+				t.Fatalf("%s: error building url: %v", testCase.description, err)
128
+			}
129
+
130
+			expectedURL := root[0:len(root)-1] + testCase.expectedPath
131
+
132
+			if url != expectedURL {
133
+				t.Fatalf("%s: %q != %q", testCase.description, url, expectedURL)
134
+			}
135
+		}
136
+	}
137
+}
138
+
139
+type builderFromRequestTestCase struct {
140
+	request *http.Request
141
+	base    string
142
+}
143
+
144
+func TestBuilderFromRequest(t *testing.T) {
145
+	u, err := url.Parse("http://example.com")
146
+	if err != nil {
147
+		t.Fatal(err)
148
+	}
149
+
150
+	forwardedProtoHeader := make(http.Header, 1)
151
+	forwardedProtoHeader.Set("X-Forwarded-Proto", "https")
152
+
153
+	testRequests := []struct {
154
+		request *http.Request
155
+		base    string
156
+	}{
157
+		{
158
+			request: &http.Request{URL: u, Host: u.Host},
159
+			base:    "http://example.com",
160
+		},
161
+		{
162
+			request: &http.Request{URL: u, Host: u.Host, Header: forwardedProtoHeader},
163
+			base:    "https://example.com",
164
+		},
165
+	}
166
+
167
+	for _, tr := range testRequests {
168
+		builder := NewURLBuilderFromRequest(tr.request)
169
+
170
+		for _, testCase := range makeURLBuilderTestCases(builder) {
171
+			url, err := testCase.build()
172
+			if err != nil {
173
+				t.Fatalf("%s: error building url: %v", testCase.description, err)
174
+			}
175
+
176
+			expectedURL := tr.base + testCase.expectedPath
177
+
178
+			if url != expectedURL {
179
+				t.Fatalf("%s: %q != %q", testCase.description, url, expectedURL)
180
+			}
181
+		}
182
+	}
183
+}
184
+
185
+func TestBuilderFromRequestWithPrefix(t *testing.T) {
186
+	u, err := url.Parse("http://example.com/prefix/v2/")
187
+	if err != nil {
188
+		t.Fatal(err)
189
+	}
190
+
191
+	forwardedProtoHeader := make(http.Header, 1)
192
+	forwardedProtoHeader.Set("X-Forwarded-Proto", "https")
193
+
194
+	testRequests := []struct {
195
+		request *http.Request
196
+		base    string
197
+	}{
198
+		{
199
+			request: &http.Request{URL: u, Host: u.Host},
200
+			base:    "http://example.com/prefix/",
201
+		},
202
+		{
203
+			request: &http.Request{URL: u, Host: u.Host, Header: forwardedProtoHeader},
204
+			base:    "https://example.com/prefix/",
205
+		},
206
+	}
207
+
208
+	for _, tr := range testRequests {
209
+		builder := NewURLBuilderFromRequest(tr.request)
210
+
211
+		for _, testCase := range makeURLBuilderTestCases(builder) {
212
+			url, err := testCase.build()
213
+			if err != nil {
214
+				t.Fatalf("%s: error building url: %v", testCase.description, err)
215
+			}
216
+
217
+			expectedURL := tr.base[0:len(tr.base)-1] + testCase.expectedPath
218
+
219
+			if url != expectedURL {
220
+				t.Fatalf("%s: %q != %q", testCase.description, url, expectedURL)
221
+			}
222
+		}
223
+	}
224
+}
... ...
@@ -87,10 +87,10 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
87 87
 		setCurrentRoute(req, match.Route)
88 88
 	}
89 89
 	if handler == nil {
90
-		if r.NotFoundHandler == nil {
91
-			r.NotFoundHandler = http.NotFoundHandler()
92
-		}
93 90
 		handler = r.NotFoundHandler
91
+		if handler == nil {
92
+			handler = http.NotFoundHandler()
93
+		}
94 94
 	}
95 95
 	if !r.KeepContext {
96 96
 		defer context.Clear(req)
... ...
@@ -463,6 +463,15 @@ func TestQueries(t *testing.T) {
463 463
 			shouldMatch: true,
464 464
 		},
465 465
 		{
466
+			title:       "Queries route, match with a query string out of order",
467
+			route:       new(Route).Host("www.example.com").Path("/api").Queries("foo", "bar", "baz", "ding"),
468
+			request:     newRequest("GET", "http://www.example.com/api?baz=ding&foo=bar"),
469
+			vars:        map[string]string{},
470
+			host:        "",
471
+			path:        "",
472
+			shouldMatch: true,
473
+		},
474
+		{
466 475
 			title:       "Queries route, bad query",
467 476
 			route:       new(Route).Queries("foo", "bar", "baz", "ding"),
468 477
 			request:     newRequest("GET", "http://localhost?foo=bar&baz=dong"),
... ...
@@ -471,6 +480,42 @@ func TestQueries(t *testing.T) {
471 471
 			path:        "",
472 472
 			shouldMatch: false,
473 473
 		},
474
+		{
475
+			title:       "Queries route with pattern, match",
476
+			route:       new(Route).Queries("foo", "{v1}"),
477
+			request:     newRequest("GET", "http://localhost?foo=bar"),
478
+			vars:        map[string]string{"v1": "bar"},
479
+			host:        "",
480
+			path:        "",
481
+			shouldMatch: true,
482
+		},
483
+		{
484
+			title:       "Queries route with multiple patterns, match",
485
+			route:       new(Route).Queries("foo", "{v1}", "baz", "{v2}"),
486
+			request:     newRequest("GET", "http://localhost?foo=bar&baz=ding"),
487
+			vars:        map[string]string{"v1": "bar", "v2": "ding"},
488
+			host:        "",
489
+			path:        "",
490
+			shouldMatch: true,
491
+		},
492
+		{
493
+			title:       "Queries route with regexp pattern, match",
494
+			route:       new(Route).Queries("foo", "{v1:[0-9]+}"),
495
+			request:     newRequest("GET", "http://localhost?foo=10"),
496
+			vars:        map[string]string{"v1": "10"},
497
+			host:        "",
498
+			path:        "",
499
+			shouldMatch: true,
500
+		},
501
+		{
502
+			title:       "Queries route with regexp pattern, regexp does not match",
503
+			route:       new(Route).Queries("foo", "{v1:[0-9]+}"),
504
+			request:     newRequest("GET", "http://localhost?foo=a"),
505
+			vars:        map[string]string{},
506
+			host:        "",
507
+			path:        "",
508
+			shouldMatch: false,
509
+		},
474 510
 	}
475 511
 
476 512
 	for _, test := range tests {
... ...
@@ -329,35 +329,6 @@ var pathMatcherTests = []pathMatcherTest{
329 329
 	},
330 330
 }
331 331
 
332
-type queryMatcherTest struct {
333
-	matcher queryMatcher
334
-	url     string
335
-	result  bool
336
-}
337
-
338
-var queryMatcherTests = []queryMatcherTest{
339
-	{
340
-		matcher: queryMatcher(map[string]string{"foo": "bar", "baz": "ding"}),
341
-		url:     "http://localhost:8080/?foo=bar&baz=ding",
342
-		result:  true,
343
-	},
344
-	{
345
-		matcher: queryMatcher(map[string]string{"foo": "", "baz": ""}),
346
-		url:     "http://localhost:8080/?foo=anything&baz=anything",
347
-		result:  true,
348
-	},
349
-	{
350
-		matcher: queryMatcher(map[string]string{"foo": "ding", "baz": "bar"}),
351
-		url:     "http://localhost:8080/?foo=bar&baz=ding",
352
-		result:  false,
353
-	},
354
-	{
355
-		matcher: queryMatcher(map[string]string{"bar": "foo", "ding": "baz"}),
356
-		url:     "http://localhost:8080/?foo=bar&baz=ding",
357
-		result:  false,
358
-	},
359
-}
360
-
361 332
 type schemeMatcherTest struct {
362 333
 	matcher schemeMatcher
363 334
 	url     string
... ...
@@ -519,23 +490,8 @@ func TestPathMatcher(t *testing.T) {
519 519
 	}
520 520
 }
521 521
 
522
-func TestQueryMatcher(t *testing.T) {
523
-	for _, v := range queryMatcherTests {
524
-		request, _ := http.NewRequest("GET", v.url, nil)
525
-		var routeMatch RouteMatch
526
-		result := v.matcher.Match(request, &routeMatch)
527
-		if result != v.result {
528
-			if v.result {
529
-				t.Errorf("%#v: should match %v.", v.matcher, v.url)
530
-			} else {
531
-				t.Errorf("%#v: should not match %v.", v.matcher, v.url)
532
-			}
533
-		}
534
-	}
535
-}
536
-
537 522
 func TestSchemeMatcher(t *testing.T) {
538
-	for _, v := range queryMatcherTests {
523
+	for _, v := range schemeMatcherTests {
539 524
 		request, _ := http.NewRequest("GET", v.url, nil)
540 525
 		var routeMatch RouteMatch
541 526
 		result := v.matcher.Match(request, &routeMatch)
... ...
@@ -735,7 +691,7 @@ func TestNewRegexp(t *testing.T) {
735 735
 	}
736 736
 
737 737
 	for pattern, paths := range tests {
738
-		p, _ = newRouteRegexp(pattern, false, false, false)
738
+		p, _ = newRouteRegexp(pattern, false, false, false, false)
739 739
 		for path, result := range paths {
740 740
 			matches = p.regexp.FindStringSubmatch(path)
741 741
 			if result == nil {
... ...
@@ -14,7 +14,7 @@ import (
14 14
 )
15 15
 
16 16
 // newRouteRegexp parses a route template and returns a routeRegexp,
17
-// used to match a host or path.
17
+// used to match a host, a path or a query string.
18 18
 //
19 19
 // It will extract named variables, assemble a regexp to be matched, create
20 20
 // a "reverse" template to build URLs and compile regexps to validate variable
... ...
@@ -23,7 +23,7 @@ import (
23 23
 // Previously we accepted only Python-like identifiers for variable
24 24
 // names ([a-zA-Z_][a-zA-Z0-9_]*), but currently the only restriction is that
25 25
 // name and pattern can't be empty, and names can't contain a colon.
26
-func newRouteRegexp(tpl string, matchHost, matchPrefix, strictSlash bool) (*routeRegexp, error) {
26
+func newRouteRegexp(tpl string, matchHost, matchPrefix, matchQuery, strictSlash bool) (*routeRegexp, error) {
27 27
 	// Check if it is well-formed.
28 28
 	idxs, errBraces := braceIndices(tpl)
29 29
 	if errBraces != nil {
... ...
@@ -33,11 +33,15 @@ func newRouteRegexp(tpl string, matchHost, matchPrefix, strictSlash bool) (*rout
33 33
 	template := tpl
34 34
 	// Now let's parse it.
35 35
 	defaultPattern := "[^/]+"
36
-	if matchHost {
36
+	if matchQuery {
37
+		defaultPattern = "[^?&]+"
38
+		matchPrefix = true
39
+	} else if matchHost {
37 40
 		defaultPattern = "[^.]+"
38
-		matchPrefix, strictSlash = false, false
41
+		matchPrefix = false
39 42
 	}
40
-	if matchPrefix {
43
+	// Only match strict slash if not matching
44
+	if matchPrefix || matchHost || matchQuery {
41 45
 		strictSlash = false
42 46
 	}
43 47
 	// Set a flag for strictSlash.
... ...
@@ -48,7 +52,10 @@ func newRouteRegexp(tpl string, matchHost, matchPrefix, strictSlash bool) (*rout
48 48
 	}
49 49
 	varsN := make([]string, len(idxs)/2)
50 50
 	varsR := make([]*regexp.Regexp, len(idxs)/2)
51
-	pattern := bytes.NewBufferString("^")
51
+	pattern := bytes.NewBufferString("")
52
+	if !matchQuery {
53
+		pattern.WriteByte('^')
54
+	}
52 55
 	reverse := bytes.NewBufferString("")
53 56
 	var end int
54 57
 	var err error
... ...
@@ -100,6 +107,7 @@ func newRouteRegexp(tpl string, matchHost, matchPrefix, strictSlash bool) (*rout
100 100
 	return &routeRegexp{
101 101
 		template:    template,
102 102
 		matchHost:   matchHost,
103
+		matchQuery:  matchQuery,
103 104
 		strictSlash: strictSlash,
104 105
 		regexp:      reg,
105 106
 		reverse:     reverse.String(),
... ...
@@ -113,8 +121,10 @@ func newRouteRegexp(tpl string, matchHost, matchPrefix, strictSlash bool) (*rout
113 113
 type routeRegexp struct {
114 114
 	// The unmodified template.
115 115
 	template string
116
-	// True for host match, false for path match.
116
+	// True for host match, false for path or query string match.
117 117
 	matchHost bool
118
+	// True for query string match, false for path and host match.
119
+	matchQuery bool
118 120
 	// The strictSlash value defined on the route, but disabled if PathPrefix was used.
119 121
 	strictSlash bool
120 122
 	// Expanded regexp.
... ...
@@ -130,7 +140,11 @@ type routeRegexp struct {
130 130
 // Match matches the regexp against the URL host or path.
131 131
 func (r *routeRegexp) Match(req *http.Request, match *RouteMatch) bool {
132 132
 	if !r.matchHost {
133
-		return r.regexp.MatchString(req.URL.Path)
133
+		if r.matchQuery {
134
+			return r.regexp.MatchString(req.URL.RawQuery)
135
+		} else {
136
+			return r.regexp.MatchString(req.URL.Path)
137
+		}
134 138
 	}
135 139
 	return r.regexp.MatchString(getHost(req))
136 140
 }
... ...
@@ -196,8 +210,9 @@ func braceIndices(s string) ([]int, error) {
196 196
 
197 197
 // routeRegexpGroup groups the route matchers that carry variables.
198 198
 type routeRegexpGroup struct {
199
-	host *routeRegexp
200
-	path *routeRegexp
199
+	host    *routeRegexp
200
+	path    *routeRegexp
201
+	queries []*routeRegexp
201 202
 }
202 203
 
203 204
 // setMatch extracts the variables from the URL once a route matches.
... ...
@@ -234,17 +249,28 @@ func (v *routeRegexpGroup) setMatch(req *http.Request, m *RouteMatch, r *Route)
234 234
 			}
235 235
 		}
236 236
 	}
237
+	// Store query string variables.
238
+	rawQuery := req.URL.RawQuery
239
+	for _, q := range v.queries {
240
+		queryVars := q.regexp.FindStringSubmatch(rawQuery)
241
+		if queryVars != nil {
242
+			for k, v := range q.varsN {
243
+				m.Vars[v] = queryVars[k+1]
244
+			}
245
+		}
246
+	}
237 247
 }
238 248
 
239 249
 // getHost tries its best to return the request host.
240 250
 func getHost(r *http.Request) string {
241
-	if !r.URL.IsAbs() {
242
-		host := r.Host
243
-		// Slice off any port information.
244
-		if i := strings.Index(host, ":"); i != -1 {
245
-			host = host[:i]
246
-		}
247
-		return host
251
+	if r.URL.IsAbs() {
252
+		return r.URL.Host
248 253
 	}
249
-	return r.URL.Host
254
+	host := r.Host
255
+	// Slice off any port information.
256
+	if i := strings.Index(host, ":"); i != -1 {
257
+		host = host[:i]
258
+	}
259
+	return host
260
+
250 261
 }
... ...
@@ -135,12 +135,12 @@ func (r *Route) addMatcher(m matcher) *Route {
135 135
 }
136 136
 
137 137
 // addRegexpMatcher adds a host or path matcher and builder to a route.
138
-func (r *Route) addRegexpMatcher(tpl string, matchHost, matchPrefix bool) error {
138
+func (r *Route) addRegexpMatcher(tpl string, matchHost, matchPrefix, matchQuery bool) error {
139 139
 	if r.err != nil {
140 140
 		return r.err
141 141
 	}
142 142
 	r.regexp = r.getRegexpGroup()
143
-	if !matchHost {
143
+	if !matchHost && !matchQuery {
144 144
 		if len(tpl) == 0 || tpl[0] != '/' {
145 145
 			return fmt.Errorf("mux: path must start with a slash, got %q", tpl)
146 146
 		}
... ...
@@ -148,10 +148,15 @@ func (r *Route) addRegexpMatcher(tpl string, matchHost, matchPrefix bool) error
148 148
 			tpl = strings.TrimRight(r.regexp.path.template, "/") + tpl
149 149
 		}
150 150
 	}
151
-	rr, err := newRouteRegexp(tpl, matchHost, matchPrefix, r.strictSlash)
151
+	rr, err := newRouteRegexp(tpl, matchHost, matchPrefix, matchQuery, r.strictSlash)
152 152
 	if err != nil {
153 153
 		return err
154 154
 	}
155
+	for _, q := range r.regexp.queries {
156
+		if err = uniqueVars(rr.varsN, q.varsN); err != nil {
157
+			return err
158
+		}
159
+	}
155 160
 	if matchHost {
156 161
 		if r.regexp.path != nil {
157 162
 			if err = uniqueVars(rr.varsN, r.regexp.path.varsN); err != nil {
... ...
@@ -165,7 +170,11 @@ func (r *Route) addRegexpMatcher(tpl string, matchHost, matchPrefix bool) error
165 165
 				return err
166 166
 			}
167 167
 		}
168
-		r.regexp.path = rr
168
+		if matchQuery {
169
+			r.regexp.queries = append(r.regexp.queries, rr)
170
+		} else {
171
+			r.regexp.path = rr
172
+		}
169 173
 	}
170 174
 	r.addMatcher(rr)
171 175
 	return nil
... ...
@@ -219,7 +228,7 @@ func (r *Route) Headers(pairs ...string) *Route {
219 219
 // Variable names must be unique in a given route. They can be retrieved
220 220
 // calling mux.Vars(request).
221 221
 func (r *Route) Host(tpl string) *Route {
222
-	r.err = r.addRegexpMatcher(tpl, true, false)
222
+	r.err = r.addRegexpMatcher(tpl, true, false, false)
223 223
 	return r
224 224
 }
225 225
 
... ...
@@ -278,7 +287,7 @@ func (r *Route) Methods(methods ...string) *Route {
278 278
 // Variable names must be unique in a given route. They can be retrieved
279 279
 // calling mux.Vars(request).
280 280
 func (r *Route) Path(tpl string) *Route {
281
-	r.err = r.addRegexpMatcher(tpl, false, false)
281
+	r.err = r.addRegexpMatcher(tpl, false, false, false)
282 282
 	return r
283 283
 }
284 284
 
... ...
@@ -294,35 +303,42 @@ func (r *Route) Path(tpl string) *Route {
294 294
 // Also note that the setting of Router.StrictSlash() has no effect on routes
295 295
 // with a PathPrefix matcher.
296 296
 func (r *Route) PathPrefix(tpl string) *Route {
297
-	r.err = r.addRegexpMatcher(tpl, false, true)
297
+	r.err = r.addRegexpMatcher(tpl, false, true, false)
298 298
 	return r
299 299
 }
300 300
 
301 301
 // Query ----------------------------------------------------------------------
302 302
 
303
-// queryMatcher matches the request against URL queries.
304
-type queryMatcher map[string]string
305
-
306
-func (m queryMatcher) Match(r *http.Request, match *RouteMatch) bool {
307
-	return matchMap(m, r.URL.Query(), false)
308
-}
309
-
310 303
 // Queries adds a matcher for URL query values.
311
-// It accepts a sequence of key/value pairs. For example:
304
+// It accepts a sequence of key/value pairs. Values may define variables.
305
+// For example:
312 306
 //
313 307
 //     r := mux.NewRouter()
314
-//     r.Queries("foo", "bar", "baz", "ding")
308
+//     r.Queries("foo", "bar", "id", "{id:[0-9]+}")
315 309
 //
316 310
 // The above route will only match if the URL contains the defined queries
317
-// values, e.g.: ?foo=bar&baz=ding.
311
+// values, e.g.: ?foo=bar&id=42.
318 312
 //
319 313
 // It the value is an empty string, it will match any value if the key is set.
314
+//
315
+// Variables can define an optional regexp pattern to me matched:
316
+//
317
+// - {name} matches anything until the next slash.
318
+//
319
+// - {name:pattern} matches the given regexp pattern.
320 320
 func (r *Route) Queries(pairs ...string) *Route {
321
-	if r.err == nil {
322
-		var queries map[string]string
323
-		queries, r.err = mapFromPairs(pairs...)
324
-		return r.addMatcher(queryMatcher(queries))
321
+	length := len(pairs)
322
+	if length%2 != 0 {
323
+		r.err = fmt.Errorf(
324
+			"mux: number of parameters must be multiple of 2, got %v", pairs)
325
+		return nil
326
+	}
327
+	for i := 0; i < length; i += 2 {
328
+		if r.err = r.addRegexpMatcher(pairs[i]+"="+pairs[i+1], false, true, true); r.err != nil {
329
+			return r
330
+		}
325 331
 	}
332
+
326 333
 	return r
327 334
 }
328 335
 
... ...
@@ -498,8 +514,9 @@ func (r *Route) getRegexpGroup() *routeRegexpGroup {
498 498
 		} else {
499 499
 			// Copy.
500 500
 			r.regexp = &routeRegexpGroup{
501
-				host: regexp.host,
502
-				path: regexp.path,
501
+				host:    regexp.host,
502
+				path:    regexp.path,
503
+				queries: regexp.queries,
503 504
 			}
504 505
 		}
505 506
 	}