Registry V2 HTTP route and error code definitions

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

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

Signed-off-by: Stephen J Day <stephen.day@docker.com>
This commit is contained in:
Stephen J Day 2014-12-12 11:27:22 -08:00 committed by Derek McGowan
parent e256a0e0bc
commit 2fcad2a10f
8 changed files with 1025 additions and 0 deletions

144
docs/v2/descriptors.go Normal file
View file

@ -0,0 +1,144 @@
package v2
import "net/http"
// TODO(stevvooe): Add route descriptors for each named route, along with
// accepted methods, parameters, returned status codes and error codes.
// ErrorDescriptor provides relevant information about a given error code.
type ErrorDescriptor struct {
// Code is the error code that this descriptor describes.
Code ErrorCode
// Value provides a unique, string key, often captilized with
// underscores, to identify the error code. This value is used as the
// keyed value when serializing api errors.
Value string
// Message is a short, human readable decription of the error condition
// included in API responses.
Message string
// Description provides a complete account of the errors purpose, suitable
// for use in documentation.
Description string
// HTTPStatusCodes provides a list of status under which this error
// condition may arise. If it is empty, the error condition may be seen
// for any status code.
HTTPStatusCodes []int
}
// ErrorDescriptors provides a list of HTTP API Error codes that may be
// encountered when interacting with the registry API.
var ErrorDescriptors = []ErrorDescriptor{
{
Code: ErrorCodeUnknown,
Value: "UNKNOWN",
Message: "unknown error",
Description: `Generic error returned when the error does not have an
API classification.`,
},
{
Code: ErrorCodeDigestInvalid,
Value: "DIGEST_INVALID",
Message: "provided digest did not match uploaded content",
Description: `When a blob is uploaded, the registry will check that
the content matches the digest provided by the client. The error may
include a detail structure with the key "digest", including the
invalid digest string. This error may also be returned when a manifest
includes an invalid layer digest.`,
HTTPStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
},
{
Code: ErrorCodeSizeInvalid,
Value: "SIZE_INVALID",
Message: "provided length did not match content length",
Description: `When a layer is uploaded, the provided size will be
checked against the uploaded content. If they do not match, this error
will be returned.`,
HTTPStatusCodes: []int{http.StatusBadRequest},
},
{
Code: ErrorCodeNameInvalid,
Value: "NAME_INVALID",
Message: "manifest name did not match URI",
Description: `During a manifest upload, if the name in the manifest
does not match the uri name, this error will be returned.`,
HTTPStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
},
{
Code: ErrorCodeTagInvalid,
Value: "TAG_INVALID",
Message: "manifest tag did not match URI",
Description: `During a manifest upload, if the tag in the manifest
does not match the uri tag, this error will be returned.`,
HTTPStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
},
{
Code: ErrorCodeNameUnknown,
Value: "NAME_UNKNOWN",
Message: "repository name not known to registry",
Description: `This is returned if the name used during an operation is
unknown to the registry.`,
HTTPStatusCodes: []int{http.StatusNotFound},
},
{
Code: ErrorCodeManifestUnknown,
Value: "MANIFEST_UNKNOWN",
Message: "manifest unknown",
Description: `This error is returned when the manifest, identified by
name and tag is unknown to the repository.`,
HTTPStatusCodes: []int{http.StatusNotFound},
},
{
Code: ErrorCodeManifestInvalid,
Value: "MANIFEST_INVALID",
Message: "manifest invalid",
Description: `During upload, manifests undergo several checks ensuring
validity. If those checks fail, this error may be returned, unless a
more specific error is included. The detail will contain information
the failed validation.`,
HTTPStatusCodes: []int{http.StatusBadRequest},
},
{
Code: ErrorCodeManifestUnverified,
Value: "MANIFEST_UNVERIFIED",
Message: "manifest failed signature verification",
Description: `During manifest upload, if the manifest fails signature
verification, this error will be returned.`,
HTTPStatusCodes: []int{http.StatusBadRequest},
},
{
Code: ErrorCodeBlobUnknown,
Value: "BLOB_UNKNOWN",
Message: "blob unknown to registry",
Description: `This error may be returned when a blob is unknown to the
registry in a specified repository. This can be returned with a
standard get or if a manifest references an unknown layer during
upload.`,
HTTPStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
},
{
Code: ErrorCodeBlobUploadUnknown,
Value: "BLOB_UPLOAD_UNKNOWN",
Message: "blob upload unknown to registry",
Description: `If a blob upload has been cancelled or was never
started, this error code may be returned.`,
HTTPStatusCodes: []int{http.StatusNotFound},
},
}
var errorCodeToDescriptors map[ErrorCode]ErrorDescriptor
var idToDescriptors map[string]ErrorDescriptor
func init() {
errorCodeToDescriptors = make(map[ErrorCode]ErrorDescriptor, len(ErrorDescriptors))
idToDescriptors = make(map[string]ErrorDescriptor, len(ErrorDescriptors))
for _, descriptor := range ErrorDescriptors {
errorCodeToDescriptors[descriptor.Code] = descriptor
idToDescriptors[descriptor.Value] = descriptor
}
}

