package proxy import ( "context" "io" "sync" "testing" "github.com/distribution/distribution/v3" "github.com/distribution/distribution/v3/manifest" "github.com/distribution/distribution/v3/manifest/schema1" "github.com/distribution/distribution/v3/reference" "github.com/distribution/distribution/v3/registry/client/auth" "github.com/distribution/distribution/v3/registry/client/auth/challenge" "github.com/distribution/distribution/v3/registry/proxy/scheduler" "github.com/distribution/distribution/v3/registry/storage" "github.com/distribution/distribution/v3/registry/storage/cache/memory" "github.com/distribution/distribution/v3/registry/storage/driver/inmemory" "github.com/distribution/distribution/v3/testutil" "github.com/docker/libtrust" "github.com/opencontainers/go-digest" ) type statsManifest struct { manifests distribution.ManifestService stats map[string]int } type manifestStoreTestEnv struct { manifestDigest digest.Digest // digest of the signed manifest in the local storage manifests proxyManifestStore } func (te manifestStoreTestEnv) LocalStats() *map[string]int { ls := te.manifests.localManifests.(statsManifest).stats return &ls } func (te manifestStoreTestEnv) RemoteStats() *map[string]int { rs := te.manifests.remoteManifests.(statsManifest).stats return &rs } func (sm statsManifest) Delete(ctx context.Context, dgst digest.Digest) error { sm.stats["delete"]++ return sm.manifests.Delete(ctx, dgst) } func (sm statsManifest) Exists(ctx context.Context, dgst digest.Digest) (bool, error) { sm.stats["exists"]++ return sm.manifests.Exists(ctx, dgst) } func (sm statsManifest) Get(ctx context.Context, dgst digest.Digest, options ...distribution.ManifestServiceOption) (distribution.Manifest, error) { sm.stats["get"]++ return sm.manifests.Get(ctx, dgst) } func (sm statsManifest) Put(ctx context.Context, manifest distribution.Manifest, options ...distribution.ManifestServiceOption) (digest.Digest, error) { sm.stats["put"]++ return sm.manifests.Put(ctx, manifest) } type mockChallenger struct { sync.Mutex count int } // Called for remote operations only func (m *mockChallenger) tryEstablishChallenges(context.Context) error { m.Lock() defer m.Unlock() m.count++ return nil } func (m *mockChallenger) credentialStore() auth.CredentialStore { return nil } func (m *mockChallenger) challengeManager() challenge.Manager { return nil } func newManifestStoreTestEnv(t *testing.T, name, tag string) *manifestStoreTestEnv { nameRef, err := reference.WithName(name) if err != nil { t.Fatalf("unable to parse reference: %s", err) } k, err := libtrust.GenerateECP256PrivateKey() if err != nil { t.Fatal(err) } ctx := context.Background() truthRegistry, err := storage.NewRegistry(ctx, inmemory.New(), storage.BlobDescriptorCacheProvider(memory.NewInMemoryBlobDescriptorCacheProvider()), storage.Schema1SigningKey(k), storage.EnableSchema1) if err != nil { t.Fatalf("error creating registry: %v", err) } truthRepo, err := truthRegistry.Repository(ctx, nameRef) if err != nil { t.Fatalf("unexpected error getting repo: %v", err) } tr, err := truthRepo.Manifests(ctx) if err != nil { t.Fatal(err.Error()) } truthManifests := statsManifest{ manifests: tr, stats: make(map[string]int), } manifestDigest, err := populateRepo(ctx, t, truthRepo, name, tag) if err != nil { t.Fatalf(err.Error()) } localRegistry, err := storage.NewRegistry(ctx, inmemory.New(), storage.BlobDescriptorCacheProvider(memory.NewInMemoryBlobDescriptorCacheProvider()), storage.EnableRedirect, storage.DisableDigestResumption, storage.Schema1SigningKey(k), storage.EnableSchema1) if err != nil { t.Fatalf("error creating registry: %v", err) } localRepo, err := localRegistry.Repository(ctx, nameRef) if err != nil { t.Fatalf("unexpected error getting repo: %v", err) } lr, err := localRepo.Manifests(ctx) if err != nil { t.Fatal(err.Error()) } localManifests := statsManifest{ manifests: lr, stats: make(map[string]int), } s := scheduler.New(ctx, inmemory.New(), "/scheduler-state.json") return &manifestStoreTestEnv{ manifestDigest: manifestDigest, manifests: proxyManifestStore{ ctx: ctx, localManifests: localManifests, remoteManifests: truthManifests, scheduler: s, repositoryName: nameRef, authChallenger: &mockChallenger{}, }, } } func populateRepo(ctx context.Context, t *testing.T, repository distribution.Repository, name, tag string) (digest.Digest, error) { m := schema1.Manifest{ Versioned: manifest.Versioned{ SchemaVersion: 1, }, Name: name, Tag: tag, } for i := 0; i < 2; i++ { wr, err := repository.Blobs(ctx).Create(ctx) if err != nil { t.Fatalf("unexpected error creating test upload: %v", err) } rs, dgst, err := testutil.CreateRandomTarFile() if err != nil { t.Fatalf("unexpected error generating test layer file") } if _, err := io.Copy(wr, rs); err != nil { t.Fatalf("unexpected error copying to upload: %v", err) } if _, err := wr.Commit(ctx, distribution.Descriptor{Digest: dgst}); err != nil { t.Fatalf("unexpected error finishing upload: %v", err) } } pk, err := libtrust.GenerateECP256PrivateKey() if err != nil { t.Fatalf("unexpected error generating private key: %v", err) } sm, err := schema1.Sign(&m, pk) if err != nil { t.Fatalf("error signing manifest: %v", err) } ms, err := repository.Manifests(ctx) if err != nil { t.Fatalf(err.Error()) } dgst, err := ms.Put(ctx, sm) if err != nil { t.Fatalf("unexpected errors putting manifest: %v", err) } return dgst, nil } // TestProxyManifests contains basic acceptance tests // for the pull-through behavior func TestProxyManifests(t *testing.T) { name := "foo/bar" env := newManifestStoreTestEnv(t, name, "latest") localStats := env.LocalStats() remoteStats := env.RemoteStats() ctx := context.Background() // Stat - must check local and remote exists, err := env.manifests.Exists(ctx, env.manifestDigest) if err != nil { t.Fatalf("Error checking existence") } if !exists { t.Errorf("Unexpected non-existent manifest") } if (*localStats)["exists"] != 1 && (*remoteStats)["exists"] != 1 { t.Errorf("Unexpected exists count : \n%v \n%v", localStats, remoteStats) } if env.manifests.authChallenger.(*mockChallenger).count != 1 { t.Fatalf("Expected 1 auth challenge, got %#v", env.manifests.authChallenger) } // Get - should succeed and pull manifest into local _, err = env.manifests.Get(ctx, env.manifestDigest) if err != nil { t.Fatal(err) } if (*localStats)["get"] != 1 && (*remoteStats)["get"] != 1 { t.Errorf("Unexpected get count") } if (*localStats)["put"] != 1 { t.Errorf("Expected local put") } if env.manifests.authChallenger.(*mockChallenger).count != 2 { t.Fatalf("Expected 2 auth challenges, got %#v", env.manifests.authChallenger) } // Stat - should only go to local exists, err = env.manifests.Exists(ctx, env.manifestDigest) if err != nil { t.Fatal(err) } if !exists { t.Errorf("Unexpected non-existent manifest") } if (*localStats)["exists"] != 2 && (*remoteStats)["exists"] != 1 { t.Errorf("Unexpected exists count") } if env.manifests.authChallenger.(*mockChallenger).count != 2 { t.Fatalf("Expected 2 auth challenges, got %#v", env.manifests.authChallenger) } // Get proxied - won't require another authchallenge _, err = env.manifests.Get(ctx, env.manifestDigest) if err != nil { t.Fatal(err) } if env.manifests.authChallenger.(*mockChallenger).count != 2 { t.Fatalf("Expected 2 auth challenges, got %#v", env.manifests.authChallenger) } }