Merge pull request #792 from stevvooe/next-generation

Initial implementation of Manifest HTTP API
This commit is contained in:
Olivier Gambier 2014-12-01 10:10:35 -08:00
commit af72f6cc94
17 changed files with 749 additions and 263 deletions

View file

@ -1,6 +1,8 @@
package registry package registry
import ( import (
"bytes"
"encoding/json"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
@ -10,7 +12,9 @@ import (
"os" "os"
"testing" "testing"
"github.com/Sirupsen/logrus" "github.com/docker/libtrust"
"github.com/docker/docker-registry/storage"
_ "github.com/docker/docker-registry/storagedriver/inmemory" _ "github.com/docker/docker-registry/storagedriver/inmemory"
"github.com/gorilla/handlers" "github.com/gorilla/handlers"
@ -34,11 +38,10 @@ func TestLayerAPI(t *testing.T) {
app := NewApp(config) app := NewApp(config)
server := httptest.NewServer(handlers.CombinedLoggingHandler(os.Stderr, app)) server := httptest.NewServer(handlers.CombinedLoggingHandler(os.Stderr, app))
router := v2APIRouter() builder, err := newURLBuilderFromString(server.URL)
u, err := url.Parse(server.URL)
if err != nil { if err != nil {
t.Fatalf("error parsing server url: %v", err) t.Fatalf("error creating url builder: %v", err)
} }
imageName := "foo/bar" imageName := "foo/bar"
@ -52,154 +55,65 @@ func TestLayerAPI(t *testing.T) {
// ----------------------------------- // -----------------------------------
// Test fetch for non-existent content // Test fetch for non-existent content
r, err := router.GetRoute(routeNameBlob).Host(u.Host). layerURL, err := builder.buildLayerURL(imageName, layerDigest)
URL("name", imageName, if err != nil {
"digest", tarSumStr) t.Fatalf("error building url: %v", err)
}
resp, err := http.Get(r.String()) resp, err := http.Get(layerURL)
if err != nil { if err != nil {
t.Fatalf("unexpected error fetching non-existent layer: %v", err) t.Fatalf("unexpected error fetching non-existent layer: %v", err)
} }
switch resp.StatusCode { checkResponse(t, "fetching non-existent content", resp, http.StatusNotFound)
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)
}
// ------------------------------------------ // ------------------------------------------
// Test head request for non-existent content // 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) resp, err = http.Head(layerURL)
if err != nil { if err != nil {
t.Fatalf("unexpected error checking head on non-existent layer: %v", err) t.Fatalf("unexpected error checking head on non-existent layer: %v", err)
} }
switch resp.StatusCode { checkResponse(t, "checking head on non-existent layer", resp, http.StatusNotFound)
case http.StatusOK:
break // expected // ------------------------------------------
default: // Upload a layer
d, err := httputil.DumpResponse(resp, true) layerUploadURL, err := builder.buildLayerUploadURL(imageName)
if err != nil { if err != nil {
t.Fatalf("unexpected status checking head on layer: %v, %v", resp.StatusCode, resp.Status) t.Fatalf("error building upload url: %v", err)
} }
t.Logf("response:\n%s", string(d)) resp, err = http.Post(layerUploadURL, "", nil)
t.Fatalf("unexpected status checking head on layer: %v, %v", resp.StatusCode, resp.Status) if err != nil {
t.Fatalf("error starting layer upload: %v", err)
} }
logrus.Infof("fetch the layer") 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! // Fetch the layer!
resp, err = http.Get(layerURL) resp, err = http.Get(layerURL)
@ -207,30 +121,299 @@ func TestLayerAPI(t *testing.T) {
t.Fatalf("unexpected error fetching layer: %v", err) t.Fatalf("unexpected error fetching layer: %v", err)
} }
switch resp.StatusCode { checkResponse(t, "fetching layer", resp, http.StatusOK)
case http.StatusOK: checkHeaders(t, resp, http.Header{
break // expected "Content-Length": []string{fmt.Sprint(layerLength)},
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)
}
// Verify the body // Verify the body
verifier := digest.NewDigestVerifier(layerDigest) verifier := digest.NewDigestVerifier(layerDigest)
io.Copy(verifier, resp.Body) io.Copy(verifier, resp.Body)
if !verifier.Verified() { 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") 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)
}
}
}
}
}