13
docs/v2/doc.go Normal file
View file

@ -0,0 +1,13 @@
// Package v2 describes routes, urls and the error codes used in the Docker
// Registry JSON HTTP API V2. In addition to declarations, descriptors are
// provided for routes and error codes that can be used for implementation and
// automatically generating documentation.
//
// Definitions here are considered to be locked down for the V2 registry api.
// Any changes must be considered carefully and should not proceed without a
// change proposal.
//
// Currently, while the HTTP API definitions are considered stable, the Go API
// exports are considered unstable. Go API consumers should take care when
// relying on these definitions until this message is deleted.
package v2

185
docs/v2/errors.go Normal file
View file

@ -0,0 +1,185 @@
package v2
import (
"fmt"
"strings"
)
// ErrorCode represents the error type. The errors are serialized via strings
// and the integer format may change and should *never* be exported.
type ErrorCode int
const (
// ErrorCodeUnknown is a catch-all for errors not defined below.
ErrorCodeUnknown ErrorCode = iota
// ErrorCodeDigestInvalid is returned when uploading a blob if the
// provided digest does not match the blob contents.
ErrorCodeDigestInvalid
// ErrorCodeSizeInvalid is returned when uploading a blob if the provided
// size does not match the content length.
ErrorCodeSizeInvalid
// ErrorCodeNameInvalid is returned when the name in the manifest does not
// match the provided name.
ErrorCodeNameInvalid
// ErrorCodeTagInvalid is returned when the tag in the manifest does not
// match the provided tag.
ErrorCodeTagInvalid
// ErrorCodeNameUnknown when the repository name is not known.
ErrorCodeNameUnknown
// ErrorCodeManifestUnknown returned when image manifest is unknown.
ErrorCodeManifestUnknown
// ErrorCodeManifestInvalid returned when an image manifest is invalid,
// typically during a PUT operation. This error encompasses all errors
// encountered during manifest validation that aren't signature errors.
ErrorCodeManifestInvalid
// ErrorCodeManifestUnverified is returned when the manifest fails
// signature verfication.
ErrorCodeManifestUnverified
// ErrorCodeBlobUnknown is returned when a blob is unknown to the
// registry. This can happen when the manifest references a nonexistent
// layer or the result is not found by a blob fetch.
ErrorCodeBlobUnknown
// ErrorCodeBlobUploadUnknown is returned when an upload is unknown.
ErrorCodeBlobUploadUnknown
)
// ParseErrorCode attempts to parse the error code string, returning
// ErrorCodeUnknown if the error is not known.
func ParseErrorCode(s string) ErrorCode {
desc, ok := idToDescriptors[s]
if !ok {
return ErrorCodeUnknown
}
return desc.Code
}
// Descriptor returns the descriptor for the error code.
func (ec ErrorCode) Descriptor() ErrorDescriptor {
d, ok := errorCodeToDescriptors[ec]
if !ok {
return ErrorCodeUnknown.Descriptor()
}
return d
}
// String returns the canonical identifier for this error code.
func (ec ErrorCode) String() string {
return ec.Descriptor().Value
}
// Message returned the human-readable error message for this error code.
func (ec ErrorCode) Message() string {
return ec.Descriptor().Message
}
// MarshalText encodes the receiver into UTF-8-encoded text and returns the
// result.
func (ec ErrorCode) MarshalText() (text []byte, err error) {
return []byte(ec.String()), nil
}
// UnmarshalText decodes the form generated by MarshalText.
func (ec *ErrorCode) UnmarshalText(text []byte) error {
desc, ok := idToDescriptors[string(text)]
if !ok {
desc = ErrorCodeUnknown.Descriptor()
}
*ec = desc.Code
return nil
}
// Error provides a wrapper around ErrorCode with extra Details provided.
type Error struct {
Code ErrorCode `json:"code"`
Message string `json:"message,omitempty"`
Detail interface{} `json:"detail,omitempty"`
}
// Error returns a human readable representation of the error.
func (e Error) Error() string {
return fmt.Sprintf("%s: %s",
strings.ToLower(strings.Replace(e.Code.String(), "_", " ", -1)),
e.Message)
}
// Errors provides the envelope for multiple errors and a few sugar methods
// for use within the application.
type Errors struct {
Errors []Error `json:"errors,omitempty"`
}
// Push pushes an error on to the error stack, with the optional detail
// argument. It is a programming error (ie panic) to push more than one
// detail at a time.
func (errs *Errors) Push(code ErrorCode, details ...interface{}) {
if len(details) > 1 {
panic("please specify zero or one detail items for this error")
}
var detail interface{}
if len(details) > 0 {
detail = details[0]
}
if err, ok := detail.(error); ok {
detail = err.Error()
}
errs.PushErr(Error{
Code: code,
Message: code.Message(),
Detail: detail,
})
}
// PushErr pushes an error interface onto the error stack.
func (errs *Errors) PushErr(err error) {
switch err.(type) {
case Error:
errs.Errors = append(errs.Errors, err.(Error))
default:
errs.Errors = append(errs.Errors, Error{Message: err.Error()})
}
}
func (errs *Errors) Error() string {
switch errs.Len() {
case 0:
return "<nil>"
case 1:
return errs.Errors[0].Error()
default:
msg := "errors:\n"
for _, err := range errs.Errors {
msg += err.Error() + "\n"
}
return msg
}
}
// Clear clears the errors.
func (errs *Errors) Clear() {
errs.Errors = errs.Errors[:0]
}
// Len returns the current number of errors.
func (errs *Errors) Len() int {
return len(errs.Errors)
}

