2015-02-11 01:25:40 +00:00
|
|
|
package handlers
|
2015-01-06 18:37:27 +00:00
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"io/ioutil"
|
|
|
|
"net/http"
|
|
|
|
"net/http/httptest"
|
|
|
|
"net/http/httputil"
|
|
|
|
"net/url"
|
|
|
|
"os"
|
2015-02-27 00:43:47 +00:00
|
|
|
"path"
|
2015-01-30 05:26:35 +00:00
|
|
|
"reflect"
|
2015-07-17 18:42:47 +00:00
|
|
|
"regexp"
|
|
|
|
"strconv"
|
2015-02-24 22:59:01 +00:00
|
|
|
"strings"
|
2015-01-06 18:37:27 +00:00
|
|
|
"testing"
|
|
|
|
|
2015-12-16 22:30:49 +00:00
|
|
|
"github.com/docker/distribution"
|
2015-01-06 18:37:27 +00:00
|
|
|
"github.com/docker/distribution/configuration"
|
2015-08-19 21:24:31 +00:00
|
|
|
"github.com/docker/distribution/context"
|
2015-01-06 18:37:27 +00:00
|
|
|
"github.com/docker/distribution/digest"
|
|
|
|
"github.com/docker/distribution/manifest"
|
2015-12-17 01:26:13 +00:00
|
|
|
"github.com/docker/distribution/manifest/manifestlist"
|
2015-08-21 04:24:30 +00:00
|
|
|
"github.com/docker/distribution/manifest/schema1"
|
2015-12-16 22:30:49 +00:00
|
|
|
"github.com/docker/distribution/manifest/schema2"
|
2015-12-15 22:35:23 +00:00
|
|
|
"github.com/docker/distribution/reference"
|
2015-05-15 01:21:39 +00:00
|
|
|
"github.com/docker/distribution/registry/api/errcode"
|
2015-02-11 02:14:23 +00:00
|
|
|
"github.com/docker/distribution/registry/api/v2"
|
2016-05-06 09:46:37 +00:00
|
|
|
_ "github.com/docker/distribution/registry/storage/driver/testdriver"
|
2015-01-06 18:37:27 +00:00
|
|
|
"github.com/docker/distribution/testutil"
|
|
|
|
"github.com/docker/libtrust"
|
|
|
|
"github.com/gorilla/handlers"
|
|
|
|
)
|
|
|
|
|
2015-08-10 21:20:52 +00:00
|
|
|
var headerConfig = http.Header{
|
|
|
|
"X-Content-Type-Options": []string{"nosniff"},
|
|
|
|
}
|
|
|
|
|
2015-01-06 18:37:27 +00:00
|
|
|
// TestCheckAPI hits the base endpoint (/v2/) ensures we return the specified
|
|
|
|
// 200 OK response.
|
|
|
|
func TestCheckAPI(t *testing.T) {
|
2015-05-27 17:52:22 +00:00
|
|
|
env := newTestEnv(t, false)
|
2015-02-07 00:19:19 +00:00
|
|
|
baseURL, err := env.builder.BuildBaseURL()
|
2015-01-06 18:37:27 +00:00
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error building base url: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
resp, err := http.Get(baseURL)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error issuing request: %v", err)
|
|
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
checkResponse(t, "issuing api base check", resp, http.StatusOK)
|
|
|
|
checkHeaders(t, resp, http.Header{
|
|
|
|
"Content-Type": []string{"application/json; charset=utf-8"},
|
|
|
|
"Content-Length": []string{"2"},
|
|
|
|
})
|
|
|
|
|
|
|
|
p, err := ioutil.ReadAll(resp.Body)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error reading response body: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if string(p) != "{}" {
|
|
|
|
t.Fatalf("unexpected response body: %v", string(p))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-07-17 18:42:47 +00:00
|
|
|
// TestCatalogAPI tests the /v2/_catalog endpoint
|
2015-07-13 20:08:13 +00:00
|
|
|
func TestCatalogAPI(t *testing.T) {
|
2015-07-17 18:42:47 +00:00
|
|
|
chunkLen := 2
|
2015-05-27 17:52:22 +00:00
|
|
|
env := newTestEnv(t, false)
|
2015-07-13 20:08:13 +00:00
|
|
|
|
2015-07-17 18:42:47 +00:00
|
|
|
values := url.Values{
|
|
|
|
"last": []string{""},
|
|
|
|
"n": []string{strconv.Itoa(chunkLen)}}
|
2015-07-13 20:08:13 +00:00
|
|
|
|
|
|
|
catalogURL, err := env.builder.BuildCatalogURL(values)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error building catalog url: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// -----------------------------------
|
|
|
|
// try to get an empty catalog
|
|
|
|
resp, err := http.Get(catalogURL)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error issuing request: %v", err)
|
|
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
checkResponse(t, "issuing catalog api check", resp, http.StatusOK)
|
|
|
|
|
|
|
|
var ctlg struct {
|
|
|
|
Repositories []string `json:"repositories"`
|
|
|
|
}
|
|
|
|
|
|
|
|
dec := json.NewDecoder(resp.Body)
|
|
|
|
if err := dec.Decode(&ctlg); err != nil {
|
|
|
|
t.Fatalf("error decoding fetched manifest: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// we haven't pushed anything to the registry yet
|
2015-07-17 18:42:47 +00:00
|
|
|
if len(ctlg.Repositories) != 0 {
|
2015-07-13 20:08:13 +00:00
|
|
|
t.Fatalf("repositories has unexpected values")
|
|
|
|
}
|
|
|
|
|
|
|
|
if resp.Header.Get("Link") != "" {
|
|
|
|
t.Fatalf("repositories has more data when none expected")
|
|
|
|
}
|
|
|
|
|
|
|
|
// -----------------------------------
|
|
|
|
// push something to the registry and try again
|
2015-07-17 18:42:47 +00:00
|
|
|
images := []string{"foo/aaaa", "foo/bbbb", "foo/cccc"}
|
|
|
|
|
|
|
|
for _, image := range images {
|
|
|
|
createRepository(env, t, image, "sometag")
|
|
|
|
}
|
|
|
|
|
|
|
|
resp, err = http.Get(catalogURL)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error issuing request: %v", err)
|
|
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
checkResponse(t, "issuing catalog api check", resp, http.StatusOK)
|
|
|
|
|
|
|
|
dec = json.NewDecoder(resp.Body)
|
|
|
|
if err = dec.Decode(&ctlg); err != nil {
|
|
|
|
t.Fatalf("error decoding fetched manifest: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(ctlg.Repositories) != chunkLen {
|
|
|
|
t.Fatalf("repositories has unexpected values")
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, image := range images[:chunkLen] {
|
|
|
|
if !contains(ctlg.Repositories, image) {
|
|
|
|
t.Fatalf("didn't find our repository '%s' in the catalog", image)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
link := resp.Header.Get("Link")
|
|
|
|
if link == "" {
|
|
|
|
t.Fatalf("repositories has less data than expected")
|
|
|
|
}
|
|
|
|
|
|
|
|
newValues := checkLink(t, link, chunkLen, ctlg.Repositories[len(ctlg.Repositories)-1])
|
|
|
|
|
|
|
|
// -----------------------------------
|
|
|
|
// get the last chunk of data
|
|
|
|
|
|
|
|
catalogURL, err = env.builder.BuildCatalogURL(newValues)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error building catalog url: %v", err)
|
|
|
|
}
|
2015-07-13 20:08:13 +00:00
|
|
|
|
|
|
|
resp, err = http.Get(catalogURL)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error issuing request: %v", err)
|
|
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
checkResponse(t, "issuing catalog api check", resp, http.StatusOK)
|
|
|
|
|
|
|
|
dec = json.NewDecoder(resp.Body)
|
|
|
|
if err = dec.Decode(&ctlg); err != nil {
|
|
|
|
t.Fatalf("error decoding fetched manifest: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(ctlg.Repositories) != 1 {
|
|
|
|
t.Fatalf("repositories has unexpected values")
|
|
|
|
}
|
|
|
|
|
2015-07-17 18:42:47 +00:00
|
|
|
lastImage := images[len(images)-1]
|
|
|
|
if !contains(ctlg.Repositories, lastImage) {
|
|
|
|
t.Fatalf("didn't find our repository '%s' in the catalog", lastImage)
|
2015-07-13 20:08:13 +00:00
|
|
|
}
|
|
|
|
|
2015-07-17 18:42:47 +00:00
|
|
|
link = resp.Header.Get("Link")
|
|
|
|
if link != "" {
|
|
|
|
t.Fatalf("catalog has unexpected data")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func checkLink(t *testing.T, urlStr string, numEntries int, last string) url.Values {
|
|
|
|
re := regexp.MustCompile("<(/v2/_catalog.*)>; rel=\"next\"")
|
|
|
|
matches := re.FindStringSubmatch(urlStr)
|
|
|
|
|
|
|
|
if len(matches) != 2 {
|
|
|
|
t.Fatalf("Catalog link address response was incorrect")
|
|
|
|
}
|
|
|
|
linkURL, _ := url.Parse(matches[1])
|
|
|
|
urlValues := linkURL.Query()
|
|
|
|
|
|
|
|
if urlValues.Get("n") != strconv.Itoa(numEntries) {
|
|
|
|
t.Fatalf("Catalog link entry size is incorrect")
|
|
|
|
}
|
|
|
|
|
|
|
|
if urlValues.Get("last") != last {
|
|
|
|
t.Fatal("Catalog link last entry is incorrect")
|
2015-07-13 20:08:13 +00:00
|
|
|
}
|
|
|
|
|
2015-07-17 18:42:47 +00:00
|
|
|
return urlValues
|
2015-07-13 20:08:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func contains(elems []string, e string) bool {
|
|
|
|
for _, elem := range elems {
|
|
|
|
if elem == e {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2015-02-24 22:59:01 +00:00
|
|
|
func TestURLPrefix(t *testing.T) {
|
|
|
|
config := configuration.Configuration{
|
|
|
|
Storage: configuration.Storage{
|
2016-05-06 09:46:37 +00:00
|
|
|
"testdriver": configuration.Parameters{},
|
2015-02-24 22:59:01 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
config.HTTP.Prefix = "/test/"
|
2015-08-10 21:20:52 +00:00
|
|
|
config.HTTP.Headers = headerConfig
|
2015-02-24 22:59:01 +00:00
|
|
|
|
|
|
|
env := newTestEnvWithConfig(t, &config)
|
|
|
|
|
|
|
|
baseURL, err := env.builder.BuildBaseURL()
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error building base url: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
parsed, _ := url.Parse(baseURL)
|
|
|
|
if !strings.HasPrefix(parsed.Path, config.HTTP.Prefix) {
|
|
|
|
t.Fatalf("Prefix %v not included in test url %v", config.HTTP.Prefix, baseURL)
|
|
|
|
}
|
|
|
|
|
|
|
|
resp, err := http.Get(baseURL)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error issuing request: %v", err)
|
|
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
checkResponse(t, "issuing api base check", resp, http.StatusOK)
|
|
|
|
checkHeaders(t, resp, http.Header{
|
|
|
|
"Content-Type": []string{"application/json; charset=utf-8"},
|
|
|
|
"Content-Length": []string{"2"},
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2015-05-27 17:52:22 +00:00
|
|
|
type blobArgs struct {
|
2015-12-15 22:35:23 +00:00
|
|
|
imageName reference.Named
|
2015-05-27 17:52:22 +00:00
|
|
|
layerFile io.ReadSeeker
|
|
|
|
layerDigest digest.Digest
|
|
|
|
}
|
2015-01-06 18:37:27 +00:00
|
|
|
|
2015-05-27 17:52:22 +00:00
|
|
|
func makeBlobArgs(t *testing.T) blobArgs {
|
2015-12-16 01:18:13 +00:00
|
|
|
layerFile, layerDigest, err := testutil.CreateRandomTarFile()
|
2015-01-06 18:37:27 +00:00
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error creating random layer file: %v", err)
|
|
|
|
}
|
|
|
|
|
2015-05-27 17:52:22 +00:00
|
|
|
args := blobArgs{
|
|
|
|
layerFile: layerFile,
|
|
|
|
layerDigest: layerDigest,
|
|
|
|
}
|
2015-12-15 22:35:23 +00:00
|
|
|
args.imageName, _ = reference.ParseNamed("foo/bar")
|
2015-05-27 17:52:22 +00:00
|
|
|
return args
|
|
|
|
}
|
|
|
|
|
|
|
|
// TestBlobAPI conducts a full test of the of the blob api.
|
|
|
|
func TestBlobAPI(t *testing.T) {
|
|
|
|
deleteEnabled := false
|
|
|
|
env := newTestEnv(t, deleteEnabled)
|
|
|
|
args := makeBlobArgs(t)
|
|
|
|
testBlobAPI(t, env, args)
|
|
|
|
|
|
|
|
deleteEnabled = true
|
|
|
|
env = newTestEnv(t, deleteEnabled)
|
|
|
|
args = makeBlobArgs(t)
|
|
|
|
testBlobAPI(t, env, args)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestBlobDelete(t *testing.T) {
|
|
|
|
deleteEnabled := true
|
|
|
|
env := newTestEnv(t, deleteEnabled)
|
|
|
|
|
|
|
|
args := makeBlobArgs(t)
|
|
|
|
env = testBlobAPI(t, env, args)
|
|
|
|
testBlobDelete(t, env, args)
|
|
|
|
}
|
|
|
|
|
2016-02-23 01:49:23 +00:00
|
|
|
func TestRelativeURL(t *testing.T) {
|
|
|
|
config := configuration.Configuration{
|
|
|
|
Storage: configuration.Storage{
|
2016-05-06 09:46:37 +00:00
|
|
|
"testdriver": configuration.Parameters{},
|
2016-02-23 01:49:23 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
config.HTTP.Headers = headerConfig
|
|
|
|
config.HTTP.RelativeURLs = false
|
|
|
|
env := newTestEnvWithConfig(t, &config)
|
|
|
|
ref, _ := reference.WithName("foo/bar")
|
|
|
|
uploadURLBaseAbs, _ := startPushLayer(t, env, ref)
|
|
|
|
|
|
|
|
u, err := url.Parse(uploadURLBaseAbs)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
if !u.IsAbs() {
|
|
|
|
t.Fatal("Relative URL returned from blob upload chunk with non-relative configuration")
|
|
|
|
}
|
|
|
|
|
|
|
|
args := makeBlobArgs(t)
|
|
|
|
resp, err := doPushLayer(t, env.builder, ref, args.layerDigest, uploadURLBaseAbs, args.layerFile)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error doing layer push relative url: %v", err)
|
|
|
|
}
|
|
|
|
checkResponse(t, "relativeurl blob upload", resp, http.StatusCreated)
|
|
|
|
u, err = url.Parse(resp.Header.Get("Location"))
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
if !u.IsAbs() {
|
|
|
|
t.Fatal("Relative URL returned from blob upload with non-relative configuration")
|
|
|
|
}
|
|
|
|
|
|
|
|
config.HTTP.RelativeURLs = true
|
|
|
|
args = makeBlobArgs(t)
|
|
|
|
uploadURLBaseRelative, _ := startPushLayer(t, env, ref)
|
|
|
|
u, err = url.Parse(uploadURLBaseRelative)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
if u.IsAbs() {
|
|
|
|
t.Fatal("Absolute URL returned from blob upload chunk with relative configuration")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Start a new upload in absolute mode to get a valid base URL
|
|
|
|
config.HTTP.RelativeURLs = false
|
|
|
|
uploadURLBaseAbs, _ = startPushLayer(t, env, ref)
|
|
|
|
u, err = url.Parse(uploadURLBaseAbs)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
if !u.IsAbs() {
|
|
|
|
t.Fatal("Relative URL returned from blob upload chunk with non-relative configuration")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Complete upload with relative URLs enabled to ensure the final location is relative
|
|
|
|
config.HTTP.RelativeURLs = true
|
|
|
|
resp, err = doPushLayer(t, env.builder, ref, args.layerDigest, uploadURLBaseAbs, args.layerFile)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error doing layer push relative url: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
checkResponse(t, "relativeurl blob upload", resp, http.StatusCreated)
|
|
|
|
u, err = url.Parse(resp.Header.Get("Location"))
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
if u.IsAbs() {
|
|
|
|
t.Fatal("Relative URL returned from blob upload with non-relative configuration")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-05-27 17:52:22 +00:00
|
|
|
func TestBlobDeleteDisabled(t *testing.T) {
|
|
|
|
deleteEnabled := false
|
|
|
|
env := newTestEnv(t, deleteEnabled)
|
|
|
|
args := makeBlobArgs(t)
|
|
|
|
|
|
|
|
imageName := args.imageName
|
|
|
|
layerDigest := args.layerDigest
|
2015-12-16 00:43:13 +00:00
|
|
|
ref, _ := reference.WithDigest(imageName, layerDigest)
|
|
|
|
layerURL, err := env.builder.BuildBlobURL(ref)
|
2015-05-27 17:52:22 +00:00
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error building url: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
resp, err := httpDelete(layerURL)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error deleting when disabled: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
checkResponse(t, "status of disabled delete", resp, http.StatusMethodNotAllowed)
|
|
|
|
}
|
|
|
|
|
|
|
|
func testBlobAPI(t *testing.T, env *testEnv, args blobArgs) *testEnv {
|
|
|
|
// TODO(stevvooe): This test code is complete junk but it should cover the
|
|
|
|
// complete flow. This must be broken down and checked against the
|
|
|
|
// specification *before* we submit the final to docker core.
|
|
|
|
imageName := args.imageName
|
|
|
|
layerFile := args.layerFile
|
|
|
|
layerDigest := args.layerDigest
|
|
|
|
|
2015-01-06 18:37:27 +00:00
|
|
|
// -----------------------------------
|
|
|
|
// Test fetch for non-existent content
|
2015-12-16 00:43:13 +00:00
|
|
|
ref, _ := reference.WithDigest(imageName, layerDigest)
|
|
|
|
layerURL, err := env.builder.BuildBlobURL(ref)
|
2015-01-06 18:37:27 +00:00
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error building url: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
resp, err := http.Get(layerURL)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error fetching non-existent layer: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
checkResponse(t, "fetching non-existent content", resp, http.StatusNotFound)
|
|
|
|
|
|
|
|
// ------------------------------------------
|
|
|
|
// Test head request for non-existent content
|
|
|
|
resp, err = http.Head(layerURL)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error checking head on non-existent layer: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
checkResponse(t, "checking head on non-existent layer", resp, http.StatusNotFound)
|
|
|
|
|
|
|
|
// ------------------------------------------
|
2015-02-27 00:43:47 +00:00
|
|
|
// Start an upload, check the status then cancel
|
2016-02-23 01:49:23 +00:00
|
|
|
uploadURLBase, uploadUUID := startPushLayer(t, env, imageName)
|
2015-02-27 00:43:47 +00:00
|
|
|
|
|
|
|
// A status check should work
|
|
|
|
resp, err = http.Get(uploadURLBase)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error getting upload status: %v", err)
|
|
|
|
}
|
|
|
|
checkResponse(t, "status of deleted upload", resp, http.StatusNoContent)
|
|
|
|
checkHeaders(t, resp, http.Header{
|
|
|
|
"Location": []string{"*"},
|
|
|
|
"Range": []string{"0-0"},
|
|
|
|
"Docker-Upload-UUID": []string{uploadUUID},
|
|
|
|
})
|
2015-01-30 05:26:35 +00:00
|
|
|
|
|
|
|
req, err := http.NewRequest("DELETE", uploadURLBase, nil)
|
2015-01-06 18:37:27 +00:00
|
|
|
if err != nil {
|
2015-01-30 05:26:35 +00:00
|
|
|
t.Fatalf("unexpected error creating delete request: %v", err)
|
2015-01-06 18:37:27 +00:00
|
|
|
}
|
|
|
|
|
2015-01-30 05:26:35 +00:00
|
|
|
resp, err = http.DefaultClient.Do(req)
|
2015-01-06 18:37:27 +00:00
|
|
|
if err != nil {
|
2015-01-30 05:26:35 +00:00
|
|
|
t.Fatalf("unexpected error sending delete request: %v", err)
|
2015-01-06 18:37:27 +00:00
|
|
|
}
|
|
|
|
|
2015-01-30 05:26:35 +00:00
|
|
|
checkResponse(t, "deleting upload", resp, http.StatusNoContent)
|
|
|
|
|
|
|
|
// A status check should result in 404
|
|
|
|
resp, err = http.Get(uploadURLBase)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error getting upload status: %v", err)
|
|
|
|
}
|
|
|
|
checkResponse(t, "status of deleted upload", resp, http.StatusNotFound)
|
|
|
|
|
|
|
|
// -----------------------------------------
|
2015-02-02 21:01:49 +00:00
|
|
|
// Do layer push with an empty body and different digest
|
2016-02-23 01:49:23 +00:00
|
|
|
uploadURLBase, uploadUUID = startPushLayer(t, env, imageName)
|
2015-02-07 00:19:19 +00:00
|
|
|
resp, err = doPushLayer(t, env.builder, imageName, layerDigest, uploadURLBase, bytes.NewReader([]byte{}))
|
2015-01-30 05:26:35 +00:00
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error doing bad layer push: %v", err)
|
|
|
|
}
|
2015-01-06 18:37:27 +00:00
|
|
|
|
2015-01-30 05:26:35 +00:00
|
|
|
checkResponse(t, "bad layer push", resp, http.StatusBadRequest)
|
2015-02-02 21:01:49 +00:00
|
|
|
checkBodyHasErrorCodes(t, "bad layer push", resp, v2.ErrorCodeDigestInvalid)
|
2015-01-30 05:26:35 +00:00
|
|
|
|
|
|
|
// -----------------------------------------
|
2015-02-02 21:01:49 +00:00
|
|
|
// Do layer push with an empty body and correct digest
|
2015-12-16 01:18:13 +00:00
|
|
|
zeroDigest, err := digest.FromReader(bytes.NewReader([]byte{}))
|
2015-02-02 21:01:49 +00:00
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error digesting empty buffer: %v", err)
|
|
|
|
}
|
2015-01-30 05:26:35 +00:00
|
|
|
|
2016-02-23 01:49:23 +00:00
|
|
|
uploadURLBase, uploadUUID = startPushLayer(t, env, imageName)
|
2015-02-07 00:19:19 +00:00
|
|
|
pushLayer(t, env.builder, imageName, zeroDigest, uploadURLBase, bytes.NewReader([]byte{}))
|
2015-02-02 21:01:49 +00:00
|
|
|
|
|
|
|
// -----------------------------------------
|
|
|
|
// Do layer push with an empty body and correct digest
|
|
|
|
|
|
|
|
// This is a valid but empty tarfile!
|
|
|
|
emptyTar := bytes.Repeat([]byte("\x00"), 1024)
|
2015-12-16 01:18:13 +00:00
|
|
|
emptyDigest, err := digest.FromReader(bytes.NewReader(emptyTar))
|
2015-01-30 05:26:35 +00:00
|
|
|
if err != nil {
|
2015-02-02 21:01:49 +00:00
|
|
|
t.Fatalf("unexpected error digesting empty tar: %v", err)
|
2015-01-30 05:26:35 +00:00
|
|
|
}
|
|
|
|
|
2016-02-23 01:49:23 +00:00
|
|
|
uploadURLBase, uploadUUID = startPushLayer(t, env, imageName)
|
2015-02-07 00:19:19 +00:00
|
|
|
pushLayer(t, env.builder, imageName, emptyDigest, uploadURLBase, bytes.NewReader(emptyTar))
|
2015-01-30 05:26:35 +00:00
|
|
|
|
|
|
|
// ------------------------------------------
|
|
|
|
// Now, actually do successful upload.
|
2015-01-06 18:37:27 +00:00
|
|
|
layerLength, _ := layerFile.Seek(0, os.SEEK_END)
|
|
|
|
layerFile.Seek(0, os.SEEK_SET)
|
|
|
|
|
2016-02-23 01:49:23 +00:00
|
|
|
uploadURLBase, uploadUUID = startPushLayer(t, env, imageName)
|
2015-02-07 00:19:19 +00:00
|
|
|
pushLayer(t, env.builder, imageName, layerDigest, uploadURLBase, layerFile)
|
2015-01-06 18:37:27 +00:00
|
|
|
|
2015-05-04 15:56:37 +00:00
|
|
|
// ------------------------------------------
|
|
|
|
// Now, push just a chunk
|
|
|
|
layerFile.Seek(0, 0)
|
|
|
|
|
2015-05-22 01:44:08 +00:00
|
|
|
canonicalDigester := digest.Canonical.New()
|
2015-05-21 06:44:08 +00:00
|
|
|
if _, err := io.Copy(canonicalDigester.Hash(), layerFile); err != nil {
|
Refactor Blob Service API
This PR refactors the blob service API to be oriented around blob descriptors.
Identified by digests, blobs become an abstract entity that can be read and
written using a descriptor as a handle. This allows blobs to take many forms,
such as a ReadSeekCloser or a simple byte buffer, allowing blob oriented
operations to better integrate with blob agnostic APIs (such as the `io`
package). The error definitions are now better organized to reflect conditions
that can only be seen when interacting with the blob API.
The main benefit of this is to separate the much smaller metadata from large
file storage. Many benefits also follow from this. Reading and writing has
been separated into discrete services. Backend implementation is also
simplified, by reducing the amount of metadata that needs to be picked up to
simply serve a read. This also improves cacheability.
"Opening" a blob simply consists of an access check (Stat) and a path
calculation. Caching is greatly simplified and we've made the mapping of
provisional to canonical hashes a first-class concept. BlobDescriptorService
and BlobProvider can be combined in different ways to achieve varying effects.
Recommend Review Approach
-------------------------
This is a very large patch. While apologies are in order, we are getting a
considerable amount of refactoring. Most changes follow from the changes to
the root package (distribution), so start there. From there, the main changes
are in storage. Looking at (*repository).Blobs will help to understand the how
the linkedBlobStore is wired. One can explore the internals within and also
branch out into understanding the changes to the caching layer. Following the
descriptions below will also help to guide you.
To reduce the chances for regressions, it was critical that major changes to
unit tests were avoided. Where possible, they are left untouched and where
not, the spirit is hopefully captured. Pay particular attention to where
behavior may have changed.
Storage
-------
The primary changes to the `storage` package, other than the interface
updates, were to merge the layerstore and blobstore. Blob access is now
layered even further. The first layer, blobStore, exposes a global
`BlobStatter` and `BlobProvider`. Operations here provide a fast path for most
read operations that don't take access control into account. The
`linkedBlobStore` layers on top of the `blobStore`, providing repository-
scoped blob link management in the backend. The `linkedBlobStore` implements
the full `BlobStore` suite, providing access-controlled, repository-local blob
writers. The abstraction between the two is slightly broken in that
`linkedBlobStore` is the only channel under which one can write into the global
blob store. The `linkedBlobStore` also provides flexibility in that it can act
over different link sets depending on configuration. This allows us to use the
same code for signature links, manifest links and blob links. Eventually, we
will fully consolidate this storage.
The improved cache flow comes from the `linkedBlobStatter` component
of `linkedBlobStore`. Using a `cachedBlobStatter`, these combine together to
provide a simple cache hierarchy that should streamline access checks on read
and write operations, or at least provide a single path to optimize. The
metrics have been changed in a slightly incompatible way since the former
operations, Fetch and Exists, are no longer relevant.
The fileWriter and fileReader have been slightly modified to support the rest
of the changes. The most interesting is the removal of the `Stat` call from
`newFileReader`. This was the source of unnecessary round trips that were only
present to look up the size of the resulting reader. Now, one must simply pass
in the size, requiring the caller to decide whether or not the `Stat` call is
appropriate. In several cases, it turned out the caller already had the size
already. The `WriterAt` implementation has been removed from `fileWriter`,
since it is no longer required for `BlobWriter`, reducing the number of paths
which writes may take.
Cache
-----
Unfortunately, the `cache` package required a near full rewrite. It was pretty
mechanical in that the cache is oriented around the `BlobDescriptorService`
slightly modified to include the ability to set the values for individual
digests. While the implementation is oriented towards caching, it can act as a
primary store. Provisions are in place to have repository local metadata, in
addition to global metadata. Fallback is implemented as a part of the storage
package to maintain this flexibility.
One unfortunate side-effect is that caching is now repository-scoped, rather
than global. This should have little effect on performance but may increase
memory usage.
Handlers
--------
The `handlers` package has been updated to leverage the new API. For the most
part, the changes are superficial or mechanical based on the API changes. This
did expose a bug in the handling of provisional vs canonical digests that was
fixed in the unit tests.
Configuration
-------------
One user-facing change has been made to the configuration and is updated in
the associated documentation. The `layerinfo` cache parameter has been
deprecated by the `blobdescriptor` cache parameter. Both are equivalent and
configuration files should be backward compatible.
Notifications
-------------
Changes the `notification` package are simply to support the interface
changes.
Context
-------
A small change has been made to the tracing log-level. Traces have been moved
from "info" to "debug" level to reduce output when not needed.
Signed-off-by: Stephen J Day <stephen.day@docker.com>
2015-05-12 07:10:29 +00:00
|
|
|
t.Fatalf("error copying to digest: %v", err)
|
|
|
|
}
|
|
|
|
canonicalDigest := canonicalDigester.Digest()
|
|
|
|
|
|
|
|
layerFile.Seek(0, 0)
|
2016-02-23 01:49:23 +00:00
|
|
|
uploadURLBase, uploadUUID = startPushLayer(t, env, imageName)
|
2015-05-04 15:56:37 +00:00
|
|
|
uploadURLBase, dgst := pushChunk(t, env.builder, imageName, uploadURLBase, layerFile, layerLength)
|
|
|
|
finishUpload(t, env.builder, imageName, uploadURLBase, dgst)
|
2015-05-27 17:52:22 +00:00
|
|
|
|
2015-01-06 18:37:27 +00:00
|
|
|
// ------------------------
|
|
|
|
// 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{
|
2015-02-26 23:47:04 +00:00
|
|
|
"Content-Length": []string{fmt.Sprint(layerLength)},
|
Refactor Blob Service API
This PR refactors the blob service API to be oriented around blob descriptors.
Identified by digests, blobs become an abstract entity that can be read and
written using a descriptor as a handle. This allows blobs to take many forms,
such as a ReadSeekCloser or a simple byte buffer, allowing blob oriented
operations to better integrate with blob agnostic APIs (such as the `io`
package). The error definitions are now better organized to reflect conditions
that can only be seen when interacting with the blob API.
The main benefit of this is to separate the much smaller metadata from large
file storage. Many benefits also follow from this. Reading and writing has
been separated into discrete services. Backend implementation is also
simplified, by reducing the amount of metadata that needs to be picked up to
simply serve a read. This also improves cacheability.
"Opening" a blob simply consists of an access check (Stat) and a path
calculation. Caching is greatly simplified and we've made the mapping of
provisional to canonical hashes a first-class concept. BlobDescriptorService
and BlobProvider can be combined in different ways to achieve varying effects.
Recommend Review Approach
-------------------------
This is a very large patch. While apologies are in order, we are getting a
considerable amount of refactoring. Most changes follow from the changes to
the root package (distribution), so start there. From there, the main changes
are in storage. Looking at (*repository).Blobs will help to understand the how
the linkedBlobStore is wired. One can explore the internals within and also
branch out into understanding the changes to the caching layer. Following the
descriptions below will also help to guide you.
To reduce the chances for regressions, it was critical that major changes to
unit tests were avoided. Where possible, they are left untouched and where
not, the spirit is hopefully captured. Pay particular attention to where
behavior may have changed.
Storage
-------
The primary changes to the `storage` package, other than the interface
updates, were to merge the layerstore and blobstore. Blob access is now
layered even further. The first layer, blobStore, exposes a global
`BlobStatter` and `BlobProvider`. Operations here provide a fast path for most
read operations that don't take access control into account. The
`linkedBlobStore` layers on top of the `blobStore`, providing repository-
scoped blob link management in the backend. The `linkedBlobStore` implements
the full `BlobStore` suite, providing access-controlled, repository-local blob
writers. The abstraction between the two is slightly broken in that
`linkedBlobStore` is the only channel under which one can write into the global
blob store. The `linkedBlobStore` also provides flexibility in that it can act
over different link sets depending on configuration. This allows us to use the
same code for signature links, manifest links and blob links. Eventually, we
will fully consolidate this storage.
The improved cache flow comes from the `linkedBlobStatter` component
of `linkedBlobStore`. Using a `cachedBlobStatter`, these combine together to
provide a simple cache hierarchy that should streamline access checks on read
and write operations, or at least provide a single path to optimize. The
metrics have been changed in a slightly incompatible way since the former
operations, Fetch and Exists, are no longer relevant.
The fileWriter and fileReader have been slightly modified to support the rest
of the changes. The most interesting is the removal of the `Stat` call from
`newFileReader`. This was the source of unnecessary round trips that were only
present to look up the size of the resulting reader. Now, one must simply pass
in the size, requiring the caller to decide whether or not the `Stat` call is
appropriate. In several cases, it turned out the caller already had the size
already. The `WriterAt` implementation has been removed from `fileWriter`,
since it is no longer required for `BlobWriter`, reducing the number of paths
which writes may take.
Cache
-----
Unfortunately, the `cache` package required a near full rewrite. It was pretty
mechanical in that the cache is oriented around the `BlobDescriptorService`
slightly modified to include the ability to set the values for individual
digests. While the implementation is oriented towards caching, it can act as a
primary store. Provisions are in place to have repository local metadata, in
addition to global metadata. Fallback is implemented as a part of the storage
package to maintain this flexibility.
One unfortunate side-effect is that caching is now repository-scoped, rather
than global. This should have little effect on performance but may increase
memory usage.
Handlers
--------
The `handlers` package has been updated to leverage the new API. For the most
part, the changes are superficial or mechanical based on the API changes. This
did expose a bug in the handling of provisional vs canonical digests that was
fixed in the unit tests.
Configuration
-------------
One user-facing change has been made to the configuration and is updated in
the associated documentation. The `layerinfo` cache parameter has been
deprecated by the `blobdescriptor` cache parameter. Both are equivalent and
configuration files should be backward compatible.
Notifications
-------------
Changes the `notification` package are simply to support the interface
changes.
Context
-------
A small change has been made to the tracing log-level. Traces have been moved
from "info" to "debug" level to reduce output when not needed.
Signed-off-by: Stephen J Day <stephen.day@docker.com>
2015-05-12 07:10:29 +00:00
|
|
|
"Docker-Content-Digest": []string{canonicalDigest.String()},
|
2015-01-06 18:37:27 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
// ----------------
|
|
|
|
// Fetch the layer!
|
|
|
|
resp, err = http.Get(layerURL)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error fetching layer: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
checkResponse(t, "fetching layer", resp, http.StatusOK)
|
|
|
|
checkHeaders(t, resp, http.Header{
|
2015-02-26 23:47:04 +00:00
|
|
|
"Content-Length": []string{fmt.Sprint(layerLength)},
|
Refactor Blob Service API
This PR refactors the blob service API to be oriented around blob descriptors.
Identified by digests, blobs become an abstract entity that can be read and
written using a descriptor as a handle. This allows blobs to take many forms,
such as a ReadSeekCloser or a simple byte buffer, allowing blob oriented
operations to better integrate with blob agnostic APIs (such as the `io`
package). The error definitions are now better organized to reflect conditions
that can only be seen when interacting with the blob API.
The main benefit of this is to separate the much smaller metadata from large
file storage. Many benefits also follow from this. Reading and writing has
been separated into discrete services. Backend implementation is also
simplified, by reducing the amount of metadata that needs to be picked up to
simply serve a read. This also improves cacheability.
"Opening" a blob simply consists of an access check (Stat) and a path
calculation. Caching is greatly simplified and we've made the mapping of
provisional to canonical hashes a first-class concept. BlobDescriptorService
and BlobProvider can be combined in different ways to achieve varying effects.
Recommend Review Approach
-------------------------
This is a very large patch. While apologies are in order, we are getting a
considerable amount of refactoring. Most changes follow from the changes to
the root package (distribution), so start there. From there, the main changes
are in storage. Looking at (*repository).Blobs will help to understand the how
the linkedBlobStore is wired. One can explore the internals within and also
branch out into understanding the changes to the caching layer. Following the
descriptions below will also help to guide you.
To reduce the chances for regressions, it was critical that major changes to
unit tests were avoided. Where possible, they are left untouched and where
not, the spirit is hopefully captured. Pay particular attention to where
behavior may have changed.
Storage
-------
The primary changes to the `storage` package, other than the interface
updates, were to merge the layerstore and blobstore. Blob access is now
layered even further. The first layer, blobStore, exposes a global
`BlobStatter` and `BlobProvider`. Operations here provide a fast path for most
read operations that don't take access control into account. The
`linkedBlobStore` layers on top of the `blobStore`, providing repository-
scoped blob link management in the backend. The `linkedBlobStore` implements
the full `BlobStore` suite, providing access-controlled, repository-local blob
writers. The abstraction between the two is slightly broken in that
`linkedBlobStore` is the only channel under which one can write into the global
blob store. The `linkedBlobStore` also provides flexibility in that it can act
over different link sets depending on configuration. This allows us to use the
same code for signature links, manifest links and blob links. Eventually, we
will fully consolidate this storage.
The improved cache flow comes from the `linkedBlobStatter` component
of `linkedBlobStore`. Using a `cachedBlobStatter`, these combine together to
provide a simple cache hierarchy that should streamline access checks on read
and write operations, or at least provide a single path to optimize. The
metrics have been changed in a slightly incompatible way since the former
operations, Fetch and Exists, are no longer relevant.
The fileWriter and fileReader have been slightly modified to support the rest
of the changes. The most interesting is the removal of the `Stat` call from
`newFileReader`. This was the source of unnecessary round trips that were only
present to look up the size of the resulting reader. Now, one must simply pass
in the size, requiring the caller to decide whether or not the `Stat` call is
appropriate. In several cases, it turned out the caller already had the size
already. The `WriterAt` implementation has been removed from `fileWriter`,
since it is no longer required for `BlobWriter`, reducing the number of paths
which writes may take.
Cache
-----
Unfortunately, the `cache` package required a near full rewrite. It was pretty
mechanical in that the cache is oriented around the `BlobDescriptorService`
slightly modified to include the ability to set the values for individual
digests. While the implementation is oriented towards caching, it can act as a
primary store. Provisions are in place to have repository local metadata, in
addition to global metadata. Fallback is implemented as a part of the storage
package to maintain this flexibility.
One unfortunate side-effect is that caching is now repository-scoped, rather
than global. This should have little effect on performance but may increase
memory usage.
Handlers
--------
The `handlers` package has been updated to leverage the new API. For the most
part, the changes are superficial or mechanical based on the API changes. This
did expose a bug in the handling of provisional vs canonical digests that was
fixed in the unit tests.
Configuration
-------------
One user-facing change has been made to the configuration and is updated in
the associated documentation. The `layerinfo` cache parameter has been
deprecated by the `blobdescriptor` cache parameter. Both are equivalent and
configuration files should be backward compatible.
Notifications
-------------
Changes the `notification` package are simply to support the interface
changes.
Context
-------
A small change has been made to the tracing log-level. Traces have been moved
from "info" to "debug" level to reduce output when not needed.
Signed-off-by: Stephen J Day <stephen.day@docker.com>
2015-05-12 07:10:29 +00:00
|
|
|
"Docker-Content-Digest": []string{canonicalDigest.String()},
|
2015-01-06 18:37:27 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
// Verify the body
|
2015-03-10 21:40:58 +00:00
|
|
|
verifier, err := digest.NewDigestVerifier(layerDigest)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error getting digest verifier: %s", err)
|
|
|
|
}
|
2015-01-06 18:37:27 +00:00
|
|
|
io.Copy(verifier, resp.Body)
|
|
|
|
|
|
|
|
if !verifier.Verified() {
|
|
|
|
t.Fatalf("response body did not pass verification")
|
|
|
|
}
|
|
|
|
|
2015-05-05 21:21:33 +00:00
|
|
|
// ----------------
|
|
|
|
// Fetch the layer with an invalid digest
|
2015-12-16 01:18:13 +00:00
|
|
|
badURL := strings.Replace(layerURL, "sha256", "sha257", 1)
|
2015-05-05 21:21:33 +00:00
|
|
|
resp, err = http.Get(badURL)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error fetching layer: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
checkResponse(t, "fetching layer bad digest", resp, http.StatusBadRequest)
|
|
|
|
|
2015-05-13 00:49:18 +00:00
|
|
|
// Cache headers
|
|
|
|
resp, err = http.Get(layerURL)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error fetching layer: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
checkResponse(t, "fetching layer", resp, http.StatusOK)
|
|
|
|
checkHeaders(t, resp, http.Header{
|
|
|
|
"Content-Length": []string{fmt.Sprint(layerLength)},
|
Refactor Blob Service API
This PR refactors the blob service API to be oriented around blob descriptors.
Identified by digests, blobs become an abstract entity that can be read and
written using a descriptor as a handle. This allows blobs to take many forms,
such as a ReadSeekCloser or a simple byte buffer, allowing blob oriented
operations to better integrate with blob agnostic APIs (such as the `io`
package). The error definitions are now better organized to reflect conditions
that can only be seen when interacting with the blob API.
The main benefit of this is to separate the much smaller metadata from large
file storage. Many benefits also follow from this. Reading and writing has
been separated into discrete services. Backend implementation is also
simplified, by reducing the amount of metadata that needs to be picked up to
simply serve a read. This also improves cacheability.
"Opening" a blob simply consists of an access check (Stat) and a path
calculation. Caching is greatly simplified and we've made the mapping of
provisional to canonical hashes a first-class concept. BlobDescriptorService
and BlobProvider can be combined in different ways to achieve varying effects.
Recommend Review Approach
-------------------------
This is a very large patch. While apologies are in order, we are getting a
considerable amount of refactoring. Most changes follow from the changes to
the root package (distribution), so start there. From there, the main changes
are in storage. Looking at (*repository).Blobs will help to understand the how
the linkedBlobStore is wired. One can explore the internals within and also
branch out into understanding the changes to the caching layer. Following the
descriptions below will also help to guide you.
To reduce the chances for regressions, it was critical that major changes to
unit tests were avoided. Where possible, they are left untouched and where
not, the spirit is hopefully captured. Pay particular attention to where
behavior may have changed.
Storage
-------
The primary changes to the `storage` package, other than the interface
updates, were to merge the layerstore and blobstore. Blob access is now
layered even further. The first layer, blobStore, exposes a global
`BlobStatter` and `BlobProvider`. Operations here provide a fast path for most
read operations that don't take access control into account. The
`linkedBlobStore` layers on top of the `blobStore`, providing repository-
scoped blob link management in the backend. The `linkedBlobStore` implements
the full `BlobStore` suite, providing access-controlled, repository-local blob
writers. The abstraction between the two is slightly broken in that
`linkedBlobStore` is the only channel under which one can write into the global
blob store. The `linkedBlobStore` also provides flexibility in that it can act
over different link sets depending on configuration. This allows us to use the
same code for signature links, manifest links and blob links. Eventually, we
will fully consolidate this storage.
The improved cache flow comes from the `linkedBlobStatter` component
of `linkedBlobStore`. Using a `cachedBlobStatter`, these combine together to
provide a simple cache hierarchy that should streamline access checks on read
and write operations, or at least provide a single path to optimize. The
metrics have been changed in a slightly incompatible way since the former
operations, Fetch and Exists, are no longer relevant.
The fileWriter and fileReader have been slightly modified to support the rest
of the changes. The most interesting is the removal of the `Stat` call from
`newFileReader`. This was the source of unnecessary round trips that were only
present to look up the size of the resulting reader. Now, one must simply pass
in the size, requiring the caller to decide whether or not the `Stat` call is
appropriate. In several cases, it turned out the caller already had the size
already. The `WriterAt` implementation has been removed from `fileWriter`,
since it is no longer required for `BlobWriter`, reducing the number of paths
which writes may take.
Cache
-----
Unfortunately, the `cache` package required a near full rewrite. It was pretty
mechanical in that the cache is oriented around the `BlobDescriptorService`
slightly modified to include the ability to set the values for individual
digests. While the implementation is oriented towards caching, it can act as a
primary store. Provisions are in place to have repository local metadata, in
addition to global metadata. Fallback is implemented as a part of the storage
package to maintain this flexibility.
One unfortunate side-effect is that caching is now repository-scoped, rather
than global. This should have little effect on performance but may increase
memory usage.
Handlers
--------
The `handlers` package has been updated to leverage the new API. For the most
part, the changes are superficial or mechanical based on the API changes. This
did expose a bug in the handling of provisional vs canonical digests that was
fixed in the unit tests.
Configuration
-------------
One user-facing change has been made to the configuration and is updated in
the associated documentation. The `layerinfo` cache parameter has been
deprecated by the `blobdescriptor` cache parameter. Both are equivalent and
configuration files should be backward compatible.
Notifications
-------------
Changes the `notification` package are simply to support the interface
changes.
Context
-------
A small change has been made to the tracing log-level. Traces have been moved
from "info" to "debug" level to reduce output when not needed.
Signed-off-by: Stephen J Day <stephen.day@docker.com>
2015-05-12 07:10:29 +00:00
|
|
|
"Docker-Content-Digest": []string{canonicalDigest.String()},
|
2015-07-24 06:03:13 +00:00
|
|
|
"ETag": []string{fmt.Sprintf(`"%s"`, canonicalDigest)},
|
Refactor Blob Service API
This PR refactors the blob service API to be oriented around blob descriptors.
Identified by digests, blobs become an abstract entity that can be read and
written using a descriptor as a handle. This allows blobs to take many forms,
such as a ReadSeekCloser or a simple byte buffer, allowing blob oriented
operations to better integrate with blob agnostic APIs (such as the `io`
package). The error definitions are now better organized to reflect conditions
that can only be seen when interacting with the blob API.
The main benefit of this is to separate the much smaller metadata from large
file storage. Many benefits also follow from this. Reading and writing has
been separated into discrete services. Backend implementation is also
simplified, by reducing the amount of metadata that needs to be picked up to
simply serve a read. This also improves cacheability.
"Opening" a blob simply consists of an access check (Stat) and a path
calculation. Caching is greatly simplified and we've made the mapping of
provisional to canonical hashes a first-class concept. BlobDescriptorService
and BlobProvider can be combined in different ways to achieve varying effects.
Recommend Review Approach
-------------------------
This is a very large patch. While apologies are in order, we are getting a
considerable amount of refactoring. Most changes follow from the changes to
the root package (distribution), so start there. From there, the main changes
are in storage. Looking at (*repository).Blobs will help to understand the how
the linkedBlobStore is wired. One can explore the internals within and also
branch out into understanding the changes to the caching layer. Following the
descriptions below will also help to guide you.
To reduce the chances for regressions, it was critical that major changes to
unit tests were avoided. Where possible, they are left untouched and where
not, the spirit is hopefully captured. Pay particular attention to where
behavior may have changed.
Storage
-------
The primary changes to the `storage` package, other than the interface
updates, were to merge the layerstore and blobstore. Blob access is now
layered even further. The first layer, blobStore, exposes a global
`BlobStatter` and `BlobProvider`. Operations here provide a fast path for most
read operations that don't take access control into account. The
`linkedBlobStore` layers on top of the `blobStore`, providing repository-
scoped blob link management in the backend. The `linkedBlobStore` implements
the full `BlobStore` suite, providing access-controlled, repository-local blob
writers. The abstraction between the two is slightly broken in that
`linkedBlobStore` is the only channel under which one can write into the global
blob store. The `linkedBlobStore` also provides flexibility in that it can act
over different link sets depending on configuration. This allows us to use the
same code for signature links, manifest links and blob links. Eventually, we
will fully consolidate this storage.
The improved cache flow comes from the `linkedBlobStatter` component
of `linkedBlobStore`. Using a `cachedBlobStatter`, these combine together to
provide a simple cache hierarchy that should streamline access checks on read
and write operations, or at least provide a single path to optimize. The
metrics have been changed in a slightly incompatible way since the former
operations, Fetch and Exists, are no longer relevant.
The fileWriter and fileReader have been slightly modified to support the rest
of the changes. The most interesting is the removal of the `Stat` call from
`newFileReader`. This was the source of unnecessary round trips that were only
present to look up the size of the resulting reader. Now, one must simply pass
in the size, requiring the caller to decide whether or not the `Stat` call is
appropriate. In several cases, it turned out the caller already had the size
already. The `WriterAt` implementation has been removed from `fileWriter`,
since it is no longer required for `BlobWriter`, reducing the number of paths
which writes may take.
Cache
-----
Unfortunately, the `cache` package required a near full rewrite. It was pretty
mechanical in that the cache is oriented around the `BlobDescriptorService`
slightly modified to include the ability to set the values for individual
digests. While the implementation is oriented towards caching, it can act as a
primary store. Provisions are in place to have repository local metadata, in
addition to global metadata. Fallback is implemented as a part of the storage
package to maintain this flexibility.
One unfortunate side-effect is that caching is now repository-scoped, rather
than global. This should have little effect on performance but may increase
memory usage.
Handlers
--------
The `handlers` package has been updated to leverage the new API. For the most
part, the changes are superficial or mechanical based on the API changes. This
did expose a bug in the handling of provisional vs canonical digests that was
fixed in the unit tests.
Configuration
-------------
One user-facing change has been made to the configuration and is updated in
the associated documentation. The `layerinfo` cache parameter has been
deprecated by the `blobdescriptor` cache parameter. Both are equivalent and
configuration files should be backward compatible.
Notifications
-------------
Changes the `notification` package are simply to support the interface
changes.
Context
-------
A small change has been made to the tracing log-level. Traces have been moved
from "info" to "debug" level to reduce output when not needed.
Signed-off-by: Stephen J Day <stephen.day@docker.com>
2015-05-12 07:10:29 +00:00
|
|
|
"Cache-Control": []string{"max-age=31536000"},
|
2015-05-13 00:49:18 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
// Matching etag, gives 304
|
|
|
|
etag := resp.Header.Get("Etag")
|
|
|
|
req, err = http.NewRequest("GET", layerURL, nil)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("Error constructing request: %s", err)
|
|
|
|
}
|
|
|
|
req.Header.Set("If-None-Match", etag)
|
2015-07-24 06:03:13 +00:00
|
|
|
|
2015-05-13 00:49:18 +00:00
|
|
|
resp, err = http.DefaultClient.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("Error constructing request: %s", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
checkResponse(t, "fetching layer with etag", resp, http.StatusNotModified)
|
|
|
|
|
|
|
|
// Non-matching etag, gives 200
|
|
|
|
req, err = http.NewRequest("GET", layerURL, nil)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("Error constructing request: %s", err)
|
|
|
|
}
|
|
|
|
req.Header.Set("If-None-Match", "")
|
|
|
|
resp, err = http.DefaultClient.Do(req)
|
|
|
|
checkResponse(t, "fetching layer with invalid etag", resp, http.StatusOK)
|
|
|
|
|
2015-01-06 18:37:27 +00:00
|
|
|
// Missing tests:
|
2015-12-16 01:18:13 +00:00
|
|
|
// - Upload the same tar file under and different repository and
|
2015-01-06 18:37:27 +00:00
|
|
|
// ensure the content remains uncorrupted.
|
2015-05-27 17:52:22 +00:00
|
|
|
return env
|
2015-01-06 18:37:27 +00:00
|
|
|
}
|
|
|
|
|
2015-05-27 17:52:22 +00:00
|
|
|
func testBlobDelete(t *testing.T, env *testEnv, args blobArgs) {
|
|
|
|
// Upload a layer
|
|
|
|
imageName := args.imageName
|
|
|
|
layerFile := args.layerFile
|
|
|
|
layerDigest := args.layerDigest
|
|
|
|
|
2015-12-16 00:43:13 +00:00
|
|
|
ref, _ := reference.WithDigest(imageName, layerDigest)
|
|
|
|
layerURL, err := env.builder.BuildBlobURL(ref)
|
2015-05-27 17:52:22 +00:00
|
|
|
if err != nil {
|
|
|
|
t.Fatalf(err.Error())
|
|
|
|
}
|
|
|
|
// ---------------
|
|
|
|
// Delete a layer
|
|
|
|
resp, err := httpDelete(layerURL)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error deleting layer: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
checkResponse(t, "deleting layer", resp, http.StatusAccepted)
|
|
|
|
checkHeaders(t, resp, http.Header{
|
|
|
|
"Content-Length": []string{"0"},
|
|
|
|
})
|
|
|
|
|
|
|
|
// ---------------
|
|
|
|
// Try and get it back
|
|
|
|
// 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 existence of deleted layer", resp, http.StatusNotFound)
|
|
|
|
|
|
|
|
// Delete already deleted layer
|
|
|
|
resp, err = httpDelete(layerURL)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error deleting layer: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
checkResponse(t, "deleting layer", resp, http.StatusNotFound)
|
|
|
|
|
|
|
|
// ----------------
|
|
|
|
// Attempt to delete a layer with an invalid digest
|
2015-12-16 01:18:13 +00:00
|
|
|
badURL := strings.Replace(layerURL, "sha256", "sha257", 1)
|
2015-05-27 17:52:22 +00:00
|
|
|
resp, err = httpDelete(badURL)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error fetching layer: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
checkResponse(t, "deleting layer bad digest", resp, http.StatusBadRequest)
|
|
|
|
|
|
|
|
// ----------------
|
|
|
|
// Reupload previously deleted blob
|
|
|
|
layerFile.Seek(0, os.SEEK_SET)
|
|
|
|
|
2016-02-23 01:49:23 +00:00
|
|
|
uploadURLBase, _ := startPushLayer(t, env, imageName)
|
2015-05-27 17:52:22 +00:00
|
|
|
pushLayer(t, env.builder, imageName, layerDigest, uploadURLBase, layerFile)
|
|
|
|
|
|
|
|
layerFile.Seek(0, os.SEEK_SET)
|
|
|
|
canonicalDigester := digest.Canonical.New()
|
|
|
|
if _, err := io.Copy(canonicalDigester.Hash(), layerFile); err != nil {
|
|
|
|
t.Fatalf("error copying to digest: %v", err)
|
|
|
|
}
|
|
|
|
canonicalDigest := canonicalDigester.Digest()
|
|
|
|
|
|
|
|
// ------------------------
|
|
|
|
// Use a head request to see if it exists
|
|
|
|
resp, err = http.Head(layerURL)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error checking head on existing layer: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
layerLength, _ := layerFile.Seek(0, os.SEEK_END)
|
|
|
|
checkResponse(t, "checking head on reuploaded layer", resp, http.StatusOK)
|
|
|
|
checkHeaders(t, resp, http.Header{
|
|
|
|
"Content-Length": []string{fmt.Sprint(layerLength)},
|
|
|
|
"Docker-Content-Digest": []string{canonicalDigest.String()},
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestDeleteDisabled(t *testing.T) {
|
|
|
|
env := newTestEnv(t, false)
|
2015-01-06 18:37:27 +00:00
|
|
|
|
2015-12-15 22:35:23 +00:00
|
|
|
imageName, _ := reference.ParseNamed("foo/bar")
|
2015-05-27 17:52:22 +00:00
|
|
|
// "build" our layer file
|
2015-12-16 01:18:13 +00:00
|
|
|
layerFile, layerDigest, err := testutil.CreateRandomTarFile()
|
2015-05-27 17:52:22 +00:00
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error creating random layer file: %v", err)
|
|
|
|
}
|
|
|
|
|
2015-12-16 00:43:13 +00:00
|
|
|
ref, _ := reference.WithDigest(imageName, layerDigest)
|
|
|
|
layerURL, err := env.builder.BuildBlobURL(ref)
|
2015-05-27 17:52:22 +00:00
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("Error building blob URL")
|
|
|
|
}
|
2016-02-23 01:49:23 +00:00
|
|
|
uploadURLBase, _ := startPushLayer(t, env, imageName)
|
2015-05-27 17:52:22 +00:00
|
|
|
pushLayer(t, env.builder, imageName, layerDigest, uploadURLBase, layerFile)
|
|
|
|
|
|
|
|
resp, err := httpDelete(layerURL)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error deleting layer: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
checkResponse(t, "deleting layer with delete disabled", resp, http.StatusMethodNotAllowed)
|
|
|
|
}
|
|
|
|
|
2015-08-06 17:34:35 +00:00
|
|
|
func TestDeleteReadOnly(t *testing.T) {
|
|
|
|
env := newTestEnv(t, true)
|
|
|
|
|
2015-12-15 22:35:23 +00:00
|
|
|
imageName, _ := reference.ParseNamed("foo/bar")
|
2015-08-06 17:34:35 +00:00
|
|
|
// "build" our layer file
|
2015-12-16 01:18:13 +00:00
|
|
|
layerFile, layerDigest, err := testutil.CreateRandomTarFile()
|
2015-08-06 17:34:35 +00:00
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error creating random layer file: %v", err)
|
|
|
|
}
|
|
|
|
|
2015-12-16 00:43:13 +00:00
|
|
|
ref, _ := reference.WithDigest(imageName, layerDigest)
|
|
|
|
layerURL, err := env.builder.BuildBlobURL(ref)
|
2015-08-06 17:34:35 +00:00
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("Error building blob URL")
|
|
|
|
}
|
2016-02-23 01:49:23 +00:00
|
|
|
uploadURLBase, _ := startPushLayer(t, env, imageName)
|
2015-08-06 17:34:35 +00:00
|
|
|
pushLayer(t, env.builder, imageName, layerDigest, uploadURLBase, layerFile)
|
|
|
|
|
|
|
|
env.app.readOnly = true
|
|
|
|
|
|
|
|
resp, err := httpDelete(layerURL)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error deleting layer: %v", err)
|
|
|
|
}
|
|
|
|
|
2015-08-07 01:02:43 +00:00
|
|
|
checkResponse(t, "deleting layer in read-only mode", resp, http.StatusMethodNotAllowed)
|
2015-08-06 17:34:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func TestStartPushReadOnly(t *testing.T) {
|
|
|
|
env := newTestEnv(t, true)
|
|
|
|
env.app.readOnly = true
|
|
|
|
|
2015-12-15 22:35:23 +00:00
|
|
|
imageName, _ := reference.ParseNamed("foo/bar")
|
2015-08-06 17:34:35 +00:00
|
|
|
|
|
|
|
layerUploadURL, err := env.builder.BuildBlobUploadURL(imageName)
|
|
|
|
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()
|
|
|
|
|
2015-08-07 01:02:43 +00:00
|
|
|
checkResponse(t, "starting push in read-only mode", resp, http.StatusMethodNotAllowed)
|
2015-08-06 17:34:35 +00:00
|
|
|
}
|
|
|
|
|
2015-05-27 17:52:22 +00:00
|
|
|
func httpDelete(url string) (*http.Response, error) {
|
|
|
|
req, err := http.NewRequest("DELETE", url, nil)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
// defer resp.Body.Close()
|
|
|
|
return resp, err
|
|
|
|
}
|
|
|
|
|
|
|
|
type manifestArgs struct {
|
2015-12-15 22:35:23 +00:00
|
|
|
imageName reference.Named
|
2015-12-16 22:30:49 +00:00
|
|
|
mediaType string
|
|
|
|
manifest distribution.Manifest
|
|
|
|
dgst digest.Digest
|
2015-05-27 17:52:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func TestManifestAPI(t *testing.T) {
|
2015-12-15 22:35:23 +00:00
|
|
|
schema1Repo, _ := reference.ParseNamed("foo/schema1")
|
|
|
|
schema2Repo, _ := reference.ParseNamed("foo/schema2")
|
|
|
|
|
2015-05-27 17:52:22 +00:00
|
|
|
deleteEnabled := false
|
|
|
|
env := newTestEnv(t, deleteEnabled)
|
2015-12-15 22:35:23 +00:00
|
|
|
testManifestAPISchema1(t, env, schema1Repo)
|
|
|
|
schema2Args := testManifestAPISchema2(t, env, schema2Repo)
|
2015-12-17 01:26:13 +00:00
|
|
|
testManifestAPIManifestList(t, env, schema2Args)
|
2015-05-27 17:52:22 +00:00
|
|
|
|
|
|
|
deleteEnabled = true
|
|
|
|
env = newTestEnv(t, deleteEnabled)
|
2015-12-15 22:35:23 +00:00
|
|
|
testManifestAPISchema1(t, env, schema1Repo)
|
|
|
|
schema2Args = testManifestAPISchema2(t, env, schema2Repo)
|
2015-12-17 01:26:13 +00:00
|
|
|
testManifestAPIManifestList(t, env, schema2Args)
|
2015-05-27 17:52:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func TestManifestDelete(t *testing.T) {
|
2015-12-15 22:35:23 +00:00
|
|
|
schema1Repo, _ := reference.ParseNamed("foo/schema1")
|
|
|
|
schema2Repo, _ := reference.ParseNamed("foo/schema2")
|
|
|
|
|
2015-05-27 17:52:22 +00:00
|
|
|
deleteEnabled := true
|
|
|
|
env := newTestEnv(t, deleteEnabled)
|
2015-12-15 22:35:23 +00:00
|
|
|
schema1Args := testManifestAPISchema1(t, env, schema1Repo)
|
2015-12-16 22:30:49 +00:00
|
|
|
testManifestDelete(t, env, schema1Args)
|
2015-12-15 22:35:23 +00:00
|
|
|
schema2Args := testManifestAPISchema2(t, env, schema2Repo)
|
2015-12-16 22:30:49 +00:00
|
|
|
testManifestDelete(t, env, schema2Args)
|
2015-05-27 17:52:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func TestManifestDeleteDisabled(t *testing.T) {
|
2015-12-15 22:35:23 +00:00
|
|
|
schema1Repo, _ := reference.ParseNamed("foo/schema1")
|
2015-05-27 17:52:22 +00:00
|
|
|
deleteEnabled := false
|
|
|
|
env := newTestEnv(t, deleteEnabled)
|
2015-12-15 22:35:23 +00:00
|
|
|
testManifestDeleteDisabled(t, env, schema1Repo)
|
2015-05-27 17:52:22 +00:00
|
|
|
}
|
|
|
|
|
2015-12-15 22:35:23 +00:00
|
|
|
func testManifestDeleteDisabled(t *testing.T, env *testEnv, imageName reference.Named) {
|
2015-12-16 00:43:13 +00:00
|
|
|
ref, _ := reference.WithDigest(imageName, digest.DigestSha256EmptyTar)
|
|
|
|
manifestURL, err := env.builder.BuildManifestURL(ref)
|
2015-05-27 17:52:22 +00:00
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error getting manifest url: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
resp, err := httpDelete(manifestURL)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error deleting manifest %v", err)
|
|
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
checkResponse(t, "status of disabled delete of manifest", resp, http.StatusMethodNotAllowed)
|
|
|
|
}
|
|
|
|
|
2015-12-15 22:35:23 +00:00
|
|
|
func testManifestAPISchema1(t *testing.T, env *testEnv, imageName reference.Named) manifestArgs {
|
2015-01-06 18:37:27 +00:00
|
|
|
tag := "thetag"
|
2015-12-16 22:30:49 +00:00
|
|
|
args := manifestArgs{imageName: imageName}
|
2015-01-06 18:37:27 +00:00
|
|
|
|
2015-12-16 00:43:13 +00:00
|
|
|
tagRef, _ := reference.WithTag(imageName, tag)
|
|
|
|
manifestURL, err := env.builder.BuildManifestURL(tagRef)
|
2015-01-06 18:37:27 +00:00
|
|
|
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)
|
2015-01-30 05:26:35 +00:00
|
|
|
checkBodyHasErrorCodes(t, "getting non-existent manifest", resp, v2.ErrorCodeManifestUnknown)
|
2015-01-06 18:37:27 +00:00
|
|
|
|
2015-02-07 00:19:19 +00:00
|
|
|
tagsURL, err := env.builder.BuildTagsURL(imageName)
|
2015-01-06 18:37:27 +00:00
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error building tags url: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
resp, err = http.Get(tagsURL)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error getting unknown tags: %v", err)
|
|
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
// Check that we get an unknown repository error when asking for tags
|
|
|
|
checkResponse(t, "getting unknown manifest tags", resp, http.StatusNotFound)
|
2015-01-30 05:26:35 +00:00
|
|
|
checkBodyHasErrorCodes(t, "getting unknown manifest tags", resp, v2.ErrorCodeNameUnknown)
|
2015-01-06 18:37:27 +00:00
|
|
|
|
|
|
|
// --------------------------------
|
|
|
|
// Attempt to push unsigned manifest with missing layers
|
2015-08-21 04:24:30 +00:00
|
|
|
unsignedManifest := &schema1.Manifest{
|
2015-02-26 23:47:04 +00:00
|
|
|
Versioned: manifest.Versioned{
|
|
|
|
SchemaVersion: 1,
|
|
|
|
},
|
2015-12-15 22:35:23 +00:00
|
|
|
Name: imageName.Name(),
|
2015-01-06 18:37:27 +00:00
|
|
|
Tag: tag,
|
2015-08-21 04:24:30 +00:00
|
|
|
FSLayers: []schema1.FSLayer{
|
2015-01-06 18:37:27 +00:00
|
|
|
{
|
|
|
|
BlobSum: "asdf",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
BlobSum: "qwer",
|
|
|
|
},
|
|
|
|
},
|
2015-11-03 19:03:17 +00:00
|
|
|
History: []schema1.History{
|
|
|
|
{
|
|
|
|
V1Compatibility: "",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
V1Compatibility: "",
|
|
|
|
},
|
|
|
|
},
|
2015-01-06 18:37:27 +00:00
|
|
|
}
|
|
|
|
|
2015-12-16 22:30:49 +00:00
|
|
|
resp = putManifest(t, "putting unsigned manifest", manifestURL, "", unsignedManifest)
|
2015-01-06 18:37:27 +00:00
|
|
|
defer resp.Body.Close()
|
2015-09-15 04:12:33 +00:00
|
|
|
checkResponse(t, "putting unsigned manifest", resp, http.StatusBadRequest)
|
2015-12-16 22:30:49 +00:00
|
|
|
_, p, counts := checkBodyHasErrorCodes(t, "putting unsigned manifest", resp, v2.ErrorCodeManifestInvalid)
|
2015-01-06 18:37:27 +00:00
|
|
|
|
2015-05-15 01:21:39 +00:00
|
|
|
expectedCounts := map[errcode.ErrorCode]int{
|
2015-09-15 04:12:33 +00:00
|
|
|
v2.ErrorCodeManifestInvalid: 1,
|
|
|
|
}
|
|
|
|
|
|
|
|
if !reflect.DeepEqual(counts, expectedCounts) {
|
|
|
|
t.Fatalf("unexpected number of error codes encountered: %v\n!=\n%v\n---\n%s", counts, expectedCounts, string(p))
|
|
|
|
}
|
|
|
|
|
|
|
|
// sign the manifest and still get some interesting errors.
|
|
|
|
sm, err := schema1.Sign(unsignedManifest, env.pk)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error signing manifest: %v", err)
|
|
|
|
}
|
|
|
|
|
2015-12-16 22:30:49 +00:00
|
|
|
resp = putManifest(t, "putting signed manifest with errors", manifestURL, "", sm)
|
2015-09-15 04:12:33 +00:00
|
|
|
defer resp.Body.Close()
|
|
|
|
checkResponse(t, "putting signed manifest with errors", resp, http.StatusBadRequest)
|
|
|
|
_, p, counts = checkBodyHasErrorCodes(t, "putting signed manifest with errors", resp,
|
|
|
|
v2.ErrorCodeManifestBlobUnknown, v2.ErrorCodeDigestInvalid)
|
|
|
|
|
|
|
|
expectedCounts = map[errcode.ErrorCode]int{
|
|
|
|
v2.ErrorCodeManifestBlobUnknown: 2,
|
|
|
|
v2.ErrorCodeDigestInvalid: 2,
|
2015-01-06 18:37:27 +00:00
|
|
|
}
|
|
|
|
|
2015-01-30 05:26:35 +00:00
|
|
|
if !reflect.DeepEqual(counts, expectedCounts) {
|
|
|
|
t.Fatalf("unexpected number of error codes encountered: %v\n!=\n%v\n---\n%s", counts, expectedCounts, string(p))
|
2015-01-06 18:37:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// TODO(stevvooe): Add a test case where we take a mostly valid registry,
|
|
|
|
// tamper with the content and ensure that we get a unverified manifest
|
|
|
|
// error.
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
2016-02-23 01:49:23 +00:00
|
|
|
uploadURLBase, _ := startPushLayer(t, env, imageName)
|
2015-02-07 00:19:19 +00:00
|
|
|
pushLayer(t, env.builder, imageName, dgst, uploadURLBase, rs)
|
2015-01-06 18:37:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// -------------------
|
|
|
|
// Push the signed manifest with all layers pushed.
|
2015-08-21 04:24:30 +00:00
|
|
|
signedManifest, err := schema1.Sign(unsignedManifest, env.pk)
|
2015-01-06 18:37:27 +00:00
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error signing manifest: %v", err)
|
|
|
|
}
|
|
|
|
|
2015-08-21 04:50:15 +00:00
|
|
|
dgst := digest.FromBytes(signedManifest.Canonical)
|
2015-12-16 22:30:49 +00:00
|
|
|
args.manifest = signedManifest
|
2015-05-27 17:52:22 +00:00
|
|
|
args.dgst = dgst
|
|
|
|
|
2015-12-16 00:43:13 +00:00
|
|
|
digestRef, _ := reference.WithDigest(imageName, dgst)
|
|
|
|
manifestDigestURL, err := env.builder.BuildManifestURL(digestRef)
|
2015-02-26 23:47:04 +00:00
|
|
|
checkErr(t, err, "building manifest url")
|
|
|
|
|
2015-12-16 22:30:49 +00:00
|
|
|
resp = putManifest(t, "putting signed manifest no error", manifestURL, "", signedManifest)
|
2015-08-21 04:50:15 +00:00
|
|
|
checkResponse(t, "putting signed manifest no error", resp, http.StatusCreated)
|
2015-02-26 23:47:04 +00:00
|
|
|
checkHeaders(t, resp, http.Header{
|
|
|
|
"Location": []string{manifestDigestURL},
|
|
|
|
"Docker-Content-Digest": []string{dgst.String()},
|
|
|
|
})
|
2015-01-06 18:37:27 +00:00
|
|
|
|
2015-02-26 23:47:04 +00:00
|
|
|
// --------------------
|
|
|
|
// Push by digest -- should get same result
|
2015-12-16 22:30:49 +00:00
|
|
|
resp = putManifest(t, "putting signed manifest", manifestDigestURL, "", signedManifest)
|
2015-07-24 17:42:02 +00:00
|
|
|
checkResponse(t, "putting signed manifest", resp, http.StatusCreated)
|
2015-02-26 23:47:04 +00:00
|
|
|
checkHeaders(t, resp, http.Header{
|
|
|
|
"Location": []string{manifestDigestURL},
|
|
|
|
"Docker-Content-Digest": []string{dgst.String()},
|
|
|
|
})
|
|
|
|
|
|
|
|
// ------------------
|
|
|
|
// Fetch by tag name
|
2015-01-06 18:37:27 +00:00
|
|
|
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)
|
2015-02-26 23:47:04 +00:00
|
|
|
checkHeaders(t, resp, http.Header{
|
|
|
|
"Docker-Content-Digest": []string{dgst.String()},
|
2015-07-24 06:03:13 +00:00
|
|
|
"ETag": []string{fmt.Sprintf(`"%s"`, dgst)},
|
2015-02-26 23:47:04 +00:00
|
|
|
})
|
2015-01-06 18:37:27 +00:00
|
|
|
|
2015-08-21 04:24:30 +00:00
|
|
|
var fetchedManifest schema1.SignedManifest
|
2015-01-30 05:26:35 +00:00
|
|
|
dec := json.NewDecoder(resp.Body)
|
2015-08-21 04:50:15 +00:00
|
|
|
|
2015-01-06 18:37:27 +00:00
|
|
|
if err := dec.Decode(&fetchedManifest); err != nil {
|
|
|
|
t.Fatalf("error decoding fetched manifest: %v", err)
|
|
|
|
}
|
|
|
|
|
2015-08-21 04:50:15 +00:00
|
|
|
if !bytes.Equal(fetchedManifest.Canonical, signedManifest.Canonical) {
|
2015-01-06 18:37:27 +00:00
|
|
|
t.Fatalf("manifests do not match")
|
|
|
|
}
|
|
|
|
|
2015-02-26 23:47:04 +00:00
|
|
|
// ---------------
|
|
|
|
// Fetch by digest
|
|
|
|
resp, err = http.Get(manifestDigestURL)
|
|
|
|
checkErr(t, err, "fetching manifest by digest")
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
checkResponse(t, "fetching uploaded manifest", resp, http.StatusOK)
|
|
|
|
checkHeaders(t, resp, http.Header{
|
|
|
|
"Docker-Content-Digest": []string{dgst.String()},
|
2015-07-24 06:03:13 +00:00
|
|
|
"ETag": []string{fmt.Sprintf(`"%s"`, dgst)},
|
2015-02-26 23:47:04 +00:00
|
|
|
})
|
|
|
|
|
2015-08-21 04:24:30 +00:00
|
|
|
var fetchedManifestByDigest schema1.SignedManifest
|
2015-02-26 23:47:04 +00:00
|
|
|
dec = json.NewDecoder(resp.Body)
|
|
|
|
if err := dec.Decode(&fetchedManifestByDigest); err != nil {
|
|
|
|
t.Fatalf("error decoding fetched manifest: %v", err)
|
|
|
|
}
|
|
|
|
|
2015-08-21 04:50:15 +00:00
|
|
|
if !bytes.Equal(fetchedManifestByDigest.Canonical, signedManifest.Canonical) {
|
2015-02-26 23:47:04 +00:00
|
|
|
t.Fatalf("manifests do not match")
|
|
|
|
}
|
|
|
|
|
2015-08-21 04:50:15 +00:00
|
|
|
// check signature was roundtripped
|
|
|
|
signatures, err := fetchedManifestByDigest.Signatures()
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(signatures) != 1 {
|
|
|
|
t.Fatalf("expected 1 signature from manifest, got: %d", len(signatures))
|
|
|
|
}
|
|
|
|
|
|
|
|
// Re-sign, push and pull the same digest
|
|
|
|
sm2, err := schema1.Sign(&fetchedManifestByDigest.Manifest, env.pk)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2016-01-18 17:59:50 +00:00
|
|
|
// Re-push with a few different Content-Types. The official schema1
|
|
|
|
// content type should work, as should application/json with/without a
|
|
|
|
// charset.
|
2016-01-18 18:26:45 +00:00
|
|
|
resp = putManifest(t, "re-putting signed manifest", manifestDigestURL, schema1.MediaTypeSignedManifest, sm2)
|
2016-01-18 17:59:50 +00:00
|
|
|
checkResponse(t, "re-putting signed manifest", resp, http.StatusCreated)
|
|
|
|
resp = putManifest(t, "re-putting signed manifest", manifestDigestURL, "application/json; charset=utf-8", sm2)
|
|
|
|
checkResponse(t, "re-putting signed manifest", resp, http.StatusCreated)
|
|
|
|
resp = putManifest(t, "re-putting signed manifest", manifestDigestURL, "application/json", sm2)
|
2015-08-21 04:50:15 +00:00
|
|
|
checkResponse(t, "re-putting signed manifest", resp, http.StatusCreated)
|
|
|
|
|
|
|
|
resp, err = http.Get(manifestDigestURL)
|
|
|
|
checkErr(t, err, "re-fetching manifest by digest")
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
checkResponse(t, "re-fetching uploaded manifest", resp, http.StatusOK)
|
|
|
|
checkHeaders(t, resp, http.Header{
|
|
|
|
"Docker-Content-Digest": []string{dgst.String()},
|
|
|
|
"ETag": []string{fmt.Sprintf(`"%s"`, dgst)},
|
|
|
|
})
|
|
|
|
|
|
|
|
dec = json.NewDecoder(resp.Body)
|
|
|
|
if err := dec.Decode(&fetchedManifestByDigest); err != nil {
|
|
|
|
t.Fatalf("error decoding fetched manifest: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// check two signatures were roundtripped
|
|
|
|
signatures, err = fetchedManifestByDigest.Signatures()
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(signatures) != 2 {
|
|
|
|
t.Fatalf("expected 2 signature from manifest, got: %d", len(signatures))
|
|
|
|
}
|
|
|
|
|
2015-06-18 23:56:05 +00:00
|
|
|
// Get by name with etag, gives 304
|
|
|
|
etag := resp.Header.Get("Etag")
|
|
|
|
req, err := http.NewRequest("GET", manifestURL, nil)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("Error constructing request: %s", err)
|
|
|
|
}
|
|
|
|
req.Header.Set("If-None-Match", etag)
|
|
|
|
resp, err = http.DefaultClient.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("Error constructing request: %s", err)
|
|
|
|
}
|
|
|
|
|
2015-08-21 04:50:15 +00:00
|
|
|
checkResponse(t, "fetching manifest by name with etag", resp, http.StatusNotModified)
|
2015-06-18 23:56:05 +00:00
|
|
|
|
|
|
|
// Get by digest with etag, gives 304
|
|
|
|
req, err = http.NewRequest("GET", manifestDigestURL, nil)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("Error constructing request: %s", err)
|
|
|
|
}
|
|
|
|
req.Header.Set("If-None-Match", etag)
|
|
|
|
resp, err = http.DefaultClient.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("Error constructing request: %s", err)
|
|
|
|
}
|
|
|
|
|
2015-08-21 04:50:15 +00:00
|
|
|
checkResponse(t, "fetching manifest by dgst with etag", resp, http.StatusNotModified)
|
2015-06-18 23:56:05 +00:00
|
|
|
|
2015-01-06 18:37:27 +00:00
|
|
|
// Ensure that the tag is listed.
|
|
|
|
resp, err = http.Get(tagsURL)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error getting unknown tags: %v", err)
|
|
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
2015-12-16 22:30:49 +00:00
|
|
|
checkResponse(t, "getting tags", resp, http.StatusOK)
|
2015-01-06 18:37:27 +00:00
|
|
|
dec = json.NewDecoder(resp.Body)
|
|
|
|
|
|
|
|
var tagsResponse tagsAPIResponse
|
|
|
|
|
|
|
|
if err := dec.Decode(&tagsResponse); err != nil {
|
|
|
|
t.Fatalf("unexpected error decoding error response: %v", err)
|
|
|
|
}
|
|
|
|
|
2015-12-15 22:35:23 +00:00
|
|
|
if tagsResponse.Name != imageName.Name() {
|
|
|
|
t.Fatalf("tags name should match image name: %v != %v", tagsResponse.Name, imageName.Name())
|
2015-01-06 18:37:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if len(tagsResponse.Tags) != 1 {
|
|
|
|
t.Fatalf("expected some tags in response: %v", tagsResponse.Tags)
|
|
|
|
}
|
|
|
|
|
|
|
|
if tagsResponse.Tags[0] != tag {
|
|
|
|
t.Fatalf("tag not as expected: %q != %q", tagsResponse.Tags[0], tag)
|
|
|
|
}
|
2015-05-27 17:52:22 +00:00
|
|
|
|
2015-11-03 19:03:17 +00:00
|
|
|
// Attempt to put a manifest with mismatching FSLayer and History array cardinalities
|
|
|
|
|
|
|
|
unsignedManifest.History = append(unsignedManifest.History, schema1.History{
|
|
|
|
V1Compatibility: "",
|
|
|
|
})
|
|
|
|
invalidSigned, err := schema1.Sign(unsignedManifest, env.pk)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error signing manifest")
|
|
|
|
}
|
|
|
|
|
2015-12-16 22:30:49 +00:00
|
|
|
resp = putManifest(t, "putting invalid signed manifest", manifestDigestURL, "", invalidSigned)
|
2015-11-03 19:03:17 +00:00
|
|
|
checkResponse(t, "putting invalid signed manifest", resp, http.StatusBadRequest)
|
|
|
|
|
2015-12-16 22:30:49 +00:00
|
|
|
return args
|
|
|
|
}
|
|
|
|
|
2015-12-15 22:35:23 +00:00
|
|
|
func testManifestAPISchema2(t *testing.T, env *testEnv, imageName reference.Named) manifestArgs {
|
2015-12-16 22:30:49 +00:00
|
|
|
tag := "schema2tag"
|
|
|
|
args := manifestArgs{
|
|
|
|
imageName: imageName,
|
|
|
|
mediaType: schema2.MediaTypeManifest,
|
|
|
|
}
|
|
|
|
|
2015-12-16 00:43:13 +00:00
|
|
|
tagRef, _ := reference.WithTag(imageName, tag)
|
|
|
|
manifestURL, err := env.builder.BuildManifestURL(tagRef)
|
2015-12-16 22:30:49 +00:00
|
|
|
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)
|
|
|
|
checkBodyHasErrorCodes(t, "getting non-existent manifest", resp, v2.ErrorCodeManifestUnknown)
|
|
|
|
|
|
|
|
tagsURL, err := env.builder.BuildTagsURL(imageName)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error building tags url: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
resp, err = http.Get(tagsURL)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error getting unknown tags: %v", err)
|
|
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
// Check that we get an unknown repository error when asking for tags
|
|
|
|
checkResponse(t, "getting unknown manifest tags", resp, http.StatusNotFound)
|
|
|
|
checkBodyHasErrorCodes(t, "getting unknown manifest tags", resp, v2.ErrorCodeNameUnknown)
|
|
|
|
|
|
|
|
// --------------------------------
|
|
|
|
// Attempt to push manifest with missing config and missing layers
|
|
|
|
manifest := &schema2.Manifest{
|
|
|
|
Versioned: manifest.Versioned{
|
|
|
|
SchemaVersion: 2,
|
2016-01-06 22:15:14 +00:00
|
|
|
MediaType: schema2.MediaTypeManifest,
|
2015-12-16 22:30:49 +00:00
|
|
|
},
|
|
|
|
Config: distribution.Descriptor{
|
|
|
|
Digest: "sha256:1a9ec845ee94c202b2d5da74a24f0ed2058318bfa9879fa541efaecba272e86b",
|
|
|
|
Size: 3253,
|
|
|
|
MediaType: schema2.MediaTypeConfig,
|
|
|
|
},
|
|
|
|
Layers: []distribution.Descriptor{
|
|
|
|
{
|
|
|
|
Digest: "sha256:463434349086340864309863409683460843608348608934092322395278926a",
|
|
|
|
Size: 6323,
|
|
|
|
MediaType: schema2.MediaTypeLayer,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Digest: "sha256:630923423623623423352523525237238023652897356239852383652aaaaaaa",
|
|
|
|
Size: 6863,
|
|
|
|
MediaType: schema2.MediaTypeLayer,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
resp = putManifest(t, "putting missing config manifest", manifestURL, schema2.MediaTypeManifest, manifest)
|
|
|
|
defer resp.Body.Close()
|
|
|
|
checkResponse(t, "putting missing config manifest", resp, http.StatusBadRequest)
|
|
|
|
_, p, counts := checkBodyHasErrorCodes(t, "putting missing config manifest", resp, v2.ErrorCodeManifestBlobUnknown)
|
|
|
|
|
|
|
|
expectedCounts := map[errcode.ErrorCode]int{
|
|
|
|
v2.ErrorCodeManifestBlobUnknown: 3,
|
|
|
|
}
|
|
|
|
|
|
|
|
if !reflect.DeepEqual(counts, expectedCounts) {
|
|
|
|
t.Fatalf("unexpected number of error codes encountered: %v\n!=\n%v\n---\n%s", counts, expectedCounts, string(p))
|
|
|
|
}
|
|
|
|
|
|
|
|
// Push a config, and reference it in the manifest
|
|
|
|
sampleConfig := []byte(`{
|
|
|
|
"architecture": "amd64",
|
|
|
|
"history": [
|
|
|
|
{
|
|
|
|
"created": "2015-10-31T22:22:54.690851953Z",
|
|
|
|
"created_by": "/bin/sh -c #(nop) ADD file:a3bc1e842b69636f9df5256c49c5374fb4eef1e281fe3f282c65fb853ee171c5 in /"
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"created": "2015-10-31T22:22:55.613815829Z",
|
|
|
|
"created_by": "/bin/sh -c #(nop) CMD [\"sh\"]"
|
|
|
|
}
|
|
|
|
],
|
|
|
|
"rootfs": {
|
|
|
|
"diff_ids": [
|
|
|
|
"sha256:c6f988f4874bb0add23a778f753c65efe992244e148a1d2ec2a8b664fb66bbd1",
|
|
|
|
"sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"
|
|
|
|
],
|
|
|
|
"type": "layers"
|
|
|
|
}
|
|
|
|
}`)
|
|
|
|
sampleConfigDigest := digest.FromBytes(sampleConfig)
|
|
|
|
|
2016-02-23 01:49:23 +00:00
|
|
|
uploadURLBase, _ := startPushLayer(t, env, imageName)
|
2015-12-16 22:30:49 +00:00
|
|
|
pushLayer(t, env.builder, imageName, sampleConfigDigest, uploadURLBase, bytes.NewReader(sampleConfig))
|
|
|
|
manifest.Config.Digest = sampleConfigDigest
|
|
|
|
manifest.Config.Size = int64(len(sampleConfig))
|
|
|
|
|
2016-02-11 00:26:29 +00:00
|
|
|
// The manifest should still be invalid, because its layer doesn't exist
|
2015-12-16 22:30:49 +00:00
|
|
|
resp = putManifest(t, "putting missing layer manifest", manifestURL, schema2.MediaTypeManifest, manifest)
|
|
|
|
defer resp.Body.Close()
|
|
|
|
checkResponse(t, "putting missing layer manifest", resp, http.StatusBadRequest)
|
|
|
|
_, p, counts = checkBodyHasErrorCodes(t, "getting unknown manifest tags", resp, v2.ErrorCodeManifestBlobUnknown)
|
|
|
|
|
|
|
|
expectedCounts = map[errcode.ErrorCode]int{
|
|
|
|
v2.ErrorCodeManifestBlobUnknown: 2,
|
|
|
|
}
|
|
|
|
|
|
|
|
if !reflect.DeepEqual(counts, expectedCounts) {
|
|
|
|
t.Fatalf("unexpected number of error codes encountered: %v\n!=\n%v\n---\n%s", counts, expectedCounts, string(p))
|
|
|
|
}
|
|
|
|
|
|
|
|
// Push 2 random layers
|
|
|
|
expectedLayers := make(map[digest.Digest]io.ReadSeeker)
|
|
|
|
|
|
|
|
for i := range manifest.Layers {
|
|
|
|
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
|
|
|
|
manifest.Layers[i].Digest = dgst
|
|
|
|
|
2016-02-23 01:49:23 +00:00
|
|
|
uploadURLBase, _ := startPushLayer(t, env, imageName)
|
2015-12-16 22:30:49 +00:00
|
|
|
pushLayer(t, env.builder, imageName, dgst, uploadURLBase, rs)
|
|
|
|
}
|
|
|
|
|
|
|
|
// -------------------
|
|
|
|
// Push the manifest with all layers pushed.
|
|
|
|
deserializedManifest, err := schema2.FromStruct(*manifest)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("could not create DeserializedManifest: %v", err)
|
|
|
|
}
|
|
|
|
_, canonical, err := deserializedManifest.Payload()
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("could not get manifest payload: %v", err)
|
|
|
|
}
|
|
|
|
dgst := digest.FromBytes(canonical)
|
|
|
|
args.dgst = dgst
|
|
|
|
args.manifest = deserializedManifest
|
|
|
|
|
2015-12-16 00:43:13 +00:00
|
|
|
digestRef, _ := reference.WithDigest(imageName, dgst)
|
|
|
|
manifestDigestURL, err := env.builder.BuildManifestURL(digestRef)
|
2015-12-16 22:30:49 +00:00
|
|
|
checkErr(t, err, "building manifest url")
|
|
|
|
|
|
|
|
resp = putManifest(t, "putting manifest no error", manifestURL, schema2.MediaTypeManifest, manifest)
|
|
|
|
checkResponse(t, "putting manifest no error", resp, http.StatusCreated)
|
|
|
|
checkHeaders(t, resp, http.Header{
|
|
|
|
"Location": []string{manifestDigestURL},
|
|
|
|
"Docker-Content-Digest": []string{dgst.String()},
|
|
|
|
})
|
|
|
|
|
|
|
|
// --------------------
|
|
|
|
// Push by digest -- should get same result
|
|
|
|
resp = putManifest(t, "putting manifest by digest", manifestDigestURL, schema2.MediaTypeManifest, manifest)
|
|
|
|
checkResponse(t, "putting manifest by digest", resp, http.StatusCreated)
|
|
|
|
checkHeaders(t, resp, http.Header{
|
|
|
|
"Location": []string{manifestDigestURL},
|
|
|
|
"Docker-Content-Digest": []string{dgst.String()},
|
|
|
|
})
|
|
|
|
|
|
|
|
// ------------------
|
|
|
|
// Fetch by tag name
|
|
|
|
req, err := http.NewRequest("GET", manifestURL, nil)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("Error constructing request: %s", err)
|
|
|
|
}
|
|
|
|
req.Header.Set("Accept", schema2.MediaTypeManifest)
|
|
|
|
resp, err = http.DefaultClient.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error fetching manifest: %v", err)
|
|
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
checkResponse(t, "fetching uploaded manifest", resp, http.StatusOK)
|
|
|
|
checkHeaders(t, resp, http.Header{
|
|
|
|
"Docker-Content-Digest": []string{dgst.String()},
|
|
|
|
"ETag": []string{fmt.Sprintf(`"%s"`, dgst)},
|
|
|
|
})
|
|
|
|
|
|
|
|
var fetchedManifest schema2.DeserializedManifest
|
|
|
|
dec := json.NewDecoder(resp.Body)
|
|
|
|
|
|
|
|
if err := dec.Decode(&fetchedManifest); err != nil {
|
|
|
|
t.Fatalf("error decoding fetched manifest: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
_, fetchedCanonical, err := fetchedManifest.Payload()
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error getting manifest payload: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if !bytes.Equal(fetchedCanonical, canonical) {
|
|
|
|
t.Fatalf("manifests do not match")
|
|
|
|
}
|
|
|
|
|
|
|
|
// ---------------
|
|
|
|
// Fetch by digest
|
|
|
|
req, err = http.NewRequest("GET", manifestDigestURL, nil)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("Error constructing request: %s", err)
|
|
|
|
}
|
|
|
|
req.Header.Set("Accept", schema2.MediaTypeManifest)
|
|
|
|
resp, err = http.DefaultClient.Do(req)
|
|
|
|
checkErr(t, err, "fetching manifest by digest")
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
checkResponse(t, "fetching uploaded manifest", resp, http.StatusOK)
|
|
|
|
checkHeaders(t, resp, http.Header{
|
|
|
|
"Docker-Content-Digest": []string{dgst.String()},
|
|
|
|
"ETag": []string{fmt.Sprintf(`"%s"`, dgst)},
|
|
|
|
})
|
|
|
|
|
|
|
|
var fetchedManifestByDigest schema2.DeserializedManifest
|
|
|
|
dec = json.NewDecoder(resp.Body)
|
|
|
|
if err := dec.Decode(&fetchedManifestByDigest); err != nil {
|
|
|
|
t.Fatalf("error decoding fetched manifest: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
_, fetchedCanonical, err = fetchedManifest.Payload()
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error getting manifest payload: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if !bytes.Equal(fetchedCanonical, canonical) {
|
|
|
|
t.Fatalf("manifests do not match")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get by name with etag, gives 304
|
|
|
|
etag := resp.Header.Get("Etag")
|
|
|
|
req, err = http.NewRequest("GET", manifestURL, nil)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("Error constructing request: %s", err)
|
|
|
|
}
|
|
|
|
req.Header.Set("If-None-Match", etag)
|
|
|
|
resp, err = http.DefaultClient.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("Error constructing request: %s", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
checkResponse(t, "fetching manifest by name with etag", resp, http.StatusNotModified)
|
|
|
|
|
|
|
|
// Get by digest with etag, gives 304
|
|
|
|
req, err = http.NewRequest("GET", manifestDigestURL, nil)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("Error constructing request: %s", err)
|
|
|
|
}
|
|
|
|
req.Header.Set("If-None-Match", etag)
|
|
|
|
resp, err = http.DefaultClient.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("Error constructing request: %s", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
checkResponse(t, "fetching manifest by dgst with etag", resp, http.StatusNotModified)
|
|
|
|
|
|
|
|
// Ensure that the tag is listed.
|
|
|
|
resp, err = http.Get(tagsURL)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error getting unknown tags: %v", err)
|
|
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
checkResponse(t, "getting unknown manifest tags", resp, http.StatusOK)
|
|
|
|
dec = json.NewDecoder(resp.Body)
|
|
|
|
|
|
|
|
var tagsResponse tagsAPIResponse
|
|
|
|
|
|
|
|
if err := dec.Decode(&tagsResponse); err != nil {
|
|
|
|
t.Fatalf("unexpected error decoding error response: %v", err)
|
|
|
|
}
|
|
|
|
|
2015-12-15 22:35:23 +00:00
|
|
|
if tagsResponse.Name != imageName.Name() {
|
2015-12-16 22:30:49 +00:00
|
|
|
t.Fatalf("tags name should match image name: %v != %v", tagsResponse.Name, imageName)
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(tagsResponse.Tags) != 1 {
|
|
|
|
t.Fatalf("expected some tags in response: %v", tagsResponse.Tags)
|
|
|
|
}
|
|
|
|
|
|
|
|
if tagsResponse.Tags[0] != tag {
|
|
|
|
t.Fatalf("tag not as expected: %q != %q", tagsResponse.Tags[0], tag)
|
|
|
|
}
|
|
|
|
|
|
|
|
// ------------------
|
|
|
|
// Fetch as a schema1 manifest
|
|
|
|
resp, err = http.Get(manifestURL)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error fetching manifest as schema1: %v", err)
|
|
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
2016-02-10 02:28:43 +00:00
|
|
|
manifestBytes, err := ioutil.ReadAll(resp.Body)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error reading response body: %v", err)
|
|
|
|
}
|
|
|
|
|
2015-12-16 22:30:49 +00:00
|
|
|
checkResponse(t, "fetching uploaded manifest as schema1", resp, http.StatusOK)
|
|
|
|
|
2016-02-10 02:28:43 +00:00
|
|
|
m, desc, err := distribution.UnmarshalManifest(schema1.MediaTypeManifest, manifestBytes)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error unmarshalling manifest: %v", err)
|
|
|
|
}
|
2015-12-16 22:30:49 +00:00
|
|
|
|
2016-02-10 02:28:43 +00:00
|
|
|
fetchedSchema1Manifest, ok := m.(*schema1.SignedManifest)
|
|
|
|
if !ok {
|
|
|
|
t.Fatalf("expecting schema1 manifest")
|
2015-12-16 22:30:49 +00:00
|
|
|
}
|
|
|
|
|
2016-02-10 02:28:43 +00:00
|
|
|
checkHeaders(t, resp, http.Header{
|
|
|
|
"Docker-Content-Digest": []string{desc.Digest.String()},
|
|
|
|
"ETag": []string{fmt.Sprintf(`"%s"`, desc.Digest)},
|
|
|
|
})
|
|
|
|
|
2015-12-16 22:30:49 +00:00
|
|
|
if fetchedSchema1Manifest.Manifest.SchemaVersion != 1 {
|
|
|
|
t.Fatal("wrong schema version")
|
|
|
|
}
|
|
|
|
if fetchedSchema1Manifest.Architecture != "amd64" {
|
|
|
|
t.Fatal("wrong architecture")
|
|
|
|
}
|
2015-12-15 22:35:23 +00:00
|
|
|
if fetchedSchema1Manifest.Name != imageName.Name() {
|
2015-12-16 22:30:49 +00:00
|
|
|
t.Fatal("wrong image name")
|
|
|
|
}
|
|
|
|
if fetchedSchema1Manifest.Tag != tag {
|
|
|
|
t.Fatal("wrong tag")
|
|
|
|
}
|
|
|
|
if len(fetchedSchema1Manifest.FSLayers) != 2 {
|
|
|
|
t.Fatal("wrong number of FSLayers")
|
|
|
|
}
|
|
|
|
for i := range manifest.Layers {
|
|
|
|
if fetchedSchema1Manifest.FSLayers[i].BlobSum != manifest.Layers[len(manifest.Layers)-i-1].Digest {
|
|
|
|
t.Fatalf("blob digest mismatch in schema1 manifest for layer %d", i)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if len(fetchedSchema1Manifest.History) != 2 {
|
|
|
|
t.Fatal("wrong number of History entries")
|
|
|
|
}
|
|
|
|
|
2016-02-11 00:26:29 +00:00
|
|
|
// Don't check V1Compatibility fields because we're using randomly-generated
|
2015-12-16 22:30:49 +00:00
|
|
|
// layers.
|
|
|
|
|
|
|
|
return args
|
2015-05-27 17:52:22 +00:00
|
|
|
}
|
|
|
|
|
2015-12-17 01:26:13 +00:00
|
|
|
func testManifestAPIManifestList(t *testing.T, env *testEnv, args manifestArgs) {
|
|
|
|
imageName := args.imageName
|
|
|
|
tag := "manifestlisttag"
|
|
|
|
|
2015-12-16 00:43:13 +00:00
|
|
|
tagRef, _ := reference.WithTag(imageName, tag)
|
|
|
|
manifestURL, err := env.builder.BuildManifestURL(tagRef)
|
2015-12-17 01:26:13 +00:00
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error getting manifest url: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// --------------------------------
|
|
|
|
// Attempt to push manifest list that refers to an unknown manifest
|
|
|
|
manifestList := &manifestlist.ManifestList{
|
|
|
|
Versioned: manifest.Versioned{
|
|
|
|
SchemaVersion: 2,
|
2016-01-06 22:15:14 +00:00
|
|
|
MediaType: manifestlist.MediaTypeManifestList,
|
2015-12-17 01:26:13 +00:00
|
|
|
},
|
|
|
|
Manifests: []manifestlist.ManifestDescriptor{
|
|
|
|
{
|
|
|
|
Descriptor: distribution.Descriptor{
|
|
|
|
Digest: "sha256:1a9ec845ee94c202b2d5da74a24f0ed2058318bfa9879fa541efaecba272e86b",
|
|
|
|
Size: 3253,
|
|
|
|
MediaType: schema2.MediaTypeManifest,
|
|
|
|
},
|
|
|
|
Platform: manifestlist.PlatformSpec{
|
|
|
|
Architecture: "amd64",
|
|
|
|
OS: "linux",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
resp := putManifest(t, "putting missing manifest manifestlist", manifestURL, manifestlist.MediaTypeManifestList, manifestList)
|
|
|
|
defer resp.Body.Close()
|
|
|
|
checkResponse(t, "putting missing manifest manifestlist", resp, http.StatusBadRequest)
|
|
|
|
_, p, counts := checkBodyHasErrorCodes(t, "putting missing manifest manifestlist", resp, v2.ErrorCodeManifestBlobUnknown)
|
|
|
|
|
|
|
|
expectedCounts := map[errcode.ErrorCode]int{
|
|
|
|
v2.ErrorCodeManifestBlobUnknown: 1,
|
|
|
|
}
|
|
|
|
|
|
|
|
if !reflect.DeepEqual(counts, expectedCounts) {
|
|
|
|
t.Fatalf("unexpected number of error codes encountered: %v\n!=\n%v\n---\n%s", counts, expectedCounts, string(p))
|
|
|
|
}
|
|
|
|
|
|
|
|
// -------------------
|
|
|
|
// Push a manifest list that references an actual manifest
|
|
|
|
manifestList.Manifests[0].Digest = args.dgst
|
|
|
|
deserializedManifestList, err := manifestlist.FromDescriptors(manifestList.Manifests)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("could not create DeserializedManifestList: %v", err)
|
|
|
|
}
|
|
|
|
_, canonical, err := deserializedManifestList.Payload()
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("could not get manifest list payload: %v", err)
|
|
|
|
}
|
|
|
|
dgst := digest.FromBytes(canonical)
|
|
|
|
|
2015-12-16 00:43:13 +00:00
|
|
|
digestRef, _ := reference.WithDigest(imageName, dgst)
|
|
|
|
manifestDigestURL, err := env.builder.BuildManifestURL(digestRef)
|
2015-12-17 01:26:13 +00:00
|
|
|
checkErr(t, err, "building manifest url")
|
|
|
|
|
|
|
|
resp = putManifest(t, "putting manifest list no error", manifestURL, manifestlist.MediaTypeManifestList, deserializedManifestList)
|
|
|
|
checkResponse(t, "putting manifest list no error", resp, http.StatusCreated)
|
|
|
|
checkHeaders(t, resp, http.Header{
|
|
|
|
"Location": []string{manifestDigestURL},
|
|
|
|
"Docker-Content-Digest": []string{dgst.String()},
|
|
|
|
})
|
|
|
|
|
|
|
|
// --------------------
|
|
|
|
// Push by digest -- should get same result
|
|
|
|
resp = putManifest(t, "putting manifest list by digest", manifestDigestURL, manifestlist.MediaTypeManifestList, deserializedManifestList)
|
|
|
|
checkResponse(t, "putting manifest list by digest", resp, http.StatusCreated)
|
|
|
|
checkHeaders(t, resp, http.Header{
|
|
|
|
"Location": []string{manifestDigestURL},
|
|
|
|
"Docker-Content-Digest": []string{dgst.String()},
|
|
|
|
})
|
|
|
|
|
|
|
|
// ------------------
|
|
|
|
// Fetch by tag name
|
|
|
|
req, err := http.NewRequest("GET", manifestURL, nil)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("Error constructing request: %s", err)
|
|
|
|
}
|
|
|
|
req.Header.Set("Accept", manifestlist.MediaTypeManifestList)
|
2016-01-18 18:26:45 +00:00
|
|
|
req.Header.Add("Accept", schema1.MediaTypeSignedManifest)
|
2015-12-17 01:26:13 +00:00
|
|
|
req.Header.Add("Accept", schema2.MediaTypeManifest)
|
|
|
|
resp, err = http.DefaultClient.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error fetching manifest list: %v", err)
|
|
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
checkResponse(t, "fetching uploaded manifest list", resp, http.StatusOK)
|
|
|
|
checkHeaders(t, resp, http.Header{
|
|
|
|
"Docker-Content-Digest": []string{dgst.String()},
|
|
|
|
"ETag": []string{fmt.Sprintf(`"%s"`, dgst)},
|
|
|
|
})
|
|
|
|
|
|
|
|
var fetchedManifestList manifestlist.DeserializedManifestList
|
|
|
|
dec := json.NewDecoder(resp.Body)
|
|
|
|
|
|
|
|
if err := dec.Decode(&fetchedManifestList); err != nil {
|
|
|
|
t.Fatalf("error decoding fetched manifest list: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
_, fetchedCanonical, err := fetchedManifestList.Payload()
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error getting manifest list payload: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if !bytes.Equal(fetchedCanonical, canonical) {
|
|
|
|
t.Fatalf("manifest lists do not match")
|
|
|
|
}
|
|
|
|
|
|
|
|
// ---------------
|
|
|
|
// Fetch by digest
|
|
|
|
req, err = http.NewRequest("GET", manifestDigestURL, nil)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("Error constructing request: %s", err)
|
|
|
|
}
|
|
|
|
req.Header.Set("Accept", manifestlist.MediaTypeManifestList)
|
|
|
|
resp, err = http.DefaultClient.Do(req)
|
|
|
|
checkErr(t, err, "fetching manifest list by digest")
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
checkResponse(t, "fetching uploaded manifest list", resp, http.StatusOK)
|
|
|
|
checkHeaders(t, resp, http.Header{
|
|
|
|
"Docker-Content-Digest": []string{dgst.String()},
|
|
|
|
"ETag": []string{fmt.Sprintf(`"%s"`, dgst)},
|
|
|
|
})
|
|
|
|
|
|
|
|
var fetchedManifestListByDigest manifestlist.DeserializedManifestList
|
|
|
|
dec = json.NewDecoder(resp.Body)
|
|
|
|
if err := dec.Decode(&fetchedManifestListByDigest); err != nil {
|
|
|
|
t.Fatalf("error decoding fetched manifest: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
_, fetchedCanonical, err = fetchedManifestListByDigest.Payload()
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error getting manifest list payload: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if !bytes.Equal(fetchedCanonical, canonical) {
|
|
|
|
t.Fatalf("manifests do not match")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get by name with etag, gives 304
|
|
|
|
etag := resp.Header.Get("Etag")
|
|
|
|
req, err = http.NewRequest("GET", manifestURL, nil)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("Error constructing request: %s", err)
|
|
|
|
}
|
|
|
|
req.Header.Set("If-None-Match", etag)
|
|
|
|
resp, err = http.DefaultClient.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("Error constructing request: %s", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
checkResponse(t, "fetching manifest by name with etag", resp, http.StatusNotModified)
|
|
|
|
|
|
|
|
// Get by digest with etag, gives 304
|
|
|
|
req, err = http.NewRequest("GET", manifestDigestURL, nil)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("Error constructing request: %s", err)
|
|
|
|
}
|
|
|
|
req.Header.Set("If-None-Match", etag)
|
|
|
|
resp, err = http.DefaultClient.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("Error constructing request: %s", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
checkResponse(t, "fetching manifest by dgst with etag", resp, http.StatusNotModified)
|
2015-12-18 01:32:11 +00:00
|
|
|
|
|
|
|
// ------------------
|
|
|
|
// Fetch as a schema1 manifest
|
|
|
|
resp, err = http.Get(manifestURL)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error fetching manifest list as schema1: %v", err)
|
|
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
2016-02-10 02:28:43 +00:00
|
|
|
manifestBytes, err := ioutil.ReadAll(resp.Body)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error reading response body: %v", err)
|
|
|
|
}
|
|
|
|
|
2015-12-18 01:32:11 +00:00
|
|
|
checkResponse(t, "fetching uploaded manifest list as schema1", resp, http.StatusOK)
|
|
|
|
|
2016-02-10 02:28:43 +00:00
|
|
|
m, desc, err := distribution.UnmarshalManifest(schema1.MediaTypeManifest, manifestBytes)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error unmarshalling manifest: %v", err)
|
|
|
|
}
|
2015-12-18 01:32:11 +00:00
|
|
|
|
2016-02-10 02:28:43 +00:00
|
|
|
fetchedSchema1Manifest, ok := m.(*schema1.SignedManifest)
|
|
|
|
if !ok {
|
|
|
|
t.Fatalf("expecting schema1 manifest")
|
2015-12-18 01:32:11 +00:00
|
|
|
}
|
|
|
|
|
2016-02-10 02:28:43 +00:00
|
|
|
checkHeaders(t, resp, http.Header{
|
|
|
|
"Docker-Content-Digest": []string{desc.Digest.String()},
|
|
|
|
"ETag": []string{fmt.Sprintf(`"%s"`, desc.Digest)},
|
|
|
|
})
|
|
|
|
|
2015-12-18 01:32:11 +00:00
|
|
|
if fetchedSchema1Manifest.Manifest.SchemaVersion != 1 {
|
|
|
|
t.Fatal("wrong schema version")
|
|
|
|
}
|
|
|
|
if fetchedSchema1Manifest.Architecture != "amd64" {
|
|
|
|
t.Fatal("wrong architecture")
|
|
|
|
}
|
2015-12-15 22:35:23 +00:00
|
|
|
if fetchedSchema1Manifest.Name != imageName.Name() {
|
2015-12-18 01:32:11 +00:00
|
|
|
t.Fatal("wrong image name")
|
|
|
|
}
|
|
|
|
if fetchedSchema1Manifest.Tag != tag {
|
|
|
|
t.Fatal("wrong tag")
|
|
|
|
}
|
|
|
|
if len(fetchedSchema1Manifest.FSLayers) != 2 {
|
|
|
|
t.Fatal("wrong number of FSLayers")
|
|
|
|
}
|
|
|
|
layers := args.manifest.(*schema2.DeserializedManifest).Layers
|
|
|
|
for i := range layers {
|
|
|
|
if fetchedSchema1Manifest.FSLayers[i].BlobSum != layers[len(layers)-i-1].Digest {
|
|
|
|
t.Fatalf("blob digest mismatch in schema1 manifest for layer %d", i)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if len(fetchedSchema1Manifest.History) != 2 {
|
|
|
|
t.Fatal("wrong number of History entries")
|
|
|
|
}
|
|
|
|
|
2016-02-11 00:26:29 +00:00
|
|
|
// Don't check V1Compatibility fields because we're using randomly-generated
|
2015-12-18 01:32:11 +00:00
|
|
|
// layers.
|
2015-12-17 01:26:13 +00:00
|
|
|
}
|
|
|
|
|
2015-05-27 17:52:22 +00:00
|
|
|
func testManifestDelete(t *testing.T, env *testEnv, args manifestArgs) {
|
|
|
|
imageName := args.imageName
|
|
|
|
dgst := args.dgst
|
2015-12-16 22:30:49 +00:00
|
|
|
manifest := args.manifest
|
2016-01-05 19:22:40 +00:00
|
|
|
|
2015-12-16 00:43:13 +00:00
|
|
|
ref, _ := reference.WithDigest(imageName, dgst)
|
|
|
|
manifestDigestURL, err := env.builder.BuildManifestURL(ref)
|
2015-05-27 17:52:22 +00:00
|
|
|
// ---------------
|
|
|
|
// Delete by digest
|
|
|
|
resp, err := httpDelete(manifestDigestURL)
|
|
|
|
checkErr(t, err, "deleting manifest by digest")
|
|
|
|
|
|
|
|
checkResponse(t, "deleting manifest", resp, http.StatusAccepted)
|
|
|
|
checkHeaders(t, resp, http.Header{
|
|
|
|
"Content-Length": []string{"0"},
|
|
|
|
})
|
|
|
|
|
|
|
|
// ---------------
|
|
|
|
// Attempt to fetch deleted manifest
|
|
|
|
resp, err = http.Get(manifestDigestURL)
|
|
|
|
checkErr(t, err, "fetching deleted manifest by digest")
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
checkResponse(t, "fetching deleted manifest", resp, http.StatusNotFound)
|
|
|
|
|
|
|
|
// ---------------
|
|
|
|
// Delete already deleted manifest by digest
|
|
|
|
resp, err = httpDelete(manifestDigestURL)
|
|
|
|
checkErr(t, err, "re-deleting manifest by digest")
|
|
|
|
|
|
|
|
checkResponse(t, "re-deleting manifest", resp, http.StatusNotFound)
|
|
|
|
|
|
|
|
// --------------------
|
|
|
|
// Re-upload manifest by digest
|
2015-12-16 22:30:49 +00:00
|
|
|
resp = putManifest(t, "putting manifest", manifestDigestURL, args.mediaType, manifest)
|
|
|
|
checkResponse(t, "putting manifest", resp, http.StatusCreated)
|
2015-05-27 17:52:22 +00:00
|
|
|
checkHeaders(t, resp, http.Header{
|
|
|
|
"Location": []string{manifestDigestURL},
|
|
|
|
"Docker-Content-Digest": []string{dgst.String()},
|
|
|
|
})
|
|
|
|
|
|
|
|
// ---------------
|
|
|
|
// Attempt to fetch re-uploaded deleted digest
|
|
|
|
resp, err = http.Get(manifestDigestURL)
|
|
|
|
checkErr(t, err, "fetching re-uploaded manifest by digest")
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
checkResponse(t, "fetching re-uploaded manifest", resp, http.StatusOK)
|
|
|
|
checkHeaders(t, resp, http.Header{
|
|
|
|
"Docker-Content-Digest": []string{dgst.String()},
|
|
|
|
})
|
|
|
|
|
|
|
|
// ---------------
|
|
|
|
// Attempt to delete an unknown manifest
|
2015-12-16 00:43:13 +00:00
|
|
|
unknownDigest := digest.Digest("sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
|
|
|
|
unknownRef, _ := reference.WithDigest(imageName, unknownDigest)
|
|
|
|
unknownManifestDigestURL, err := env.builder.BuildManifestURL(unknownRef)
|
2015-05-27 17:52:22 +00:00
|
|
|
checkErr(t, err, "building unknown manifest url")
|
|
|
|
|
|
|
|
resp, err = httpDelete(unknownManifestDigestURL)
|
|
|
|
checkErr(t, err, "delting unknown manifest by digest")
|
|
|
|
checkResponse(t, "fetching deleted manifest", resp, http.StatusNotFound)
|
|
|
|
|
2016-01-05 19:22:40 +00:00
|
|
|
// --------------------
|
2015-12-16 00:43:13 +00:00
|
|
|
// Upload manifest by tag
|
2016-01-11 20:52:21 +00:00
|
|
|
tag := "atag"
|
2015-12-16 00:43:13 +00:00
|
|
|
tagRef, _ := reference.WithTag(imageName, tag)
|
|
|
|
manifestTagURL, err := env.builder.BuildManifestURL(tagRef)
|
|
|
|
resp = putManifest(t, "putting manifest by tag", manifestTagURL, args.mediaType, manifest)
|
|
|
|
checkResponse(t, "putting manifest by tag", resp, http.StatusCreated)
|
2016-01-05 19:22:40 +00:00
|
|
|
checkHeaders(t, resp, http.Header{
|
|
|
|
"Location": []string{manifestDigestURL},
|
|
|
|
"Docker-Content-Digest": []string{dgst.String()},
|
|
|
|
})
|
|
|
|
|
|
|
|
tagsURL, err := env.builder.BuildTagsURL(imageName)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error building tags url: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Ensure that the tag is listed.
|
|
|
|
resp, err = http.Get(tagsURL)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error getting unknown tags: %v", err)
|
|
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
dec := json.NewDecoder(resp.Body)
|
|
|
|
var tagsResponse tagsAPIResponse
|
|
|
|
if err := dec.Decode(&tagsResponse); err != nil {
|
|
|
|
t.Fatalf("unexpected error decoding error response: %v", err)
|
|
|
|
}
|
|
|
|
|
2015-12-15 22:35:23 +00:00
|
|
|
if tagsResponse.Name != imageName.Name() {
|
2016-01-05 19:22:40 +00:00
|
|
|
t.Fatalf("tags name should match image name: %v != %v", tagsResponse.Name, imageName)
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(tagsResponse.Tags) != 1 {
|
|
|
|
t.Fatalf("expected some tags in response: %v", tagsResponse.Tags)
|
|
|
|
}
|
|
|
|
|
|
|
|
if tagsResponse.Tags[0] != tag {
|
|
|
|
t.Fatalf("tag not as expected: %q != %q", tagsResponse.Tags[0], tag)
|
|
|
|
}
|
|
|
|
|
|
|
|
// ---------------
|
|
|
|
// Delete by digest
|
|
|
|
resp, err = httpDelete(manifestDigestURL)
|
|
|
|
checkErr(t, err, "deleting manifest by digest")
|
|
|
|
|
|
|
|
checkResponse(t, "deleting manifest with tag", resp, http.StatusAccepted)
|
|
|
|
checkHeaders(t, resp, http.Header{
|
|
|
|
"Content-Length": []string{"0"},
|
|
|
|
})
|
|
|
|
|
|
|
|
// Ensure that the tag is not listed.
|
|
|
|
resp, err = http.Get(tagsURL)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error getting unknown tags: %v", err)
|
|
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
dec = json.NewDecoder(resp.Body)
|
|
|
|
if err := dec.Decode(&tagsResponse); err != nil {
|
|
|
|
t.Fatalf("unexpected error decoding error response: %v", err)
|
|
|
|
}
|
|
|
|
|
2015-12-15 22:35:23 +00:00
|
|
|
if tagsResponse.Name != imageName.Name() {
|
2016-01-05 19:22:40 +00:00
|
|
|
t.Fatalf("tags name should match image name: %v != %v", tagsResponse.Name, imageName)
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(tagsResponse.Tags) != 0 {
|
|
|
|
t.Fatalf("expected 0 tags in response: %v", tagsResponse.Tags)
|
|
|
|
}
|
|
|
|
|
2015-01-06 18:37:27 +00:00
|
|
|
}
|
|
|
|
|
2015-02-07 00:19:19 +00:00
|
|
|
type testEnv struct {
|
|
|
|
pk libtrust.PrivateKey
|
|
|
|
ctx context.Context
|
|
|
|
config configuration.Configuration
|
|
|
|
app *App
|
|
|
|
server *httptest.Server
|
|
|
|
builder *v2.URLBuilder
|
|
|
|
}
|
|
|
|
|
2015-08-11 18:00:30 +00:00
|
|
|
func newTestEnvMirror(t *testing.T, deleteEnabled bool) *testEnv {
|
|
|
|
config := configuration.Configuration{
|
|
|
|
Storage: configuration.Storage{
|
2016-05-06 09:46:37 +00:00
|
|
|
"testdriver": configuration.Parameters{},
|
|
|
|
"delete": configuration.Parameters{"enabled": deleteEnabled},
|
2015-08-11 18:00:30 +00:00
|
|
|
},
|
|
|
|
Proxy: configuration.Proxy{
|
|
|
|
RemoteURL: "http://example.com",
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
return newTestEnvWithConfig(t, &config)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2015-05-27 17:52:22 +00:00
|
|
|
func newTestEnv(t *testing.T, deleteEnabled bool) *testEnv {
|
2015-02-07 00:19:19 +00:00
|
|
|
config := configuration.Configuration{
|
|
|
|
Storage: configuration.Storage{
|
2016-05-06 09:46:37 +00:00
|
|
|
"testdriver": configuration.Parameters{},
|
|
|
|
"delete": configuration.Parameters{"enabled": deleteEnabled},
|
2015-02-07 00:19:19 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2015-08-10 21:20:52 +00:00
|
|
|
config.HTTP.Headers = headerConfig
|
|
|
|
|
2015-02-24 22:59:01 +00:00
|
|
|
return newTestEnvWithConfig(t, &config)
|
|
|
|
}
|
|
|
|
|
|
|
|
func newTestEnvWithConfig(t *testing.T, config *configuration.Configuration) *testEnv {
|
|
|
|
ctx := context.Background()
|
|
|
|
|
2015-08-20 20:56:36 +00:00
|
|
|
app := NewApp(ctx, config)
|
2015-02-07 00:19:19 +00:00
|
|
|
server := httptest.NewServer(handlers.CombinedLoggingHandler(os.Stderr, app))
|
2016-02-23 01:49:23 +00:00
|
|
|
builder, err := v2.NewURLBuilderFromString(server.URL+config.HTTP.Prefix, false)
|
2015-02-07 00:19:19 +00:00
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error creating url builder: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
pk, err := libtrust.GenerateECP256PrivateKey()
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error generating private key: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return &testEnv{
|
|
|
|
pk: pk,
|
|
|
|
ctx: ctx,
|
2015-02-24 22:59:01 +00:00
|
|
|
config: *config,
|
2015-02-07 00:19:19 +00:00
|
|
|
app: app,
|
|
|
|
server: server,
|
|
|
|
builder: builder,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-12-16 22:30:49 +00:00
|
|
|
func putManifest(t *testing.T, msg, url, contentType string, v interface{}) *http.Response {
|
2015-01-06 18:37:27 +00:00
|
|
|
var body []byte
|
2015-08-21 04:50:15 +00:00
|
|
|
|
2015-12-17 01:26:13 +00:00
|
|
|
switch m := v.(type) {
|
|
|
|
case *schema1.SignedManifest:
|
|
|
|
_, pl, err := m.Payload()
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error getting payload: %v", err)
|
|
|
|
}
|
|
|
|
body = pl
|
|
|
|
case *manifestlist.DeserializedManifestList:
|
|
|
|
_, pl, err := m.Payload()
|
2015-08-21 04:50:15 +00:00
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error getting payload: %v", err)
|
|
|
|
}
|
|
|
|
body = pl
|
2015-12-17 01:26:13 +00:00
|
|
|
default:
|
2015-01-06 18:37:27 +00:00
|
|
|
var err error
|
|
|
|
body, err = json.MarshalIndent(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)
|
|
|
|
}
|
|
|
|
|
2015-12-16 22:30:49 +00:00
|
|
|
if contentType != "" {
|
|
|
|
req.Header.Set("Content-Type", contentType)
|
|
|
|
}
|
|
|
|
|
2015-01-06 18:37:27 +00:00
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error doing put request while %s: %v", msg, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return resp
|
|
|
|
}
|
|
|
|
|
2016-02-23 01:49:23 +00:00
|
|
|
func startPushLayer(t *testing.T, env *testEnv, name reference.Named) (location string, uuid string) {
|
|
|
|
layerUploadURL, err := env.builder.BuildBlobUploadURL(name)
|
2015-01-06 18:37:27 +00:00
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error building layer upload url: %v", err)
|
|
|
|
}
|
|
|
|
|
2016-02-23 01:49:23 +00:00
|
|
|
u, err := url.Parse(layerUploadURL)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error parsing layer upload URL: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
base, err := url.Parse(env.server.URL)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error parsing server URL: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
layerUploadURL = base.ResolveReference(u).String()
|
2015-01-06 18:37:27 +00:00
|
|
|
resp, err := http.Post(layerUploadURL, "", nil)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error starting layer push: %v", err)
|
|
|
|
}
|
2016-02-23 01:49:23 +00:00
|
|
|
|
2015-01-06 18:37:27 +00:00
|
|
|
defer resp.Body.Close()
|
|
|
|
|
2015-12-15 22:35:23 +00:00
|
|
|
checkResponse(t, fmt.Sprintf("pushing starting layer push %v", name.String()), resp, http.StatusAccepted)
|
2015-02-27 00:43:47 +00:00
|
|
|
|
2016-02-23 01:49:23 +00:00
|
|
|
u, err = url.Parse(resp.Header.Get("Location"))
|
2015-02-27 00:43:47 +00:00
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error parsing location header: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
uuid = path.Base(u.Path)
|
2015-01-06 18:37:27 +00:00
|
|
|
checkHeaders(t, resp, http.Header{
|
2015-02-27 00:43:47 +00:00
|
|
|
"Location": []string{"*"},
|
|
|
|
"Content-Length": []string{"0"},
|
|
|
|
"Docker-Upload-UUID": []string{uuid},
|
2015-01-06 18:37:27 +00:00
|
|
|
})
|
|
|
|
|
2015-02-27 00:43:47 +00:00
|
|
|
return resp.Header.Get("Location"), uuid
|
2015-01-06 18:37:27 +00:00
|
|
|
}
|
|
|
|
|
2015-01-30 05:26:35 +00:00
|
|
|
// doPushLayer pushes the layer content returning the url on success returning
|
|
|
|
// the response. If you're only expecting a successful response, use pushLayer.
|
2015-12-15 22:35:23 +00:00
|
|
|
func doPushLayer(t *testing.T, ub *v2.URLBuilder, name reference.Named, dgst digest.Digest, uploadURLBase string, body io.Reader) (*http.Response, error) {
|
2015-01-06 18:37:27 +00:00
|
|
|
u, err := url.Parse(uploadURLBase)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error parsing pushLayer url: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
u.RawQuery = url.Values{
|
|
|
|
"_state": u.Query()["_state"],
|
|
|
|
"digest": []string{dgst.String()},
|
|
|
|
}.Encode()
|
|
|
|
|
|
|
|
uploadURL := u.String()
|
|
|
|
|
|
|
|
// Just do a monolithic upload
|
2015-01-30 05:26:35 +00:00
|
|
|
req, err := http.NewRequest("PUT", uploadURL, body)
|
2015-01-06 18:37:27 +00:00
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error creating new request: %v", err)
|
|
|
|
}
|
|
|
|
|
2015-01-30 05:26:35 +00:00
|
|
|
return http.DefaultClient.Do(req)
|
|
|
|
}
|
|
|
|
|
|
|
|
// pushLayer pushes the layer content returning the url on success.
|
2015-12-15 22:35:23 +00:00
|
|
|
func pushLayer(t *testing.T, ub *v2.URLBuilder, name reference.Named, dgst digest.Digest, uploadURLBase string, body io.Reader) string {
|
2015-05-22 01:44:08 +00:00
|
|
|
digester := digest.Canonical.New()
|
2015-03-05 04:26:56 +00:00
|
|
|
|
2015-05-21 06:44:08 +00:00
|
|
|
resp, err := doPushLayer(t, ub, name, dgst, uploadURLBase, io.TeeReader(body, digester.Hash()))
|
2015-01-06 18:37:27 +00:00
|
|
|
if err != nil {
|
2015-01-30 05:26:35 +00:00
|
|
|
t.Fatalf("unexpected error doing push layer request: %v", err)
|
2015-01-06 18:37:27 +00:00
|
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
checkResponse(t, "putting monolithic chunk", resp, http.StatusCreated)
|
|
|
|
|
2015-03-05 04:26:56 +00:00
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error generating sha256 digest of body")
|
|
|
|
}
|
|
|
|
|
|
|
|
sha256Dgst := digester.Digest()
|
|
|
|
|
2015-12-16 00:43:13 +00:00
|
|
|
ref, _ := reference.WithDigest(name, sha256Dgst)
|
|
|
|
expectedLayerURL, err := ub.BuildBlobURL(ref)
|
2015-01-06 18:37:27 +00:00
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error building expected layer url: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
checkHeaders(t, resp, http.Header{
|
2015-02-26 23:47:04 +00:00
|
|
|
"Location": []string{expectedLayerURL},
|
|
|
|
"Content-Length": []string{"0"},
|
2015-03-05 04:26:56 +00:00
|
|
|
"Docker-Content-Digest": []string{sha256Dgst.String()},
|
2015-01-06 18:37:27 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
return resp.Header.Get("Location")
|
|
|
|
}
|
|
|
|
|
2015-12-15 22:35:23 +00:00
|
|
|
func finishUpload(t *testing.T, ub *v2.URLBuilder, name reference.Named, uploadURLBase string, dgst digest.Digest) string {
|
2015-05-04 15:56:37 +00:00
|
|
|
resp, err := doPushLayer(t, ub, name, dgst, uploadURLBase, nil)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error doing push layer request: %v", err)
|
|
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
checkResponse(t, "putting monolithic chunk", resp, http.StatusCreated)
|
|
|
|
|
2015-12-16 00:43:13 +00:00
|
|
|
ref, _ := reference.WithDigest(name, dgst)
|
|
|
|
expectedLayerURL, err := ub.BuildBlobURL(ref)
|
2015-05-04 15:56:37 +00:00
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error building expected layer url: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
checkHeaders(t, resp, http.Header{
|
|
|
|
"Location": []string{expectedLayerURL},
|
|
|
|
"Content-Length": []string{"0"},
|
|
|
|
"Docker-Content-Digest": []string{dgst.String()},
|
|
|
|
})
|
|
|
|
|
|
|
|
return resp.Header.Get("Location")
|
|
|
|
}
|
|
|
|
|
|
|
|
func doPushChunk(t *testing.T, uploadURLBase string, body io.Reader) (*http.Response, digest.Digest, error) {
|
|
|
|
u, err := url.Parse(uploadURLBase)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error parsing pushLayer url: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
u.RawQuery = url.Values{
|
|
|
|
"_state": u.Query()["_state"],
|
|
|
|
}.Encode()
|
|
|
|
|
|
|
|
uploadURL := u.String()
|
|
|
|
|
2015-05-22 01:44:08 +00:00
|
|
|
digester := digest.Canonical.New()
|
2015-05-04 15:56:37 +00:00
|
|
|
|
2015-05-21 06:44:08 +00:00
|
|
|
req, err := http.NewRequest("PATCH", uploadURL, io.TeeReader(body, digester.Hash()))
|
2015-05-04 15:56:37 +00:00
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error creating new request: %v", err)
|
|
|
|
}
|
|
|
|
req.Header.Set("Content-Type", "application/octet-stream")
|
|
|
|
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
|
|
|
|
|
|
return resp, digester.Digest(), err
|
|
|
|
}
|
|
|
|
|
2015-12-15 22:35:23 +00:00
|
|
|
func pushChunk(t *testing.T, ub *v2.URLBuilder, name reference.Named, uploadURLBase string, body io.Reader, length int64) (string, digest.Digest) {
|
2015-05-04 15:56:37 +00:00
|
|
|
resp, dgst, err := doPushChunk(t, uploadURLBase, body)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error doing push layer request: %v", err)
|
|
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
checkResponse(t, "putting chunk", resp, http.StatusAccepted)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error generating sha256 digest of body")
|
|
|
|
}
|
|
|
|
|
|
|
|
checkHeaders(t, resp, http.Header{
|
|
|
|
"Range": []string{fmt.Sprintf("0-%d", length-1)},
|
|
|
|
"Content-Length": []string{"0"},
|
|
|
|
})
|
|
|
|
|
|
|
|
return resp.Header.Get("Location"), dgst
|
|
|
|
}
|
|
|
|
|
2015-01-06 18:37:27 +00:00
|
|
|
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()
|
|
|
|
}
|
2015-08-10 21:20:52 +00:00
|
|
|
|
2015-08-18 20:33:26 +00:00
|
|
|
// We expect the headers included in the configuration, unless the
|
|
|
|
// status code is 405 (Method Not Allowed), which means the handler
|
|
|
|
// doesn't even get called.
|
|
|
|
if resp.StatusCode != 405 && !reflect.DeepEqual(resp.Header["X-Content-Type-Options"], []string{"nosniff"}) {
|
2015-08-10 21:20:52 +00:00
|
|
|
t.Logf("missing or incorrect header X-Content-Type-Options %s", msg)
|
|
|
|
maybeDumpResponse(t, resp)
|
2015-01-06 18:37:27 +00:00
|
|
|
|
|
|
|
t.FailNow()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-01-30 05:26:35 +00:00
|
|
|
// checkBodyHasErrorCodes ensures the body is an error body and has the
|
|
|
|
// expected error codes, returning the error structure, the json slice and a
|
|
|
|
// count of the errors by code.
|
2015-05-15 01:21:39 +00:00
|
|
|
func checkBodyHasErrorCodes(t *testing.T, msg string, resp *http.Response, errorCodes ...errcode.ErrorCode) (errcode.Errors, []byte, map[errcode.ErrorCode]int) {
|
2015-01-30 05:26:35 +00:00
|
|
|
p, err := ioutil.ReadAll(resp.Body)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error reading body %s: %v", msg, err)
|
|
|
|
}
|
|
|
|
|
2015-05-15 01:21:39 +00:00
|
|
|
var errs errcode.Errors
|
2015-01-30 05:26:35 +00:00
|
|
|
if err := json.Unmarshal(p, &errs); err != nil {
|
|
|
|
t.Fatalf("unexpected error decoding error response: %v", err)
|
|
|
|
}
|
|
|
|
|
2015-05-27 00:18:32 +00:00
|
|
|
if len(errs) == 0 {
|
2015-01-30 05:26:35 +00:00
|
|
|
t.Fatalf("expected errors in response")
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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; charset=utf-8" {
|
|
|
|
// t.Fatalf("unexpected content type: %v != 'application/json'",
|
|
|
|
// resp.Header.Get("Content-Type"))
|
|
|
|
// }
|
|
|
|
|
2015-05-15 01:21:39 +00:00
|
|
|
expected := map[errcode.ErrorCode]struct{}{}
|
|
|
|
counts := map[errcode.ErrorCode]int{}
|
2015-01-30 05:26:35 +00:00
|
|
|
|
|
|
|
// Initialize map with zeros for expected
|
|
|
|
for _, code := range errorCodes {
|
|
|
|
expected[code] = struct{}{}
|
|
|
|
counts[code] = 0
|
|
|
|
}
|
|
|
|
|
2015-06-03 13:52:39 +00:00
|
|
|
for _, e := range errs {
|
|
|
|
err, ok := e.(errcode.ErrorCoder)
|
|
|
|
if !ok {
|
|
|
|
t.Fatalf("not an ErrorCoder: %#v", e)
|
2015-01-30 05:26:35 +00:00
|
|
|
}
|
2015-06-03 13:52:39 +00:00
|
|
|
if _, ok := expected[err.ErrorCode()]; !ok {
|
|
|
|
t.Fatalf("unexpected error code %v encountered during %s: %s ", err.ErrorCode(), msg, string(p))
|
|
|
|
}
|
|
|
|
counts[err.ErrorCode()]++
|
2015-01-30 05:26:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Ensure that counts of expected errors were all non-zero
|
|
|
|
for code := range expected {
|
|
|
|
if counts[code] == 0 {
|
2015-02-02 21:01:49 +00:00
|
|
|
t.Fatalf("expected error code %v not encounterd during %s: %s", code, msg, string(p))
|
2015-01-30 05:26:35 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return errs, p, counts
|
|
|
|
}
|
|
|
|
|
2015-01-06 18:37:27 +00:00
|
|
|
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.
|
2015-07-24 06:03:13 +00:00
|
|
|
if len(resp.Header[http.CanonicalHeaderKey(k)]) > 0 {
|
2015-01-06 18:37:27 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-07-24 06:03:13 +00:00
|
|
|
for _, hv := range resp.Header[http.CanonicalHeaderKey(k)] {
|
2015-01-06 18:37:27 +00:00
|
|
|
if hv != v {
|
2015-05-27 17:52:22 +00:00
|
|
|
t.Fatalf("%+v %v header value not matched in response: %q != %q", resp.Header, k, hv, v)
|
2015-01-06 18:37:27 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2015-02-26 23:47:04 +00:00
|
|
|
|
|
|
|
func checkErr(t *testing.T, err error, msg string) {
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error %s: %v", msg, err)
|
|
|
|
}
|
|
|
|
}
|
2015-07-13 20:08:13 +00:00
|
|
|
|
2015-08-21 04:50:15 +00:00
|
|
|
func createRepository(env *testEnv, t *testing.T, imageName string, tag string) digest.Digest {
|
2015-12-15 22:35:23 +00:00
|
|
|
imageNameRef, err := reference.ParseNamed(imageName)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unable to parse reference: %v", err)
|
|
|
|
}
|
|
|
|
|
2015-08-21 04:24:30 +00:00
|
|
|
unsignedManifest := &schema1.Manifest{
|
2015-07-13 20:08:13 +00:00
|
|
|
Versioned: manifest.Versioned{
|
|
|
|
SchemaVersion: 1,
|
|
|
|
},
|
|
|
|
Name: imageName,
|
|
|
|
Tag: tag,
|
2015-08-21 04:24:30 +00:00
|
|
|
FSLayers: []schema1.FSLayer{
|
2015-07-13 20:08:13 +00:00
|
|
|
{
|
|
|
|
BlobSum: "asdf",
|
|
|
|
},
|
2015-11-03 19:03:17 +00:00
|
|
|
},
|
|
|
|
History: []schema1.History{
|
2015-07-13 20:08:13 +00:00
|
|
|
{
|
2015-11-03 19:03:17 +00:00
|
|
|
V1Compatibility: "",
|
2015-07-13 20:08:13 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
2016-02-23 01:49:23 +00:00
|
|
|
uploadURLBase, _ := startPushLayer(t, env, imageNameRef)
|
2015-12-15 22:35:23 +00:00
|
|
|
pushLayer(t, env.builder, imageNameRef, dgst, uploadURLBase, rs)
|
2015-07-13 20:08:13 +00:00
|
|
|
}
|
|
|
|
|
2015-08-21 04:24:30 +00:00
|
|
|
signedManifest, err := schema1.Sign(unsignedManifest, env.pk)
|
2015-07-13 20:08:13 +00:00
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error signing manifest: %v", err)
|
|
|
|
}
|
|
|
|
|
2015-08-21 04:50:15 +00:00
|
|
|
dgst := digest.FromBytes(signedManifest.Canonical)
|
2015-07-13 20:08:13 +00:00
|
|
|
|
2015-08-21 04:50:15 +00:00
|
|
|
// Create this repository by tag to ensure the tag mapping is made in the registry
|
2015-12-16 00:43:13 +00:00
|
|
|
tagRef, _ := reference.WithTag(imageNameRef, tag)
|
|
|
|
manifestDigestURL, err := env.builder.BuildManifestURL(tagRef)
|
2015-07-13 20:08:13 +00:00
|
|
|
checkErr(t, err, "building manifest url")
|
|
|
|
|
2015-12-16 00:43:13 +00:00
|
|
|
digestRef, _ := reference.WithDigest(imageNameRef, dgst)
|
|
|
|
location, err := env.builder.BuildManifestURL(digestRef)
|
2015-08-21 04:50:15 +00:00
|
|
|
checkErr(t, err, "building location URL")
|
|
|
|
|
2015-12-16 22:30:49 +00:00
|
|
|
resp := putManifest(t, "putting signed manifest", manifestDigestURL, "", signedManifest)
|
2015-07-28 17:59:11 +00:00
|
|
|
checkResponse(t, "putting signed manifest", resp, http.StatusCreated)
|
2015-07-13 20:08:13 +00:00
|
|
|
checkHeaders(t, resp, http.Header{
|
2015-08-21 04:50:15 +00:00
|
|
|
"Location": []string{location},
|
2015-07-13 20:08:13 +00:00
|
|
|
"Docker-Content-Digest": []string{dgst.String()},
|
|
|
|
})
|
2015-08-21 04:50:15 +00:00
|
|
|
return dgst
|
2015-07-13 20:08:13 +00:00
|
|
|
}
|
2015-08-11 18:00:30 +00:00
|
|
|
|
|
|
|
// Test mutation operations on a registry configured as a cache. Ensure that they return
|
|
|
|
// appropriate errors.
|
|
|
|
func TestRegistryAsCacheMutationAPIs(t *testing.T) {
|
|
|
|
deleteEnabled := true
|
|
|
|
env := newTestEnvMirror(t, deleteEnabled)
|
|
|
|
|
2015-12-15 22:35:23 +00:00
|
|
|
imageName, _ := reference.ParseNamed("foo/bar")
|
2015-08-11 18:00:30 +00:00
|
|
|
tag := "latest"
|
2015-12-16 00:43:13 +00:00
|
|
|
tagRef, _ := reference.WithTag(imageName, tag)
|
|
|
|
manifestURL, err := env.builder.BuildManifestURL(tagRef)
|
2015-08-11 18:00:30 +00:00
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error building base url: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Manifest upload
|
2015-09-15 04:12:33 +00:00
|
|
|
m := &schema1.Manifest{
|
2015-08-11 18:00:30 +00:00
|
|
|
Versioned: manifest.Versioned{
|
|
|
|
SchemaVersion: 1,
|
|
|
|
},
|
2015-12-15 22:35:23 +00:00
|
|
|
Name: imageName.Name(),
|
2015-08-11 18:00:30 +00:00
|
|
|
Tag: tag,
|
2015-08-21 04:24:30 +00:00
|
|
|
FSLayers: []schema1.FSLayer{},
|
2015-11-03 19:03:17 +00:00
|
|
|
History: []schema1.History{},
|
2015-08-11 18:00:30 +00:00
|
|
|
}
|
2015-09-15 04:12:33 +00:00
|
|
|
|
|
|
|
sm, err := schema1.Sign(m, env.pk)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("error signing manifest: %v", err)
|
|
|
|
}
|
|
|
|
|
2015-12-16 22:30:49 +00:00
|
|
|
resp := putManifest(t, "putting unsigned manifest", manifestURL, "", sm)
|
2015-08-11 18:00:30 +00:00
|
|
|
checkResponse(t, "putting signed manifest to cache", resp, errcode.ErrorCodeUnsupported.Descriptor().HTTPStatusCode)
|
|
|
|
|
|
|
|
// Manifest Delete
|
|
|
|
resp, err = httpDelete(manifestURL)
|
|
|
|
checkResponse(t, "deleting signed manifest from cache", resp, errcode.ErrorCodeUnsupported.Descriptor().HTTPStatusCode)
|
|
|
|
|
|
|
|
// Blob upload initialization
|
|
|
|
layerUploadURL, err := env.builder.BuildBlobUploadURL(imageName)
|
|
|
|
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("starting layer push to cache %v", imageName), resp, errcode.ErrorCodeUnsupported.Descriptor().HTTPStatusCode)
|
|
|
|
|
|
|
|
// Blob Delete
|
2015-12-16 00:43:13 +00:00
|
|
|
ref, _ := reference.WithDigest(imageName, digest.DigestSha256EmptyTar)
|
|
|
|
blobURL, err := env.builder.BuildBlobURL(ref)
|
2015-08-11 18:00:30 +00:00
|
|
|
resp, err = httpDelete(blobURL)
|
|
|
|
checkResponse(t, "deleting blob from cache", resp, errcode.ErrorCodeUnsupported.Descriptor().HTTPStatusCode)
|
|
|
|
|
|
|
|
}
|
2015-08-19 18:37:53 +00:00
|
|
|
|
|
|
|
// TestCheckContextNotifier makes sure the API endpoints get a ResponseWriter
|
|
|
|
// that implements http.ContextNotifier.
|
|
|
|
func TestCheckContextNotifier(t *testing.T) {
|
|
|
|
env := newTestEnv(t, false)
|
|
|
|
|
|
|
|
// Register a new endpoint for testing
|
|
|
|
env.app.router.Handle("/unittest/{name}/", env.app.dispatcher(func(ctx *Context, r *http.Request) http.Handler {
|
|
|
|
return handlers.MethodHandler{
|
|
|
|
"GET": http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
if _, ok := w.(http.CloseNotifier); !ok {
|
|
|
|
t.Fatal("could not cast ResponseWriter to CloseNotifier")
|
|
|
|
}
|
|
|
|
w.WriteHeader(200)
|
|
|
|
}),
|
|
|
|
}
|
|
|
|
}))
|
|
|
|
|
|
|
|
resp, err := http.Get(env.server.URL + "/unittest/reponame/")
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error issuing request: %v", err)
|
|
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
if resp.StatusCode != 200 {
|
|
|
|
t.Fatalf("wrong status code - expected 200, got %d", resp.StatusCode)
|
|
|
|
}
|
|
|
|
}
|
2015-08-21 04:50:15 +00:00
|
|
|
|
|
|
|
func TestProxyManifestGetByTag(t *testing.T) {
|
|
|
|
truthConfig := configuration.Configuration{
|
|
|
|
Storage: configuration.Storage{
|
2016-05-06 09:46:37 +00:00
|
|
|
"testdriver": configuration.Parameters{},
|
2015-08-21 04:50:15 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
truthConfig.HTTP.Headers = headerConfig
|
|
|
|
|
2015-12-15 22:35:23 +00:00
|
|
|
imageName, _ := reference.ParseNamed("foo/bar")
|
2015-08-21 04:50:15 +00:00
|
|
|
tag := "latest"
|
|
|
|
|
|
|
|
truthEnv := newTestEnvWithConfig(t, &truthConfig)
|
|
|
|
// create a repository in the truth registry
|
2015-12-15 22:35:23 +00:00
|
|
|
dgst := createRepository(truthEnv, t, imageName.Name(), tag)
|
2015-08-21 04:50:15 +00:00
|
|
|
|
|
|
|
proxyConfig := configuration.Configuration{
|
|
|
|
Storage: configuration.Storage{
|
2016-05-06 09:46:37 +00:00
|
|
|
"testdriver": configuration.Parameters{},
|
2015-08-21 04:50:15 +00:00
|
|
|
},
|
|
|
|
Proxy: configuration.Proxy{
|
|
|
|
RemoteURL: truthEnv.server.URL,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
proxyConfig.HTTP.Headers = headerConfig
|
|
|
|
|
|
|
|
proxyEnv := newTestEnvWithConfig(t, &proxyConfig)
|
|
|
|
|
2015-12-16 00:43:13 +00:00
|
|
|
digestRef, _ := reference.WithDigest(imageName, dgst)
|
|
|
|
manifestDigestURL, err := proxyEnv.builder.BuildManifestURL(digestRef)
|
2015-08-21 04:50:15 +00:00
|
|
|
checkErr(t, err, "building manifest url")
|
|
|
|
|
|
|
|
resp, err := http.Get(manifestDigestURL)
|
|
|
|
checkErr(t, err, "fetching manifest from proxy by digest")
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
2015-12-16 00:43:13 +00:00
|
|
|
tagRef, _ := reference.WithTag(imageName, tag)
|
|
|
|
manifestTagURL, err := proxyEnv.builder.BuildManifestURL(tagRef)
|
2015-08-21 04:50:15 +00:00
|
|
|
checkErr(t, err, "building manifest url")
|
|
|
|
|
|
|
|
resp, err = http.Get(manifestTagURL)
|
|
|
|
checkErr(t, err, "fetching manifest from proxy by tag")
|
|
|
|
defer resp.Body.Close()
|
|
|
|
checkResponse(t, "fetching manifest from proxy by tag", resp, http.StatusOK)
|
|
|
|
checkHeaders(t, resp, http.Header{
|
|
|
|
"Docker-Content-Digest": []string{dgst.String()},
|
|
|
|
})
|
|
|
|
|
|
|
|
// Create another manifest in the remote with the same image/tag pair
|
2015-12-15 22:35:23 +00:00
|
|
|
newDigest := createRepository(truthEnv, t, imageName.Name(), tag)
|
2015-08-21 04:50:15 +00:00
|
|
|
if dgst == newDigest {
|
|
|
|
t.Fatalf("non-random test data")
|
|
|
|
}
|
|
|
|
|
|
|
|
// fetch it with the same proxy URL as before. Ensure the updated content is at the same tag
|
|
|
|
resp, err = http.Get(manifestTagURL)
|
|
|
|
checkErr(t, err, "fetching manifest from proxy by tag")
|
|
|
|
defer resp.Body.Close()
|
|
|
|
checkResponse(t, "fetching manifest from proxy by tag", resp, http.StatusOK)
|
|
|
|
checkHeaders(t, resp, http.Header{
|
|
|
|
"Docker-Content-Digest": []string{newDigest.String()},
|
|
|
|
})
|
|
|
|
}
|