diff --git a/api/errors/descriptors.go b/api/v2/descriptors.go similarity index 90% rename from api/errors/descriptors.go rename to api/v2/descriptors.go index 1d71162f..68d18241 100644 --- a/api/errors/descriptors.go +++ b/api/v2/descriptors.go @@ -1,7 +1,10 @@ -package errors +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. @@ -26,9 +29,9 @@ type ErrorDescriptor struct { HTTPStatusCodes []int } -// Descriptors provides a list of HTTP API Error codes that may be encountered -// when interacting with the registry API. -var Descriptors = []ErrorDescriptor{ +// 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", @@ -131,10 +134,10 @@ var errorCodeToDescriptors map[ErrorCode]ErrorDescriptor var idToDescriptors map[string]ErrorDescriptor func init() { - errorCodeToDescriptors = make(map[ErrorCode]ErrorDescriptor, len(Descriptors)) - idToDescriptors = make(map[string]ErrorDescriptor, len(Descriptors)) + errorCodeToDescriptors = make(map[ErrorCode]ErrorDescriptor, len(ErrorDescriptors)) + idToDescriptors = make(map[string]ErrorDescriptor, len(ErrorDescriptors)) - for _, descriptor := range Descriptors { + for _, descriptor := range ErrorDescriptors { errorCodeToDescriptors[descriptor.Code] = descriptor idToDescriptors[descriptor.Value] = descriptor } diff --git a/api/v2/doc.go b/api/v2/doc.go new file mode 100644 index 00000000..cde01195 --- /dev/null +++ b/api/v2/doc.go @@ -0,0 +1,9 @@ +// 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 in docker core. +package v2 diff --git a/api/errors/errors.go b/api/v2/errors.go similarity index 91% rename from api/errors/errors.go rename to api/v2/errors.go index b6e64e2a..8c85d3a9 100644 --- a/api/errors/errors.go +++ b/api/v2/errors.go @@ -1,12 +1,4 @@ -// Package errors describes the error codes that may be returned via the -// Docker Registry JSON HTTP API V2. In addition to declaractions, -// descriptions about the error codes and the conditions causing them are -// avialable in detail. -// -// Error 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 in docker core. -package errors +package v2 import ( "fmt" diff --git a/api/errors/errors_test.go b/api/v2/errors_test.go similarity index 98% rename from api/errors/errors_test.go rename to api/v2/errors_test.go index 7a68fe90..d2fc091a 100644 --- a/api/errors/errors_test.go +++ b/api/v2/errors_test.go @@ -1,4 +1,4 @@ -package errors +package v2 import ( "encoding/json" @@ -11,7 +11,7 @@ import ( // TestErrorCodes ensures that error code format, mappings and // marshaling/unmarshaling. round trips are stable. func TestErrorCodes(t *testing.T) { - for _, desc := range Descriptors { + for _, desc := range ErrorDescriptors { if desc.Code.String() != desc.Value { t.Fatalf("error code string incorrect: %q != %q", desc.Code.String(), desc.Value) } diff --git a/routes.go b/api/v2/routes.go similarity index 67% rename from routes.go rename to api/v2/routes.go index b291ee4b..7ebe61d6 100644 --- a/routes.go +++ b/api/v2/routes.go @@ -1,66 +1,69 @@ -package registry +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" - routeNameImageManifest = "image-manifest" - routeNameTags = "tags" - routeNameBlob = "blob" - routeNameBlobUpload = "blob-upload" - routeNameBlobUploadResume = "blob-upload-resume" + RouteNameBase = "base" + RouteNameManifest = "manifest" + RouteNameTags = "tags" + RouteNameBlob = "blob" + RouteNameBlobUpload = "blob-upload" + RouteNameBlobUploadChunk = "blob-upload-chunk" ) var allEndpoints = []string{ - routeNameImageManifest, - routeNameTags, - routeNameBlob, - routeNameBlobUpload, - routeNameBlobUploadResume, + RouteNameManifest, + RouteNameTags, + RouteNameBlob, + RouteNameBlobUpload, + RouteNameBlobUploadChunk, } -// v2APIRouter builds a gorilla router with named routes for the various API -// methods. We may export this for use by the client. -func v2APIRouter() *mux.Router { +// Router 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) + Name(RouteNameBase) // GET /v2//manifest/ Image Manifest Fetch the image manifest identified by name and tag. // PUT /v2//manifest/ Image Manifest Upload the image manifest identified by name and tag. // DELETE /v2//manifest/ Image Manifest Delete the image identified by name and tag. router. Path("/v2/{name:" + common.RepositoryNameRegexp.String() + "}/manifests/{tag:" + common.TagNameRegexp.String() + "}"). - Name(routeNameImageManifest) + Name(RouteNameManifest) // GET /v2//tags/list Tags Fetch the tags under the repository identified by name. router. Path("/v2/{name:" + common.RepositoryNameRegexp.String() + "}/tags/list"). - Name(routeNameTags) + Name(RouteNameTags) // GET /v2//blob/ 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) + Name(RouteNameBlob) // POST /v2//blob/upload/ Layer Upload Initiate an upload of the layer identified by tarsum. router. Path("/v2/{name:" + common.RepositoryNameRegexp.String() + "}/blobs/uploads/"). - Name(routeNameBlobUpload) + Name(RouteNameBlobUpload) // GET /v2//blob/upload/ Layer Upload Get the status of the upload identified by tarsum and uuid. // PUT /v2//blob/upload/ Layer Upload Upload all or a chunk of the upload identified by tarsum and uuid. // DELETE /v2//blob/upload/ Layer Upload Cancel the upload identified by layer and uuid router. Path("/v2/{name:" + common.RepositoryNameRegexp.String() + "}/blobs/uploads/{uuid}"). - Name(routeNameBlobUploadResume) + Name(RouteNameBlobUploadChunk) return router } diff --git a/routes_test.go b/api/v2/routes_test.go similarity index 89% rename from routes_test.go rename to api/v2/routes_test.go index 6d684a61..9969ebcc 100644 --- a/routes_test.go +++ b/api/v2/routes_test.go @@ -1,4 +1,4 @@ -package registry +package v2 import ( "encoding/json" @@ -25,7 +25,7 @@ type routeTestCase struct { // This may go away as the application structure comes together. func TestRouter(t *testing.T) { - router := v2APIRouter() + router := Router() testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { testCase := routeTestCase{ @@ -47,12 +47,12 @@ func TestRouter(t *testing.T) { for _, testcase := range []routeTestCase{ { - RouteName: routeNameBase, + RouteName: RouteNameBase, RequestURI: "/v2/", Vars: map[string]string{}, }, { - RouteName: routeNameImageManifest, + RouteName: RouteNameManifest, RequestURI: "/v2/foo/bar/manifests/tag", Vars: map[string]string{ "name": "foo/bar", @@ -60,14 +60,14 @@ func TestRouter(t *testing.T) { }, }, { - RouteName: routeNameTags, + RouteName: RouteNameTags, RequestURI: "/v2/foo/bar/tags/list", Vars: map[string]string{ "name": "foo/bar", }, }, { - RouteName: routeNameBlob, + RouteName: RouteNameBlob, RequestURI: "/v2/foo/bar/blobs/tarsum.dev+foo:abcdef0919234", Vars: map[string]string{ "name": "foo/bar", @@ -75,7 +75,7 @@ func TestRouter(t *testing.T) { }, }, { - RouteName: routeNameBlob, + RouteName: RouteNameBlob, RequestURI: "/v2/foo/bar/blobs/sha256:abcdef0919234", Vars: map[string]string{ "name": "foo/bar", @@ -83,14 +83,14 @@ func TestRouter(t *testing.T) { }, }, { - RouteName: routeNameBlobUpload, + RouteName: RouteNameBlobUpload, RequestURI: "/v2/foo/bar/blobs/uploads/", Vars: map[string]string{ "name": "foo/bar", }, }, { - RouteName: routeNameBlobUploadResume, + RouteName: RouteNameBlobUploadChunk, RequestURI: "/v2/foo/bar/blobs/uploads/uuid", Vars: map[string]string{ "name": "foo/bar", @@ -98,7 +98,7 @@ func TestRouter(t *testing.T) { }, }, { - RouteName: routeNameBlobUploadResume, + RouteName: RouteNameBlobUploadChunk, RequestURI: "/v2/foo/bar/blobs/uploads/D95306FA-FAD3-4E36-8D41-CF1C93EF8286", Vars: map[string]string{ "name": "foo/bar", @@ -106,7 +106,7 @@ func TestRouter(t *testing.T) { }, }, { - RouteName: routeNameBlobUploadResume, + RouteName: RouteNameBlobUploadChunk, RequestURI: "/v2/foo/bar/blobs/uploads/RDk1MzA2RkEtRkFEMy00RTM2LThENDEtQ0YxQzkzRUY4Mjg2IA==", Vars: map[string]string{ "name": "foo/bar", @@ -117,7 +117,7 @@ func TestRouter(t *testing.T) { // Check ambiguity: ensure we can distinguish between tags for // "foo/bar/image/image" and image for "foo/bar/image" with tag // "tags" - RouteName: routeNameImageManifest, + RouteName: RouteNameManifest, RequestURI: "/v2/foo/bar/manifests/manifests/tags", Vars: map[string]string{ "name": "foo/bar/manifests", @@ -127,14 +127,14 @@ func TestRouter(t *testing.T) { { // This case presents an ambiguity between foo/bar with tag="tags" // and list tags for "foo/bar/manifest" - RouteName: routeNameTags, + RouteName: RouteNameTags, RequestURI: "/v2/foo/bar/manifests/tags/list", Vars: map[string]string{ "name": "foo/bar/manifests", }, }, { - RouteName: routeNameBlobUploadResume, + RouteName: RouteNameBlobUploadChunk, RequestURI: "/v2/foo/../../blob/uploads/D95306FA-FAD3-4E36-8D41-CF1C93EF8286", StatusCode: http.StatusNotFound, }, diff --git a/api/v2/urls.go b/api/v2/urls.go new file mode 100644 index 00000000..72f44299 --- /dev/null +++ b/api/v2/urls.go @@ -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() +} diff --git a/api/v2/urls_test.go b/api/v2/urls_test.go new file mode 100644 index 00000000..a9590dba --- /dev/null +++ b/api/v2/urls_test.go @@ -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) + } + } + +} diff --git a/api_test.go b/api_test.go index d6cf34dd..a650a102 100644 --- a/api_test.go +++ b/api_test.go @@ -13,7 +13,7 @@ import ( "os" "testing" - "github.com/docker/docker-registry/api/errors" + "github.com/docker/docker-registry/api/v2" "github.com/docker/docker-registry/common/testutil" "github.com/docker/docker-registry/configuration" "github.com/docker/docker-registry/digest" @@ -34,13 +34,13 @@ func TestCheckAPI(t *testing.T) { app := NewApp(config) server := httptest.NewServer(handlers.CombinedLoggingHandler(os.Stderr, app)) - builder, err := newURLBuilderFromString(server.URL) + builder, err := v2.NewURLBuilderFromString(server.URL) if err != nil { t.Fatalf("error creating url builder: %v", err) } - baseURL, err := builder.buildBaseURL() + baseURL, err := builder.BuildBaseURL() if err != nil { t.Fatalf("unexpected error building base url: %v", err) } @@ -81,7 +81,7 @@ func TestLayerAPI(t *testing.T) { app := NewApp(config) server := httptest.NewServer(handlers.CombinedLoggingHandler(os.Stderr, app)) - builder, err := newURLBuilderFromString(server.URL) + builder, err := v2.NewURLBuilderFromString(server.URL) if err != nil { t.Fatalf("error creating url builder: %v", err) @@ -98,7 +98,7 @@ func TestLayerAPI(t *testing.T) { // ----------------------------------- // Test fetch for non-existent content - layerURL, err := builder.buildLayerURL(imageName, layerDigest) + layerURL, err := builder.BuildBlobURL(imageName, layerDigest) if err != nil { t.Fatalf("error building url: %v", err) } @@ -121,7 +121,7 @@ func TestLayerAPI(t *testing.T) { // ------------------------------------------ // Upload a layer - layerUploadURL, err := builder.buildLayerUploadURL(imageName) + layerUploadURL, err := builder.BuildBlobUploadURL(imageName) if err != nil { t.Fatalf("error building upload url: %v", err) } @@ -196,7 +196,7 @@ func TestManifestAPI(t *testing.T) { app := NewApp(config) server := httptest.NewServer(handlers.CombinedLoggingHandler(os.Stderr, app)) - builder, err := newURLBuilderFromString(server.URL) + builder, err := v2.NewURLBuilderFromString(server.URL) if err != nil { t.Fatalf("unexpected error creating url builder: %v", err) } @@ -204,7 +204,7 @@ func TestManifestAPI(t *testing.T) { imageName := "foo/bar" tag := "thetag" - manifestURL, err := builder.buildManifestURL(imageName, tag) + manifestURL, err := builder.BuildManifestURL(imageName, tag) if err != nil { t.Fatalf("unexpected error getting manifest url: %v", err) } @@ -227,7 +227,7 @@ func TestManifestAPI(t *testing.T) { // } dec := json.NewDecoder(resp.Body) - var respErrs errors.Errors + var respErrs v2.Errors if err := dec.Decode(&respErrs); err != nil { t.Fatalf("unexpected error decoding error response: %v", err) } @@ -236,11 +236,11 @@ func TestManifestAPI(t *testing.T) { t.Fatalf("expected errors in response") } - if respErrs.Errors[0].Code != errors.ErrorCodeManifestUnknown { + if respErrs.Errors[0].Code != v2.ErrorCodeManifestUnknown { t.Fatalf("expected manifest unknown error: got %v", respErrs) } - tagsURL, err := builder.buildTagsURL(imageName) + tagsURL, err := builder.BuildTagsURL(imageName) if err != nil { t.Fatalf("unexpected error building tags url: %v", err) } @@ -262,7 +262,7 @@ func TestManifestAPI(t *testing.T) { t.Fatalf("expected errors in response") } - if respErrs.Errors[0].Code != errors.ErrorCodeNameUnknown { + if respErrs.Errors[0].Code != v2.ErrorCodeNameUnknown { t.Fatalf("expected respository unknown error: got %v", respErrs) } @@ -296,11 +296,11 @@ func TestManifestAPI(t *testing.T) { for _, err := range respErrs.Errors { switch err.Code { - case errors.ErrorCodeManifestUnverified: + case v2.ErrorCodeManifestUnverified: unverified++ - case errors.ErrorCodeBlobUnknown: + case v2.ErrorCodeBlobUnknown: missingLayers++ - case errors.ErrorCodeDigestInvalid: + case v2.ErrorCodeDigestInvalid: // TODO(stevvooe): This error isn't quite descriptive enough -- // the layer with an invalid digest isn't identified. invalidDigests++ @@ -427,8 +427,8 @@ func putManifest(t *testing.T, msg, url string, v interface{}) *http.Response { return resp } -func startPushLayer(t *testing.T, ub *urlBuilder, name string) string { - layerUploadURL, err := ub.buildLayerUploadURL(name) +func startPushLayer(t *testing.T, ub *v2.URLBuilder, name string) string { + layerUploadURL, err := ub.BuildBlobUploadURL(name) if err != nil { t.Fatalf("unexpected error building layer upload url: %v", err) } @@ -449,14 +449,21 @@ func startPushLayer(t *testing.T, ub *urlBuilder, name string) string { } // pushLayer pushes the layer content returning the url on success. -func pushLayer(t *testing.T, ub *urlBuilder, name string, dgst digest.Digest, uploadURLBase string, rs io.ReadSeeker) string { +func pushLayer(t *testing.T, ub *v2.URLBuilder, name string, dgst digest.Digest, uploadURLBase string, rs io.ReadSeeker) string { rsLength, _ := rs.Seek(0, os.SEEK_END) rs.Seek(0, os.SEEK_SET) - uploadURL := appendValues(uploadURLBase, url.Values{ + u, err := url.Parse(uploadURLBase) + if err != nil { + t.Fatalf("unexpected error parsing pushLayer url: %v", err) + } + + u.RawQuery = url.Values{ "digest": []string{dgst.String()}, "size": []string{fmt.Sprint(rsLength)}, - }) + }.Encode() + + uploadURL := u.String() // Just do a monolithic upload req, err := http.NewRequest("PUT", uploadURL, rs) @@ -472,7 +479,7 @@ func pushLayer(t *testing.T, ub *urlBuilder, name string, dgst digest.Digest, up checkResponse(t, "putting monolithic chunk", resp, http.StatusCreated) - expectedLayerURL, err := ub.buildLayerURL(name, dgst) + expectedLayerURL, err := ub.BuildBlobURL(name, dgst) if err != nil { t.Fatalf("error building expected layer url: %v", err) } diff --git a/app.go b/app.go index 76605f1b..5a770c6c 100644 --- a/app.go +++ b/app.go @@ -4,6 +4,7 @@ import ( "fmt" "net/http" + "github.com/docker/docker-registry/api/v2" "github.com/docker/docker-registry/storagedriver" "github.com/docker/docker-registry/storagedriver/factory" @@ -35,18 +36,18 @@ type App struct { func NewApp(configuration configuration.Configuration) *App { app := &App{ Config: configuration, - router: v2APIRouter(), + router: v2.Router(), } // Register the handler dispatchers. - app.register(routeNameBase, func(ctx *Context, r *http.Request) http.Handler { + app.register(v2.RouteNameBase, func(ctx *Context, r *http.Request) http.Handler { return http.HandlerFunc(apiBase) }) - app.register(routeNameImageManifest, imageManifestDispatcher) - app.register(routeNameTags, tagsDispatcher) - app.register(routeNameBlob, layerDispatcher) - app.register(routeNameBlobUpload, layerUploadDispatcher) - app.register(routeNameBlobUploadResume, layerUploadDispatcher) + app.register(v2.RouteNameManifest, imageManifestDispatcher) + app.register(v2.RouteNameTags, tagsDispatcher) + app.register(v2.RouteNameBlob, layerDispatcher) + app.register(v2.RouteNameBlobUpload, layerUploadDispatcher) + app.register(v2.RouteNameBlobUploadChunk, layerUploadDispatcher) driver, err := factory.Create(configuration.Storage.Type(), configuration.Storage.Parameters()) @@ -114,7 +115,7 @@ func (app *App) dispatcher(dispatch dispatchFunc) http.Handler { context := &Context{ App: app, Name: vars["name"], - urlBuilder: newURLBuilderFromRequest(r), + urlBuilder: v2.NewURLBuilderFromRequest(r), } // Store vars for underlying handlers. diff --git a/app_test.go b/app_test.go index bb78044a..f256c968 100644 --- a/app_test.go +++ b/app_test.go @@ -6,6 +6,7 @@ import ( "net/url" "testing" + "github.com/docker/docker-registry/api/v2" "github.com/docker/docker-registry/configuration" ) @@ -16,10 +17,10 @@ import ( func TestAppDispatcher(t *testing.T) { app := &App{ Config: configuration.Configuration{}, - router: v2APIRouter(), + router: v2.Router(), } server := httptest.NewServer(app) - router := v2APIRouter() + router := v2.Router() serverURL, err := url.Parse(server.URL) if err != nil { @@ -71,33 +72,33 @@ func TestAppDispatcher(t *testing.T) { vars []string }{ { - endpoint: routeNameImageManifest, + endpoint: v2.RouteNameManifest, vars: []string{ "name", "foo/bar", "tag", "sometag", }, }, { - endpoint: routeNameTags, + endpoint: v2.RouteNameTags, vars: []string{ "name", "foo/bar", }, }, { - endpoint: routeNameBlob, + endpoint: v2.RouteNameBlob, vars: []string{ "name", "foo/bar", "digest", "tarsum.v1+bogus:abcdef0123456789", }, }, { - endpoint: routeNameBlobUpload, + endpoint: v2.RouteNameBlobUpload, vars: []string{ "name", "foo/bar", }, }, { - endpoint: routeNameBlobUploadResume, + endpoint: v2.RouteNameBlobUploadChunk, vars: []string{ "name", "foo/bar", "uuid", "theuuid", diff --git a/client/client.go b/client/client.go index 8f31cb4e..e25fbff4 100644 --- a/client/client.go +++ b/client/client.go @@ -10,7 +10,7 @@ import ( "regexp" "strconv" - "github.com/docker/docker-registry/api/errors" + "github.com/docker/docker-registry/api/v2" "github.com/docker/docker-registry/digest" "github.com/docker/docker-registry/storage" ) @@ -96,7 +96,7 @@ func (r *clientImpl) GetImageManifest(name, tag string) (*storage.SignedManifest case response.StatusCode == http.StatusNotFound: return nil, &ImageManifestNotFoundError{Name: name, Tag: tag} case response.StatusCode >= 400 && response.StatusCode < 500: - var errs errors.Errors + var errs v2.Errors decoder := json.NewDecoder(response.Body) err = decoder.Decode(&errs) @@ -136,7 +136,7 @@ func (r *clientImpl) PutImageManifest(name, tag string, manifest *storage.Signed case response.StatusCode == http.StatusOK: return nil case response.StatusCode >= 400 && response.StatusCode < 500: - var errors errors.Errors + var errors v2.Errors decoder := json.NewDecoder(response.Body) err = decoder.Decode(&errors) if err != nil { @@ -169,7 +169,7 @@ func (r *clientImpl) DeleteImage(name, tag string) error { case response.StatusCode == http.StatusNotFound: return &ImageManifestNotFoundError{Name: name, Tag: tag} case response.StatusCode >= 400 && response.StatusCode < 500: - var errs errors.Errors + var errs v2.Errors decoder := json.NewDecoder(response.Body) err = decoder.Decode(&errs) if err != nil { @@ -197,7 +197,7 @@ func (r *clientImpl) ListImageTags(name string) ([]string, error) { case response.StatusCode == http.StatusNotFound: return nil, &RepositoryNotFoundError{Name: name} case response.StatusCode >= 400 && response.StatusCode < 500: - var errs errors.Errors + var errs v2.Errors decoder := json.NewDecoder(response.Body) err = decoder.Decode(&errs) if err != nil { @@ -240,7 +240,7 @@ func (r *clientImpl) BlobLength(name string, dgst digest.Digest) (int, error) { case response.StatusCode == http.StatusNotFound: return -1, nil case response.StatusCode >= 400 && response.StatusCode < 500: - var errs errors.Errors + var errs v2.Errors decoder := json.NewDecoder(response.Body) err = decoder.Decode(&errs) if err != nil { @@ -279,7 +279,7 @@ func (r *clientImpl) GetBlob(name string, dgst digest.Digest, byteOffset int) (i response.Body.Close() return nil, 0, &BlobNotFoundError{Name: name, Digest: dgst} case response.StatusCode >= 400 && response.StatusCode < 500: - var errs errors.Errors + var errs v2.Errors decoder := json.NewDecoder(response.Body) err = decoder.Decode(&errs) if err != nil { @@ -312,7 +312,7 @@ func (r *clientImpl) InitiateBlobUpload(name string) (string, error) { // case response.StatusCode == http.StatusNotFound: // return case response.StatusCode >= 400 && response.StatusCode < 500: - var errs errors.Errors + var errs v2.Errors decoder := json.NewDecoder(response.Body) err = decoder.Decode(&errs) if err != nil { @@ -338,7 +338,7 @@ func (r *clientImpl) GetBlobUploadStatus(location string) (int, int, error) { case response.StatusCode == http.StatusNotFound: return 0, 0, &BlobUploadNotFoundError{Location: location} case response.StatusCode >= 400 && response.StatusCode < 500: - var errs errors.Errors + var errs v2.Errors decoder := json.NewDecoder(response.Body) err = decoder.Decode(&errs) if err != nil { @@ -379,7 +379,7 @@ func (r *clientImpl) UploadBlob(location string, blob io.ReadCloser, length int, case response.StatusCode == http.StatusNotFound: return &BlobUploadNotFoundError{Location: location} case response.StatusCode >= 400 && response.StatusCode < 500: - var errs errors.Errors + var errs v2.Errors decoder := json.NewDecoder(response.Body) err = decoder.Decode(&errs) if err != nil { @@ -430,7 +430,7 @@ func (r *clientImpl) UploadBlobChunk(location string, blobChunk io.ReadCloser, l case response.StatusCode == http.StatusNotFound: return &BlobUploadNotFoundError{Location: location} case response.StatusCode >= 400 && response.StatusCode < 500: - var errs errors.Errors + var errs v2.Errors decoder := json.NewDecoder(response.Body) err = decoder.Decode(&errs) if err != nil { @@ -472,7 +472,7 @@ func (r *clientImpl) FinishChunkedBlobUpload(location string, length int, dgst d case response.StatusCode == http.StatusNotFound: return &BlobUploadNotFoundError{Location: location} case response.StatusCode >= 400 && response.StatusCode < 500: - var errs errors.Errors + var errs v2.Errors decoder := json.NewDecoder(response.Body) err = decoder.Decode(&errs) if err != nil { @@ -504,7 +504,7 @@ func (r *clientImpl) CancelBlobUpload(location string) error { case response.StatusCode == http.StatusNotFound: return &BlobUploadNotFoundError{Location: location} case response.StatusCode >= 400 && response.StatusCode < 500: - var errs errors.Errors + var errs v2.Errors decoder := json.NewDecoder(response.Body) err = decoder.Decode(&errs) if err != nil { diff --git a/client/objectstore.go b/client/objectstore.go index 06fba3d8..55ab20a5 100644 --- a/client/objectstore.go +++ b/client/objectstore.go @@ -2,7 +2,6 @@ package client import ( "bytes" - "errors" "fmt" "io" "sync" @@ -14,11 +13,11 @@ import ( var ( // ErrLayerAlreadyExists is returned when attempting to create a layer with // a tarsum that is already in use. - ErrLayerAlreadyExists = errors.New("Layer already exists") + ErrLayerAlreadyExists = fmt.Errorf("Layer already exists") // ErrLayerLocked is returned when attempting to write to a layer which is // currently being written to. - ErrLayerLocked = errors.New("Layer locked") + ErrLayerLocked = fmt.Errorf("Layer locked") ) // ObjectStore is an interface which is designed to approximate the docker diff --git a/client/push.go b/client/push.go index 61853b53..aac3fc40 100644 --- a/client/push.go +++ b/client/push.go @@ -1,11 +1,10 @@ package client import ( - "errors" - - "github.com/docker/docker-registry/storage" + "fmt" log "github.com/Sirupsen/logrus" + "github.com/docker/docker-registry/storage" ) // simultaneousLayerPushWindow is the size of the parallel layer push window. @@ -100,7 +99,7 @@ func pushLayer(c Client, objectStore ObjectStore, name string, fsLayer storage.F "currentSize": layerReader.CurrentSize(), "size": layerReader.Size(), }).Warn("Local layer incomplete") - return errors.New("Local layer incomplete") + return fmt.Errorf("Local layer incomplete") } length, err := c.BlobLength(name, fsLayer.BlobSum) diff --git a/cmd/registry-api-doctable-gen/main.go b/cmd/registry-api-doctable-gen/main.go index f76c249e..a9e71fff 100644 --- a/cmd/registry-api-doctable-gen/main.go +++ b/cmd/registry-api-doctable-gen/main.go @@ -17,7 +17,7 @@ import ( "strings" "text/tabwriter" - "github.com/docker/docker-registry/api/errors" + "github.com/docker/docker-registry/api/v2" ) func main() { @@ -40,7 +40,7 @@ func dumpErrors(wr io.Writer) { defer writer.Flush() fmt.Fprint(writer, "|") - dtype := reflect.TypeOf(errors.ErrorDescriptor{}) + dtype := reflect.TypeOf(v2.ErrorDescriptor{}) var fieldsPrinted int for i := 0; i < dtype.NumField(); i++ { field := dtype.Field(i) @@ -61,7 +61,7 @@ func dumpErrors(wr io.Writer) { fmt.Fprintln(writer, "\n"+divider) - for _, descriptor := range errors.Descriptors { + for _, descriptor := range v2.ErrorDescriptors { fmt.Fprint(writer, "|") v := reflect.ValueOf(descriptor) diff --git a/context.go b/context.go index a1e47abe..0c5ba587 100644 --- a/context.go +++ b/context.go @@ -2,7 +2,7 @@ package registry import ( "github.com/Sirupsen/logrus" - "github.com/docker/docker-registry/api/errors" + "github.com/docker/docker-registry/api/v2" ) // Context should contain the request specific context for use in across @@ -19,7 +19,7 @@ type Context struct { // Errors is a collection of errors encountered during the request to be // returned to the client API. If errors are added to the collection, the // handler *must not* start the response via http.ResponseWriter. - Errors errors.Errors + Errors v2.Errors // vars contains the extracted gorilla/mux variables that can be used for // assignment. @@ -28,5 +28,5 @@ type Context struct { // log provides a context specific logger. log *logrus.Entry - urlBuilder *urlBuilder + urlBuilder *v2.URLBuilder } diff --git a/images.go b/images.go index 74ae067e..5a373f1f 100644 --- a/images.go +++ b/images.go @@ -5,7 +5,7 @@ import ( "fmt" "net/http" - "github.com/docker/docker-registry/api/errors" + "github.com/docker/docker-registry/api/v2" "github.com/docker/docker-registry/digest" "github.com/docker/docker-registry/storage" "github.com/gorilla/handlers" @@ -41,7 +41,7 @@ func (imh *imageManifestHandler) GetImageManifest(w http.ResponseWriter, r *http manifest, err := manifests.Get(imh.Name, imh.Tag) if err != nil { - imh.Errors.Push(errors.ErrorCodeManifestUnknown, err) + imh.Errors.Push(v2.ErrorCodeManifestUnknown, err) w.WriteHeader(http.StatusNotFound) return } @@ -58,7 +58,7 @@ func (imh *imageManifestHandler) PutImageManifest(w http.ResponseWriter, r *http var manifest storage.SignedManifest if err := dec.Decode(&manifest); err != nil { - imh.Errors.Push(errors.ErrorCodeManifestInvalid, err) + imh.Errors.Push(v2.ErrorCodeManifestInvalid, err) w.WriteHeader(http.StatusBadRequest) return } @@ -71,14 +71,14 @@ func (imh *imageManifestHandler) PutImageManifest(w http.ResponseWriter, r *http for _, verificationError := range err { switch verificationError := verificationError.(type) { case storage.ErrUnknownLayer: - imh.Errors.Push(errors.ErrorCodeBlobUnknown, verificationError.FSLayer) + imh.Errors.Push(v2.ErrorCodeBlobUnknown, verificationError.FSLayer) case storage.ErrManifestUnverified: - imh.Errors.Push(errors.ErrorCodeManifestUnverified) + imh.Errors.Push(v2.ErrorCodeManifestUnverified) default: if verificationError == digest.ErrDigestInvalidFormat { // TODO(stevvooe): We need to really need to move all // errors to types. Its much more straightforward. - imh.Errors.Push(errors.ErrorCodeDigestInvalid) + imh.Errors.Push(v2.ErrorCodeDigestInvalid) } else { imh.Errors.PushErr(verificationError) } @@ -99,10 +99,10 @@ func (imh *imageManifestHandler) DeleteImageManifest(w http.ResponseWriter, r *h if err := manifests.Delete(imh.Name, imh.Tag); err != nil { switch err := err.(type) { case storage.ErrUnknownManifest: - imh.Errors.Push(errors.ErrorCodeManifestUnknown, err) + imh.Errors.Push(v2.ErrorCodeManifestUnknown, err) w.WriteHeader(http.StatusNotFound) default: - imh.Errors.Push(errors.ErrorCodeUnknown, err) + imh.Errors.Push(v2.ErrorCodeUnknown, err) w.WriteHeader(http.StatusBadRequest) } return diff --git a/layer.go b/layer.go index 4da7723a..094a54cf 100644 --- a/layer.go +++ b/layer.go @@ -3,7 +3,7 @@ package registry import ( "net/http" - "github.com/docker/docker-registry/api/errors" + "github.com/docker/docker-registry/api/v2" "github.com/docker/docker-registry/digest" "github.com/docker/docker-registry/storage" "github.com/gorilla/handlers" @@ -15,7 +15,7 @@ func layerDispatcher(ctx *Context, r *http.Request) http.Handler { if err != nil { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ctx.Errors.Push(errors.ErrorCodeDigestInvalid, err) + ctx.Errors.Push(v2.ErrorCodeDigestInvalid, err) }) } @@ -50,9 +50,9 @@ func (lh *layerHandler) GetLayer(w http.ResponseWriter, r *http.Request) { switch err := err.(type) { case storage.ErrUnknownLayer: w.WriteHeader(http.StatusNotFound) - lh.Errors.Push(errors.ErrorCodeBlobUnknown, err.FSLayer) + lh.Errors.Push(v2.ErrorCodeBlobUnknown, err.FSLayer) default: - lh.Errors.Push(errors.ErrorCodeUnknown, err) + lh.Errors.Push(v2.ErrorCodeUnknown, err) } return } diff --git a/layerupload.go b/layerupload.go index af8bd457..b65c8ef2 100644 --- a/layerupload.go +++ b/layerupload.go @@ -7,7 +7,7 @@ import ( "strconv" "github.com/Sirupsen/logrus" - "github.com/docker/docker-registry/api/errors" + "github.com/docker/docker-registry/api/v2" "github.com/docker/docker-registry/digest" "github.com/docker/docker-registry/storage" "github.com/gorilla/handlers" @@ -39,7 +39,7 @@ func layerUploadDispatcher(ctx *Context, r *http.Request) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { logrus.Infof("error resolving upload: %v", err) w.WriteHeader(http.StatusInternalServerError) - luh.Errors.Push(errors.ErrorCodeUnknown, err) + luh.Errors.Push(v2.ErrorCodeUnknown, err) }) } @@ -67,7 +67,7 @@ func (luh *layerUploadHandler) StartLayerUpload(w http.ResponseWriter, r *http.R upload, err := layers.Upload(luh.Name) if err != nil { w.WriteHeader(http.StatusInternalServerError) // Error conditions here? - luh.Errors.Push(errors.ErrorCodeUnknown, err) + luh.Errors.Push(v2.ErrorCodeUnknown, err) return } @@ -76,7 +76,7 @@ func (luh *layerUploadHandler) StartLayerUpload(w http.ResponseWriter, r *http.R if err := luh.layerUploadResponse(w, r); err != nil { w.WriteHeader(http.StatusInternalServerError) // Error conditions here? - luh.Errors.Push(errors.ErrorCodeUnknown, err) + luh.Errors.Push(v2.ErrorCodeUnknown, err) return } w.WriteHeader(http.StatusAccepted) @@ -86,12 +86,12 @@ func (luh *layerUploadHandler) StartLayerUpload(w http.ResponseWriter, r *http.R func (luh *layerUploadHandler) GetUploadStatus(w http.ResponseWriter, r *http.Request) { if luh.Upload == nil { w.WriteHeader(http.StatusNotFound) - luh.Errors.Push(errors.ErrorCodeBlobUploadUnknown) + luh.Errors.Push(v2.ErrorCodeBlobUploadUnknown) } if err := luh.layerUploadResponse(w, r); err != nil { w.WriteHeader(http.StatusInternalServerError) // Error conditions here? - luh.Errors.Push(errors.ErrorCodeUnknown, err) + luh.Errors.Push(v2.ErrorCodeUnknown, err) return } @@ -103,7 +103,7 @@ func (luh *layerUploadHandler) GetUploadStatus(w http.ResponseWriter, r *http.Re func (luh *layerUploadHandler) PutLayerChunk(w http.ResponseWriter, r *http.Request) { if luh.Upload == nil { w.WriteHeader(http.StatusNotFound) - luh.Errors.Push(errors.ErrorCodeBlobUploadUnknown) + luh.Errors.Push(v2.ErrorCodeBlobUploadUnknown) } var finished bool @@ -120,14 +120,14 @@ func (luh *layerUploadHandler) PutLayerChunk(w http.ResponseWriter, r *http.Requ if err := luh.maybeCompleteUpload(w, r); err != nil { if err != errNotReadyToComplete { w.WriteHeader(http.StatusInternalServerError) - luh.Errors.Push(errors.ErrorCodeUnknown, err) + luh.Errors.Push(v2.ErrorCodeUnknown, err) return } } if err := luh.layerUploadResponse(w, r); err != nil { w.WriteHeader(http.StatusInternalServerError) // Error conditions here? - luh.Errors.Push(errors.ErrorCodeUnknown, err) + luh.Errors.Push(v2.ErrorCodeUnknown, err) return } @@ -142,7 +142,7 @@ func (luh *layerUploadHandler) PutLayerChunk(w http.ResponseWriter, r *http.Requ func (luh *layerUploadHandler) CancelLayerUpload(w http.ResponseWriter, r *http.Request) { if luh.Upload == nil { w.WriteHeader(http.StatusNotFound) - luh.Errors.Push(errors.ErrorCodeBlobUploadUnknown) + luh.Errors.Push(v2.ErrorCodeBlobUploadUnknown) } } @@ -151,7 +151,7 @@ func (luh *layerUploadHandler) CancelLayerUpload(w http.ResponseWriter, r *http. // chunk responses. This sets the correct headers but the response status is // left to the caller. func (luh *layerUploadHandler) layerUploadResponse(w http.ResponseWriter, r *http.Request) error { - uploadURL, err := luh.urlBuilder.forLayerUpload(luh.Upload) + uploadURL, err := luh.urlBuilder.BuildBlobUploadChunkURL(luh.Upload.Name(), luh.Upload.UUID()) if err != nil { logrus.Infof("error building upload url: %s", err) return err @@ -195,14 +195,14 @@ func (luh *layerUploadHandler) maybeCompleteUpload(w http.ResponseWriter, r *htt func (luh *layerUploadHandler) completeUpload(w http.ResponseWriter, r *http.Request, size int64, dgst digest.Digest) { layer, err := luh.Upload.Finish(size, dgst) if err != nil { - luh.Errors.Push(errors.ErrorCodeUnknown, err) + luh.Errors.Push(v2.ErrorCodeUnknown, err) w.WriteHeader(http.StatusInternalServerError) return } - layerURL, err := luh.urlBuilder.forLayer(layer) + layerURL, err := luh.urlBuilder.BuildBlobURL(layer.Name(), layer.Digest()) if err != nil { - luh.Errors.Push(errors.ErrorCodeUnknown, err) + luh.Errors.Push(v2.ErrorCodeUnknown, err) w.WriteHeader(http.StatusInternalServerError) return } diff --git a/tags.go b/tags.go index 12a5062f..04d994b9 100644 --- a/tags.go +++ b/tags.go @@ -4,7 +4,7 @@ import ( "encoding/json" "net/http" - "github.com/docker/docker-registry/api/errors" + "github.com/docker/docker-registry/api/v2" "github.com/docker/docker-registry/storage" "github.com/gorilla/handlers" ) @@ -40,7 +40,7 @@ func (th *tagsHandler) GetTags(w http.ResponseWriter, r *http.Request) { switch err := err.(type) { case storage.ErrUnknownRepository: w.WriteHeader(404) - th.Errors.Push(errors.ErrorCodeNameUnknown, map[string]string{"name": th.Name}) + th.Errors.Push(v2.ErrorCodeNameUnknown, map[string]string{"name": th.Name}) default: th.Errors.PushErr(err) } diff --git a/urls.go b/urls.go deleted file mode 100644 index 92233da4..00000000 --- a/urls.go +++ /dev/null @@ -1,169 +0,0 @@ -package registry - -import ( - "net/http" - "net/url" - - "github.com/docker/docker-registry/digest" - "github.com/docker/docker-registry/storage" - "github.com/gorilla/mux" -) - -type urlBuilder struct { - url *url.URL // url root (ie http://localhost/) - router *mux.Router -} - -func newURLBuilder(root *url.URL) *urlBuilder { - return &urlBuilder{ - url: root, - router: v2APIRouter(), - } -} - -func newURLBuilderFromRequest(r *http.Request) *urlBuilder { - u := &url.URL{ - Scheme: r.URL.Scheme, - Host: r.Host, - } - - return newURLBuilder(u) -} - -func newURLBuilderFromString(root string) (*urlBuilder, error) { - u, err := url.Parse(root) - if err != nil { - return nil, err - } - - return newURLBuilder(u), nil -} - -func (ub *urlBuilder) buildBaseURL() (string, error) { - route := clonedRoute(ub.router, routeNameBase) - - baseURL, err := route. - Schemes(ub.url.Scheme). - Host(ub.url.Host). - URL() - if err != nil { - return "", err - } - - return baseURL.String(), nil -} - -func (ub *urlBuilder) buildTagsURL(name string) (string, error) { - route := clonedRoute(ub.router, routeNameTags) - - tagsURL, err := route. - Schemes(ub.url.Scheme). - Host(ub.url.Host). - URL("name", name) - if err != nil { - return "", err - } - - return tagsURL.String(), nil -} - -func (ub *urlBuilder) forManifest(m *storage.Manifest) (string, error) { - return ub.buildManifestURL(m.Name, m.Tag) -} - -func (ub *urlBuilder) buildManifestURL(name, tag string) (string, error) { - route := clonedRoute(ub.router, routeNameImageManifest) - - manifestURL, err := route. - Schemes(ub.url.Scheme). - Host(ub.url.Host). - URL("name", name, "tag", tag) - if err != nil { - return "", err - } - - return manifestURL.String(), nil -} - -func (ub *urlBuilder) forLayer(l storage.Layer) (string, error) { - return ub.buildLayerURL(l.Name(), l.Digest()) -} - -func (ub *urlBuilder) buildLayerURL(name string, dgst digest.Digest) (string, error) { - route := clonedRoute(ub.router, routeNameBlob) - - layerURL, err := route. - Schemes(ub.url.Scheme). - Host(ub.url.Host). - URL("name", name, "digest", dgst.String()) - if err != nil { - return "", err - } - - return layerURL.String(), nil -} - -func (ub *urlBuilder) buildLayerUploadURL(name string) (string, error) { - route := clonedRoute(ub.router, routeNameBlobUpload) - - uploadURL, err := route. - Schemes(ub.url.Scheme). - Host(ub.url.Host). - URL("name", name) - if err != nil { - return "", err - } - - return uploadURL.String(), nil -} - -func (ub *urlBuilder) forLayerUpload(layerUpload storage.LayerUpload) (string, error) { - return ub.buildLayerUploadResumeURL(layerUpload.Name(), layerUpload.UUID()) -} - -func (ub *urlBuilder) buildLayerUploadResumeURL(name, uuid string, values ...url.Values) (string, error) { - route := clonedRoute(ub.router, routeNameBlobUploadResume) - - uploadURL, err := route. - Schemes(ub.url.Scheme). - Host(ub.url.Host). - URL("name", name, "uuid", uuid) - if err != nil { - return "", err - } - - return appendValuesURL(uploadURL, values...).String(), nil -} - -// 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() -} - -// clondedRoute returns a clone of the named route from the router. -func clonedRoute(router *mux.Router, name string) *mux.Route { - route := new(mux.Route) - *route = *router.GetRoute(name) // clone the route - return route -}