165
docs/v2/errors_test.go Normal file
View file

@ -0,0 +1,165 @@
package v2
import (
"encoding/json"
"reflect"
"testing"
"github.com/docker/docker-registry/digest"
)
// TestErrorCodes ensures that error code format, mappings and
// marshaling/unmarshaling. round trips are stable.
func TestErrorCodes(t *testing.T) {
for _, desc := range ErrorDescriptors {
if desc.Code.String() != desc.Value {
t.Fatalf("error code string incorrect: %q != %q", desc.Code.String(), desc.Value)
}
if desc.Code.Message() != desc.Message {
t.Fatalf("incorrect message for error code %v: %q != %q", desc.Code, desc.Code.Message(), desc.Message)
}
// Serialize the error code using the json library to ensure that we
// get a string and it works round trip.
p, err := json.Marshal(desc.Code)
if err != nil {
t.Fatalf("error marshaling error code %v: %v", desc.Code, err)
}
if len(p) <= 0 {
t.Fatalf("expected content in marshaled before for error code %v", desc.Code)
}
// First, unmarshal to interface and ensure we have a string.
var ecUnspecified interface{}
if err := json.Unmarshal(p, &ecUnspecified); err != nil {
t.Fatalf("error unmarshaling error code %v: %v", desc.Code, err)
}
if _, ok := ecUnspecified.(string); !ok {
t.Fatalf("expected a string for error code %v on unmarshal got a %T", desc.Code, ecUnspecified)
}
// Now, unmarshal with the error code type and ensure they are equal
var ecUnmarshaled ErrorCode
if err := json.Unmarshal(p, &ecUnmarshaled); err != nil {
t.Fatalf("error unmarshaling error code %v: %v", desc.Code, err)
}
if ecUnmarshaled != desc.Code {
t.Fatalf("unexpected error code during error code marshal/unmarshal: %v != %v", ecUnmarshaled, desc.Code)
}
}
}
// TestErrorsManagement does a quick check of the Errors type to ensure that
// members are properly pushed and marshaled.
func TestErrorsManagement(t *testing.T) {
var errs Errors
errs.Push(ErrorCodeDigestInvalid)
errs.Push(ErrorCodeBlobUnknown,
map[string]digest.Digest{"digest": "sometestblobsumdoesntmatter"})
p, err := json.Marshal(errs)
if err != nil {
t.Fatalf("error marashaling errors: %v", err)
}
expectedJSON := "{\"errors\":[{\"code\":\"DIGEST_INVALID\",\"message\":\"provided digest did not match uploaded content\"},{\"code\":\"BLOB_UNKNOWN\",\"message\":\"blob unknown to registry\",\"detail\":{\"digest\":\"sometestblobsumdoesntmatter\"}}]}"
if string(p) != expectedJSON {
t.Fatalf("unexpected json: %q != %q", string(p), expectedJSON)
}
errs.Clear()
errs.Push(ErrorCodeUnknown)
expectedJSON = "{\"errors\":[{\"code\":\"UNKNOWN\",\"message\":\"unknown error\"}]}"
p, err = json.Marshal(errs)
if err != nil {
t.Fatalf("error marashaling errors: %v", err)
}
if string(p) != expectedJSON {
t.Fatalf("unexpected json: %q != %q", string(p), expectedJSON)
}
}
// TestMarshalUnmarshal ensures that api errors can round trip through json
// without losing information.
func TestMarshalUnmarshal(t *testing.T) {
var errors Errors
for _, testcase := range []struct {
description string
err Error
}{
{
description: "unknown error",
err: Error{
Code: ErrorCodeUnknown,
Message: ErrorCodeUnknown.Descriptor().Message,
},
},
{
description: "unknown manifest",
err: Error{
Code: ErrorCodeManifestUnknown,
Message: ErrorCodeManifestUnknown.Descriptor().Message,
},
},
{
description: "unknown manifest",
err: Error{
Code: ErrorCodeBlobUnknown,
Message: ErrorCodeBlobUnknown.Descriptor().Message,
Detail: map[string]interface{}{"digest": "asdfqwerqwerqwerqwer"},
},
},
} {
fatalf := func(format string, args ...interface{}) {
t.Fatalf(testcase.description+": "+format, args...)
}
unexpectedErr := func(err error) {
fatalf("unexpected error: %v", err)
}
p, err := json.Marshal(testcase.err)
if err != nil {
unexpectedErr(err)
}
var unmarshaled Error
if err := json.Unmarshal(p, &unmarshaled); err != nil {
unexpectedErr(err)
}
if !reflect.DeepEqual(unmarshaled, testcase.err) {
fatalf("errors not equal after round trip: %#v != %#v", unmarshaled, testcase.err)
}
// Roll everything up into an error response envelope.
errors.PushErr(testcase.err)
}
p, err := json.Marshal(errors)
if err != nil {
t.Fatalf("unexpected error marshaling error envelope: %v", err)
}
var unmarshaled Errors
if err := json.Unmarshal(p, &unmarshaled); err != nil {
t.Fatalf("unexpected error unmarshaling error envelope: %v", err)
}
if !reflect.DeepEqual(unmarshaled, errors) {
t.Fatalf("errors not equal after round trip: %#v != %#v", unmarshaled, errors)
}
}

