Merge pull request #792 from stevvooe/next-generation
Initial implementation of Manifest HTTP API
This commit is contained in:
commit
af72f6cc94
17 changed files with 749 additions and 263 deletions
487
api_test.go
487
api_test.go
|
@ -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
1
app.go
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
27
errors.go
27
errors.go
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
69
images.go
69
images.go
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
26
layer.go
26
layer.go
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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{
|
||||||
|
|
|
@ -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
141
urls.go
Normal 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
|
||||||
|
}
|
Loading…
Reference in a new issue