d8fcbeeb71
The registry uses partial Named values which the named parsers no longer support. To allow the registry service to continue to operate without canonicalization, switch to use WithName. In the future, the registry should start using fully canonical values on the backend and WithName should no longer support creating partial values. Signed-off-by: Derek McGowan <derek@mcgstyle.net> (github: dmcgowan)
218 lines
6.4 KiB
Go
218 lines
6.4 KiB
Go
package storage
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"reflect"
|
|
"strconv"
|
|
"testing"
|
|
|
|
"github.com/docker/distribution"
|
|
"github.com/docker/distribution/context"
|
|
"github.com/opencontainers/go-digest"
|
|
|
|
"github.com/docker/distribution/reference"
|
|
"github.com/docker/distribution/testutil"
|
|
)
|
|
|
|
func TestLinkedBlobStoreCreateWithMountFrom(t *testing.T) {
|
|
fooRepoName, _ := reference.WithName("nm/foo")
|
|
fooEnv := newManifestStoreTestEnv(t, fooRepoName, "thetag")
|
|
ctx := context.Background()
|
|
stats, err := mockRegistry(t, fooEnv.registry)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Build up some test layers and add them to the manifest, saving the
|
|
// readseekers for upload later.
|
|
testLayers := map[digest.Digest]io.ReadSeeker{}
|
|
for i := 0; i < 2; i++ {
|
|
rs, ds, err := testutil.CreateRandomTarFile()
|
|
if err != nil {
|
|
t.Fatalf("unexpected error generating test layer file")
|
|
}
|
|
dgst := digest.Digest(ds)
|
|
|
|
testLayers[digest.Digest(dgst)] = rs
|
|
}
|
|
|
|
// upload the layers to foo/bar
|
|
for dgst, rs := range testLayers {
|
|
wr, err := fooEnv.repository.Blobs(fooEnv.ctx).Create(fooEnv.ctx)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error creating test upload: %v", err)
|
|
}
|
|
|
|
if _, err := io.Copy(wr, rs); err != nil {
|
|
t.Fatalf("unexpected error copying to upload: %v", err)
|
|
}
|
|
|
|
if _, err := wr.Commit(fooEnv.ctx, distribution.Descriptor{Digest: dgst}); err != nil {
|
|
t.Fatalf("unexpected error finishing upload: %v", err)
|
|
}
|
|
}
|
|
|
|
// create another repository nm/bar
|
|
barRepoName, _ := reference.WithName("nm/bar")
|
|
barRepo, err := fooEnv.registry.Repository(ctx, barRepoName)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error getting repo: %v", err)
|
|
}
|
|
|
|
// cross-repo mount the test layers into a nm/bar
|
|
for dgst := range testLayers {
|
|
fooCanonical, _ := reference.WithDigest(fooRepoName, dgst)
|
|
option := WithMountFrom(fooCanonical)
|
|
// ensure we can instrospect it
|
|
createOpts := distribution.CreateOptions{}
|
|
if err := option.Apply(&createOpts); err != nil {
|
|
t.Fatalf("failed to apply MountFrom option: %v", err)
|
|
}
|
|
if !createOpts.Mount.ShouldMount || createOpts.Mount.From.String() != fooCanonical.String() {
|
|
t.Fatalf("unexpected create options: %#+v", createOpts.Mount)
|
|
}
|
|
|
|
_, err := barRepo.Blobs(ctx).Create(ctx, WithMountFrom(fooCanonical))
|
|
if err == nil {
|
|
t.Fatalf("unexpected non-error while mounting from %q: %v", fooRepoName.String(), err)
|
|
}
|
|
if _, ok := err.(distribution.ErrBlobMounted); !ok {
|
|
t.Fatalf("expected ErrMountFrom error, not %T: %v", err, err)
|
|
}
|
|
}
|
|
for dgst := range testLayers {
|
|
fooCanonical, _ := reference.WithDigest(fooRepoName, dgst)
|
|
count, exists := stats[fooCanonical.String()]
|
|
if !exists {
|
|
t.Errorf("expected entry %q not found among handled stat calls", fooCanonical.String())
|
|
} else if count != 1 {
|
|
t.Errorf("expected exactly one stat call for entry %q, not %d", fooCanonical.String(), count)
|
|
}
|
|
}
|
|
|
|
clearStats(stats)
|
|
|
|
// create yet another repository nm/baz
|
|
bazRepoName, _ := reference.WithName("nm/baz")
|
|
bazRepo, err := fooEnv.registry.Repository(ctx, bazRepoName)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error getting repo: %v", err)
|
|
}
|
|
|
|
// cross-repo mount them into a nm/baz and provide a prepopulated blob descriptor
|
|
for dgst := range testLayers {
|
|
fooCanonical, _ := reference.WithDigest(fooRepoName, dgst)
|
|
size, err := strconv.ParseInt("0x"+dgst.Hex()[:8], 0, 64)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
prepolutatedDescriptor := distribution.Descriptor{
|
|
Digest: dgst,
|
|
Size: size,
|
|
MediaType: "application/octet-stream",
|
|
}
|
|
_, err = bazRepo.Blobs(ctx).Create(ctx, WithMountFrom(fooCanonical), &statCrossMountCreateOption{
|
|
desc: prepolutatedDescriptor,
|
|
})
|
|
blobMounted, ok := err.(distribution.ErrBlobMounted)
|
|
if !ok {
|
|
t.Errorf("expected ErrMountFrom error, not %T: %v", err, err)
|
|
continue
|
|
}
|
|
if !reflect.DeepEqual(blobMounted.Descriptor, prepolutatedDescriptor) {
|
|
t.Errorf("unexpected descriptor: %#+v != %#+v", blobMounted.Descriptor, prepolutatedDescriptor)
|
|
}
|
|
}
|
|
// this time no stat calls will be made
|
|
if len(stats) != 0 {
|
|
t.Errorf("unexpected number of stats made: %d != %d", len(stats), len(testLayers))
|
|
}
|
|
}
|
|
|
|
func clearStats(stats map[string]int) {
|
|
for k := range stats {
|
|
delete(stats, k)
|
|
}
|
|
}
|
|
|
|
// mockRegistry sets a mock blob descriptor service factory that overrides
|
|
// statter's Stat method to note each attempt to stat a blob in any repository.
|
|
// Returned stats map contains canonical references to blobs with a number of
|
|
// attempts.
|
|
func mockRegistry(t *testing.T, nm distribution.Namespace) (map[string]int, error) {
|
|
registry, ok := nm.(*registry)
|
|
if !ok {
|
|
return nil, fmt.Errorf("not an expected type of registry: %T", nm)
|
|
}
|
|
stats := make(map[string]int)
|
|
|
|
registry.blobDescriptorServiceFactory = &mockBlobDescriptorServiceFactory{
|
|
t: t,
|
|
stats: stats,
|
|
}
|
|
|
|
return stats, nil
|
|
}
|
|
|
|
type mockBlobDescriptorServiceFactory struct {
|
|
t *testing.T
|
|
stats map[string]int
|
|
}
|
|
|
|
func (f *mockBlobDescriptorServiceFactory) BlobAccessController(svc distribution.BlobDescriptorService) distribution.BlobDescriptorService {
|
|
return &mockBlobDescriptorService{
|
|
BlobDescriptorService: svc,
|
|
t: f.t,
|
|
stats: f.stats,
|
|
}
|
|
}
|
|
|
|
type mockBlobDescriptorService struct {
|
|
distribution.BlobDescriptorService
|
|
t *testing.T
|
|
stats map[string]int
|
|
}
|
|
|
|
var _ distribution.BlobDescriptorService = &mockBlobDescriptorService{}
|
|
|
|
func (bs *mockBlobDescriptorService) Stat(ctx context.Context, dgst digest.Digest) (distribution.Descriptor, error) {
|
|
statter, ok := bs.BlobDescriptorService.(*linkedBlobStatter)
|
|
if !ok {
|
|
return distribution.Descriptor{}, fmt.Errorf("unexpected blob descriptor service: %T", bs.BlobDescriptorService)
|
|
}
|
|
|
|
name := statter.repository.Named()
|
|
canonical, err := reference.WithDigest(name, dgst)
|
|
if err != nil {
|
|
return distribution.Descriptor{}, fmt.Errorf("failed to make canonical reference: %v", err)
|
|
}
|
|
|
|
bs.stats[canonical.String()]++
|
|
bs.t.Logf("calling Stat on %s", canonical.String())
|
|
|
|
return bs.BlobDescriptorService.Stat(ctx, dgst)
|
|
}
|
|
|
|
// statCrossMountCreateOptions ensures the expected options type is passed, and optionally pre-fills the cross-mount stat info
|
|
type statCrossMountCreateOption struct {
|
|
desc distribution.Descriptor
|
|
}
|
|
|
|
var _ distribution.BlobCreateOption = statCrossMountCreateOption{}
|
|
|
|
func (f statCrossMountCreateOption) Apply(v interface{}) error {
|
|
opts, ok := v.(*distribution.CreateOptions)
|
|
if !ok {
|
|
return fmt.Errorf("Unexpected create options: %#v", v)
|
|
}
|
|
|
|
if !opts.Mount.ShouldMount {
|
|
return nil
|
|
}
|
|
|
|
opts.Mount.Stat = &f.desc
|
|
|
|
return nil
|
|
}
|