69
docs/v2/routes.go Normal file
View file

@ -0,0 +1,69 @@
package v2
import (
"github.com/docker/docker-registry/common"
"github.com/gorilla/mux"
)
// The following are definitions of the name under which all V2 routes are
// registered. These symbols can be used to look up a route based on the name.
const (
RouteNameBase = "base"
RouteNameManifest = "manifest"
RouteNameTags = "tags"
RouteNameBlob = "blob"
RouteNameBlobUpload = "blob-upload"
RouteNameBlobUploadChunk = "blob-upload-chunk"
)
var allEndpoints = []string{
RouteNameManifest,
RouteNameTags,
RouteNameBlob,
RouteNameBlobUpload,
RouteNameBlobUploadChunk,
}
// Router builds a gorilla router with named routes for the various API
// methods. This can be used directly by both server implementations and
// clients.
func Router() *mux.Router {
router := mux.NewRouter().
StrictSlash(true)
// GET /v2/ Check Check that the registry implements API version 2(.1)
router.
Path("/v2/").
Name(RouteNameBase)
// GET /v2/<name>/manifest/<tag> Image Manifest Fetch the image manifest identified by name and tag.
// PUT /v2/<name>/manifest/<tag> Image Manifest Upload the image manifest identified by name and tag.
// DELETE /v2/<name>/manifest/<tag> Image Manifest Delete the image identified by name and tag.
router.
Path("/v2/{name:" + common.RepositoryNameRegexp.String() + "}/manifests/{tag:" + common.TagNameRegexp.String() + "}").
Name(RouteNameManifest)
// GET /v2/<name>/tags/list Tags Fetch the tags under the repository identified by name.
router.
Path("/v2/{name:" + common.RepositoryNameRegexp.String() + "}/tags/list").
Name(RouteNameTags)
// GET /v2/<name>/blob/<digest> Layer Fetch the blob identified by digest.
router.
Path("/v2/{name:" + common.RepositoryNameRegexp.String() + "}/blobs/{digest:[a-zA-Z0-9-_+.]+:[a-zA-Z0-9-_+.=]+}").
Name(RouteNameBlob)
// POST /v2/<name>/blob/upload/ Layer Upload Initiate an upload of the layer identified by tarsum.
router.
Path("/v2/{name:" + common.RepositoryNameRegexp.String() + "}/blobs/uploads/").
Name(RouteNameBlobUpload)
// GET /v2/<name>/blob/upload/<uuid> Layer Upload Get the status of the upload identified by tarsum and uuid.
// PUT /v2/<name>/blob/upload/<uuid> Layer Upload Upload all or a chunk of the upload identified by tarsum and uuid.
// DELETE /v2/<name>/blob/upload/<uuid> Layer Upload Cancel the upload identified by layer and uuid
router.
Path("/v2/{name:" + common.RepositoryNameRegexp.String() + "}/blobs/uploads/{uuid}").
Name(RouteNameBlobUploadChunk)
return router
}