1
app.go
View file

@ -110,6 +110,7 @@ func (app *App) dispatcher(dispatch dispatchFunc) http.Handler {
context := &Context{ context := &Context{
App: app, App: app,
Name: vars["name"], Name: vars["name"],
urlBuilder: newURLBuilderFromRequest(r),
} }
// Store vars for underlying handlers. // Store vars for underlying handlers.

View file

@ -24,4 +24,6 @@ type Context struct {
// log provides a context specific logger. // log provides a context specific logger.
log *logrus.Entry log *logrus.Entry
urlBuilder *urlBuilder
} }

View file

@ -34,6 +34,14 @@ const (
// match the provided tag. // match the provided tag.
ErrorCodeInvalidTag 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 // ErrorCodeUnverifiedManifest is returned when the manifest fails signature
// validation. // validation.
ErrorCodeUnverifiedManifest ErrorCodeUnverifiedManifest
@ -56,6 +64,8 @@ var errorCodeStrings = map[ErrorCode]string{
ErrorCodeInvalidLength: "INVALID_LENGTH", ErrorCodeInvalidLength: "INVALID_LENGTH",
ErrorCodeInvalidName: "INVALID_NAME", ErrorCodeInvalidName: "INVALID_NAME",
ErrorCodeInvalidTag: "INVALID_TAG", ErrorCodeInvalidTag: "INVALID_TAG",
ErrorCodeUnknownManifest: "UNKNOWN_MANIFEST",
ErrorCodeInvalidManifest: "INVALID_MANIFEST",
ErrorCodeUnverifiedManifest: "UNVERIFIED_MANIFEST", ErrorCodeUnverifiedManifest: "UNVERIFIED_MANIFEST",
ErrorCodeUnknownLayer: "UNKNOWN_LAYER", ErrorCodeUnknownLayer: "UNKNOWN_LAYER",
ErrorCodeUnknownLayerUpload: "UNKNOWN_LAYER_UPLOAD", ErrorCodeUnknownLayerUpload: "UNKNOWN_LAYER_UPLOAD",
@ -66,12 +76,14 @@ var errorCodesMessages = map[ErrorCode]string{
ErrorCodeUnknown: "unknown error", ErrorCodeUnknown: "unknown error",
ErrorCodeInvalidDigest: "provided digest did not match uploaded content", ErrorCodeInvalidDigest: "provided digest did not match uploaded content",
ErrorCodeInvalidLength: "provided length did not match content length", ErrorCodeInvalidLength: "provided length did not match content length",
ErrorCodeInvalidName: "Manifest name did not match URI", ErrorCodeInvalidName: "manifest name did not match URI",
ErrorCodeInvalidTag: "Manifest tag did not match URI", ErrorCodeInvalidTag: "manifest tag did not match URI",
ErrorCodeUnverifiedManifest: "Manifest failed signature validation", ErrorCodeUnknownManifest: "manifest not known",
ErrorCodeUnknownLayer: "Referenced layer not available", ErrorCodeInvalidManifest: "manifest is invalid",
ErrorCodeUnverifiedManifest: "manifest failed signature validation",
ErrorCodeUnknownLayer: "referenced layer not available",
ErrorCodeUnknownLayerUpload: "cannot resume unknown layer upload", ErrorCodeUnknownLayerUpload: "cannot resume unknown layer upload",
ErrorCodeUntrustedSignature: "Manifest signed by untrusted source", ErrorCodeUntrustedSignature: "manifest signed by untrusted source",
} }
var stringToErrorCode map[string]ErrorCode 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. // PushErr pushes an error interface onto the error stack.
func (errs *Errors) PushErr(err error) { func (errs *Errors) PushErr(err error) {
switch err.(type) {
case Error:
errs.Errors = append(errs.Errors, err) errs.Errors = append(errs.Errors, err)
default:
errs.Errors = append(errs.Errors, Error{Message: err.Error()})
}
} }
func (errs *Errors) Error() string { func (errs *Errors) Error() string {

View file

@ -69,7 +69,7 @@ func TestErrorsManagement(t *testing.T) {
t.Fatalf("error marashaling errors: %v", err) 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 { if string(p) != expectedJSON {
t.Fatalf("unexpected json: %q != %q", string(p), expectedJSON) t.Fatalf("unexpected json: %q != %q", string(p), expectedJSON)

View file

@ -4,8 +4,6 @@ import (
"encoding/json" "encoding/json"
"io" "io"
"net/http" "net/http"
"github.com/gorilla/mux"
) )
// serveJSON marshals v and sets the content-type header to // 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) 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
}

View file

@ -1,8 +1,13 @@
package registry package registry
import ( import (
"encoding/json"
"fmt"
"net/http" "net/http"
"github.com/docker/docker-registry/digest"
"github.com/docker/docker-registry/storage"
"github.com/gorilla/handlers" "github.com/gorilla/handlers"
) )
@ -32,15 +37,77 @@ type imageManifestHandler struct {
// GetImageManifest fetches the image manifest from the storage backend, if it exists. // GetImageManifest fetches the image manifest from the storage backend, if it exists.
func (imh *imageManifestHandler) GetImageManifest(w http.ResponseWriter, r *http.Request) { 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. // PutImageManifest validates and stores and image in the registry.
func (imh *imageManifestHandler) PutImageManifest(w http.ResponseWriter, r *http.Request) { 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. // DeleteImageManifest removes the image with the given tag from the registry.
func (imh *imageManifestHandler) DeleteImageManifest(w http.ResponseWriter, r *http.Request) { 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)
} }

View file

@ -6,7 +6,6 @@ import (
"github.com/docker/docker-registry/digest" "github.com/docker/docker-registry/digest"
"github.com/docker/docker-registry/storage" "github.com/docker/docker-registry/storage"
"github.com/gorilla/handlers" "github.com/gorilla/handlers"
"github.com/gorilla/mux"
) )
// layerDispatcher uses the request context to build a layerHandler. // 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) layer, err := layers.Fetch(lh.Name, lh.Digest)
if err != nil { if err != nil {
switch err { switch err := err.(type) {
case storage.ErrLayerUnknown: case storage.ErrUnknownLayer:
w.WriteHeader(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
lh.Errors.Push(ErrorCodeUnknownLayer, lh.Errors.Push(ErrorCodeUnknownLayer, err.FSLayer)
map[string]interface{}{
"unknown": storage.FSLayer{BlobSum: lh.Digest},
})
return
default: default:
lh.Errors.Push(ErrorCodeUnknown, err) lh.Errors.Push(ErrorCodeUnknown, err)
return
} }
return
} }
defer layer.Close() defer layer.Close()
http.ServeContent(w, r, layer.Digest().String(), layer.CreatedAt(), layer) 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
}

View file

@ -10,7 +10,6 @@ import (
"github.com/docker/docker-registry/digest" "github.com/docker/docker-registry/digest"
"github.com/docker/docker-registry/storage" "github.com/docker/docker-registry/storage"
"github.com/gorilla/handlers" "github.com/gorilla/handlers"
"github.com/gorilla/mux"
) )
// layerUploadDispatcher constructs and returns the layer upload handler for // 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 // chunk responses. This sets the correct headers but the response status is
// left to the caller. // left to the caller.
func (luh *layerUploadHandler) layerUploadResponse(w http.ResponseWriter, r *http.Request) error { 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 { if err != nil {
logrus.Infof("error building upload url: %s", err) logrus.Infof("error building upload url: %s", err)
return 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 { func (luh *layerUploadHandler) maybeCompleteUpload(w http.ResponseWriter, r *http.Request) error {
// If we get a digest and length, we can finish the upload. // If we get a digest and length, we can finish the upload.
dgstStr := r.FormValue("digest") // TODO(stevvooe): Support multiple digest parameters! dgstStr := r.FormValue("digest") // TODO(stevvooe): Support multiple digest parameters!
sizeStr := r.FormValue("length") sizeStr := r.FormValue("size")
if dgstStr == "" || sizeStr == "" { if dgstStr == "" || sizeStr == "" {
return errNotReadyToComplete return errNotReadyToComplete
@ -200,7 +199,7 @@ func (luh *layerUploadHandler) completeUpload(w http.ResponseWriter, r *http.Req
return return
} }
layerURL, err := buildLayerURL(luh.router, r, layer) layerURL, err := luh.urlBuilder.forLayer(layer)
if err != nil { if err != nil {
luh.Errors.Push(ErrorCodeUnknown, err) luh.Errors.Push(ErrorCodeUnknown, err)
w.WriteHeader(http.StatusInternalServerError) 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.Header().Set("Content-Length", "0")
w.WriteHeader(http.StatusCreated) 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
}

View file

@ -53,9 +53,6 @@ type LayerUpload interface {
} }
var ( var (
// ErrLayerUnknown returned when layer cannot be found.
ErrLayerUnknown = fmt.Errorf("unknown layer")
// ErrLayerExists returned when layer already exists // ErrLayerExists returned when layer already exists
ErrLayerExists = fmt.Errorf("layer exists") ErrLayerExists = fmt.Errorf("layer exists")
@ -65,9 +62,6 @@ var (
// ErrLayerUploadUnknown returned when upload is not found. // ErrLayerUploadUnknown returned when upload is not found.
ErrLayerUploadUnknown = fmt.Errorf("layer upload unknown") ErrLayerUploadUnknown = fmt.Errorf("layer upload unknown")
// ErrLayerInvalidDigest returned when tarsum check fails.
ErrLayerInvalidDigest = fmt.Errorf("invalid layer digest")
// ErrLayerInvalidLength returned when length check fails. // ErrLayerInvalidLength returned when length check fails.
ErrLayerInvalidLength = fmt.Errorf("invalid layer length") ErrLayerInvalidLength = fmt.Errorf("invalid layer length")
@ -75,3 +69,21 @@ var (
// Layer or LayerUpload. // Layer or LayerUpload.
ErrLayerClosed = fmt.Errorf("layer closed") ErrLayerClosed = fmt.Errorf("layer closed")
) )
// ErrUnknownLayer returned when layer cannot be found.
type ErrUnknownLayer struct {
FSLayer FSLayer
}
func (err ErrUnknownLayer) Error() string {
return fmt.Sprintf("unknown layer %v", err.FSLayer.BlobSum)
}
// ErrLayerInvalidDigest returned when tarsum check fails.
type ErrLayerInvalidDigest struct {
FSLayer FSLayer
}
func (err ErrLayerInvalidDigest) Error() string {
return fmt.Sprintf("invalid digest for referenced layer: %v", err.FSLayer.BlobSum)
}

View file

@ -169,11 +169,13 @@ func TestSimpleLayerRead(t *testing.T) {
t.Fatalf("error expected fetching unknown layer") t.Fatalf("error expected fetching unknown layer")
} }
if err != ErrLayerUnknown { switch err.(type) {
t.Fatalf("unexpected error fetching non-existent layer: %v", err) case ErrUnknownLayer:
} else {
err = nil err = nil
default:
t.Fatalf("unexpected error fetching non-existent layer: %v", err)
} }
randomLayerDigest, err := writeTestLayer(driver, ls.pathMapper, imageName, dgst, randomLayerReader) randomLayerDigest, err := writeTestLayer(driver, ls.pathMapper, imageName, dgst, randomLayerReader)
if err != nil { if err != nil {
t.Fatalf("unexpected error writing test layer: %v", err) t.Fatalf("unexpected error writing test layer: %v", err)

View file

@ -19,7 +19,8 @@ func (ls *layerStore) Exists(name string, digest digest.Digest) (bool, error) {
_, err := ls.Fetch(name, digest) _, err := ls.Fetch(name, digest)
if err != nil { if err != nil {
if err == ErrLayerUnknown { switch err.(type) {
case ErrUnknownLayer:
return false, nil return false, nil
} }
@ -34,7 +35,7 @@ func (ls *layerStore) Fetch(name string, digest digest.Digest) (Layer, error) {
if err != nil { if err != nil {
switch err := err.(type) { switch err := err.(type) {
case storagedriver.PathNotFoundError, *storagedriver.PathNotFoundError: case storagedriver.PathNotFoundError, *storagedriver.PathNotFoundError:
return nil, ErrLayerUnknown return nil, ErrUnknownLayer{FSLayer{BlobSum: digest}}
default: default:
return nil, err return nil, err
} }
@ -44,7 +45,7 @@ func (ls *layerStore) Fetch(name string, digest digest.Digest) (Layer, error) {
if err != nil { if err != nil {
switch err := err.(type) { switch err := err.(type) {
case storagedriver.PathNotFoundError, *storagedriver.PathNotFoundError: case storagedriver.PathNotFoundError, *storagedriver.PathNotFoundError:
return nil, ErrLayerUnknown return nil, ErrUnknownLayer{FSLayer{BlobSum: digest}}
default: default:
return nil, err return nil, err
} }

View file

@ -275,7 +275,7 @@ func (luc *layerUploadController) validateLayer(fp layerFile, size int64, dgst d
} }
if !digestVerifier.Verified() { if !digestVerifier.Verified() {
return "", ErrLayerInvalidDigest return "", ErrLayerInvalidDigest{FSLayer{BlobSum: dgst}}
} }
return dgst, nil return dgst, nil

View file

@ -1,23 +1,48 @@
package storage package storage
import ( import (
"crypto/x509"
"encoding/json" "encoding/json"
"fmt" "fmt"
"strings"
"github.com/docker/libtrust" "github.com/docker/libtrust"
"github.com/docker/docker-registry/digest" "github.com/docker/docker-registry/digest"
) )
var ( // ErrUnknownManifest is returned if the manifest is not known by the
// ErrManifestUnknown is returned if the manifest is not known by the
// registry. // registry.
ErrManifestUnknown = fmt.Errorf("unknown manifest") type ErrUnknownManifest struct {
Name string
Tag string
}
func (err ErrUnknownManifest) Error() string {
return fmt.Sprintf("unknown manifest name=%s tag=%s", err.Name, err.Tag)
}
// ErrManifestUnverified is returned when the registry is unable to verify // ErrManifestUnverified is returned when the registry is unable to verify
// the manifest. // the manifest.
ErrManifestUnverified = fmt.Errorf("unverified manifest") type ErrManifestUnverified struct{}
)
func (ErrManifestUnverified) Error() string {
return fmt.Sprintf("unverified manifest")
}
// ErrManifestVerification provides a type to collect errors encountered
// during manifest verification. Currently, it accepts errors of all types,
// but it may be narrowed to those involving manifest verification.
type ErrManifestVerification []error
func (errs ErrManifestVerification) Error() string {
var parts []string
for _, err := range errs {
parts = append(parts, err.Error())
}
return fmt.Sprintf("errors verifying manifest: %v", strings.Join(parts, ","))
}
// Versioned provides a struct with just the manifest schemaVersion. Incoming // Versioned provides a struct with just the manifest schemaVersion. Incoming
// content with unknown schema version can be decoded against this struct to // content with unknown schema version can be decoded against this struct to
@ -78,7 +103,37 @@ func (m *Manifest) Sign(pk libtrust.PrivateKey) (*SignedManifest, error) {
}, nil }, nil
} }
// SignedManifest provides an envelope for // SignWithChain signs the manifest with the given private key and x509 chain.
// The public key of the first element in the chain must be the public key
// corresponding with the sign key.
func (m *Manifest) SignWithChain(key libtrust.PrivateKey, chain []*x509.Certificate) (*SignedManifest, error) {
p, err := json.Marshal(m)
if err != nil {
return nil, err
}
js, err := libtrust.NewJSONSignature(p)
if err != nil {
return nil, err
}
if err := js.SignWithChain(key, chain); err != nil {
return nil, err
}
pretty, err := js.PrettySignature("signatures")
if err != nil {
return nil, err
}
return &SignedManifest{
Manifest: *m,
Raw: pretty,
}, nil
}
// SignedManifest provides an envelope for a signed image manifest, including
// the format sensitive raw bytes. It contains fields to
type SignedManifest struct { type SignedManifest struct {
Manifest Manifest
@ -88,28 +143,51 @@ type SignedManifest struct {
Raw []byte `json:"-"` Raw []byte `json:"-"`
} }
// Verify verifies the signature of the signed manifest returning the public
// keys used during signing.
func (sm *SignedManifest) Verify() ([]libtrust.PublicKey, error) {
js, err := libtrust.ParsePrettySignature(sm.Raw, "signatures")
if err != nil {
return nil, err
}
return js.Verify()
}
// VerifyChains verifies the signature of the signed manifest against the
// certificate pool returning the list of verified chains. Signatures without
// an x509 chain are not checked.
func (sm *SignedManifest) VerifyChains(ca *x509.CertPool) ([][]*x509.Certificate, error) {
js, err := libtrust.ParsePrettySignature(sm.Raw, "signatures")
if err != nil {
return nil, err
}
return js.VerifyChains(ca)
}
// UnmarshalJSON populates a new ImageManifest struct from JSON data. // UnmarshalJSON populates a new ImageManifest struct from JSON data.
func (m *SignedManifest) UnmarshalJSON(b []byte) error { func (sm *SignedManifest) UnmarshalJSON(b []byte) error {
var manifest Manifest var manifest Manifest
if err := json.Unmarshal(b, &manifest); err != nil { if err := json.Unmarshal(b, &manifest); err != nil {
return err return err
} }
m.Manifest = manifest sm.Manifest = manifest
m.Raw = b sm.Raw = b
return nil return nil
} }
// MarshalJSON returns the contents of raw. If Raw is nil, marshals the inner // MarshalJSON returns the contents of raw. If Raw is nil, marshals the inner
// contents. // contents.
func (m *SignedManifest) MarshalJSON() ([]byte, error) { func (sm *SignedManifest) MarshalJSON() ([]byte, error) {
if len(m.Raw) > 0 { if len(sm.Raw) > 0 {
return m.Raw, nil return sm.Raw, nil
} }
// If the raw data is not available, just dump the inner content. // If the raw data is not available, just dump the inner content.
return json.Marshal(&m.Manifest) return json.Marshal(&sm.Manifest)
} }
// FSLayer is a container struct for BlobSums defined in an image manifest // FSLayer is a container struct for BlobSums defined in an image manifest

View file

@ -33,8 +33,13 @@ func TestManifestStorage(t *testing.T) {
t.Fatalf("manifest should not exist") t.Fatalf("manifest should not exist")
} }
if _, err := ms.Get(name, tag); err != ErrManifestUnknown { if _, err := ms.Get(name, tag); true {
t.Fatalf("expected manifest unknown error: %v != %v", err, ErrManifestUnknown) switch err.(type) {
case ErrUnknownManifest:
break
default:
t.Fatalf("expected manifest unknown error: %#v", err)
}
} }
manifest := Manifest{ manifest := Manifest{

View file

@ -4,9 +4,8 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/docker/libtrust"
"github.com/docker/docker-registry/storagedriver" "github.com/docker/docker-registry/storagedriver"
"github.com/docker/libtrust"
) )
type manifestStore struct { type manifestStore struct {
@ -45,7 +44,7 @@ func (ms *manifestStore) Get(name, tag string) (*SignedManifest, error) {
if err != nil { if err != nil {
switch err := err.(type) { switch err := err.(type) {
case storagedriver.PathNotFoundError, *storagedriver.PathNotFoundError: case storagedriver.PathNotFoundError, *storagedriver.PathNotFoundError:
return nil, ErrManifestUnknown return nil, ErrUnknownManifest{Name: name, Tag: tag}
default: default:
return nil, err return nil, err
} }
@ -73,13 +72,28 @@ func (ms *manifestStore) Put(name, tag string, manifest *SignedManifest) error {
return err return err
} }
// TODO(stevvooe): Should we get manifest first? // TODO(stevvooe): Should we get old manifest first? Perhaps, write, then
// move to ensure a valid manifest?
return ms.driver.PutContent(p, manifest.Raw) return ms.driver.PutContent(p, manifest.Raw)
} }
func (ms *manifestStore) Delete(name, tag string) error { func (ms *manifestStore) Delete(name, tag string) error {
panic("not implemented") p, err := ms.path(name, tag)
if err != nil {
return err
}
if err := ms.driver.Delete(p); err != nil {
switch err := err.(type) {
case storagedriver.PathNotFoundError, *storagedriver.PathNotFoundError:
return ErrUnknownManifest{Name: name, Tag: tag}
default:
return err
}
}
return nil
} }
func (ms *manifestStore) path(name, tag string) (string, error) { func (ms *manifestStore) path(name, tag string) (string, error) {
@ -90,6 +104,12 @@ func (ms *manifestStore) path(name, tag string) (string, error) {
} }
func (ms *manifestStore) verifyManifest(name, tag string, manifest *SignedManifest) error { func (ms *manifestStore) verifyManifest(name, tag string, manifest *SignedManifest) error {
// TODO(stevvooe): This verification is present here, but this needs to be
// lifted out of the storage infrastructure and moved into a package
// oriented towards defining verifiers and reporting them with
// granularity.
var errs ErrManifestVerification
if manifest.Name != name { if manifest.Name != name {
return fmt.Errorf("name does not match manifest name") return fmt.Errorf("name does not match manifest name")
} }
@ -98,37 +118,34 @@ func (ms *manifestStore) verifyManifest(name, tag string, manifest *SignedManife
return fmt.Errorf("tag does not match manifest tag") return fmt.Errorf("tag does not match manifest tag")
} }
var errs []error // TODO(stevvooe): These pubkeys need to be checked with either Verify or
// VerifyWithChains. We need to define the exact source of the CA.
// Perhaps, its a configuration value injected into manifest store.
if _, err := manifest.Verify(); err != nil {
switch err {
case libtrust.ErrMissingSignatureKey, libtrust.ErrInvalidJSONContent, libtrust.ErrMissingSignatureKey:
errs = append(errs, ErrManifestUnverified{})
default:
errs = append(errs, err)
}
}
for _, fsLayer := range manifest.FSLayers { for _, fsLayer := range manifest.FSLayers {
exists, err := ms.layerService.Exists(name, fsLayer.BlobSum) exists, err := ms.layerService.Exists(name, fsLayer.BlobSum)
if err != nil { if err != nil {
// TODO(stevvooe): Need to store information about missing blob.
errs = append(errs, err) errs = append(errs, err)
} }
if !exists { if !exists {
errs = append(errs, fmt.Errorf("missing layer %v", fsLayer.BlobSum)) errs = append(errs, ErrUnknownLayer{FSLayer: fsLayer})
} }
} }
if len(errs) != 0 { if len(errs) != 0 {
// TODO(stevvooe): These need to be recoverable by a caller. // TODO(stevvooe): These need to be recoverable by a caller.
return fmt.Errorf("missing layers: %v", errs) return errs
} }
js, err := libtrust.ParsePrettySignature(manifest.Raw, "signatures")
if err != nil {
return err
}
_, err = js.Verify() // These pubkeys need to be checked.
if err != nil {
return err
}
// TODO(sday): Pubkey checks need to go here. This where things get fancy.
// Perhaps, an injected service would reduce coupling here.
return nil return nil
} }

141
urls.go Normal file
View file

@ -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
}