mirror of
https://github.com/regclient/regclient.git
synced 2025-04-18 22:44:00 +03:00
I feel like I need to explain, this is all to move the descriptor package. The platform package could not use the predefined errors in types because of a circular dependency from descriptor. The most appropriate way to reorg this is to move descriptor out of the type package since it was more complex than a self contained type. When doing that, type aliases were needed to avoid breaking changes to existing users. Those aliases themselves caused circular dependency loops because of the media types and errors, so those were also pulled out to separate packages. All of the old values were aliased and deprecated, and to fix the linter, those deprecations were fixed by updating the imports... everywhere. Signed-off-by: Brandon Mitchell <git@bmitch.net>
1351 lines
37 KiB
Go
1351 lines
37 KiB
Go
package reg
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/opencontainers/go-digest"
|
|
"github.com/sirupsen/logrus"
|
|
|
|
"github.com/regclient/regclient/config"
|
|
"github.com/regclient/regclient/internal/reqresp"
|
|
"github.com/regclient/regclient/types/descriptor"
|
|
"github.com/regclient/regclient/types/errs"
|
|
"github.com/regclient/regclient/types/ref"
|
|
)
|
|
|
|
func TestBlobGet(t *testing.T) {
|
|
t.Parallel()
|
|
blobRepo := "/proj/repo"
|
|
externalRepo := "/proj/external"
|
|
privateRepo := "/proj/private"
|
|
ctx := context.Background()
|
|
// include a random blob
|
|
seed := time.Now().UTC().Unix()
|
|
t.Logf("Using seed %d", seed)
|
|
blobLen := 1024 // must be greater than 512 for retry test
|
|
d1, blob1 := reqresp.NewRandomBlob(blobLen, seed)
|
|
d2, blob2 := reqresp.NewRandomBlob(blobLen, seed+1)
|
|
dMissing := digest.FromBytes([]byte("missing"))
|
|
// define req/resp entries
|
|
rrs := []reqresp.ReqResp{
|
|
// head
|
|
{
|
|
ReqEntry: reqresp.ReqEntry{
|
|
Name: "HEAD for d1",
|
|
Method: "HEAD",
|
|
Path: "/v2" + blobRepo + "/blobs/" + d1.String(),
|
|
},
|
|
RespEntry: reqresp.RespEntry{
|
|
Status: http.StatusOK,
|
|
Headers: http.Header{
|
|
"Content-Length": {fmt.Sprintf("%d", blobLen)},
|
|
"Content-Type": {"application/octet-stream"},
|
|
"Docker-Content-Digest": {d1.String()},
|
|
},
|
|
},
|
|
},
|
|
// get
|
|
{
|
|
ReqEntry: reqresp.ReqEntry{
|
|
Name: "GET for d1",
|
|
Method: "GET",
|
|
Path: "/v2" + blobRepo + "/blobs/" + d1.String(),
|
|
},
|
|
RespEntry: reqresp.RespEntry{
|
|
Status: http.StatusOK,
|
|
Body: blob1,
|
|
Headers: http.Header{
|
|
"Content-Length": {fmt.Sprintf("%d", blobLen)},
|
|
"Content-Type": {"application/octet-stream"},
|
|
"Docker-Content-Digest": {d1.String()},
|
|
},
|
|
},
|
|
},
|
|
// head
|
|
{
|
|
ReqEntry: reqresp.ReqEntry{
|
|
Name: "external direct HEAD for d1",
|
|
Method: "HEAD",
|
|
Path: "/v2" + externalRepo + "/blobs/" + d1.String(),
|
|
},
|
|
RespEntry: reqresp.RespEntry{
|
|
Status: http.StatusNotFound,
|
|
},
|
|
},
|
|
// get
|
|
{
|
|
ReqEntry: reqresp.ReqEntry{
|
|
Name: "external direct GET for d1",
|
|
Method: "GET",
|
|
Path: "/v2" + externalRepo + "/blobs/" + d1.String(),
|
|
},
|
|
RespEntry: reqresp.RespEntry{
|
|
Status: http.StatusNotFound,
|
|
},
|
|
}, // external head
|
|
{
|
|
ReqEntry: reqresp.ReqEntry{
|
|
Name: "external HEAD for d1",
|
|
Method: "HEAD",
|
|
Path: "/external/" + d1.String(),
|
|
},
|
|
RespEntry: reqresp.RespEntry{
|
|
Status: http.StatusOK,
|
|
Headers: http.Header{
|
|
"Content-Length": {fmt.Sprintf("%d", blobLen)},
|
|
"Content-Type": {"application/octet-stream"},
|
|
},
|
|
},
|
|
},
|
|
// external get
|
|
{
|
|
ReqEntry: reqresp.ReqEntry{
|
|
Name: "external GET for d1",
|
|
Method: "GET",
|
|
Path: "/external/" + d1.String(),
|
|
},
|
|
RespEntry: reqresp.RespEntry{
|
|
Status: http.StatusOK,
|
|
Body: blob1,
|
|
Headers: http.Header{
|
|
"Content-Length": {fmt.Sprintf("%d", blobLen)},
|
|
"Content-Type": {"application/octet-stream"},
|
|
},
|
|
},
|
|
},
|
|
// missing
|
|
{
|
|
ReqEntry: reqresp.ReqEntry{
|
|
Name: "GET Missing",
|
|
Method: "GET",
|
|
Path: "/v2" + blobRepo + "/blobs/" + dMissing.String(),
|
|
},
|
|
RespEntry: reqresp.RespEntry{
|
|
Status: http.StatusNotFound,
|
|
},
|
|
},
|
|
// TODO: test unauthorized
|
|
// TODO: test range read
|
|
// head for d2
|
|
{
|
|
ReqEntry: reqresp.ReqEntry{
|
|
Name: "HEAD for d2",
|
|
Method: "HEAD",
|
|
Path: "/v2" + blobRepo + "/blobs/" + d2.String(),
|
|
},
|
|
RespEntry: reqresp.RespEntry{
|
|
Status: http.StatusOK,
|
|
Headers: http.Header{
|
|
"Accept-Ranges": {"bytes"},
|
|
"Content-Length": {fmt.Sprintf("%d", blobLen)},
|
|
"Content-Type": {"application/octet-stream"},
|
|
"Docker-Content-Digest": {d2.String()},
|
|
},
|
|
},
|
|
},
|
|
// get range
|
|
{
|
|
ReqEntry: reqresp.ReqEntry{
|
|
Name: "GET for d2, range for second part",
|
|
Method: "GET",
|
|
Path: "/v2" + blobRepo + "/blobs/" + d2.String(),
|
|
Headers: http.Header{
|
|
"Range": {fmt.Sprintf("bytes=512-%d", blobLen)},
|
|
},
|
|
},
|
|
RespEntry: reqresp.RespEntry{
|
|
Status: http.StatusOK,
|
|
Body: blob2[512:],
|
|
Headers: http.Header{
|
|
"Content-Length": {fmt.Sprintf("%d", blobLen-512)},
|
|
"Content-Range": {fmt.Sprintf("bytes %d-%d/%d", 512, blobLen, blobLen)},
|
|
"Content-Type": {"application/octet-stream"},
|
|
"Docker-Content-Digest": {d2.String()},
|
|
},
|
|
},
|
|
},
|
|
// get that stops early
|
|
{
|
|
ReqEntry: reqresp.ReqEntry{
|
|
Name: "GET for d2, short read",
|
|
Method: "GET",
|
|
Path: "/v2" + blobRepo + "/blobs/" + d2.String(),
|
|
},
|
|
RespEntry: reqresp.RespEntry{
|
|
Status: http.StatusOK,
|
|
Body: blob2[0:512],
|
|
Headers: http.Header{
|
|
"Content-Length": {fmt.Sprintf("%d", blobLen)},
|
|
"Content-Type": {"application/octet-stream"},
|
|
"Docker-Content-Digest": {d2.String()},
|
|
},
|
|
},
|
|
},
|
|
// forbidden
|
|
{
|
|
ReqEntry: reqresp.ReqEntry{
|
|
Name: "GET Forbidden",
|
|
Method: "GET",
|
|
Path: "/v2" + privateRepo + "/blobs/" + d1.String(),
|
|
},
|
|
RespEntry: reqresp.RespEntry{
|
|
Status: http.StatusForbidden,
|
|
},
|
|
},
|
|
}
|
|
rrs = append(rrs, reqresp.BaseEntries...)
|
|
// create a server
|
|
ts := httptest.NewServer(reqresp.NewHandler(t, rrs))
|
|
defer ts.Close()
|
|
// setup the reg
|
|
tsURL, _ := url.Parse(ts.URL)
|
|
tsHost := tsURL.Host
|
|
rcHosts := []*config.Host{
|
|
{
|
|
Name: tsHost,
|
|
Hostname: tsHost,
|
|
TLS: config.TLSDisabled,
|
|
},
|
|
}
|
|
log := &logrus.Logger{
|
|
Out: os.Stderr,
|
|
Formatter: new(logrus.TextFormatter),
|
|
Hooks: make(logrus.LevelHooks),
|
|
Level: logrus.WarnLevel,
|
|
}
|
|
delayInit, _ := time.ParseDuration("0.05s")
|
|
delayMax, _ := time.ParseDuration("0.10s")
|
|
reg := New(
|
|
WithConfigHosts(rcHosts),
|
|
WithLog(log),
|
|
WithDelay(delayInit, delayMax),
|
|
)
|
|
|
|
// Test successful blob
|
|
t.Run("Get", func(t *testing.T) {
|
|
r, err := ref.New(tsURL.Host + blobRepo)
|
|
if err != nil {
|
|
t.Fatalf("Failed creating ref: %v", err)
|
|
}
|
|
br, err := reg.BlobGet(ctx, r, descriptor.Descriptor{Digest: d1})
|
|
if err != nil {
|
|
t.Fatalf("Failed running BlobGet: %v", err)
|
|
}
|
|
defer br.Close()
|
|
brBlob, err := io.ReadAll(br)
|
|
if err != nil {
|
|
t.Fatalf("Failed reading blob: %v", err)
|
|
}
|
|
if !bytes.Equal(blob1, brBlob) {
|
|
t.Errorf("Blob does not match")
|
|
}
|
|
})
|
|
|
|
t.Run("Head", func(t *testing.T) {
|
|
r, err := ref.New(tsURL.Host + blobRepo)
|
|
if err != nil {
|
|
t.Fatalf("Failed creating ref: %v", err)
|
|
}
|
|
br, err := reg.BlobHead(ctx, r, descriptor.Descriptor{Digest: d1})
|
|
if err != nil {
|
|
t.Fatalf("Failed running BlobHead: %v", err)
|
|
}
|
|
defer br.Close()
|
|
if br.GetDescriptor().Size != int64(blobLen) {
|
|
t.Errorf("Failed comparing blob length")
|
|
}
|
|
})
|
|
|
|
// Test successful blob
|
|
t.Run("External Get", func(t *testing.T) {
|
|
r, err := ref.New(tsURL.Host + externalRepo)
|
|
if err != nil {
|
|
t.Fatalf("Failed creating ref: %v", err)
|
|
}
|
|
br, err := reg.BlobGet(ctx, r, descriptor.Descriptor{Digest: d1, URLs: []string{tsURL.Scheme + "://" + tsURL.Host + "/external/" + d1.String()}})
|
|
if err != nil {
|
|
t.Fatalf("Failed running external BlobGet: %v", err)
|
|
}
|
|
defer br.Close()
|
|
brBlob, err := io.ReadAll(br)
|
|
if err != nil {
|
|
t.Fatalf("Failed reading external blob: %v", err)
|
|
}
|
|
if !bytes.Equal(blob1, brBlob) {
|
|
t.Errorf("External blob does not match")
|
|
}
|
|
})
|
|
|
|
t.Run("External Head", func(t *testing.T) {
|
|
r, err := ref.New(tsURL.Host + externalRepo)
|
|
if err != nil {
|
|
t.Fatalf("Failed creating ref: %v", err)
|
|
}
|
|
br, err := reg.BlobHead(ctx, r, descriptor.Descriptor{Digest: d1, URLs: []string{tsURL.Scheme + "://" + tsURL.Host + "/external/" + d1.String()}})
|
|
if err != nil {
|
|
t.Fatalf("Failed running external BlobHead: %v", err)
|
|
}
|
|
defer br.Close()
|
|
if br.GetDescriptor().Size != int64(blobLen) {
|
|
t.Errorf("Failed comparing external blob length")
|
|
}
|
|
})
|
|
|
|
t.Run("Missing", func(t *testing.T) {
|
|
r, err := ref.New(tsURL.Host + blobRepo)
|
|
if err != nil {
|
|
t.Fatalf("Failed creating ref: %v", err)
|
|
}
|
|
br, err := reg.BlobGet(ctx, r, descriptor.Descriptor{Digest: dMissing})
|
|
if err == nil {
|
|
defer br.Close()
|
|
t.Fatalf("Unexpected success running BlobGet")
|
|
}
|
|
if !errors.Is(err, errs.ErrNotFound) {
|
|
t.Errorf("Error does not match \"ErrNotFound\": %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("Retry", func(t *testing.T) {
|
|
r, err := ref.New(tsURL.Host + blobRepo)
|
|
if err != nil {
|
|
t.Fatalf("Failed creating ref: %v", err)
|
|
}
|
|
br, err := reg.BlobGet(ctx, r, descriptor.Descriptor{Digest: d2})
|
|
if err != nil {
|
|
t.Fatalf("Failed running BlobGet: %v", err)
|
|
}
|
|
defer br.Close()
|
|
brBlob, err := io.ReadAll(br)
|
|
if err != nil {
|
|
t.Fatalf("Failed reading blob: %v", err)
|
|
}
|
|
if !bytes.Equal(blob2, brBlob) {
|
|
t.Errorf("Blob does not match")
|
|
}
|
|
})
|
|
|
|
t.Run("Forbidden", func(t *testing.T) {
|
|
r, err := ref.New(tsURL.Host + privateRepo)
|
|
if err != nil {
|
|
t.Fatalf("Failed creating ref: %v", err)
|
|
}
|
|
br, err := reg.BlobGet(ctx, r, descriptor.Descriptor{Digest: d1})
|
|
if err == nil {
|
|
defer br.Close()
|
|
t.Fatalf("Unexpected success running BlobGet")
|
|
}
|
|
if !errors.Is(err, errs.ErrHTTPUnauthorized) {
|
|
t.Errorf("Error does not match \"ErrUnauthorized\": %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestBlobPut(t *testing.T) {
|
|
t.Parallel()
|
|
blobRepo := "/proj/repo"
|
|
blobRepo5 := "/proj/repo5"
|
|
blobRepo6 := "/proj/repo6"
|
|
// privateRepo := "/proj/private"
|
|
ctx := context.Background()
|
|
// include a random blob
|
|
seed := time.Now().UTC().Unix()
|
|
t.Logf("Using seed %d", seed)
|
|
blobChunk := 512
|
|
blobLen := 1024 // must be blobChunk < blobLen <= blobChunk * 2
|
|
blobLen3 := 1000 // blob without a full final chunk
|
|
blobLen4 := 2048 // must be blobChunk < blobLen <= blobChunk * 2
|
|
blobLen5 := 500 // single chunk
|
|
d1, blob1 := reqresp.NewRandomBlob(blobLen, seed)
|
|
d2, blob2 := reqresp.NewRandomBlob(blobLen, seed+1)
|
|
d2Bad := digest.Canonical.FromString("digest 2 bad")
|
|
d3, blob3 := reqresp.NewRandomBlob(blobLen3, seed+2)
|
|
d4, blob4 := reqresp.NewRandomBlob(blobLen4, seed+3)
|
|
d5, blob5 := reqresp.NewRandomBlob(blobLen5, seed+4)
|
|
blob6 := []byte{}
|
|
d6 := digest.Canonical.FromBytes(blob6)
|
|
uuid1 := reqresp.NewRandomID(seed + 10)
|
|
uuid2 := reqresp.NewRandomID(seed + 11)
|
|
uuid2Bad := reqresp.NewRandomID(seed + 12)
|
|
uuid3 := reqresp.NewRandomID(seed + 13)
|
|
uuid4 := reqresp.NewRandomID(seed + 14)
|
|
uuid5 := reqresp.NewRandomID(seed + 15)
|
|
uuid6 := reqresp.NewRandomID(seed + 16)
|
|
// dMissing := digest.FromBytes([]byte("missing"))
|
|
user := "testing"
|
|
pass := "password"
|
|
|
|
// create an external blob server (e.g. S3 storage)
|
|
blobRRS := []reqresp.ReqResp{
|
|
{
|
|
ReqEntry: reqresp.ReqEntry{
|
|
Name: "PUT for d1",
|
|
Method: "PUT",
|
|
Path: "/v2" + blobRepo + "/blobs/uploads/" + uuid1,
|
|
Query: map[string][]string{
|
|
"digest": {d1.String()},
|
|
},
|
|
Headers: http.Header{
|
|
"Content-Length": {fmt.Sprintf("%d", len(blob1))},
|
|
"Content-Type": {"application/octet-stream"},
|
|
"Authorization": {fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte(user+":"+pass)))},
|
|
},
|
|
Body: blob1,
|
|
},
|
|
RespEntry: reqresp.RespEntry{
|
|
Status: http.StatusCreated,
|
|
Headers: http.Header{
|
|
"Content-Length": {"0"},
|
|
"Location": {"/v2" + blobRepo + "/blobs/" + d1.String()},
|
|
"Docker-Content-Digest": {d1.String()},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
ReqEntry: reqresp.ReqEntry{
|
|
Name: "PUT for d1 unauth",
|
|
Method: "PUT",
|
|
Path: "/v2" + blobRepo + "/blobs/uploads/" + uuid1,
|
|
Query: map[string][]string{
|
|
"digest": {d1.String()},
|
|
},
|
|
Headers: http.Header{
|
|
"Content-Length": {fmt.Sprintf("%d", len(blob1))},
|
|
"Content-Type": {"application/octet-stream"},
|
|
},
|
|
Body: blob1,
|
|
},
|
|
RespEntry: reqresp.RespEntry{
|
|
Status: http.StatusUnauthorized,
|
|
Headers: http.Header{
|
|
"WWW-Authenticate": {"Basic realm=\"testing\""},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
blobTS := httptest.NewServer(reqresp.NewHandler(t, blobRRS))
|
|
defer blobTS.Close()
|
|
blobURL, _ := url.Parse(blobTS.URL)
|
|
blobHost := blobURL.Host
|
|
|
|
// define req/resp entries
|
|
rrs := []reqresp.ReqResp{
|
|
// get upload location
|
|
{
|
|
ReqEntry: reqresp.ReqEntry{
|
|
Name: "POST for d1",
|
|
Method: "POST",
|
|
Path: "/v2" + blobRepo + "/blobs/uploads/",
|
|
Query: map[string][]string{
|
|
"mount": {d1.String()},
|
|
},
|
|
Headers: http.Header{
|
|
"Authorization": {fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte(user+":"+pass)))},
|
|
},
|
|
},
|
|
RespEntry: reqresp.RespEntry{
|
|
Status: http.StatusAccepted,
|
|
Headers: http.Header{
|
|
"Content-Length": {"0"},
|
|
"Location": {fmt.Sprintf("http://%s/v2%s/blobs/uploads/%s", blobHost, blobRepo, uuid1)},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
ReqEntry: reqresp.ReqEntry{
|
|
Name: "POST for d1 unauth",
|
|
Method: "POST",
|
|
Path: "/v2" + blobRepo + "/blobs/uploads/",
|
|
Query: map[string][]string{
|
|
"mount": {d1.String()},
|
|
},
|
|
},
|
|
RespEntry: reqresp.RespEntry{
|
|
Status: http.StatusUnauthorized,
|
|
Headers: http.Header{
|
|
"Content-Length": {"0"},
|
|
"WWW-Authenticate": {"Basic realm=\"testing\""},
|
|
},
|
|
},
|
|
},
|
|
// get upload2 location
|
|
{
|
|
ReqEntry: reqresp.ReqEntry{
|
|
Name: "POST for d2",
|
|
Method: "POST",
|
|
Path: "/v2" + blobRepo + "/blobs/uploads/",
|
|
Query: map[string][]string{
|
|
"mount": {d2.String()},
|
|
},
|
|
},
|
|
RespEntry: reqresp.RespEntry{
|
|
Status: http.StatusAccepted,
|
|
Headers: http.Header{
|
|
"Content-Length": {"0"},
|
|
"Location": {uuid2},
|
|
},
|
|
},
|
|
},
|
|
// upload put for d2
|
|
{
|
|
ReqEntry: reqresp.ReqEntry{
|
|
DelOnUse: false,
|
|
Name: "PUT for patched d2",
|
|
Method: "PUT",
|
|
Path: "/v2" + blobRepo + "/blobs/uploads/" + uuid2,
|
|
Query: map[string][]string{
|
|
"digest": {d2.String()},
|
|
"chunk": {"3"},
|
|
},
|
|
Headers: http.Header{
|
|
"Content-Length": {"0"},
|
|
"Content-Type": {"application/octet-stream"},
|
|
},
|
|
},
|
|
RespEntry: reqresp.RespEntry{
|
|
Status: http.StatusCreated,
|
|
Headers: http.Header{
|
|
"Content-Length": {"0"},
|
|
"Location": {"/v2" + blobRepo + "/blobs/" + d2.String()},
|
|
"Docker-Content-Digest": {d2.String()},
|
|
},
|
|
},
|
|
},
|
|
// upload patch 2b for d2
|
|
{
|
|
ReqEntry: reqresp.ReqEntry{
|
|
DelOnUse: false,
|
|
Name: "PATCH 2b for d2",
|
|
Method: "PATCH",
|
|
Path: "/v2" + blobRepo + "/blobs/uploads/" + uuid2,
|
|
Query: map[string][]string{
|
|
"chunk": {"2b"},
|
|
},
|
|
Headers: http.Header{
|
|
"Content-Length": {fmt.Sprintf("%d", blobLen-blobChunk-20)},
|
|
"Content-Range": {fmt.Sprintf("%d-%d", blobChunk+20, blobLen-1)},
|
|
"Content-Type": {"application/octet-stream"},
|
|
},
|
|
Body: blob2[blobChunk+20:],
|
|
},
|
|
RespEntry: reqresp.RespEntry{
|
|
Status: http.StatusAccepted,
|
|
Headers: http.Header{
|
|
"Content-Length": {fmt.Sprintf("%d", 0)},
|
|
"Range": {fmt.Sprintf("bytes=0-%d", blobLen-1)},
|
|
"Location": {uuid2 + "?chunk=3"},
|
|
},
|
|
},
|
|
},
|
|
// upload patch 2 for d2
|
|
{
|
|
ReqEntry: reqresp.ReqEntry{
|
|
DelOnUse: false,
|
|
Name: "PATCH 2 for d2",
|
|
Method: "PATCH",
|
|
Path: "/v2" + blobRepo + "/blobs/uploads/" + uuid2,
|
|
Query: map[string][]string{
|
|
"chunk": {"2"},
|
|
},
|
|
Headers: http.Header{
|
|
"Content-Length": {fmt.Sprintf("%d", blobLen-blobChunk)},
|
|
"Content-Range": {fmt.Sprintf("%d-%d", blobChunk, blobLen-1)},
|
|
"Content-Type": {"application/octet-stream"},
|
|
},
|
|
Body: blob2[blobChunk:],
|
|
},
|
|
RespEntry: reqresp.RespEntry{
|
|
Status: http.StatusRequestedRangeNotSatisfiable,
|
|
Headers: http.Header{
|
|
"Content-Length": {fmt.Sprintf("%d", 0)},
|
|
"Range": {fmt.Sprintf("bytes=0-%d", blobChunk+20-1)},
|
|
"Location": {uuid2 + "?chunk=2b"},
|
|
},
|
|
},
|
|
},
|
|
// upload patch 1 for d2
|
|
{
|
|
ReqEntry: reqresp.ReqEntry{
|
|
DelOnUse: false,
|
|
Name: "PATCH 1 for d2",
|
|
Method: "PATCH",
|
|
Path: "/v2" + blobRepo + "/blobs/uploads/" + uuid2,
|
|
Query: map[string][]string{},
|
|
Headers: http.Header{
|
|
"Content-Length": {fmt.Sprintf("%d", blobChunk)},
|
|
"Content-Range": {fmt.Sprintf("0-%d", blobChunk-1)},
|
|
"Content-Type": {"application/octet-stream"},
|
|
},
|
|
Body: blob2[0:blobChunk],
|
|
},
|
|
RespEntry: reqresp.RespEntry{
|
|
Status: http.StatusAccepted,
|
|
Headers: http.Header{
|
|
"Content-Length": {fmt.Sprintf("%d", 0)},
|
|
"Range": {fmt.Sprintf("bytes=0-%d", blobChunk-1)},
|
|
"Location": {uuid2 + "?chunk=2"},
|
|
},
|
|
},
|
|
},
|
|
// upload blob
|
|
{
|
|
ReqEntry: reqresp.ReqEntry{
|
|
DelOnUse: false,
|
|
Name: "PUT for d2",
|
|
Method: "PUT",
|
|
Path: "/v2" + blobRepo + "/blobs/uploads/" + uuid2,
|
|
Query: map[string][]string{
|
|
"digest": {d2.String()},
|
|
},
|
|
Headers: http.Header{
|
|
"Content-Length": {fmt.Sprintf("%d", len(blob2))},
|
|
"Content-Type": {"application/octet-stream"},
|
|
},
|
|
Body: blob2,
|
|
},
|
|
RespEntry: reqresp.RespEntry{
|
|
Status: http.StatusGatewayTimeout,
|
|
Headers: http.Header{
|
|
"Content-Length": {fmt.Sprintf("%d", 0)},
|
|
},
|
|
},
|
|
},
|
|
// cancel the d2 upload
|
|
{
|
|
ReqEntry: reqresp.ReqEntry{
|
|
DelOnUse: false,
|
|
Name: "DELETE for d2",
|
|
Method: "DELETE",
|
|
Path: "/v2" + blobRepo + "/blobs/uploads/" + uuid2,
|
|
},
|
|
RespEntry: reqresp.RespEntry{
|
|
Status: http.StatusAccepted,
|
|
},
|
|
},
|
|
// get upload2 location
|
|
{
|
|
ReqEntry: reqresp.ReqEntry{
|
|
Name: "POST for d2Bad",
|
|
Method: "POST",
|
|
Path: "/v2" + blobRepo + "/blobs/uploads/",
|
|
Query: map[string][]string{
|
|
"mount": {d2Bad.String()},
|
|
},
|
|
},
|
|
RespEntry: reqresp.RespEntry{
|
|
Status: http.StatusAccepted,
|
|
Headers: http.Header{
|
|
"Content-Length": {"0"},
|
|
"Location": {uuid2Bad},
|
|
},
|
|
},
|
|
},
|
|
// upload put for d2Bad
|
|
{
|
|
ReqEntry: reqresp.ReqEntry{
|
|
DelOnUse: false,
|
|
Name: "PUT for patched d2Bad",
|
|
Method: "PUT",
|
|
Path: "/v2" + blobRepo + "/blobs/uploads/" + uuid2Bad,
|
|
Query: map[string][]string{
|
|
"digest": {d2Bad.String()},
|
|
"chunk": {"3"},
|
|
},
|
|
Headers: http.Header{
|
|
"Content-Length": {"0"},
|
|
"Content-Type": {"application/octet-stream"},
|
|
},
|
|
},
|
|
RespEntry: reqresp.RespEntry{
|
|
Status: http.StatusBadRequest,
|
|
Headers: http.Header{
|
|
"Content-Length": {"0"},
|
|
// "Location": {"/v2" + blobRepo + "/blobs/" + d2Bad.String()},
|
|
// "Docker-Content-Digest": {d2Bad.String()},
|
|
},
|
|
},
|
|
},
|
|
// upload patch 2b for d2
|
|
{
|
|
ReqEntry: reqresp.ReqEntry{
|
|
DelOnUse: false,
|
|
Name: "PATCH 2b for d2Bad",
|
|
Method: "PATCH",
|
|
Path: "/v2" + blobRepo + "/blobs/uploads/" + uuid2Bad,
|
|
Query: map[string][]string{
|
|
"chunk": {"2b"},
|
|
},
|
|
Headers: http.Header{
|
|
"Content-Length": {fmt.Sprintf("%d", blobLen-blobChunk-20)},
|
|
"Content-Range": {fmt.Sprintf("%d-%d", blobChunk+20, blobLen-1)},
|
|
"Content-Type": {"application/octet-stream"},
|
|
},
|
|
Body: blob2[blobChunk+20:],
|
|
},
|
|
RespEntry: reqresp.RespEntry{
|
|
Status: http.StatusAccepted,
|
|
Headers: http.Header{
|
|
"Content-Length": {fmt.Sprintf("%d", 0)},
|
|
"Range": {fmt.Sprintf("bytes=0-%d", blobLen-1)},
|
|
"Location": {uuid2Bad + "?chunk=3"},
|
|
},
|
|
},
|
|
},
|
|
// upload patch 2 for d2
|
|
{
|
|
ReqEntry: reqresp.ReqEntry{
|
|
DelOnUse: false,
|
|
Name: "PATCH 2 for d2Bad",
|
|
Method: "PATCH",
|
|
Path: "/v2" + blobRepo + "/blobs/uploads/" + uuid2Bad,
|
|
Query: map[string][]string{
|
|
"chunk": {"2"},
|
|
},
|
|
Headers: http.Header{
|
|
"Content-Length": {fmt.Sprintf("%d", blobLen-blobChunk)},
|
|
"Content-Range": {fmt.Sprintf("%d-%d", blobChunk, blobLen-1)},
|
|
"Content-Type": {"application/octet-stream"},
|
|
},
|
|
Body: blob2[blobChunk:],
|
|
},
|
|
RespEntry: reqresp.RespEntry{
|
|
Status: http.StatusRequestedRangeNotSatisfiable,
|
|
Headers: http.Header{
|
|
"Content-Length": {fmt.Sprintf("%d", 0)},
|
|
"Range": {fmt.Sprintf("bytes=0-%d", blobChunk+20-1)},
|
|
"Location": {uuid2Bad + "?chunk=2b"},
|
|
},
|
|
},
|
|
},
|
|
// upload patch 1 for d2
|
|
{
|
|
ReqEntry: reqresp.ReqEntry{
|
|
DelOnUse: false,
|
|
Name: "PATCH 1 for d2Bad",
|
|
Method: "PATCH",
|
|
Path: "/v2" + blobRepo + "/blobs/uploads/" + uuid2Bad,
|
|
Query: map[string][]string{},
|
|
Headers: http.Header{
|
|
"Content-Length": {fmt.Sprintf("%d", blobChunk)},
|
|
"Content-Range": {fmt.Sprintf("0-%d", blobChunk-1)},
|
|
"Content-Type": {"application/octet-stream"},
|
|
},
|
|
Body: blob2[0:blobChunk],
|
|
},
|
|
RespEntry: reqresp.RespEntry{
|
|
Status: http.StatusAccepted,
|
|
Headers: http.Header{
|
|
"Content-Length": {fmt.Sprintf("%d", 0)},
|
|
"Range": {fmt.Sprintf("bytes=0-%d", blobChunk-1)},
|
|
"Location": {uuid2Bad + "?chunk=2"},
|
|
},
|
|
},
|
|
},
|
|
// upload blob
|
|
{
|
|
ReqEntry: reqresp.ReqEntry{
|
|
DelOnUse: false,
|
|
Name: "PUT for d2Bad",
|
|
Method: "PUT",
|
|
Path: "/v2" + blobRepo + "/blobs/uploads/" + uuid2Bad,
|
|
Query: map[string][]string{
|
|
"digest": {d2Bad.String()},
|
|
},
|
|
Headers: http.Header{
|
|
"Content-Length": {fmt.Sprintf("%d", len(blob2))},
|
|
"Content-Type": {"application/octet-stream"},
|
|
},
|
|
Body: blob2,
|
|
},
|
|
RespEntry: reqresp.RespEntry{
|
|
Status: http.StatusGatewayTimeout,
|
|
Headers: http.Header{
|
|
"Content-Length": {fmt.Sprintf("%d", 0)},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
ReqEntry: reqresp.ReqEntry{
|
|
DelOnUse: false,
|
|
Name: "DELETE for d2Bad",
|
|
Method: "DELETE",
|
|
Path: "/v2" + blobRepo + "/blobs/uploads/" + uuid2Bad,
|
|
},
|
|
RespEntry: reqresp.RespEntry{
|
|
Status: http.StatusAccepted,
|
|
},
|
|
},
|
|
|
|
// get upload3 location
|
|
{
|
|
ReqEntry: reqresp.ReqEntry{
|
|
Name: "POST for d3",
|
|
Method: "POST",
|
|
Path: "/v2" + blobRepo + "/blobs/uploads/",
|
|
Query: map[string][]string{
|
|
"mount": {d3.String()},
|
|
},
|
|
},
|
|
RespEntry: reqresp.RespEntry{
|
|
Status: http.StatusAccepted,
|
|
Headers: http.Header{
|
|
"Content-Length": {"0"},
|
|
"Location": {uuid3},
|
|
},
|
|
},
|
|
},
|
|
// upload put for d3
|
|
{
|
|
ReqEntry: reqresp.ReqEntry{
|
|
DelOnUse: false,
|
|
Name: "PUT for patched d3",
|
|
Method: "PUT",
|
|
Path: "/v2" + blobRepo + "/blobs/uploads/" + uuid3,
|
|
Query: map[string][]string{
|
|
"digest": {d3.String()},
|
|
"chunk": {"3"},
|
|
},
|
|
Headers: http.Header{
|
|
"Content-Length": {"0"},
|
|
"Content-Type": {"application/octet-stream"},
|
|
},
|
|
},
|
|
RespEntry: reqresp.RespEntry{
|
|
Status: http.StatusCreated,
|
|
Headers: http.Header{
|
|
"Content-Length": {"0"},
|
|
"Location": {"/v2" + blobRepo + "/blobs/" + d3.String()},
|
|
"Docker-Content-Digest": {d3.String()},
|
|
},
|
|
},
|
|
},
|
|
// upload patch 2 for d3
|
|
{
|
|
ReqEntry: reqresp.ReqEntry{
|
|
DelOnUse: false,
|
|
Name: "PATCH 2 for d3",
|
|
Method: "PATCH",
|
|
Path: "/v2" + blobRepo + "/blobs/uploads/" + uuid3,
|
|
Query: map[string][]string{
|
|
"chunk": {"2"},
|
|
},
|
|
Headers: http.Header{
|
|
"Content-Length": {fmt.Sprintf("%d", blobLen3-blobChunk)},
|
|
"Content-Range": {fmt.Sprintf("%d-%d", blobChunk, blobLen3-1)},
|
|
"Content-Type": {"application/octet-stream"},
|
|
},
|
|
Body: blob3[blobChunk:],
|
|
},
|
|
RespEntry: reqresp.RespEntry{
|
|
Status: http.StatusTooManyRequests,
|
|
},
|
|
},
|
|
// get status for d3 after failed attempt of chunk 2
|
|
{
|
|
ReqEntry: reqresp.ReqEntry{
|
|
DelOnUse: false,
|
|
Name: "GET 2 for d3",
|
|
Method: "GET",
|
|
Path: "/v2" + blobRepo + "/blobs/uploads/" + uuid3,
|
|
Query: map[string][]string{
|
|
"chunk": {"2"},
|
|
},
|
|
},
|
|
RespEntry: reqresp.RespEntry{
|
|
Status: http.StatusNoContent,
|
|
Headers: http.Header{
|
|
"Content-Length": {fmt.Sprintf("%d", 0)},
|
|
"Range": {fmt.Sprintf("bytes=0-%d", blobChunk-1)},
|
|
"Location": {uuid3 + "?chunk=2b"},
|
|
},
|
|
},
|
|
},
|
|
// upload patch 2b for d3
|
|
{
|
|
ReqEntry: reqresp.ReqEntry{
|
|
DelOnUse: false,
|
|
Name: "PATCH 2b for d3",
|
|
Method: "PATCH",
|
|
Path: "/v2" + blobRepo + "/blobs/uploads/" + uuid3,
|
|
Query: map[string][]string{
|
|
"chunk": {"2b"},
|
|
},
|
|
Headers: http.Header{
|
|
"Content-Length": {fmt.Sprintf("%d", blobLen3-blobChunk)},
|
|
"Content-Range": {fmt.Sprintf("%d-%d", blobChunk, blobLen3-1)},
|
|
"Content-Type": {"application/octet-stream"},
|
|
},
|
|
Body: blob3[blobChunk:],
|
|
},
|
|
RespEntry: reqresp.RespEntry{
|
|
Status: http.StatusAccepted,
|
|
Headers: http.Header{
|
|
"Content-Length": {fmt.Sprintf("%d", 0)},
|
|
"Range": {fmt.Sprintf("bytes=0-%d", blobLen3-1)},
|
|
"Location": {uuid3 + "?chunk=3"},
|
|
},
|
|
},
|
|
},
|
|
// upload patch 1 for d3
|
|
{
|
|
ReqEntry: reqresp.ReqEntry{
|
|
DelOnUse: false,
|
|
Name: "PATCH 1 for d3",
|
|
Method: "PATCH",
|
|
Path: "/v2" + blobRepo + "/blobs/uploads/" + uuid3,
|
|
Query: map[string][]string{},
|
|
Headers: http.Header{
|
|
"Content-Length": {fmt.Sprintf("%d", blobChunk)},
|
|
"Content-Range": {fmt.Sprintf("0-%d", blobChunk-1)},
|
|
"Content-Type": {"application/octet-stream"},
|
|
},
|
|
Body: blob3[0:blobChunk],
|
|
},
|
|
RespEntry: reqresp.RespEntry{
|
|
Status: http.StatusAccepted,
|
|
Headers: http.Header{
|
|
"Content-Length": {fmt.Sprintf("%d", 0)},
|
|
"Range": {fmt.Sprintf("bytes=0-%d", blobChunk-1)},
|
|
"Location": {uuid3 + "?chunk=2"},
|
|
},
|
|
},
|
|
},
|
|
// upload blob d3
|
|
{
|
|
ReqEntry: reqresp.ReqEntry{
|
|
DelOnUse: false,
|
|
Name: "PUT for d3",
|
|
Method: "PUT",
|
|
Path: "/v2" + blobRepo + "/blobs/uploads/" + uuid3,
|
|
Query: map[string][]string{
|
|
"digest": {d3.String()},
|
|
},
|
|
Headers: http.Header{
|
|
"Content-Length": {fmt.Sprintf("%d", len(blob3))},
|
|
"Content-Type": {"application/octet-stream"},
|
|
},
|
|
Body: blob3,
|
|
},
|
|
RespEntry: reqresp.RespEntry{
|
|
Status: http.StatusGatewayTimeout,
|
|
Headers: http.Header{
|
|
"Content-Length": {fmt.Sprintf("%d", 0)},
|
|
},
|
|
},
|
|
},
|
|
|
|
// get upload4 location
|
|
{
|
|
ReqEntry: reqresp.ReqEntry{
|
|
Name: "POST for d4",
|
|
Method: "POST",
|
|
Path: "/v2" + blobRepo + "/blobs/uploads/",
|
|
Query: map[string][]string{
|
|
"mount": {d4.String()},
|
|
},
|
|
},
|
|
RespEntry: reqresp.RespEntry{
|
|
Status: http.StatusAccepted,
|
|
Headers: http.Header{
|
|
"Content-Length": {"0"},
|
|
"Location": {uuid4},
|
|
blobChunkMinHeader: {fmt.Sprintf("%d", blobLen4/2)},
|
|
},
|
|
},
|
|
},
|
|
// upload put for d4
|
|
{
|
|
ReqEntry: reqresp.ReqEntry{
|
|
DelOnUse: false,
|
|
Name: "PUT for patched d4",
|
|
Method: "PUT",
|
|
Path: "/v2" + blobRepo + "/blobs/uploads/" + uuid4,
|
|
Query: map[string][]string{
|
|
"digest": {d4.String()},
|
|
"chunk": {"3"},
|
|
},
|
|
Headers: http.Header{
|
|
"Content-Length": {"0"},
|
|
"Content-Type": {"application/octet-stream"},
|
|
},
|
|
},
|
|
RespEntry: reqresp.RespEntry{
|
|
Status: http.StatusCreated,
|
|
Headers: http.Header{
|
|
"Content-Length": {"0"},
|
|
"Location": {"/v2" + blobRepo + "/blobs/" + d4.String()},
|
|
"Docker-Content-Digest": {d4.String()},
|
|
},
|
|
},
|
|
},
|
|
// upload patch 2 for d4
|
|
{
|
|
ReqEntry: reqresp.ReqEntry{
|
|
DelOnUse: false,
|
|
Name: "PATCH 2 for d4",
|
|
Method: "PATCH",
|
|
Path: "/v2" + blobRepo + "/blobs/uploads/" + uuid4,
|
|
Query: map[string][]string{
|
|
"chunk": {"2"},
|
|
},
|
|
Headers: http.Header{
|
|
"Content-Length": {fmt.Sprintf("%d", blobLen4/2)},
|
|
"Content-Range": {fmt.Sprintf("%d-%d", blobLen4/2, blobLen4-1)},
|
|
"Content-Type": {"application/octet-stream"},
|
|
},
|
|
Body: blob4[blobLen4/2:],
|
|
},
|
|
RespEntry: reqresp.RespEntry{
|
|
Status: http.StatusAccepted,
|
|
Headers: http.Header{
|
|
"Content-Length": {fmt.Sprintf("%d", 0)},
|
|
"Range": {fmt.Sprintf("bytes=0-%d", blobLen4-1)},
|
|
"Location": {uuid4 + "?chunk=3"},
|
|
},
|
|
},
|
|
},
|
|
// upload patch 1 for d4
|
|
{
|
|
ReqEntry: reqresp.ReqEntry{
|
|
DelOnUse: false,
|
|
Name: "PATCH 1 for d4",
|
|
Method: "PATCH",
|
|
Path: "/v2" + blobRepo + "/blobs/uploads/" + uuid4,
|
|
Query: map[string][]string{},
|
|
Headers: http.Header{
|
|
"Content-Length": {fmt.Sprintf("%d", blobLen4/2)},
|
|
"Content-Range": {fmt.Sprintf("0-%d", blobLen4/2-1)},
|
|
"Content-Type": {"application/octet-stream"},
|
|
},
|
|
Body: blob4[0 : blobLen4/2],
|
|
},
|
|
RespEntry: reqresp.RespEntry{
|
|
Status: http.StatusAccepted,
|
|
Headers: http.Header{
|
|
"Content-Length": {fmt.Sprintf("%d", 0)},
|
|
"Range": {fmt.Sprintf("bytes=0-%d", blobLen4/2-1)},
|
|
"Location": {uuid4 + "?chunk=2"},
|
|
},
|
|
},
|
|
},
|
|
// get upload5 location
|
|
{
|
|
ReqEntry: reqresp.ReqEntry{
|
|
Name: "POST for d5",
|
|
Method: "POST",
|
|
Path: "/v2" + blobRepo5 + "/blobs/uploads/",
|
|
},
|
|
RespEntry: reqresp.RespEntry{
|
|
Status: http.StatusAccepted,
|
|
Headers: http.Header{
|
|
"Content-Length": {"0"},
|
|
"Location": {uuid5},
|
|
},
|
|
},
|
|
},
|
|
// upload put for d5
|
|
{
|
|
ReqEntry: reqresp.ReqEntry{
|
|
DelOnUse: false,
|
|
Name: "PUT for chunked d5",
|
|
Method: "PUT",
|
|
Path: "/v2" + blobRepo5 + "/blobs/uploads/" + uuid5,
|
|
Query: map[string][]string{
|
|
"digest": {d5.String()},
|
|
"chunk": {"1"},
|
|
},
|
|
Headers: http.Header{
|
|
"Content-Length": {"0"},
|
|
"Content-Type": {"application/octet-stream"},
|
|
},
|
|
},
|
|
RespEntry: reqresp.RespEntry{
|
|
Status: http.StatusCreated,
|
|
Headers: http.Header{
|
|
"Content-Length": {"0"},
|
|
"Location": {"/v2" + blobRepo5 + "/blobs/" + d5.String()},
|
|
"Docker-Content-Digest": {d5.String()},
|
|
},
|
|
},
|
|
},
|
|
// upload patch d5
|
|
{
|
|
ReqEntry: reqresp.ReqEntry{
|
|
DelOnUse: false,
|
|
Name: "PATCH for d5",
|
|
Method: "PATCH",
|
|
Path: "/v2" + blobRepo5 + "/blobs/uploads/" + uuid5,
|
|
Headers: http.Header{
|
|
"Content-Length": {fmt.Sprintf("%d", blobLen5)},
|
|
"Content-Range": {fmt.Sprintf("%d-%d", 0, blobLen5-1)},
|
|
"Content-Type": {"application/octet-stream"},
|
|
},
|
|
Body: blob5,
|
|
},
|
|
RespEntry: reqresp.RespEntry{
|
|
Status: http.StatusAccepted,
|
|
Headers: http.Header{
|
|
"Content-Length": {fmt.Sprintf("%d", 0)},
|
|
"Range": {fmt.Sprintf("bytes=0-%d", blobLen5-1)},
|
|
"Location": {uuid5 + "?chunk=1"},
|
|
},
|
|
},
|
|
},
|
|
// get upload6 location
|
|
{
|
|
ReqEntry: reqresp.ReqEntry{
|
|
Name: "POST for d6",
|
|
Method: "POST",
|
|
Path: "/v2" + blobRepo6 + "/blobs/uploads/",
|
|
},
|
|
RespEntry: reqresp.RespEntry{
|
|
Status: http.StatusAccepted,
|
|
Headers: http.Header{
|
|
"Content-Length": {"0"},
|
|
"Location": {uuid6},
|
|
},
|
|
},
|
|
},
|
|
// upload put for d6
|
|
{
|
|
ReqEntry: reqresp.ReqEntry{
|
|
DelOnUse: false,
|
|
Name: "PUT for d6",
|
|
Method: "PUT",
|
|
Path: "/v2" + blobRepo6 + "/blobs/uploads/" + uuid6,
|
|
Query: map[string][]string{
|
|
"digest": {d6.String()},
|
|
},
|
|
Headers: http.Header{
|
|
"Content-Length": {"0"},
|
|
"Content-Type": {"application/octet-stream"},
|
|
},
|
|
},
|
|
RespEntry: reqresp.RespEntry{
|
|
Status: http.StatusCreated,
|
|
Headers: http.Header{
|
|
"Content-Length": {"0"},
|
|
"Location": {"/v2" + blobRepo6 + "/blobs/" + d6.String()},
|
|
"Docker-Content-Digest": {d6.String()},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
rrs = append(rrs, reqresp.BaseEntries...)
|
|
// create a server
|
|
ts := httptest.NewServer(reqresp.NewHandler(t, rrs))
|
|
defer ts.Close()
|
|
// setup the reg
|
|
tsURL, _ := url.Parse(ts.URL)
|
|
tsHost := tsURL.Host
|
|
rcHosts := []*config.Host{
|
|
{
|
|
Name: tsHost,
|
|
Hostname: tsHost,
|
|
TLS: config.TLSDisabled,
|
|
BlobChunk: int64(blobChunk),
|
|
BlobMax: int64(-1),
|
|
User: user,
|
|
Pass: pass,
|
|
},
|
|
{
|
|
Name: "chunked." + tsHost,
|
|
Hostname: tsHost,
|
|
TLS: config.TLSDisabled,
|
|
BlobChunk: int64(blobChunk),
|
|
BlobMax: int64(blobChunk * 3),
|
|
User: user,
|
|
Pass: pass,
|
|
},
|
|
{
|
|
Name: "retry." + tsHost,
|
|
Hostname: tsHost,
|
|
TLS: config.TLSDisabled,
|
|
BlobChunk: int64(blobChunk),
|
|
BlobMax: int64(-1),
|
|
},
|
|
}
|
|
log := &logrus.Logger{
|
|
Out: os.Stderr,
|
|
Formatter: new(logrus.TextFormatter),
|
|
Hooks: make(logrus.LevelHooks),
|
|
Level: logrus.WarnLevel,
|
|
}
|
|
// use short delays for fast tests
|
|
delayInit, _ := time.ParseDuration("0.05s")
|
|
delayMax, _ := time.ParseDuration("0.10s")
|
|
reg := New(
|
|
WithConfigHosts(rcHosts),
|
|
WithLog(log),
|
|
WithDelay(delayInit, delayMax),
|
|
)
|
|
|
|
t.Run("Put", func(t *testing.T) {
|
|
r, err := ref.New(tsURL.Host + blobRepo)
|
|
if err != nil {
|
|
t.Fatalf("Failed creating ref: %v", err)
|
|
}
|
|
br := bytes.NewReader(blob1)
|
|
dp, err := reg.BlobPut(ctx, r, descriptor.Descriptor{Digest: d1, Size: int64(len(blob1))}, br)
|
|
if err != nil {
|
|
t.Fatalf("Failed running BlobPut: %v", err)
|
|
}
|
|
if dp.Digest.String() != d1.String() {
|
|
t.Errorf("Digest mismatch, expected %s, received %s", d1.String(), dp.Digest.String())
|
|
}
|
|
if dp.Size != int64(len(blob1)) {
|
|
t.Errorf("Content length mismatch, expected %d, received %d", len(blob1), dp.Size)
|
|
}
|
|
})
|
|
|
|
t.Run("Retry", func(t *testing.T) {
|
|
r, err := ref.New("retry." + tsURL.Host + blobRepo)
|
|
if err != nil {
|
|
t.Fatalf("Failed creating ref: %v", err)
|
|
}
|
|
br := bytes.NewReader(blob2)
|
|
mt := "application/vnd.example.test"
|
|
dp, err := reg.BlobPut(ctx, r, descriptor.Descriptor{MediaType: mt, Digest: d2, Size: int64(len(blob2))}, br)
|
|
if err != nil {
|
|
t.Fatalf("Failed running BlobPut: %v", err)
|
|
}
|
|
if dp.Digest.String() != d2.String() {
|
|
t.Errorf("Digest mismatch, expected %s, received %s", d2.String(), dp.Digest.String())
|
|
}
|
|
if dp.Size != int64(len(blob2)) {
|
|
t.Errorf("Content length mismatch, expected %d, received %d", len(blob2), dp.Size)
|
|
}
|
|
if dp.MediaType != mt {
|
|
t.Errorf("Blob put did not preserve descriptor media type: expected %s, received %s", mt, dp.MediaType)
|
|
}
|
|
})
|
|
|
|
t.Run("Not retryable", func(t *testing.T) {
|
|
r, err := ref.New("retry." + tsURL.Host + blobRepo)
|
|
if err != nil {
|
|
t.Fatalf("Failed creating ref: %v", err)
|
|
}
|
|
br := bytes.NewReader(blob2)
|
|
_, err = reg.BlobPut(ctx, r, descriptor.Descriptor{Digest: d2, Size: int64(len(blob2))}, io.NopCloser(br))
|
|
if err == nil {
|
|
t.Fatalf("Blob put succeeded on a gateway timeout")
|
|
}
|
|
if !errors.Is(err, errs.ErrHTTPStatus) {
|
|
t.Errorf("unexpected err, expected %v, received %v", errs.ErrHTTPStatus, err)
|
|
}
|
|
})
|
|
|
|
t.Run("Invalid digest", func(t *testing.T) {
|
|
r, err := ref.New("retry." + tsURL.Host + blobRepo)
|
|
if err != nil {
|
|
t.Fatalf("Failed creating ref: %v", err)
|
|
}
|
|
br := bytes.NewReader(blob2)
|
|
mt := "application/vnd.example.test"
|
|
_, err = reg.BlobPut(ctx, r, descriptor.Descriptor{MediaType: mt, Digest: d2Bad, Size: int64(len(blob2))}, br)
|
|
if err == nil || !errors.Is(err, errs.ErrDigestMismatch) {
|
|
t.Errorf("unexpected error, expected %v, received %v", errs.ErrDigestMismatch, err)
|
|
}
|
|
})
|
|
|
|
t.Run("Invalid size", func(t *testing.T) {
|
|
r, err := ref.New("retry." + tsURL.Host + blobRepo)
|
|
if err != nil {
|
|
t.Fatalf("Failed creating ref: %v", err)
|
|
}
|
|
br := bytes.NewReader(blob2)
|
|
mt := "application/vnd.example.test"
|
|
_, err = reg.BlobPut(ctx, r, descriptor.Descriptor{MediaType: mt, Digest: d2, Size: int64(len(blob2) - 2)}, br)
|
|
if err == nil || !errors.Is(err, errs.ErrMismatch) {
|
|
t.Errorf("unexpected error, expected %v, received %v", errs.ErrMismatch, err)
|
|
}
|
|
})
|
|
|
|
t.Run("PartialChunk", func(t *testing.T) {
|
|
r, err := ref.New(tsURL.Host + blobRepo)
|
|
if err != nil {
|
|
t.Fatalf("Failed creating ref: %v", err)
|
|
}
|
|
br := bytes.NewReader(blob3)
|
|
dp, err := reg.BlobPut(ctx, r, descriptor.Descriptor{Digest: d3, Size: int64(len(blob3))}, br)
|
|
if err != nil {
|
|
t.Fatalf("Failed running BlobPut: %v", err)
|
|
}
|
|
if dp.Digest.String() != d3.String() {
|
|
t.Errorf("Digest mismatch, expected %s, received %s", d3.String(), dp.Digest.String())
|
|
}
|
|
if dp.Size != int64(len(blob3)) {
|
|
t.Errorf("Content length mismatch, expected %d, received %d", len(blob3), dp.Size)
|
|
}
|
|
})
|
|
|
|
t.Run("Chunk resized", func(t *testing.T) {
|
|
r, err := ref.New("chunked." + tsURL.Host + blobRepo)
|
|
if err != nil {
|
|
t.Fatalf("Failed creating ref: %v", err)
|
|
}
|
|
br := bytes.NewReader(blob4)
|
|
dp, err := reg.BlobPut(ctx, r, descriptor.Descriptor{Digest: d4, Size: int64(len(blob4))}, br)
|
|
if err != nil {
|
|
t.Fatalf("Failed running BlobPut: %v", err)
|
|
}
|
|
if dp.Digest.String() != d4.String() {
|
|
t.Errorf("Digest mismatch, expected %s, received %s", d4.String(), dp.Digest.String())
|
|
}
|
|
if dp.Size != int64(len(blob4)) {
|
|
t.Errorf("Content length mismatch, expected %d, received %d", len(blob4), dp.Size)
|
|
}
|
|
})
|
|
|
|
// test put without a descriptor
|
|
t.Run("No descriptor", func(t *testing.T) {
|
|
r, err := ref.New(tsURL.Host + blobRepo5)
|
|
if err != nil {
|
|
t.Fatalf("Failed creating ref: %v", err)
|
|
}
|
|
br := bytes.NewReader(blob5)
|
|
dp, err := reg.BlobPut(ctx, r, descriptor.Descriptor{}, br)
|
|
if err != nil {
|
|
t.Fatalf("Failed running BlobPut: %v", err)
|
|
}
|
|
if dp.Digest.String() != d5.String() {
|
|
t.Errorf("Digest mismatch, expected %s, received %s", d5.String(), dp.Digest.String())
|
|
}
|
|
if dp.Size != int64(len(blob5)) {
|
|
t.Errorf("Content length mismatch, expected %d, received %d", len(blob5), dp.Size)
|
|
}
|
|
})
|
|
|
|
// test put of a zero length blob
|
|
t.Run("Empty blob", func(t *testing.T) {
|
|
r, err := ref.New(tsURL.Host + blobRepo6)
|
|
if err != nil {
|
|
t.Fatalf("Failed creating ref: %v", err)
|
|
}
|
|
br := bytes.NewReader(blob6)
|
|
dp, err := reg.BlobPut(ctx, r, descriptor.Descriptor{Digest: d6, Size: int64(len(blob6))}, br)
|
|
if err != nil {
|
|
t.Fatalf("Failed running BlobPut: %v", err)
|
|
}
|
|
if dp.Digest.String() != d6.String() {
|
|
t.Errorf("Digest mismatch, expected %s, received %s", d6.String(), dp.Digest.String())
|
|
}
|
|
if dp.Size != int64(len(blob6)) {
|
|
t.Errorf("Content length mismatch, expected %d, received %d", len(blob6), dp.Size)
|
|
}
|
|
})
|
|
|
|
// TODO: test failed mount (blobGetUploadURL)
|
|
}
|