184
docs/v2/routes_test.go Normal file
View file

@ -0,0 +1,184 @@
package v2
import (
"encoding/json"
"net/http"
"net/http/httptest"
"reflect"
"testing"
"github.com/gorilla/mux"
)
type routeTestCase struct {
RequestURI string
Vars map[string]string
RouteName string
StatusCode int
}
// TestRouter registers a test handler with all the routes and ensures that
// each route returns the expected path variables. Not method verification is
// present. This not meant to be exhaustive but as check to ensure that the
// expected variables are extracted.
//
// This may go away as the application structure comes together.
func TestRouter(t *testing.T) {
router := Router()
testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
testCase := routeTestCase{
RequestURI: r.RequestURI,
Vars: mux.Vars(r),
RouteName: mux.CurrentRoute(r).GetName(),
}
enc := json.NewEncoder(w)
if err := enc.Encode(testCase); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
})
// Startup test server
server := httptest.NewServer(router)
for _, testcase := range []routeTestCase{
{
RouteName: RouteNameBase,
RequestURI: "/v2/",
Vars: map[string]string{},
},
{
RouteName: RouteNameManifest,
RequestURI: "/v2/foo/bar/manifests/tag",
Vars: map[string]string{
"name": "foo/bar",
"tag": "tag",
},
},
{
RouteName: RouteNameTags,
RequestURI: "/v2/foo/bar/tags/list",
Vars: map[string]string{
"name": "foo/bar",
},
},
{
RouteName: RouteNameBlob,
RequestURI: "/v2/foo/bar/blobs/tarsum.dev+foo:abcdef0919234",
Vars: map[string]string{
"name": "foo/bar",
"digest": "tarsum.dev+foo:abcdef0919234",
},
},
{
RouteName: RouteNameBlob,
RequestURI: "/v2/foo/bar/blobs/sha256:abcdef0919234",
Vars: map[string]string{
"name": "foo/bar",
"digest": "sha256:abcdef0919234",
},
},
{
RouteName: RouteNameBlobUpload,
RequestURI: "/v2/foo/bar/blobs/uploads/",
Vars: map[string]string{
"name": "foo/bar",
},
},
{
RouteName: RouteNameBlobUploadChunk,
RequestURI: "/v2/foo/bar/blobs/uploads/uuid",
Vars: map[string]string{
"name": "foo/bar",
"uuid": "uuid",
},
},
{
RouteName: RouteNameBlobUploadChunk,
RequestURI: "/v2/foo/bar/blobs/uploads/D95306FA-FAD3-4E36-8D41-CF1C93EF8286",
Vars: map[string]string{
"name": "foo/bar",
"uuid": "D95306FA-FAD3-4E36-8D41-CF1C93EF8286",
},
},
{
RouteName: RouteNameBlobUploadChunk,
RequestURI: "/v2/foo/bar/blobs/uploads/RDk1MzA2RkEtRkFEMy00RTM2LThENDEtQ0YxQzkzRUY4Mjg2IA==",
Vars: map[string]string{
"name": "foo/bar",
"uuid": "RDk1MzA2RkEtRkFEMy00RTM2LThENDEtQ0YxQzkzRUY4Mjg2IA==",
},
},
{
// Check ambiguity: ensure we can distinguish between tags for
// "foo/bar/image/image" and image for "foo/bar/image" with tag
// "tags"
RouteName: RouteNameManifest,
RequestURI: "/v2/foo/bar/manifests/manifests/tags",
Vars: map[string]string{
"name": "foo/bar/manifests",
"tag": "tags",
},
},
{
// This case presents an ambiguity between foo/bar with tag="tags"
// and list tags for "foo/bar/manifest"
RouteName: RouteNameTags,
RequestURI: "/v2/foo/bar/manifests/tags/list",
Vars: map[string]string{
"name": "foo/bar/manifests",
},
},
{
RouteName: RouteNameBlobUploadChunk,
RequestURI: "/v2/foo/../../blob/uploads/D95306FA-FAD3-4E36-8D41-CF1C93EF8286",
StatusCode: http.StatusNotFound,
},
} {
// Register the endpoint
router.GetRoute(testcase.RouteName).Handler(testHandler)
u := server.URL + testcase.RequestURI
resp, err := http.Get(u)
if err != nil {
t.Fatalf("error issuing get request: %v", err)
}
if testcase.StatusCode == 0 {
// Override default, zero-value
testcase.StatusCode = http.StatusOK
}
if resp.StatusCode != testcase.StatusCode {
t.Fatalf("unexpected status for %s: %v %v", u, resp.Status, resp.StatusCode)
}
if testcase.StatusCode != http.StatusOK {
// We don't care about json response.
continue
}
dec := json.NewDecoder(resp.Body)
var actualRouteInfo routeTestCase
if err := dec.Decode(&actualRouteInfo); err != nil {
t.Fatalf("error reading json response: %v", err)
}
// Needs to be set out of band
actualRouteInfo.StatusCode = resp.StatusCode
if actualRouteInfo.RouteName != testcase.RouteName {
t.Fatalf("incorrect route %q matched, expected %q", actualRouteInfo.RouteName, testcase.RouteName)
}
if !reflect.DeepEqual(actualRouteInfo, testcase) {
t.Fatalf("actual does not equal expected: %#v != %#v", actualRouteInfo, testcase)
}
}
}

