152af63ec5
This integrates the new module, which was extracted from this repository
at commit b9b19409cf458dcb9e1253ff44ba75bd0620faa6;
# install filter-repo (https://github.com/newren/git-filter-repo/blob/main/INSTALL.md)
brew install git-filter-repo
# create a temporary clone of docker
cd ~/Projects
git clone https://github.com/distribution/distribution.git reference
cd reference
# commit taken from
git rev-parse --verify HEAD
b9b19409cf
# remove all code, except for general files, 'reference/', and rename to /
git filter-repo \
--path .github/workflows/codeql-analysis.yml \
--path .github/workflows/fossa.yml \
--path .golangci.yml \
--path distribution-logo.svg \
--path CODE-OF-CONDUCT.md \
--path CONTRIBUTING.md \
--path GOVERNANCE.md \
--path README.md \
--path LICENSE \
--path MAINTAINERS \
--path-glob 'reference/*.*' \
--path-rename reference/:
# initialize go.mod
go mod init github.com/distribution/reference
go mod tidy -go=1.20
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
282 lines
7.6 KiB
Go
282 lines
7.6 KiB
Go
package proxy
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/distribution/distribution/v3"
|
|
"github.com/distribution/distribution/v3/manifest/schema2"
|
|
"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/distribution/reference"
|
|
"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)
|
|
}
|
|
|
|
ctx := context.Background()
|
|
truthRegistry, err := storage.NewRegistry(ctx, inmemory.New(),
|
|
storage.BlobDescriptorCacheProvider(memory.NewInMemoryBlobDescriptorCacheProvider(memory.UnlimitedSize)))
|
|
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(memory.UnlimitedSize)), storage.EnableRedirect, storage.DisableDigestResumption)
|
|
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, storage.SkipLayerVerification())
|
|
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) {
|
|
config := []byte(`{"name": "foo"}`)
|
|
configDigest := digest.FromBytes(config)
|
|
configReader := bytes.NewReader(config)
|
|
|
|
if err := testutil.PushBlob(ctx, repository, configReader, configDigest); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
m := schema2.Manifest{
|
|
Versioned: schema2.SchemaVersion,
|
|
Config: distribution.Descriptor{
|
|
MediaType: "foo/bar",
|
|
Digest: configDigest,
|
|
},
|
|
}
|
|
|
|
for i := 0; i < 2; i++ {
|
|
rs, dgst, err := testutil.CreateRandomTarFile()
|
|
if err != nil {
|
|
t.Fatalf("unexpected error generating test layer file")
|
|
}
|
|
|
|
if err := testutil.PushBlob(ctx, repository, rs, dgst); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
ms, err := repository.Manifests(ctx)
|
|
if err != nil {
|
|
t.Fatal(err.Error())
|
|
}
|
|
sm, err := schema2.FromStruct(m)
|
|
if err != nil {
|
|
t.Fatal(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)
|
|
}
|
|
}
|
|
|
|
func TestProxyManifestsWithoutScheduler(t *testing.T) {
|
|
name := "foo/bar"
|
|
env := newManifestStoreTestEnv(t, name, "latest")
|
|
env.manifests.scheduler = nil
|
|
|
|
ctx := context.Background()
|
|
exists, err := env.manifests.Exists(ctx, env.manifestDigest)
|
|
if err != nil {
|
|
t.Fatalf("Error checking existence")
|
|
}
|
|
if !exists {
|
|
t.Errorf("Unexpected non-existent manifest")
|
|
}
|
|
|
|
// Get - should succeed without scheduler
|
|
_, err = env.manifests.Get(ctx, env.manifestDigest)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|