diff --git a/api_test.go b/api_test.go index c850f141..cc27e5b0 100644 --- a/api_test.go +++ b/api_test.go @@ -1,6 +1,8 @@ package registry import ( + "bytes" + "encoding/json" "fmt" "io" "net/http" @@ -10,7 +12,9 @@ import ( "os" "testing" - "github.com/Sirupsen/logrus" + "github.com/docker/libtrust" + + "github.com/docker/docker-registry/storage" _ "github.com/docker/docker-registry/storagedriver/inmemory" "github.com/gorilla/handlers" @@ -34,11 +38,10 @@ func TestLayerAPI(t *testing.T) { app := NewApp(config) server := httptest.NewServer(handlers.CombinedLoggingHandler(os.Stderr, app)) - router := v2APIRouter() + builder, err := newURLBuilderFromString(server.URL) - u, err := url.Parse(server.URL) if err != nil { - t.Fatalf("error parsing server url: %v", err) + t.Fatalf("error creating url builder: %v", err) } imageName := "foo/bar" @@ -52,154 +55,65 @@ func TestLayerAPI(t *testing.T) { // ----------------------------------- // Test fetch for non-existent content - r, err := router.GetRoute(routeNameBlob).Host(u.Host). - URL("name", imageName, - "digest", tarSumStr) + layerURL, err := builder.buildLayerURL(imageName, layerDigest) + if err != nil { + t.Fatalf("error building url: %v", err) + } - resp, err := http.Get(r.String()) + resp, err := http.Get(layerURL) if err != nil { t.Fatalf("unexpected error fetching non-existent layer: %v", err) } - switch resp.StatusCode { - case http.StatusNotFound: - break // expected - default: - d, err := httputil.DumpResponse(resp, true) - if err != nil { - t.Fatalf("unexpected status fetching non-existent layer: %v, %v", resp.StatusCode, resp.Status) - } - - t.Logf("response:\n%s", string(d)) - t.Fatalf("unexpected status fetching non-existent layer: %v, %v", resp.StatusCode, resp.Status) - } + checkResponse(t, "fetching non-existent content", resp, http.StatusNotFound) // ------------------------------------------ // Test head request for non-existent content - resp, err = http.Head(r.String()) - if err != nil { - t.Fatalf("unexpected error checking head on non-existent layer: %v", err) - } - - switch resp.StatusCode { - case http.StatusNotFound: - break // expected - default: - d, err := httputil.DumpResponse(resp, true) - if err != nil { - t.Fatalf("unexpected status checking head on non-existent layer: %v, %v", resp.StatusCode, resp.Status) - } - - t.Logf("response:\n%s", string(d)) - t.Fatalf("unexpected status checking head on non-existent layer: %v, %v", resp.StatusCode, resp.Status) - } - - // ------------------------------------------ - // Upload a layer - r, err = router.GetRoute(routeNameBlobUpload).Host(u.Host). - URL("name", imageName) - if err != nil { - t.Fatalf("error starting layer upload: %v", err) - } - - resp, err = http.Post(r.String(), "", nil) - if err != nil { - t.Fatalf("error starting layer upload: %v", err) - } - - if resp.StatusCode != http.StatusAccepted { - d, err := httputil.DumpResponse(resp, true) - if err != nil { - t.Fatalf("unexpected status starting layer upload: %v, %v", resp.StatusCode, resp.Status) - } - - t.Logf("response:\n%s", string(d)) - t.Fatalf("unexpected status starting layer upload: %v, %v", resp.StatusCode, resp.Status) - } - - if resp.Header.Get("Location") == "" { // TODO(stevvooe): Need better check here. - t.Fatalf("unexpected Location: %q != %q", resp.Header.Get("Location"), "foo") - } - - if resp.Header.Get("Content-Length") != "0" { - t.Fatalf("unexpected content-length: %q != %q", resp.Header.Get("Content-Length"), "0") - } - - layerLength, _ := layerFile.Seek(0, os.SEEK_END) - layerFile.Seek(0, os.SEEK_SET) - - uploadURLStr := resp.Header.Get("Location") - - // TODO(sday): Cancel the layer upload here and restart. - - query := url.Values{ - "digest": []string{layerDigest.String()}, - "length": []string{fmt.Sprint(layerLength)}, - } - - uploadURL, err := url.Parse(uploadURLStr) - if err != nil { - t.Fatalf("unexpected error parsing url: %v", err) - } - - uploadURL.RawQuery = query.Encode() - - // Just do a monolithic upload - req, err := http.NewRequest("PUT", uploadURL.String(), layerFile) - if err != nil { - t.Fatalf("unexpected error creating new request: %v", err) - } - - resp, err = http.DefaultClient.Do(req) - if err != nil { - t.Fatalf("unexpected error doing put: %v", err) - } - defer resp.Body.Close() - - switch resp.StatusCode { - case http.StatusCreated: - break // expected - default: - d, err := httputil.DumpResponse(resp, true) - if err != nil { - t.Fatalf("unexpected status putting chunk: %v, %v", resp.StatusCode, resp.Status) - } - - t.Logf("response:\n%s", string(d)) - t.Fatalf("unexpected status putting chunk: %v, %v", resp.StatusCode, resp.Status) - } - - if resp.Header.Get("Location") == "" { - t.Fatalf("unexpected Location: %q", resp.Header.Get("Location")) - } - - if resp.Header.Get("Content-Length") != "0" { - t.Fatalf("unexpected content-length: %q != %q", resp.Header.Get("Content-Length"), "0") - } - - layerURL := resp.Header.Get("Location") - - // ------------------------ - // Use a head request to see if the layer exists. resp, err = http.Head(layerURL) if err != nil { t.Fatalf("unexpected error checking head on non-existent layer: %v", err) } - switch resp.StatusCode { - case http.StatusOK: - break // expected - default: - d, err := httputil.DumpResponse(resp, true) - if err != nil { - t.Fatalf("unexpected status checking head on layer: %v, %v", resp.StatusCode, resp.Status) - } + checkResponse(t, "checking head on non-existent layer", resp, http.StatusNotFound) - t.Logf("response:\n%s", string(d)) - t.Fatalf("unexpected status checking head on layer: %v, %v", resp.StatusCode, resp.Status) + // ------------------------------------------ + // Upload a layer + layerUploadURL, err := builder.buildLayerUploadURL(imageName) + if err != nil { + t.Fatalf("error building upload url: %v", err) } - logrus.Infof("fetch the layer") + resp, err = http.Post(layerUploadURL, "", nil) + if err != nil { + t.Fatalf("error starting layer upload: %v", err) + } + + checkResponse(t, "starting layer upload", resp, http.StatusAccepted) + checkHeaders(t, resp, http.Header{ + "Location": []string{"*"}, + "Content-Length": []string{"0"}, + }) + + layerLength, _ := layerFile.Seek(0, os.SEEK_END) + layerFile.Seek(0, os.SEEK_SET) + + // TODO(sday): Cancel the layer upload here and restart. + + uploadURLBase := startPushLayer(t, builder, imageName) + pushLayer(t, builder, imageName, layerDigest, uploadURLBase, layerFile) + + // ------------------------ + // Use a head request to see if the layer exists. + resp, err = http.Head(layerURL) + if err != nil { + t.Fatalf("unexpected error checking head on existing layer: %v", err) + } + + checkResponse(t, "checking head on existing layer", resp, http.StatusOK) + checkHeaders(t, resp, http.Header{ + "Content-Length": []string{fmt.Sprint(layerLength)}, + }) + // ---------------- // Fetch the layer! resp, err = http.Get(layerURL) @@ -207,30 +121,299 @@ func TestLayerAPI(t *testing.T) { t.Fatalf("unexpected error fetching layer: %v", err) } - switch resp.StatusCode { - case http.StatusOK: - break // expected - default: - d, err := httputil.DumpResponse(resp, true) - if err != nil { - t.Fatalf("unexpected status fetching layer: %v, %v", resp.StatusCode, resp.Status) - } - - t.Logf("response:\n%s", string(d)) - t.Fatalf("unexpected status fetching layer: %v, %v", resp.StatusCode, resp.Status) - } + checkResponse(t, "fetching layer", resp, http.StatusOK) + checkHeaders(t, resp, http.Header{ + "Content-Length": []string{fmt.Sprint(layerLength)}, + }) // Verify the body verifier := digest.NewDigestVerifier(layerDigest) io.Copy(verifier, resp.Body) if !verifier.Verified() { - d, err := httputil.DumpResponse(resp, true) - if err != nil { - t.Fatalf("unexpected status checking head on layer ayo!: %v, %v", resp.StatusCode, resp.Status) - } - - t.Logf("response:\n%s", string(d)) t.Fatalf("response body did not pass verification") } } + +func TestManifestAPI(t *testing.T) { + pk, err := libtrust.GenerateECP256PrivateKey() + if err != nil { + t.Fatalf("unexpected error generating private key: %v", err) + } + + config := configuration.Configuration{ + Storage: configuration.Storage{ + "inmemory": configuration.Parameters{}, + }, + } + + app := NewApp(config) + server := httptest.NewServer(handlers.CombinedLoggingHandler(os.Stderr, app)) + builder, err := newURLBuilderFromString(server.URL) + if err != nil { + t.Fatalf("unexpected error creating url builder: %v", err) + } + + imageName := "foo/bar" + tag := "thetag" + + manifestURL, err := builder.buildManifestURL(imageName, tag) + if err != nil { + t.Fatalf("unexpected error getting manifest url: %v", err) + } + + // ----------------------------- + // Attempt to fetch the manifest + resp, err := http.Get(manifestURL) + if err != nil { + t.Fatalf("unexpected error getting manifest: %v", err) + } + defer resp.Body.Close() + + checkResponse(t, "getting non-existent manifest", resp, http.StatusNotFound) + + // TODO(stevvooe): Shoot. The error setup is not working out. The content- + // type headers are being set after writing the status code. + // if resp.Header.Get("Content-Type") != "application/json" { + // t.Fatalf("unexpected content type: %v != 'application/json'", + // resp.Header.Get("Content-Type")) + // } + dec := json.NewDecoder(resp.Body) + + var respErrs struct { + Errors []Error + } + if err := dec.Decode(&respErrs); err != nil { + t.Fatalf("unexpected error decoding error response: %v", err) + } + + if len(respErrs.Errors) == 0 { + t.Fatalf("expected errors in response") + } + + if respErrs.Errors[0].Code != ErrorCodeUnknownManifest { + t.Fatalf("expected manifest unknown error: got %v", respErrs) + } + + // -------------------------------- + // Attempt to push unsigned manifest with missing layers + unsignedManifest := &storage.Manifest{ + Name: imageName, + Tag: tag, + FSLayers: []storage.FSLayer{ + { + BlobSum: "asdf", + }, + { + BlobSum: "qwer", + }, + }, + } + + resp = putManifest(t, "putting unsigned manifest", manifestURL, unsignedManifest) + defer resp.Body.Close() + checkResponse(t, "posting unsigned manifest", resp, http.StatusBadRequest) + + dec = json.NewDecoder(resp.Body) + if err := dec.Decode(&respErrs); err != nil { + t.Fatalf("unexpected error decoding error response: %v", err) + } + + var unverified int + var missingLayers int + var invalidDigests int + + for _, err := range respErrs.Errors { + switch err.Code { + case ErrorCodeUnverifiedManifest: + unverified++ + case ErrorCodeUnknownLayer: + missingLayers++ + case ErrorCodeInvalidDigest: + // TODO(stevvooe): This error isn't quite descriptive enough -- + // the layer with an invalid digest isn't identified. + invalidDigests++ + default: + t.Fatalf("unexpected error: %v", err) + } + } + + if unverified != 1 { + t.Fatalf("should have received one unverified manifest error: %v", respErrs) + } + + if missingLayers != 2 { + t.Fatalf("should have received two missing layer errors: %v", respErrs) + } + + if invalidDigests != 2 { + t.Fatalf("should have received two invalid digest errors: %v", respErrs) + } + + // Push 2 random layers + expectedLayers := make(map[digest.Digest]io.ReadSeeker) + + for i := range unsignedManifest.FSLayers { + rs, dgstStr, err := testutil.CreateRandomTarFile() + + if err != nil { + t.Fatalf("error creating random layer %d: %v", i, err) + } + dgst := digest.Digest(dgstStr) + + expectedLayers[dgst] = rs + unsignedManifest.FSLayers[i].BlobSum = dgst + + uploadURLBase := startPushLayer(t, builder, imageName) + pushLayer(t, builder, imageName, dgst, uploadURLBase, rs) + } + + // ------------------- + // Push the signed manifest with all layers pushed. + signedManifest, err := unsignedManifest.Sign(pk) + if err != nil { + t.Fatalf("unexpected error signing manifest: %v", err) + } + + resp = putManifest(t, "putting signed manifest", manifestURL, signedManifest) + + checkResponse(t, "putting manifest", resp, http.StatusOK) + + resp, err = http.Get(manifestURL) + if err != nil { + t.Fatalf("unexpected error fetching manifest: %v", err) + } + defer resp.Body.Close() + + checkResponse(t, "fetching uploaded manifest", resp, http.StatusOK) + + var fetchedManifest storage.SignedManifest + dec = json.NewDecoder(resp.Body) + if err := dec.Decode(&fetchedManifest); err != nil { + t.Fatalf("error decoding fetched manifest: %v", err) + } + + if !bytes.Equal(fetchedManifest.Raw, signedManifest.Raw) { + t.Fatalf("manifests do not match") + } +} + +func putManifest(t *testing.T, msg, url string, v interface{}) *http.Response { + body, err := json.Marshal(v) + if err != nil { + t.Fatalf("unexpected error marshaling %v: %v", v, err) + } + + req, err := http.NewRequest("PUT", url, bytes.NewReader(body)) + if err != nil { + t.Fatalf("error creating request for %s: %v", msg, err) + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + t.Fatalf("error doing put request while %s: %v", msg, err) + } + + return resp +} + +func startPushLayer(t *testing.T, ub *urlBuilder, name string) string { + layerUploadURL, err := ub.buildLayerUploadURL(name) + if err != nil { + t.Fatalf("unexpected error building layer upload url: %v", err) + } + + resp, err := http.Post(layerUploadURL, "", nil) + if err != nil { + t.Fatalf("unexpected error starting layer push: %v", err) + } + defer resp.Body.Close() + + checkResponse(t, fmt.Sprintf("pushing starting layer push %v", name), resp, http.StatusAccepted) + checkHeaders(t, resp, http.Header{ + "Location": []string{"*"}, + "Content-Length": []string{"0"}, + }) + + return resp.Header.Get("Location") +} + +// 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 { + rsLength, _ := rs.Seek(0, os.SEEK_END) + rs.Seek(0, os.SEEK_SET) + + uploadURL := appendValues(uploadURLBase, url.Values{ + "digest": []string{dgst.String()}, + "size": []string{fmt.Sprint(rsLength)}, + }) + + // Just do a monolithic upload + req, err := http.NewRequest("PUT", uploadURL, rs) + if err != nil { + t.Fatalf("unexpected error creating new request: %v", err) + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + t.Fatalf("unexpected error doing put: %v", err) + } + defer resp.Body.Close() + + checkResponse(t, "putting monolithic chunk", resp, http.StatusCreated) + + expectedLayerURL, err := ub.buildLayerURL(name, dgst) + if err != nil { + t.Fatalf("error building expected layer url: %v", err) + } + + checkHeaders(t, resp, http.Header{ + "Location": []string{expectedLayerURL}, + "Content-Length": []string{"0"}, + }) + + return resp.Header.Get("Location") +} + +func checkResponse(t *testing.T, msg string, resp *http.Response, expectedStatus int) { + if resp.StatusCode != expectedStatus { + t.Logf("unexpected status %s: %v != %v", msg, resp.StatusCode, expectedStatus) + maybeDumpResponse(t, resp) + + t.FailNow() + } +} + +func maybeDumpResponse(t *testing.T, resp *http.Response) { + if d, err := httputil.DumpResponse(resp, true); err != nil { + t.Logf("error dumping response: %v", err) + } else { + t.Logf("response:\n%s", string(d)) + } +} + +// matchHeaders checks that the response has at least the headers. If not, the +// test will fail. If a passed in header value is "*", any non-zero value will +// suffice as a match. +func checkHeaders(t *testing.T, resp *http.Response, headers http.Header) { + for k, vs := range headers { + if resp.Header.Get(k) == "" { + t.Fatalf("response missing header %q", k) + } + + for _, v := range vs { + if v == "*" { + // Just ensure there is some value. + if len(resp.Header[k]) > 0 { + continue + } + } + + for _, hv := range resp.Header[k] { + if hv != v { + t.Fatalf("header value not matched in response: %q != %q", hv, v) + } + } + } + } +} diff --git a/app.go b/app.go index 25bf572d..324cad29 100644 --- a/app.go +++ b/app.go @@ -108,8 +108,9 @@ func (app *App) dispatcher(dispatch dispatchFunc) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) context := &Context{ - App: app, - Name: vars["name"], + App: app, + Name: vars["name"], + urlBuilder: newURLBuilderFromRequest(r), } // Store vars for underlying handlers. diff --git a/context.go b/context.go index c246d6ac..8d894e0f 100644 --- a/context.go +++ b/context.go @@ -24,4 +24,6 @@ type Context struct { // log provides a context specific logger. log *logrus.Entry + + urlBuilder *urlBuilder } diff --git a/errors.go b/errors.go index 113097dd..9593741d 100644 --- a/errors.go +++ b/errors.go @@ -34,6 +34,14 @@ const ( // match the provided tag. ErrorCodeInvalidTag + // ErrorCodeUnknownManifest returned when image manifest name and tag is + // unknown, accompanied by a 404 status. + ErrorCodeUnknownManifest + + // ErrorCodeInvalidManifest returned when an image manifest is invalid, + // typically during a PUT operation. + ErrorCodeInvalidManifest + // ErrorCodeUnverifiedManifest is returned when the manifest fails signature // validation. ErrorCodeUnverifiedManifest @@ -56,6 +64,8 @@ var errorCodeStrings = map[ErrorCode]string{ ErrorCodeInvalidLength: "INVALID_LENGTH", ErrorCodeInvalidName: "INVALID_NAME", ErrorCodeInvalidTag: "INVALID_TAG", + ErrorCodeUnknownManifest: "UNKNOWN_MANIFEST", + ErrorCodeInvalidManifest: "INVALID_MANIFEST", ErrorCodeUnverifiedManifest: "UNVERIFIED_MANIFEST", ErrorCodeUnknownLayer: "UNKNOWN_LAYER", ErrorCodeUnknownLayerUpload: "UNKNOWN_LAYER_UPLOAD", @@ -66,12 +76,14 @@ var errorCodesMessages = map[ErrorCode]string{ ErrorCodeUnknown: "unknown error", ErrorCodeInvalidDigest: "provided digest did not match uploaded content", ErrorCodeInvalidLength: "provided length did not match content length", - ErrorCodeInvalidName: "Manifest name did not match URI", - ErrorCodeInvalidTag: "Manifest tag did not match URI", - ErrorCodeUnverifiedManifest: "Manifest failed signature validation", - ErrorCodeUnknownLayer: "Referenced layer not available", + ErrorCodeInvalidName: "manifest name did not match URI", + ErrorCodeInvalidTag: "manifest tag did not match URI", + ErrorCodeUnknownManifest: "manifest not known", + ErrorCodeInvalidManifest: "manifest is invalid", + ErrorCodeUnverifiedManifest: "manifest failed signature validation", + ErrorCodeUnknownLayer: "referenced layer not available", ErrorCodeUnknownLayerUpload: "cannot resume unknown layer upload", - ErrorCodeUntrustedSignature: "Manifest signed by untrusted source", + ErrorCodeUntrustedSignature: "manifest signed by untrusted source", } var stringToErrorCode map[string]ErrorCode @@ -178,7 +190,12 @@ func (errs *Errors) Push(code ErrorCode, details ...interface{}) { // PushErr pushes an error interface onto the error stack. func (errs *Errors) PushErr(err error) { - errs.Errors = append(errs.Errors, err) + switch err.(type) { + case Error: + errs.Errors = append(errs.Errors, err) + default: + errs.Errors = append(errs.Errors, Error{Message: err.Error()}) + } } func (errs *Errors) Error() string { diff --git a/errors_test.go b/errors_test.go index 709b6ced..e0392eb6 100644 --- a/errors_test.go +++ b/errors_test.go @@ -69,7 +69,7 @@ func TestErrorsManagement(t *testing.T) { t.Fatalf("error marashaling errors: %v", err) } - expectedJSON := "{\"errors\":[{\"code\":\"INVALID_DIGEST\",\"message\":\"provided digest did not match uploaded content\"},{\"code\":\"UNKNOWN_LAYER\",\"message\":\"Referenced layer not available\",\"detail\":{\"unknown\":{\"blobSum\":\"sometestblobsumdoesntmatter\"}}}]}" + expectedJSON := "{\"errors\":[{\"code\":\"INVALID_DIGEST\",\"message\":\"provided digest did not match uploaded content\"},{\"code\":\"UNKNOWN_LAYER\",\"message\":\"referenced layer not available\",\"detail\":{\"unknown\":{\"blobSum\":\"sometestblobsumdoesntmatter\"}}}]}" if string(p) != expectedJSON { t.Fatalf("unexpected json: %q != %q", string(p), expectedJSON) diff --git a/helpers.go b/helpers.go index 7714d029..6fce84a2 100644 --- a/helpers.go +++ b/helpers.go @@ -4,8 +4,6 @@ import ( "encoding/json" "io" "net/http" - - "github.com/gorilla/mux" ) // serveJSON marshals v and sets the content-type header to @@ -32,10 +30,3 @@ func closeResources(handler http.Handler, closers ...io.Closer) http.Handler { handler.ServeHTTP(w, r) }) } - -// 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 -} diff --git a/images.go b/images.go index f16a3560..495e193a 100644 --- a/images.go +++ b/images.go @@ -1,8 +1,13 @@ package registry import ( + "encoding/json" + "fmt" "net/http" + "github.com/docker/docker-registry/digest" + + "github.com/docker/docker-registry/storage" "github.com/gorilla/handlers" ) @@ -32,15 +37,77 @@ type imageManifestHandler struct { // GetImageManifest fetches the image manifest from the storage backend, if it exists. func (imh *imageManifestHandler) GetImageManifest(w http.ResponseWriter, r *http.Request) { + manifests := imh.services.Manifests() + manifest, err := manifests.Get(imh.Name, imh.Tag) + if err != nil { + imh.Errors.Push(ErrorCodeUnknownManifest, err) + w.WriteHeader(http.StatusNotFound) + return + } + + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Content-Length", fmt.Sprint(len(manifest.Raw))) + w.Write(manifest.Raw) } // PutImageManifest validates and stores and image in the registry. func (imh *imageManifestHandler) PutImageManifest(w http.ResponseWriter, r *http.Request) { + manifests := imh.services.Manifests() + dec := json.NewDecoder(r.Body) + var manifest storage.SignedManifest + if err := dec.Decode(&manifest); err != nil { + imh.Errors.Push(ErrorCodeInvalidManifest, err) + w.WriteHeader(http.StatusBadRequest) + return + } + + if err := manifests.Put(imh.Name, imh.Tag, &manifest); err != nil { + // TODO(stevvooe): These error handling switches really need to be + // handled by an app global mapper. + switch err := err.(type) { + case storage.ErrManifestVerification: + for _, verificationError := range err { + switch verificationError := verificationError.(type) { + case storage.ErrUnknownLayer: + imh.Errors.Push(ErrorCodeUnknownLayer, verificationError.FSLayer) + case storage.ErrManifestUnverified: + imh.Errors.Push(ErrorCodeUnverifiedManifest) + 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(ErrorCodeInvalidDigest) + } else { + imh.Errors.PushErr(verificationError) + } + } + } + default: + imh.Errors.PushErr(err) + } + + w.WriteHeader(http.StatusBadRequest) + return + } } // DeleteImageManifest removes the image with the given tag from the registry. func (imh *imageManifestHandler) DeleteImageManifest(w http.ResponseWriter, r *http.Request) { + manifests := imh.services.Manifests() + if err := manifests.Delete(imh.Name, imh.Tag); err != nil { + switch err := err.(type) { + case storage.ErrUnknownManifest: + imh.Errors.Push(ErrorCodeUnknownManifest, err) + w.WriteHeader(http.StatusNotFound) + default: + imh.Errors.Push(ErrorCodeUnknown, err) + w.WriteHeader(http.StatusBadRequest) + } + return + } + w.Header().Set("Content-Length", "0") + w.WriteHeader(http.StatusAccepted) } diff --git a/layer.go b/layer.go index 5e1c6f45..4d937c64 100644 --- a/layer.go +++ b/layer.go @@ -6,7 +6,6 @@ import ( "github.com/docker/docker-registry/digest" "github.com/docker/docker-registry/storage" "github.com/gorilla/handlers" - "github.com/gorilla/mux" ) // layerDispatcher uses the request context to build a layerHandler. @@ -47,33 +46,16 @@ func (lh *layerHandler) GetLayer(w http.ResponseWriter, r *http.Request) { layer, err := layers.Fetch(lh.Name, lh.Digest) if err != nil { - switch err { - case storage.ErrLayerUnknown: + switch err := err.(type) { + case storage.ErrUnknownLayer: w.WriteHeader(http.StatusNotFound) - lh.Errors.Push(ErrorCodeUnknownLayer, - map[string]interface{}{ - "unknown": storage.FSLayer{BlobSum: lh.Digest}, - }) - return + lh.Errors.Push(ErrorCodeUnknownLayer, err.FSLayer) default: lh.Errors.Push(ErrorCodeUnknown, err) - return } + return } defer layer.Close() http.ServeContent(w, r, layer.Digest().String(), layer.CreatedAt(), layer) } - -func buildLayerURL(router *mux.Router, r *http.Request, layer storage.Layer) (string, error) { - route := clonedRoute(router, routeNameBlob) - - layerURL, err := route.Schemes(r.URL.Scheme).Host(r.Host). - URL("name", layer.Name(), - "digest", layer.Digest().String()) - if err != nil { - return "", err - } - - return layerURL.String(), nil -} diff --git a/layerupload.go b/layerupload.go index d1ec4206..d7aaa24f 100644 --- a/layerupload.go +++ b/layerupload.go @@ -10,7 +10,6 @@ import ( "github.com/docker/docker-registry/digest" "github.com/docker/docker-registry/storage" "github.com/gorilla/handlers" - "github.com/gorilla/mux" ) // layerUploadDispatcher constructs and returns the layer upload handler for @@ -151,7 +150,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 := buildLayerUploadURL(luh.router, r, luh.Upload) + uploadURL, err := luh.urlBuilder.forLayerUpload(luh.Upload) if err != nil { logrus.Infof("error building upload url: %s", err) return err @@ -171,7 +170,7 @@ var errNotReadyToComplete = fmt.Errorf("not ready to complete upload") func (luh *layerUploadHandler) maybeCompleteUpload(w http.ResponseWriter, r *http.Request) error { // If we get a digest and length, we can finish the upload. dgstStr := r.FormValue("digest") // TODO(stevvooe): Support multiple digest parameters! - sizeStr := r.FormValue("length") + sizeStr := r.FormValue("size") if dgstStr == "" || sizeStr == "" { return errNotReadyToComplete @@ -200,7 +199,7 @@ func (luh *layerUploadHandler) completeUpload(w http.ResponseWriter, r *http.Req return } - layerURL, err := buildLayerURL(luh.router, r, layer) + layerURL, err := luh.urlBuilder.forLayer(layer) if err != nil { luh.Errors.Push(ErrorCodeUnknown, err) w.WriteHeader(http.StatusInternalServerError) @@ -211,15 +210,3 @@ func (luh *layerUploadHandler) completeUpload(w http.ResponseWriter, r *http.Req w.Header().Set("Content-Length", "0") w.WriteHeader(http.StatusCreated) } - -func buildLayerUploadURL(router *mux.Router, r *http.Request, upload storage.LayerUpload) (string, error) { - route := clonedRoute(router, routeNameBlobUploadResume) - - uploadURL, err := route.Schemes(r.URL.Scheme).Host(r.Host). - URL("name", upload.Name(), "uuid", upload.UUID()) - if err != nil { - return "", err - } - - return uploadURL.String(), nil -} diff --git a/urls.go b/urls.go new file mode 100644 index 00000000..d9e77f5e --- /dev/null +++ b/urls.go @@ -0,0 +1,141 @@ +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) 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 +}