165
docs/v2/urls.go Normal file
View file

@ -0,0 +1,165 @@
package v2
import (
"net/http"
"net/url"
"github.com/docker/docker-registry/digest"
"github.com/gorilla/mux"
)
// URLBuilder creates registry API urls from a single base endpoint. It can be
// used to create urls for use in a registry client or server.
//
// All urls will be created from the given base, including the api version.
// For example, if a root of "/foo/" is provided, urls generated will be fall
// under "/foo/v2/...". Most application will only provide a schema, host and
// port, such as "https://localhost:5000/".
type URLBuilder struct {
root *url.URL // url root (ie http://localhost/)
router *mux.Router
}
// NewURLBuilder creates a URLBuilder with provided root url object.
func NewURLBuilder(root *url.URL) *URLBuilder {
return &URLBuilder{
root: root,
router: Router(),
}
}
// NewURLBuilderFromString workes identically to NewURLBuilder except it takes
// a string argument for the root, returning an error if it is not a valid
// url.
func NewURLBuilderFromString(root string) (*URLBuilder, error) {
u, err := url.Parse(root)
if err != nil {
return nil, err
}
return NewURLBuilder(u), nil
}
// NewURLBuilderFromRequest uses information from an *http.Request to
// construct the root url.
func NewURLBuilderFromRequest(r *http.Request) *URLBuilder {
u := &url.URL{
Scheme: r.URL.Scheme,
Host: r.Host,
}
return NewURLBuilder(u)
}
// BuildBaseURL constructs a base url for the API, typically just "/v2/".
func (ub *URLBuilder) BuildBaseURL() (string, error) {
route := ub.cloneRoute(RouteNameBase)
baseURL, err := route.URL()
if err != nil {
return "", err
}
return baseURL.String(), nil
}
// BuildTagsURL constructs a url to list the tags in the named repository.
func (ub *URLBuilder) BuildTagsURL(name string) (string, error) {
route := ub.cloneRoute(RouteNameTags)
tagsURL, err := route.URL("name", name)
if err != nil {
return "", err
}
return tagsURL.String(), nil
}
// BuildManifestURL constructs a url for the manifest identified by name and tag.
func (ub *URLBuilder) BuildManifestURL(name, tag string) (string, error) {
route := ub.cloneRoute(RouteNameManifest)
manifestURL, err := route.URL("name", name, "tag", tag)
if err != nil {
return "", err
}
return manifestURL.String(), nil
}
// BuildBlobURL constructs the url for the blob identified by name and dgst.
func (ub *URLBuilder) BuildBlobURL(name string, dgst digest.Digest) (string, error) {
route := ub.cloneRoute(RouteNameBlob)
layerURL, err := route.URL("name", name, "digest", dgst.String())
if err != nil {
return "", err
}
return layerURL.String(), nil
}
// BuildBlobUploadURL constructs a url to begin a blob upload in the
// repository identified by name.
func (ub *URLBuilder) BuildBlobUploadURL(name string, values ...url.Values) (string, error) {
route := ub.cloneRoute(RouteNameBlobUpload)
uploadURL, err := route.URL("name", name)
if err != nil {
return "", err
}
return appendValuesURL(uploadURL, values...).String(), nil
}
// BuildBlobUploadChunkURL constructs a url for the upload identified by uuid,
// including any url values. This should generally not be used by clients, as
// this url is provided by server implementations during the blob upload
// process.
func (ub *URLBuilder) BuildBlobUploadChunkURL(name, uuid string, values ...url.Values) (string, error) {
route := ub.cloneRoute(RouteNameBlobUploadChunk)
uploadURL, err := route.URL("name", name, "uuid", uuid)
if err != nil {
return "", err
}
return appendValuesURL(uploadURL, values...).String(), nil
}
// clondedRoute returns a clone of the named route from the router. Routes
// must be cloned to avoid modifying them during url generation.
func (ub *URLBuilder) cloneRoute(name string) *mux.Route {
route := new(mux.Route)
*route = *ub.router.GetRoute(name) // clone the route
return route.
Schemes(ub.root.Scheme).
Host(ub.root.Host)
}
// appendValuesURL appends the parameters to the url.
func appendValuesURL(u *url.URL, values ...url.Values) *url.URL {
merged := u.Query()
for _, v := range values {
for k, vv := range v {
merged[k] = append(merged[k], vv...)
}
}
u.RawQuery = merged.Encode()
return u
}
// appendValues appends the parameters to the url. Panics if the string is not
// a url.
func appendValues(u string, values ...url.Values) string {
up, err := url.Parse(u)
if err != nil {
panic(err) // should never happen
}
return appendValuesURL(up, values...).String()
}

100
docs/v2/urls_test.go Normal file
View file

@ -0,0 +1,100 @@
package v2
import (
"net/url"
"testing"
)
type urlBuilderTestCase struct {
description string
expected string
build func() (string, error)
}
// TestURLBuilder tests the various url building functions, ensuring they are
// returning the expected values.
func TestURLBuilder(t *testing.T) {
root := "http://localhost:5000/"
urlBuilder, err := NewURLBuilderFromString(root)
if err != nil {
t.Fatalf("unexpected error creating urlbuilder: %v", err)
}
for _, testcase := range []struct {
description string
expected string
build func() (string, error)
}{
{
description: "test base url",
expected: "http://localhost:5000/v2/",
build: urlBuilder.BuildBaseURL,
},
{
description: "test tags url",
expected: "http://localhost:5000/v2/foo/bar/tags/list",
build: func() (string, error) {
return urlBuilder.BuildTagsURL("foo/bar")
},
},
{
description: "test manifest url",
expected: "http://localhost:5000/v2/foo/bar/manifests/tag",
build: func() (string, error) {
return urlBuilder.BuildManifestURL("foo/bar", "tag")
},
},
{
description: "build blob url",
expected: "http://localhost:5000/v2/foo/bar/blobs/tarsum.v1+sha256:abcdef0123456789",
build: func() (string, error) {
return urlBuilder.BuildBlobURL("foo/bar", "tarsum.v1+sha256:abcdef0123456789")
},
},
{
description: "build blob upload url",
expected: "http://localhost:5000/v2/foo/bar/blobs/uploads/",
build: func() (string, error) {
return urlBuilder.BuildBlobUploadURL("foo/bar")
},
},
{
description: "build blob upload url with digest and size",
expected: "http://localhost:5000/v2/foo/bar/blobs/uploads/?digest=tarsum.v1%2Bsha256%3Aabcdef0123456789&size=10000",
build: func() (string, error) {
return urlBuilder.BuildBlobUploadURL("foo/bar", url.Values{
"size": []string{"10000"},
"digest": []string{"tarsum.v1+sha256:abcdef0123456789"},
})
},
},
{
description: "build blob upload chunk url",
expected: "http://localhost:5000/v2/foo/bar/blobs/uploads/uuid-part",
build: func() (string, error) {
return urlBuilder.BuildBlobUploadChunkURL("foo/bar", "uuid-part")
},
},
{
description: "build blob upload chunk url with digest and size",
expected: "http://localhost:5000/v2/foo/bar/blobs/uploads/uuid-part?digest=tarsum.v1%2Bsha256%3Aabcdef0123456789&size=10000",
build: func() (string, error) {
return urlBuilder.BuildBlobUploadChunkURL("foo/bar", "uuid-part", url.Values{
"size": []string{"10000"},
"digest": []string{"tarsum.v1+sha256:abcdef0123456789"},
})
},
},
} {
u, err := testcase.build()
if err != nil {
t.Fatalf("%s: error building url: %v", testcase.description, err)
}
if u != testcase.expected {
t.Fatalf("%s: %q != %q", testcase.description, u, testcase.expected)
}
}
}