From 05fb30a9eef5ad6baa9505af0f35d1f6c0648fb2 Mon Sep 17 00:00:00 2001 From: "Lixia (Sylvia) Lei" Date: Thu, 27 Oct 2022 16:00:40 +0800 Subject: [PATCH] feat: index referrers on manifest push (#348) Signed-off-by: Lixia (Sylvia) Lei --- registry/remote/example_test.go | 69 +- registry/remote/repository.go | 225 +++++- registry/remote/repository_test.go | 1136 +++++++++++++++++++++++++--- registry/remote/utils.go | 6 +- 4 files changed, 1304 insertions(+), 132 deletions(-) diff --git a/registry/remote/example_test.go b/registry/remote/example_test.go index 9dc19a78..55d1043d 100644 --- a/registry/remote/example_test.go +++ b/registry/remote/example_test.go @@ -40,7 +40,7 @@ import ( const ( exampleRepositoryName = "example" exampleTag = "latest" - exampleManifest = "Example manifest content" + exampleConfig = "Example config content" exampleLayer = "Example layer content" exampleUploadUUid = "0bc84d80-837c-41d9-824e-1907463c53b3" ManifestDigest = "sha256:0b696106ecd0654e031f19e0a8cbd1aee4ad457d7c9cea881f07b12a930cd307" @@ -49,12 +49,19 @@ const ( ) var ( - exampleLayerDigest = digest.FromBytes([]byte(exampleLayer)).String() - exampleManifestDigest = digest.FromBytes([]byte(exampleManifest)).String() - exampleManifestDescriptor = ocispec.Descriptor{ - MediaType: ocispec.MediaTypeImageManifest, - Digest: digest.Digest(exampleManifestDigest), - Size: int64(len(exampleManifest))} + exampleLayerDescriptor = content.NewDescriptorFromBytes(ocispec.MediaTypeImageLayer, []byte(exampleLayer)) + exampleLayerDigest = exampleLayerDescriptor.Digest.String() + exampleManifest, _ = json.Marshal(ocispec.Manifest{ + Versioned: specs.Versioned{ + SchemaVersion: 2, // historical value. does not pertain to OCI or docker version + }, + Config: content.NewDescriptorFromBytes(ocispec.MediaTypeImageConfig, []byte(exampleConfig)), + Layers: []ocispec.Descriptor{ + exampleLayerDescriptor, + }, + }) + exampleManifestDescriptor = content.NewDescriptorFromBytes(ocispec.MediaTypeImageManifest, exampleManifest) + exampleManifestDigest = exampleManifestDescriptor.Digest.String() exampleSignatureManifest, _ = json.Marshal(ocispec.Artifact{ MediaType: ocispec.MediaTypeArtifactManifest, ArtifactType: "example/signature", @@ -322,8 +329,8 @@ func ExampleRepository_Resolve_byTag() { // Output: // application/vnd.oci.image.manifest.v1+json - // sha256:00e5ffa7d914b4e6aa3f1a324f37df0625ccc400be333deea5ecaa199f9eff5b - // 24 + // sha256:b53dc03a49f383ba230d8ac2b78a9c4aec132e4a9f36cc96524df98163202cc7 + // 337 } // ExampleRepository_Resolve_byDigest gives example snippets for resolving a digest to a manifest descriptor. @@ -333,7 +340,7 @@ func ExampleRepository_Resolve_byDigest() { panic(err) } ctx := context.Background() - exampleDigest := "sha256:00e5ffa7d914b4e6aa3f1a324f37df0625ccc400be333deea5ecaa199f9eff5b" + exampleDigest := "sha256:b53dc03a49f383ba230d8ac2b78a9c4aec132e4a9f36cc96524df98163202cc7" descriptor, err := repo.Resolve(ctx, exampleDigest) if err != nil { panic(err) @@ -345,8 +352,8 @@ func ExampleRepository_Resolve_byDigest() { // Output: // application/vnd.oci.image.manifest.v1+json - // sha256:00e5ffa7d914b4e6aa3f1a324f37df0625ccc400be333deea5ecaa199f9eff5b - // 24 + // sha256:b53dc03a49f383ba230d8ac2b78a9c4aec132e4a9f36cc96524df98163202cc7 + // 337 } // ExampleRepository_Fetch_byTag gives example snippets for downloading a manifest by tag. @@ -375,7 +382,7 @@ func ExampleRepository_Fetch_manifestByTag() { fmt.Println(string(pulledBlob)) // Output: - // Example manifest content + // {"schemaVersion":2,"config":{"mediaType":"application/vnd.oci.image.config.v1+json","digest":"sha256:569224ae188c06e97b9fcadaeb2358fb0fb7c4eb105d49aee2620b2719abea43","size":22},"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar","digest":"sha256:ef79e47691ad1bc702d7a256da6323ec369a8fc3159b4f1798a47136f3b38c10","size":21}]} } // ExampleRepository_Fetch_manifestByDigest gives example snippets for downloading a manifest by digest. @@ -386,7 +393,7 @@ func ExampleRepository_Fetch_manifestByDigest() { } ctx := context.Background() - exampleDigest := "sha256:00e5ffa7d914b4e6aa3f1a324f37df0625ccc400be333deea5ecaa199f9eff5b" + exampleDigest := "sha256:b53dc03a49f383ba230d8ac2b78a9c4aec132e4a9f36cc96524df98163202cc7" // resolve the blob descriptor to obtain the size of the blob descriptor, err := repo.Resolve(ctx, exampleDigest) if err != nil { @@ -404,7 +411,7 @@ func ExampleRepository_Fetch_manifestByDigest() { fmt.Println(string(pulled)) // Output: - // Example manifest content + // {"schemaVersion":2,"config":{"mediaType":"application/vnd.oci.image.config.v1+json","digest":"sha256:569224ae188c06e97b9fcadaeb2358fb0fb7c4eb105d49aee2620b2719abea43","size":22},"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar","digest":"sha256:ef79e47691ad1bc702d7a256da6323ec369a8fc3159b4f1798a47136f3b38c10","size":21}]} } // ExampleRepository_Fetch_artifactReferenceManifest gives an example of fetching @@ -443,8 +450,8 @@ func ExampleRepository_Fetch_artifactReferenceManifest() { panic(err) } // Output: - // {"mediaType":"application/vnd.oci.artifact.manifest.v1+json","artifactType":"example/SBoM","subject":{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:00e5ffa7d914b4e6aa3f1a324f37df0625ccc400be333deea5ecaa199f9eff5b","size":24}} - // {"mediaType":"application/vnd.oci.artifact.manifest.v1+json","artifactType":"example/signature","subject":{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:00e5ffa7d914b4e6aa3f1a324f37df0625ccc400be333deea5ecaa199f9eff5b","size":24}} + // {"mediaType":"application/vnd.oci.artifact.manifest.v1+json","artifactType":"example/SBoM","subject":{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:b53dc03a49f383ba230d8ac2b78a9c4aec132e4a9f36cc96524df98163202cc7","size":337}} + // {"mediaType":"application/vnd.oci.artifact.manifest.v1+json","artifactType":"example/signature","subject":{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:b53dc03a49f383ba230d8ac2b78a9c4aec132e4a9f36cc96524df98163202cc7","size":337}} } // ExampleRepository_fetchArtifactBlobs gives an example of pulling the blobs @@ -457,7 +464,7 @@ func ExampleRepository_fetchArtifactBlobs() { ctx := context.Background() // 1. Fetch the artifact manifest by digest. - exampleDigest := "sha256:1907bb31b7add4d47d74d2c5c1c10d67b757a996f8e8186e562113bc9879b1a3" + exampleDigest := "sha256:f3550fd0947402d140fd0470702abc92c69f7e9b08d5ca2438f42f8a0ea3fd97" descriptor, rc, err := repo.FetchReference(ctx, exampleDigest) if err != nil { panic(err) @@ -484,7 +491,7 @@ func ExampleRepository_fetchArtifactBlobs() { } // Output: - // {"mediaType":"application/vnd.oci.artifact.manifest.v1+json","artifactType":"example/manifest","blobs":[{"mediaType":"application/tar","digest":"sha256:8d6497c94694a292c04f85cd055d8b5c03eda835dd311e20dfbbf029ff9748cc","size":20}],"subject":{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:00e5ffa7d914b4e6aa3f1a324f37df0625ccc400be333deea5ecaa199f9eff5b","size":24}} + // {"mediaType":"application/vnd.oci.artifact.manifest.v1+json","artifactType":"example/manifest","blobs":[{"mediaType":"application/tar","digest":"sha256:8d6497c94694a292c04f85cd055d8b5c03eda835dd311e20dfbbf029ff9748cc","size":20}],"subject":{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:b53dc03a49f383ba230d8ac2b78a9c4aec132e4a9f36cc96524df98163202cc7","size":337}} // example blob content } @@ -510,7 +517,7 @@ func ExampleRepository_FetchReference_manifestByTag() { fmt.Println(string(pulledBlob)) // Output: - // Example manifest content + // {"schemaVersion":2,"config":{"mediaType":"application/vnd.oci.image.config.v1+json","digest":"sha256:569224ae188c06e97b9fcadaeb2358fb0fb7c4eb105d49aee2620b2719abea43","size":22},"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar","digest":"sha256:ef79e47691ad1bc702d7a256da6323ec369a8fc3159b4f1798a47136f3b38c10","size":21}]} } // ExampleRepository_FetchReference_manifestByDigest gives example snippets for downloading a manifest by digest. @@ -521,7 +528,7 @@ func ExampleRepository_FetchReference_manifestByDigest() { } ctx := context.Background() - exampleDigest := "sha256:00e5ffa7d914b4e6aa3f1a324f37df0625ccc400be333deea5ecaa199f9eff5b" + exampleDigest := "sha256:b53dc03a49f383ba230d8ac2b78a9c4aec132e4a9f36cc96524df98163202cc7" descriptor, rc, err := repo.FetchReference(ctx, exampleDigest) if err != nil { panic(err) @@ -535,7 +542,7 @@ func ExampleRepository_FetchReference_manifestByDigest() { fmt.Println(string(pulled)) // Output: - // Example manifest content + // {"schemaVersion":2,"config":{"mediaType":"application/vnd.oci.image.config.v1+json","digest":"sha256:569224ae188c06e97b9fcadaeb2358fb0fb7c4eb105d49aee2620b2719abea43","size":22},"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar","digest":"sha256:ef79e47691ad1bc702d7a256da6323ec369a8fc3159b4f1798a47136f3b38c10","size":21}]} } // ExampleRepository_Fetch_layer gives example snippets for downloading a layer blob by digest. @@ -593,7 +600,7 @@ func ExampleRepository_Tag() { } ctx := context.Background() - exampleDigest := "sha256:00e5ffa7d914b4e6aa3f1a324f37df0625ccc400be333deea5ecaa199f9eff5b" + exampleDigest := "sha256:b53dc03a49f383ba230d8ac2b78a9c4aec132e4a9f36cc96524df98163202cc7" descriptor, err := repo.Resolve(ctx, exampleDigest) if err != nil { panic(err) @@ -623,7 +630,7 @@ func ExampleRepository_TagReference() { } // tag a manifest referenced by the exampleDigest below - exampleDigest := "sha256:00e5ffa7d914b4e6aa3f1a324f37df0625ccc400be333deea5ecaa199f9eff5b" + exampleDigest := "sha256:b53dc03a49f383ba230d8ac2b78a9c4aec132e4a9f36cc96524df98163202cc7" tag := "latest" err = oras.Tag(ctx, repo, exampleDigest, tag) if err != nil { @@ -685,9 +692,9 @@ func Example_pullByTag() { fmt.Println(string(pulledBlob)) // Output: - // sha256:00e5ffa7d914b4e6aa3f1a324f37df0625ccc400be333deea5ecaa199f9eff5b - // 24 - // Example manifest content + // sha256:b53dc03a49f383ba230d8ac2b78a9c4aec132e4a9f36cc96524df98163202cc7 + // 337 + // {"schemaVersion":2,"config":{"mediaType":"application/vnd.oci.image.config.v1+json","digest":"sha256:569224ae188c06e97b9fcadaeb2358fb0fb7c4eb105d49aee2620b2719abea43","size":22},"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar","digest":"sha256:ef79e47691ad1bc702d7a256da6323ec369a8fc3159b4f1798a47136f3b38c10","size":21}]} } func Example_pullByDigest() { @@ -697,7 +704,7 @@ func Example_pullByDigest() { } ctx := context.Background() - exampleDigest := "sha256:00e5ffa7d914b4e6aa3f1a324f37df0625ccc400be333deea5ecaa199f9eff5b" + exampleDigest := "sha256:b53dc03a49f383ba230d8ac2b78a9c4aec132e4a9f36cc96524df98163202cc7" // 1. resolve the descriptor descriptor, err := repo.Resolve(ctx, exampleDigest) if err != nil { @@ -714,9 +721,9 @@ func Example_pullByDigest() { fmt.Println(string(pulledBlob)) // Output: - // sha256:00e5ffa7d914b4e6aa3f1a324f37df0625ccc400be333deea5ecaa199f9eff5b - // 24 - // Example manifest content + // sha256:b53dc03a49f383ba230d8ac2b78a9c4aec132e4a9f36cc96524df98163202cc7 + // 337 + // {"schemaVersion":2,"config":{"mediaType":"application/vnd.oci.image.config.v1+json","digest":"sha256:569224ae188c06e97b9fcadaeb2358fb0fb7c4eb105d49aee2620b2719abea43","size":22},"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar","digest":"sha256:ef79e47691ad1bc702d7a256da6323ec369a8fc3159b4f1798a47136f3b38c10","size":21}]} } // Example_pushAndTag gives example snippet of pushing an OCI image with a tag. diff --git a/registry/remote/repository.go b/registry/remote/repository.go index 8d0c3dcf..1f8bf730 100644 --- a/registry/remote/repository.go +++ b/registry/remote/repository.go @@ -26,10 +26,13 @@ import ( "net/http" "strconv" "strings" + "sync" "sync/atomic" "github.com/opencontainers/go-digest" + specs "github.com/opencontainers/image-spec/specs-go" ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "oras.land/oras-go/v2/content" "oras.land/oras-go/v2/errdef" "oras.land/oras-go/v2/internal/cas" "oras.land/oras-go/v2/internal/httputil" @@ -116,6 +119,9 @@ type Repository struct { // referrersState represents that if the repository supports Referrers API. // default: referrersStateUnknown referrersState referrersState + + // referrersTagLocks maps a referrers tag to a lock. + referrersTagLocks sync.Map // map[string]sync.Mutex } // NewRepository creates a client to the remote repository identified by a @@ -165,6 +171,23 @@ func (r *Repository) loadReferrersState() referrersState { return atomic.LoadInt32(&r.referrersState) } +// lockReferrersTag locks a referrers tag. +func (r *Repository) lockReferrersTag(tag string) { + v, _ := r.referrersTagLocks.LoadOrStore(tag, &sync.Mutex{}) + lock := v.(*sync.Mutex) + lock.Lock() +} + +// unlockReferrersTag unlocks a referrers tag. +func (r *Repository) unlockReferrersTag(tag string) { + v, ok := r.referrersTagLocks.Load(tag) + if !ok { + return + } + lock := v.(*sync.Mutex) + lock.Unlock() +} + // client returns an HTTP client used to access the remote repository. // A default HTTP client is return if the client is not configured. func (r *Repository) client() Client { @@ -457,14 +480,14 @@ func (r *Repository) referrersPageByAPI(ctx context.Context, artifactType string return parseLink(resp) } -// referrersByTagSchema lists the descriptors of manifests directly referencing -// the given manifest descriptor by requesting referrers tag. +// referrersByTagSchema lists the descriptors of manifests directly +// referencing the given manifest descriptor by requesting referrers tag. // fn is called for the referrers result. If artifactType is not empty, // only referrers of the same artifact type are fed to fn. // reference: https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#backwards-compatibility func (r *Repository) referrersByTagSchema(ctx context.Context, desc ocispec.Descriptor, artifactType string, fn func(referrers []ocispec.Descriptor) error) error { referrersTag := buildReferrersTag(desc) - desc, rc, err := r.FetchReference(ctx, referrersTag) + _, referrers, err := r.referrersFromIndex(ctx, referrersTag) if err != nil { if errors.Is(err, errdef.ErrNotFound) { // no referrers to the manifest @@ -472,21 +495,33 @@ func (r *Repository) referrersByTagSchema(ctx context.Context, desc ocispec.Desc } return err } + + filtered := filterReferrers(referrers, artifactType) + if len(filtered) == 0 { + return nil + } + return fn(filtered) +} + +// referrersFromIndex queries the referrers index using the the given referrers +// tag. If Succeeded, returns the descriptor of referrers index and the +// referrers list. +func (r *Repository) referrersFromIndex(ctx context.Context, referrersTag string) (ocispec.Descriptor, []ocispec.Descriptor, error) { + desc, rc, err := r.FetchReference(ctx, referrersTag) + if err != nil { + return ocispec.Descriptor{}, nil, err + } defer rc.Close() if err := limitSize(desc, r.MaxMetadataBytes); err != nil { - return fmt.Errorf("failed to read referrers index from referrers tag %s: %w", referrersTag, err) + return ocispec.Descriptor{}, nil, fmt.Errorf("failed to read referrers index from referrers tag %s: %w", referrersTag, err) } var index ocispec.Index if err := decodeJSON(rc, desc, &index); err != nil { - return fmt.Errorf("failed to decode referrers index from referrers tag %s: %w", referrersTag, err) + return ocispec.Descriptor{}, nil, fmt.Errorf("failed to decode referrers index from referrers tag %s: %w", referrersTag, err) } - referrers := filterReferrers(index.Manifests, artifactType) - if len(referrers) == 0 { - return nil - } - return fn(referrers) + return desc, index.Manifests, nil } // buildReferrersTag builds the referrers tag for the given manifest descriptor. @@ -878,7 +913,7 @@ func (s *manifestStore) Fetch(ctx context.Context, target ocispec.Descriptor) (r // Push pushes the content, matching the expected descriptor. func (s *manifestStore) Push(ctx context.Context, expected ocispec.Descriptor, content io.Reader) error { - return s.push(ctx, expected, content, expected.Digest.String()) + return s.pushWithIndexing(ctx, expected, content, expected.Digest.String()) } // Exists returns true if the described content exists. @@ -992,7 +1027,7 @@ func (s *manifestStore) PushReference(ctx context.Context, expected ocispec.Desc if err != nil { return err } - return s.push(ctx, expected, content, ref.Reference) + return s.pushWithIndexing(ctx, expected, content, ref.Reference) } // push pushes the manifest content, matching the expected descriptor. @@ -1051,6 +1086,152 @@ func (s *manifestStore) push(ctx context.Context, expected ocispec.Descriptor, c return verifyContentDigest(resp, expected.Digest) } +// pushWithIndexing pushes the manifest content matching the expected descriptor, +// and indexes referrers for the manifest when needed. +func (s *manifestStore) pushWithIndexing(ctx context.Context, expected ocispec.Descriptor, r io.Reader, reference string) error { + switch expected.MediaType { + case ocispec.MediaTypeArtifactManifest, ocispec.MediaTypeImageManifest: + if state := s.repo.loadReferrersState(); state == referrersStateSupported { + // referrers API is available, no client-side indexing needed + return s.push(ctx, expected, r, reference) + } + + if err := limitSize(expected, s.repo.MaxMetadataBytes); err != nil { + return err + } + manifestJSON, err := content.ReadAll(r, expected) + if err != nil { + return err + } + if err := s.push(ctx, expected, bytes.NewReader(manifestJSON), reference); err != nil { + return err + } + return s.indexReferrers(ctx, expected, manifestJSON) + default: + return s.push(ctx, expected, r, reference) + } +} + +// indexReferrers indexes referrers for image or artifact manifest with +// the subject field. +// Reference: https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-manifests-with-subject +func (s *manifestStore) indexReferrers(ctx context.Context, desc ocispec.Descriptor, manifestJSON []byte) error { + var subject ocispec.Descriptor + switch desc.MediaType { + case ocispec.MediaTypeArtifactManifest: + var manifest ocispec.Artifact + if err := json.Unmarshal(manifestJSON, &manifest); err != nil { + return fmt.Errorf("failed to decode manifest: %s: %s: %w", desc.Digest, desc.MediaType, err) + } + if manifest.Subject == nil { + // no subject, no indexing needed + return nil + } + subject = *manifest.Subject + desc.ArtifactType = manifest.ArtifactType + desc.Annotations = manifest.Annotations + case ocispec.MediaTypeImageManifest: + var manifest ocispec.Manifest + if err := json.Unmarshal(manifestJSON, &manifest); err != nil { + return fmt.Errorf("failed to decode manifest: %s: %s: %w", desc.Digest, desc.MediaType, err) + } + if manifest.Subject == nil { + // no subject, no indexing needed + return nil + } + subject = *manifest.Subject + desc.ArtifactType = manifest.Config.MediaType + desc.Annotations = manifest.Annotations + default: + return nil + } + + yes, err := s.repo.isReferrersAPIAvailable(ctx, subject) + if err != nil { + return err + } + if yes { + // referrers API is available, no client-side indexing needed + return nil + } + return s.updateReferrersIndex(ctx, desc, subject) +} + +// updateReferrersIndex updates the referrers index for desc referencing subject. +func (s *manifestStore) updateReferrersIndex(ctx context.Context, desc, subject ocispec.Descriptor) error { + // there can be multiple go-routines updating the referrers tag concurrently + referrersTag := buildReferrersTag(subject) + s.repo.lockReferrersTag(referrersTag) + defer s.repo.unlockReferrersTag(referrersTag) + + var skipDelete bool + oldIndexDesc, referrers, err := s.repo.referrersFromIndex(ctx, referrersTag) + if err != nil { + if !errors.Is(err, errdef.ErrNotFound) { + return err + } + // no old index found, skip delete + skipDelete = true + } + + for _, r := range referrers { + if content.Equal(r, desc) { + // desc is already in the referrers list, skip update + return nil + } + } + referrers = append(referrers, desc) + newIndexDesc, newIndex, err := generateIndex(referrers) + if err != nil { + return fmt.Errorf("failed to generate referrers index for referrers tag %s: %w", referrersTag, err) + } + if err := s.push(ctx, newIndexDesc, bytes.NewReader(newIndex), referrersTag); err != nil { + return fmt.Errorf("failed to push referrers index tagged by %s: %w", referrersTag, err) + } + + if skipDelete { + return nil + } + return s.repo.delete(ctx, oldIndexDesc, true) +} + +// isReferrersAPIAvailable returns true if the Referrers API is available for r. +func (r *Repository) isReferrersAPIAvailable(ctx context.Context, desc ocispec.Descriptor) (bool, error) { + switch r.loadReferrersState() { + case referrersStateSupported: + return true, nil + case referrersStateUnsupported: + return false, nil + } + + // referrers state is unknown + ref := r.Reference + ref.Reference = desc.Digest.String() + ctx = registryutil.WithScopeHint(ctx, ref, auth.ActionPull) + + url := buildReferrersURL(r.PlainHTTP, ref, "") + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return false, err + } + resp, err := r.client().Do(req) + if err != nil { + return false, err + } + defer resp.Body.Close() + + switch resp.StatusCode { + case http.StatusOK: + r.SetReferrersCapability(true) + return true, nil + case http.StatusNotFound: + r.SetReferrersCapability(false) + return false, nil + default: + return false, errutil.ParseErrorResponse(resp) + } +} + // ParseReference parses a reference to a fully qualified reference. func (s *manifestStore) ParseReference(reference string) (registry.Reference, error) { return s.repo.ParseReference(reference) @@ -1190,3 +1371,23 @@ func verifyContentDigest(resp *http.Response, expected digest.Digest) error { return nil } + +// generateIndex generates an image index containing the given manifests list. +func generateIndex(manifests []ocispec.Descriptor) (ocispec.Descriptor, []byte, error) { + if manifests == nil { + manifests = []ocispec.Descriptor{} // make it an empty array to prevent potential server-side bugs + } + index := ocispec.Index{ + Versioned: specs.Versioned{ + SchemaVersion: 2, // historical value. does not pertain to OCI or docker version + }, + MediaType: ocispec.MediaTypeImageIndex, + Manifests: manifests, + } + indexJSON, err := json.Marshal(index) + if err != nil { + return ocispec.Descriptor{}, nil, err + } + indexDesc := content.NewDescriptorFromBytes(index.MediaType, indexJSON) + return indexDesc, indexJSON, nil +} diff --git a/registry/remote/repository_test.go b/registry/remote/repository_test.go index 188a1f0b..a3ded7d9 100644 --- a/registry/remote/repository_test.go +++ b/registry/remote/repository_test.go @@ -33,8 +33,9 @@ import ( "testing" "github.com/opencontainers/go-digest" - "github.com/opencontainers/image-spec/specs-go" + specs "github.com/opencontainers/image-spec/specs-go" ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "oras.land/oras-go/v2/content" "oras.land/oras-go/v2/errdef" "oras.land/oras-go/v2/internal/interfaces" "oras.land/oras-go/v2/registry" @@ -1016,8 +1017,8 @@ func TestRepository_Referrers(t *testing.T) { ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { path := "/v2/test/referrers/" + manifestDesc.Digest.String() if r.Method != http.MethodGet || r.URL.Path != path { - refTag := strings.Replace(manifestDesc.Digest.String(), ":", "-", 1) - if r.URL.Path != "/v2/test/manifests/"+refTag { + referrersTag := strings.Replace(manifestDesc.Digest.String(), ":", "-", 1) + if r.URL.Path != "/v2/test/manifests/"+referrersTag { t.Errorf("unexpected access: %s %q", r.Method, r.URL) } w.WriteHeader(http.StatusNotFound) @@ -1182,8 +1183,8 @@ func TestRepository_Referrers_TagSchemaFallback(t *testing.T) { }, } ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - refTag := strings.Replace(manifestDesc.Digest.String(), ":", "-", 1) - path := "/v2/test/manifests/" + refTag + referrersTag := strings.Replace(manifestDesc.Digest.String(), ":", "-", 1) + path := "/v2/test/manifests/" + referrersTag if r.Method != http.MethodGet || r.URL.Path != path { if r.URL.Path != "/v2/test/referrers/"+manifestDesc.Digest.String() { t.Errorf("unexpected access: %s %q", r.Method, r.URL) @@ -1286,8 +1287,8 @@ func TestRepository_Referrers_TagSchemaFallback_NotFound(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { referrersUrl := "/v2/test/referrers/" + manifestDesc.Digest.String() - refTag := strings.Replace(manifestDesc.Digest.String(), ":", "-", 1) - tagSchemaUrl := "/v2/test/manifests/" + refTag + referrersTag := strings.Replace(manifestDesc.Digest.String(), ":", "-", 1) + tagSchemaUrl := "/v2/test/manifests/" + referrersTag if r.Method == http.MethodGet || r.URL.Path == referrersUrl || r.URL.Path == tagSchemaUrl { @@ -1353,8 +1354,8 @@ func TestRepository_Referrers_BadRequest(t *testing.T) { } ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { referrersUrl := "/v2/test/referrers/" + manifestDesc.Digest.String() - refTag := strings.Replace(manifestDesc.Digest.String(), ":", "-", 1) - tagSchemaUrl := "/v2/test/manifests/" + refTag + referrersTag := strings.Replace(manifestDesc.Digest.String(), ":", "-", 1) + tagSchemaUrl := "/v2/test/manifests/" + referrersTag if r.Method == http.MethodGet || r.URL.Path == referrersUrl || r.URL.Path == tagSchemaUrl { @@ -1750,8 +1751,8 @@ func TestRepository_Referrers_TagSchemaFallback_ClientFiltering(t *testing.T) { } ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - refTag := strings.Replace(manifestDesc.Digest.String(), ":", "-", 1) - path := "/v2/test/manifests/" + refTag + referrersTag := strings.Replace(manifestDesc.Digest.String(), ":", "-", 1) + path := "/v2/test/manifests/" + referrersTag if r.Method != http.MethodGet || r.URL.Path != path { if r.URL.Path != "/v2/test/referrers/"+manifestDesc.Digest.String() { t.Errorf("unexpected access: %s %q", r.Method, r.URL) @@ -2767,6 +2768,369 @@ func Test_ManifestStore_Push(t *testing.T) { } } +func Test_ManifestStore_Push_ReferrersAPIAvailable(t *testing.T) { + // generate test content + subject := []byte(`{"layers":[]}`) + subjectDesc := content.NewDescriptorFromBytes(ocispec.MediaTypeArtifactManifest, subject) + artifact := ocispec.Artifact{ + MediaType: ocispec.MediaTypeArtifactManifest, + Subject: &subjectDesc, + } + artifactJSON, err := json.Marshal(artifact) + if err != nil { + t.Errorf("failed to marshal manifest: %v", err) + } + artifactDesc := content.NewDescriptorFromBytes(artifact.MediaType, artifactJSON) + manifest := ocispec.Manifest{ + MediaType: ocispec.MediaTypeImageManifest, + Subject: &subjectDesc, + } + manifestJSON, err := json.Marshal(manifest) + if err != nil { + t.Errorf("failed to marshal manifest: %v", err) + } + manifestDesc := content.NewDescriptorFromBytes(manifest.MediaType, manifestJSON) + + var gotManifest []byte + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+artifactDesc.Digest.String(): + if contentType := r.Header.Get("Content-Type"); contentType != artifactDesc.MediaType { + w.WriteHeader(http.StatusBadRequest) + break + } + buf := bytes.NewBuffer(nil) + if _, err := buf.ReadFrom(r.Body); err != nil { + t.Errorf("fail to read: %v", err) + } + gotManifest = buf.Bytes() + w.Header().Set("Docker-Content-Digest", artifactDesc.Digest.String()) + w.WriteHeader(http.StatusCreated) + case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+manifestDesc.Digest.String(): + if contentType := r.Header.Get("Content-Type"); contentType != manifestDesc.MediaType { + w.WriteHeader(http.StatusBadRequest) + break + } + buf := bytes.NewBuffer(nil) + if _, err := buf.ReadFrom(r.Body); err != nil { + t.Errorf("fail to read: %v", err) + } + gotManifest = buf.Bytes() + w.Header().Set("Docker-Content-Digest", manifestDesc.Digest.String()) + w.WriteHeader(http.StatusCreated) + case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+subjectDesc.Digest.String(): + result := ocispec.Index{ + Versioned: specs.Versioned{ + SchemaVersion: 2, // historical value. does not pertain to OCI or docker version + }, + MediaType: ocispec.MediaTypeImageIndex, + Manifests: []ocispec.Descriptor{}, + } + if err := json.NewEncoder(w).Encode(result); err != nil { + t.Errorf("failed to write response: %v", err) + } + default: + t.Errorf("unexpected access: %s %s", r.Method, r.URL) + w.WriteHeader(http.StatusNotFound) + } + })) + defer ts.Close() + uri, err := url.Parse(ts.URL) + if err != nil { + t.Fatalf("invalid test http server: %v", err) + } + + ctx := context.Background() + repo, err := NewRepository(uri.Host + "/test") + if err != nil { + t.Fatalf("NewRepository() error = %v", err) + } + repo.PlainHTTP = true + + // test push artifact with subject + if state := repo.loadReferrersState(); state != referrersStateUnknown { + t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) + } + err = repo.Push(ctx, artifactDesc, bytes.NewReader(artifactJSON)) + if err != nil { + t.Fatalf("Manifests.Push() error = %v", err) + } + if !bytes.Equal(gotManifest, artifactJSON) { + t.Errorf("Manifests.Push() = %v, want %v", string(gotManifest), string(artifactJSON)) + } + + // test push image manifest with subject + if state := repo.loadReferrersState(); state != referrersStateSupported { + t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported) + } + err = repo.Push(ctx, manifestDesc, bytes.NewReader(manifestJSON)) + if err != nil { + t.Fatalf("Manifests.Push() error = %v", err) + } + if !bytes.Equal(gotManifest, manifestJSON) { + t.Errorf("Manifests.Push() = %v, want %v", string(gotManifest), string(manifestJSON)) + } +} + +func Test_ManifestStore_Push_ReferrersAPIUnavailable(t *testing.T) { + // generate test content + subject := []byte(`{"layers":[]}`) + subjectDesc := content.NewDescriptorFromBytes(ocispec.MediaTypeArtifactManifest, subject) + referrersTag := strings.Replace(subjectDesc.Digest.String(), ":", "-", 1) + artifact := ocispec.Artifact{ + MediaType: ocispec.MediaTypeArtifactManifest, + Subject: &subjectDesc, + ArtifactType: "application/vnd.test", + Annotations: map[string]string{"foo": "bar"}, + } + artifactJSON, err := json.Marshal(artifact) + if err != nil { + t.Errorf("failed to marshal manifest: %v", err) + } + artifactDesc := content.NewDescriptorFromBytes(artifact.MediaType, artifactJSON) + artifactDesc.ArtifactType = artifact.ArtifactType + artifactDesc.Annotations = artifact.Annotations + + // test push artifact with subject + index_1 := ocispec.Index{ + Versioned: specs.Versioned{ + SchemaVersion: 2, // historical value. does not pertain to OCI or docker version + }, + MediaType: ocispec.MediaTypeImageIndex, + Manifests: []ocispec.Descriptor{ + artifactDesc, + }, + } + indexJSON_1, err := json.Marshal(index_1) + if err != nil { + t.Errorf("failed to marshal manifest: %v", err) + } + indexDesc_1 := content.NewDescriptorFromBytes(index_1.MediaType, indexJSON_1) + var gotManifest []byte + var gotReferrerIndex []byte + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+artifactDesc.Digest.String(): + if contentType := r.Header.Get("Content-Type"); contentType != artifactDesc.MediaType { + w.WriteHeader(http.StatusBadRequest) + break + } + buf := bytes.NewBuffer(nil) + if _, err := buf.ReadFrom(r.Body); err != nil { + t.Errorf("fail to read: %v", err) + } + gotManifest = buf.Bytes() + w.Header().Set("Docker-Content-Digest", artifactDesc.Digest.String()) + w.WriteHeader(http.StatusCreated) + case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+subjectDesc.Digest.String(): + w.WriteHeader(http.StatusNotFound) + case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+referrersTag: + w.WriteHeader(http.StatusNotFound) + case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+referrersTag: + if contentType := r.Header.Get("Content-Type"); contentType != ocispec.MediaTypeImageIndex { + w.WriteHeader(http.StatusBadRequest) + break + } + buf := bytes.NewBuffer(nil) + if _, err := buf.ReadFrom(r.Body); err != nil { + t.Errorf("fail to read: %v", err) + } + gotReferrerIndex = buf.Bytes() + w.Header().Set("Docker-Content-Digest", indexDesc_1.Digest.String()) + w.WriteHeader(http.StatusCreated) + default: + t.Errorf("unexpected access: %s %s", r.Method, r.URL) + w.WriteHeader(http.StatusNotFound) + } + })) + defer ts.Close() + uri, err := url.Parse(ts.URL) + if err != nil { + t.Fatalf("invalid test http server: %v", err) + } + + ctx := context.Background() + repo, err := NewRepository(uri.Host + "/test") + if err != nil { + t.Fatalf("NewRepository() error = %v", err) + } + repo.PlainHTTP = true + + if state := repo.loadReferrersState(); state != referrersStateUnknown { + t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) + } + err = repo.Push(ctx, artifactDesc, bytes.NewReader(artifactJSON)) + if err != nil { + t.Fatalf("Manifests.Push() error = %v", err) + } + if !bytes.Equal(gotManifest, artifactJSON) { + t.Errorf("Manifests.Push() = %v, want %v", string(gotManifest), string(artifactJSON)) + } + if !bytes.Equal(gotReferrerIndex, indexJSON_1) { + t.Errorf("got referrers index = %v, want %v", string(gotReferrerIndex), string(indexJSON_1)) + } + if state := repo.loadReferrersState(); state != referrersStateUnsupported { + t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) + } + + // test push image manifest with subject, referrer list should be updated + manifest := ocispec.Manifest{ + MediaType: ocispec.MediaTypeImageManifest, + Config: ocispec.Descriptor{ + MediaType: "testconfig", + }, + Subject: &subjectDesc, + Annotations: map[string]string{"foo": "bar"}, + } + manifestJSON, err := json.Marshal(manifest) + if err != nil { + t.Errorf("failed to marshal manifest: %v", err) + } + manifestDesc := content.NewDescriptorFromBytes(manifest.MediaType, manifestJSON) + manifestDesc.ArtifactType = manifest.Config.MediaType + manifestDesc.Annotations = manifest.Annotations + index_2 := ocispec.Index{ + Versioned: specs.Versioned{ + SchemaVersion: 2, // historical value. does not pertain to OCI or docker version + }, + MediaType: ocispec.MediaTypeImageIndex, + Manifests: []ocispec.Descriptor{ + artifactDesc, + manifestDesc, + }, + } + indexJSON_2, err := json.Marshal(index_2) + if err != nil { + t.Errorf("failed to marshal manifest: %v", err) + } + indexDesc_2 := content.NewDescriptorFromBytes(index_2.MediaType, indexJSON_2) + var manifestDeleted bool + ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+manifestDesc.Digest.String(): + if contentType := r.Header.Get("Content-Type"); contentType != manifestDesc.MediaType { + w.WriteHeader(http.StatusBadRequest) + break + } + buf := bytes.NewBuffer(nil) + if _, err := buf.ReadFrom(r.Body); err != nil { + t.Errorf("fail to read: %v", err) + } + gotManifest = buf.Bytes() + w.Header().Set("Docker-Content-Digest", manifestDesc.Digest.String()) + w.WriteHeader(http.StatusCreated) + case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+subjectDesc.Digest.String(): + w.WriteHeader(http.StatusNotFound) + case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+referrersTag: + w.Write(indexJSON_1) + case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+referrersTag: + if contentType := r.Header.Get("Content-Type"); contentType != ocispec.MediaTypeImageIndex { + w.WriteHeader(http.StatusBadRequest) + break + } + buf := bytes.NewBuffer(nil) + if _, err := buf.ReadFrom(r.Body); err != nil { + t.Errorf("fail to read: %v", err) + } + gotReferrerIndex = buf.Bytes() + w.Header().Set("Docker-Content-Digest", indexDesc_2.Digest.String()) + w.WriteHeader(http.StatusCreated) + case r.Method == http.MethodDelete && r.URL.Path == "/v2/test/manifests/"+indexDesc_1.Digest.String(): + manifestDeleted = true + // no "Docker-Content-Digest" header for manifest deletion + w.WriteHeader(http.StatusAccepted) + default: + t.Errorf("unexpected access: %s %s", r.Method, r.URL) + w.WriteHeader(http.StatusNotFound) + } + })) + defer ts.Close() + uri, err = url.Parse(ts.URL) + if err != nil { + t.Fatalf("invalid test http server: %v", err) + } + + ctx = context.Background() + repo, err = NewRepository(uri.Host + "/test") + if err != nil { + t.Fatalf("NewRepository() error = %v", err) + } + repo.PlainHTTP = true + if state := repo.loadReferrersState(); state != referrersStateUnknown { + t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) + } + err = repo.Push(ctx, manifestDesc, bytes.NewReader(manifestJSON)) + if err != nil { + t.Fatalf("Manifests.Push() error = %v", err) + } + if !bytes.Equal(gotManifest, manifestJSON) { + t.Errorf("Manifests.Push() = %v, want %v", string(gotManifest), string(manifestJSON)) + } + if !bytes.Equal(gotReferrerIndex, indexJSON_2) { + t.Errorf("got referrers index = %v, want %v", string(gotReferrerIndex), string(indexJSON_2)) + } + if !manifestDeleted { + t.Errorf("manifestDeleted = %v, want %v", manifestDeleted, true) + } + if state := repo.loadReferrersState(); state != referrersStateUnsupported { + t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) + } + + // test push image manifest with subject again, referrers list should not be changed + ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+manifestDesc.Digest.String(): + if contentType := r.Header.Get("Content-Type"); contentType != manifestDesc.MediaType { + w.WriteHeader(http.StatusBadRequest) + break + } + buf := bytes.NewBuffer(nil) + if _, err := buf.ReadFrom(r.Body); err != nil { + t.Errorf("fail to read: %v", err) + } + gotManifest = buf.Bytes() + w.Header().Set("Docker-Content-Digest", manifestDesc.Digest.String()) + w.WriteHeader(http.StatusCreated) + case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+subjectDesc.Digest.String(): + w.WriteHeader(http.StatusNotFound) + case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+referrersTag: + w.Write(indexJSON_2) + default: + t.Errorf("unexpected access: %s %s", r.Method, r.URL) + w.WriteHeader(http.StatusNotFound) + } + })) + defer ts.Close() + uri, err = url.Parse(ts.URL) + if err != nil { + t.Fatalf("invalid test http server: %v", err) + } + + ctx = context.Background() + repo, err = NewRepository(uri.Host + "/test") + if err != nil { + t.Fatalf("NewRepository() error = %v", err) + } + repo.PlainHTTP = true + if state := repo.loadReferrersState(); state != referrersStateUnknown { + t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) + } + err = repo.Push(ctx, manifestDesc, bytes.NewReader(manifestJSON)) + if err != nil { + t.Fatalf("Manifests.Push() error = %v", err) + } + if !bytes.Equal(gotManifest, manifestJSON) { + t.Errorf("Manifests.Push() = %v, want %v", string(gotManifest), string(manifestJSON)) + } + // referrers list should not be changed + if !bytes.Equal(gotReferrerIndex, indexJSON_2) { + t.Errorf("got referrers index = %v, want %v", string(gotReferrerIndex), string(indexJSON_2)) + } + if state := repo.loadReferrersState(); state != referrersStateUnsupported { + t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) + } +} + func Test_ManifestStore_Exists(t *testing.T) { manifest := []byte(`{"layers":[]}`) manifestDesc := ocispec.Descriptor{ @@ -3116,41 +3480,408 @@ func Test_ManifestStore_FetchReference(t *testing.T) { if got := buf.Bytes(); !bytes.Equal(got, manifest) { t.Errorf("Manifests.FetchReference() = %v, want %v", got, manifest) } -} - -func Test_ManifestStore_Tag(t *testing.T) { - blob := []byte("hello world") - blobDesc := ocispec.Descriptor{ - MediaType: "test", - Digest: digest.FromBytes(blob), - Size: int64(len(blob)), +} + +func Test_ManifestStore_Tag(t *testing.T) { + blob := []byte("hello world") + blobDesc := ocispec.Descriptor{ + MediaType: "test", + Digest: digest.FromBytes(blob), + Size: int64(len(blob)), + } + index := []byte(`{"manifests":[]}`) + indexDesc := ocispec.Descriptor{ + MediaType: ocispec.MediaTypeImageIndex, + Digest: digest.FromBytes(index), + Size: int64(len(index)), + } + var gotIndex []byte + ref := "foobar" + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+blobDesc.Digest.String(): + w.WriteHeader(http.StatusNotFound) + case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+indexDesc.Digest.String(): + if accept := r.Header.Get("Accept"); !strings.Contains(accept, indexDesc.MediaType) { + t.Errorf("manifest not convertable: %s", accept) + w.WriteHeader(http.StatusBadRequest) + return + } + w.Header().Set("Content-Type", indexDesc.MediaType) + w.Header().Set("Docker-Content-Digest", indexDesc.Digest.String()) + if _, err := w.Write(index); err != nil { + t.Errorf("failed to write %q: %v", r.URL, err) + } + case r.Method == http.MethodPut && + r.URL.Path == "/v2/test/manifests/"+ref || r.URL.Path == "/v2/test/manifests/"+indexDesc.Digest.String(): + if contentType := r.Header.Get("Content-Type"); contentType != indexDesc.MediaType { + w.WriteHeader(http.StatusBadRequest) + break + } + buf := bytes.NewBuffer(nil) + if _, err := buf.ReadFrom(r.Body); err != nil { + t.Errorf("fail to read: %v", err) + } + gotIndex = buf.Bytes() + w.Header().Set("Docker-Content-Digest", indexDesc.Digest.String()) + w.WriteHeader(http.StatusCreated) + return + default: + t.Errorf("unexpected access: %s %s", r.Method, r.URL) + w.WriteHeader(http.StatusForbidden) + } + })) + defer ts.Close() + uri, err := url.Parse(ts.URL) + if err != nil { + t.Fatalf("invalid test http server: %v", err) + } + + repo, err := NewRepository(uri.Host + "/test") + if err != nil { + t.Fatalf("NewRepository() error = %v", err) + } + store := repo.Manifests() + repo.PlainHTTP = true + ctx := context.Background() + + err = store.Tag(ctx, blobDesc, ref) + if err == nil { + t.Errorf("Repository.Tag() error = %v, wantErr %v", err, true) + } + + err = store.Tag(ctx, indexDesc, ref) + if err != nil { + t.Fatalf("Repository.Tag() error = %v", err) + } + if !bytes.Equal(gotIndex, index) { + t.Errorf("Repository.Tag() = %v, want %v", gotIndex, index) + } + + gotIndex = nil + err = store.Tag(ctx, indexDesc, indexDesc.Digest.String()) + if err != nil { + t.Fatalf("Repository.Tag() error = %v", err) + } + if !bytes.Equal(gotIndex, index) { + t.Errorf("Repository.Tag() = %v, want %v", gotIndex, index) + } +} + +func Test_ManifestStore_PushReference(t *testing.T) { + index := []byte(`{"manifests":[]}`) + indexDesc := ocispec.Descriptor{ + MediaType: ocispec.MediaTypeImageIndex, + Digest: digest.FromBytes(index), + Size: int64(len(index)), + } + var gotIndex []byte + ref := "foobar" + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+ref: + if contentType := r.Header.Get("Content-Type"); contentType != indexDesc.MediaType { + w.WriteHeader(http.StatusBadRequest) + break + } + buf := bytes.NewBuffer(nil) + if _, err := buf.ReadFrom(r.Body); err != nil { + t.Errorf("fail to read: %v", err) + } + gotIndex = buf.Bytes() + w.Header().Set("Docker-Content-Digest", indexDesc.Digest.String()) + w.WriteHeader(http.StatusCreated) + return + default: + t.Errorf("unexpected access: %s %s", r.Method, r.URL) + w.WriteHeader(http.StatusForbidden) + } + })) + defer ts.Close() + uri, err := url.Parse(ts.URL) + if err != nil { + t.Fatalf("invalid test http server: %v", err) + } + + repo, err := NewRepository(uri.Host + "/test") + if err != nil { + t.Fatalf("NewRepository() error = %v", err) + } + store := repo.Manifests() + repo.PlainHTTP = true + ctx := context.Background() + err = store.PushReference(ctx, indexDesc, bytes.NewReader(index), ref) + if err != nil { + t.Fatalf("Repository.PushReference() error = %v", err) + } + if !bytes.Equal(gotIndex, index) { + t.Errorf("Repository.PushReference() = %v, want %v", gotIndex, index) + } +} + +func Test_ManifestStore_PushReference_ReferrersAPIAvailable(t *testing.T) { + // generate test content + subject := []byte(`{"layers":[]}`) + subjectDesc := content.NewDescriptorFromBytes(ocispec.MediaTypeArtifactManifest, subject) + artifact := ocispec.Artifact{ + MediaType: ocispec.MediaTypeArtifactManifest, + Subject: &subjectDesc, + } + artifactJSON, err := json.Marshal(artifact) + if err != nil { + t.Errorf("failed to marshal manifest: %v", err) + } + artifactDesc := content.NewDescriptorFromBytes(artifact.MediaType, artifactJSON) + artifactRef := "foo" + + manifest := ocispec.Manifest{ + MediaType: ocispec.MediaTypeImageManifest, + Subject: &subjectDesc, + } + manifestJSON, err := json.Marshal(manifest) + if err != nil { + t.Errorf("failed to marshal manifest: %v", err) + } + manifestDesc := content.NewDescriptorFromBytes(manifest.MediaType, manifestJSON) + manifestRef := "bar" + + var gotManifest []byte + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+artifactRef: + if contentType := r.Header.Get("Content-Type"); contentType != artifactDesc.MediaType { + w.WriteHeader(http.StatusBadRequest) + break + } + buf := bytes.NewBuffer(nil) + if _, err := buf.ReadFrom(r.Body); err != nil { + t.Errorf("fail to read: %v", err) + } + gotManifest = buf.Bytes() + w.Header().Set("Docker-Content-Digest", artifactDesc.Digest.String()) + w.WriteHeader(http.StatusCreated) + case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+manifestRef: + if contentType := r.Header.Get("Content-Type"); contentType != manifestDesc.MediaType { + w.WriteHeader(http.StatusBadRequest) + break + } + buf := bytes.NewBuffer(nil) + if _, err := buf.ReadFrom(r.Body); err != nil { + t.Errorf("fail to read: %v", err) + } + gotManifest = buf.Bytes() + w.Header().Set("Docker-Content-Digest", manifestDesc.Digest.String()) + w.WriteHeader(http.StatusCreated) + case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+subjectDesc.Digest.String(): + result := ocispec.Index{ + Versioned: specs.Versioned{ + SchemaVersion: 2, // historical value. does not pertain to OCI or docker version + }, + MediaType: ocispec.MediaTypeImageIndex, + Manifests: []ocispec.Descriptor{}, + } + if err := json.NewEncoder(w).Encode(result); err != nil { + t.Errorf("failed to write response: %v", err) + } + default: + t.Errorf("unexpected access: %s %s", r.Method, r.URL) + w.WriteHeader(http.StatusNotFound) + } + })) + defer ts.Close() + uri, err := url.Parse(ts.URL) + if err != nil { + t.Fatalf("invalid test http server: %v", err) + } + + ctx := context.Background() + repo, err := NewRepository(uri.Host + "/test") + if err != nil { + t.Fatalf("NewRepository() error = %v", err) + } + repo.PlainHTTP = true + + // test push artifact with subject + if state := repo.loadReferrersState(); state != referrersStateUnknown { + t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) + } + err = repo.PushReference(ctx, artifactDesc, bytes.NewReader(artifactJSON), artifactRef) + if err != nil { + t.Fatalf("Manifests.Push() error = %v", err) + } + if !bytes.Equal(gotManifest, artifactJSON) { + t.Errorf("Manifests.Push() = %v, want %v", string(gotManifest), string(artifactJSON)) + } + + // test push image manifest with subject + if state := repo.loadReferrersState(); state != referrersStateSupported { + t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported) + } + err = repo.PushReference(ctx, manifestDesc, bytes.NewReader(manifestJSON), manifestRef) + if err != nil { + t.Fatalf("Manifests.Push() error = %v", err) + } + if !bytes.Equal(gotManifest, manifestJSON) { + t.Errorf("Manifests.Push() = %v, want %v", string(gotManifest), string(manifestJSON)) + } +} + +func Test_ManifestStore_PushReference_ReferrersAPIUnavailable(t *testing.T) { + // generate test content + subject := []byte(`{"layers":[]}`) + subjectDesc := content.NewDescriptorFromBytes(ocispec.MediaTypeArtifactManifest, subject) + referrersTag := strings.Replace(subjectDesc.Digest.String(), ":", "-", 1) + artifact := ocispec.Artifact{ + MediaType: ocispec.MediaTypeArtifactManifest, + Subject: &subjectDesc, + ArtifactType: "application/vnd.test", + Annotations: map[string]string{"foo": "bar"}, + } + artifactJSON, err := json.Marshal(artifact) + if err != nil { + t.Errorf("failed to marshal manifest: %v", err) + } + artifactDesc := content.NewDescriptorFromBytes(artifact.MediaType, artifactJSON) + artifactDesc.ArtifactType = artifact.ArtifactType + artifactDesc.Annotations = artifact.Annotations + artifactRef := "foo" + + // test push artifact with subject + index_1 := ocispec.Index{ + Versioned: specs.Versioned{ + SchemaVersion: 2, // historical value. does not pertain to OCI or docker version + }, + MediaType: ocispec.MediaTypeImageIndex, + Manifests: []ocispec.Descriptor{ + artifactDesc, + }, + } + indexJSON_1, err := json.Marshal(index_1) + if err != nil { + t.Errorf("failed to marshal manifest: %v", err) + } + indexDesc_1 := content.NewDescriptorFromBytes(index_1.MediaType, indexJSON_1) + var gotManifest []byte + var gotReferrerIndex []byte + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+artifactRef: + if contentType := r.Header.Get("Content-Type"); contentType != artifactDesc.MediaType { + w.WriteHeader(http.StatusBadRequest) + break + } + buf := bytes.NewBuffer(nil) + if _, err := buf.ReadFrom(r.Body); err != nil { + t.Errorf("fail to read: %v", err) + } + gotManifest = buf.Bytes() + w.Header().Set("Docker-Content-Digest", artifactDesc.Digest.String()) + w.WriteHeader(http.StatusCreated) + case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+subjectDesc.Digest.String(): + w.WriteHeader(http.StatusNotFound) + case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+referrersTag: + w.WriteHeader(http.StatusNotFound) + case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+referrersTag: + if contentType := r.Header.Get("Content-Type"); contentType != ocispec.MediaTypeImageIndex { + w.WriteHeader(http.StatusBadRequest) + break + } + buf := bytes.NewBuffer(nil) + if _, err := buf.ReadFrom(r.Body); err != nil { + t.Errorf("fail to read: %v", err) + } + gotReferrerIndex = buf.Bytes() + w.Header().Set("Docker-Content-Digest", indexDesc_1.Digest.String()) + w.WriteHeader(http.StatusCreated) + default: + t.Errorf("unexpected access: %s %s", r.Method, r.URL) + w.WriteHeader(http.StatusNotFound) + } + })) + defer ts.Close() + uri, err := url.Parse(ts.URL) + if err != nil { + t.Fatalf("invalid test http server: %v", err) + } + + ctx := context.Background() + repo, err := NewRepository(uri.Host + "/test") + if err != nil { + t.Fatalf("NewRepository() error = %v", err) + } + repo.PlainHTTP = true + + if state := repo.loadReferrersState(); state != referrersStateUnknown { + t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) + } + err = repo.PushReference(ctx, artifactDesc, bytes.NewReader(artifactJSON), artifactRef) + if err != nil { + t.Fatalf("Manifests.Push() error = %v", err) + } + if !bytes.Equal(gotManifest, artifactJSON) { + t.Errorf("Manifests.Push() = %v, want %v", string(gotManifest), string(artifactJSON)) + } + if !bytes.Equal(gotReferrerIndex, indexJSON_1) { + t.Errorf("got referrers index = %v, want %v", string(gotReferrerIndex), string(indexJSON_1)) + } + if state := repo.loadReferrersState(); state != referrersStateUnsupported { + t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) + } + + // test push image manifest with subject, referrers list should be updated + manifest := ocispec.Manifest{ + MediaType: ocispec.MediaTypeImageManifest, + Config: ocispec.Descriptor{ + MediaType: "testconfig", + }, + Subject: &subjectDesc, + Annotations: map[string]string{"foo": "bar"}, + } + manifestJSON, err := json.Marshal(manifest) + if err != nil { + t.Errorf("failed to marshal manifest: %v", err) } - index := []byte(`{"manifests":[]}`) - indexDesc := ocispec.Descriptor{ + manifestDesc := content.NewDescriptorFromBytes(manifest.MediaType, manifestJSON) + manifestDesc.ArtifactType = manifest.Config.MediaType + manifestDesc.Annotations = manifest.Annotations + manifestRef := "bar" + + index_2 := ocispec.Index{ + Versioned: specs.Versioned{ + SchemaVersion: 2, // historical value. does not pertain to OCI or docker version + }, MediaType: ocispec.MediaTypeImageIndex, - Digest: digest.FromBytes(index), - Size: int64(len(index)), + Manifests: []ocispec.Descriptor{ + artifactDesc, + manifestDesc, + }, } - var gotIndex []byte - ref := "foobar" - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + indexJSON_2, err := json.Marshal(index_2) + if err != nil { + t.Errorf("failed to marshal manifest: %v", err) + } + indexDesc_2 := content.NewDescriptorFromBytes(index_2.MediaType, indexJSON_2) + var manifestDeleted bool + ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { - case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+blobDesc.Digest.String(): - w.WriteHeader(http.StatusNotFound) - case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+indexDesc.Digest.String(): - if accept := r.Header.Get("Accept"); !strings.Contains(accept, indexDesc.MediaType) { - t.Errorf("manifest not convertable: %s", accept) + case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+manifestRef: + if contentType := r.Header.Get("Content-Type"); contentType != manifestDesc.MediaType { w.WriteHeader(http.StatusBadRequest) - return + break } - w.Header().Set("Content-Type", indexDesc.MediaType) - w.Header().Set("Docker-Content-Digest", indexDesc.Digest.String()) - if _, err := w.Write(index); err != nil { - t.Errorf("failed to write %q: %v", r.URL, err) + buf := bytes.NewBuffer(nil) + if _, err := buf.ReadFrom(r.Body); err != nil { + t.Errorf("fail to read: %v", err) } - case r.Method == http.MethodPut && - r.URL.Path == "/v2/test/manifests/"+ref || r.URL.Path == "/v2/test/manifests/"+indexDesc.Digest.String(): - if contentType := r.Header.Get("Content-Type"); contentType != indexDesc.MediaType { + gotManifest = buf.Bytes() + w.Header().Set("Docker-Content-Digest", manifestDesc.Digest.String()) + w.WriteHeader(http.StatusCreated) + case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+subjectDesc.Digest.String(): + w.WriteHeader(http.StatusNotFound) + case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+referrersTag: + w.Write(indexJSON_1) + case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+referrersTag: + if contentType := r.Header.Get("Content-Type"); contentType != ocispec.MediaTypeImageIndex { w.WriteHeader(http.StatusBadRequest) break } @@ -3158,65 +3889,55 @@ func Test_ManifestStore_Tag(t *testing.T) { if _, err := buf.ReadFrom(r.Body); err != nil { t.Errorf("fail to read: %v", err) } - gotIndex = buf.Bytes() - w.Header().Set("Docker-Content-Digest", indexDesc.Digest.String()) + gotReferrerIndex = buf.Bytes() + w.Header().Set("Docker-Content-Digest", indexDesc_2.Digest.String()) w.WriteHeader(http.StatusCreated) - return + case r.Method == http.MethodDelete && r.URL.Path == "/v2/test/manifests/"+indexDesc_1.Digest.String(): + manifestDeleted = true + // no "Docker-Content-Digest" header for manifest deletion + w.WriteHeader(http.StatusAccepted) default: t.Errorf("unexpected access: %s %s", r.Method, r.URL) - w.WriteHeader(http.StatusForbidden) + w.WriteHeader(http.StatusNotFound) } })) defer ts.Close() - uri, err := url.Parse(ts.URL) + uri, err = url.Parse(ts.URL) if err != nil { t.Fatalf("invalid test http server: %v", err) } - repo, err := NewRepository(uri.Host + "/test") + ctx = context.Background() + repo, err = NewRepository(uri.Host + "/test") if err != nil { t.Fatalf("NewRepository() error = %v", err) } - store := repo.Manifests() repo.PlainHTTP = true - ctx := context.Background() - - err = store.Tag(ctx, blobDesc, ref) - if err == nil { - t.Errorf("Repository.Tag() error = %v, wantErr %v", err, true) + if state := repo.loadReferrersState(); state != referrersStateUnknown { + t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) } - - err = store.Tag(ctx, indexDesc, ref) + err = repo.PushReference(ctx, manifestDesc, bytes.NewReader(manifestJSON), manifestRef) if err != nil { - t.Fatalf("Repository.Tag() error = %v", err) + t.Fatalf("Manifests.PushReference() error = %v", err) } - if !bytes.Equal(gotIndex, index) { - t.Errorf("Repository.Tag() = %v, want %v", gotIndex, index) + if !bytes.Equal(gotManifest, manifestJSON) { + t.Errorf("Manifests.PushReference() = %v, want %v", string(gotManifest), string(manifestJSON)) } - - gotIndex = nil - err = store.Tag(ctx, indexDesc, indexDesc.Digest.String()) - if err != nil { - t.Fatalf("Repository.Tag() error = %v", err) + if !bytes.Equal(gotReferrerIndex, indexJSON_2) { + t.Errorf("got referrers index = %v, want %v", string(gotReferrerIndex), string(indexJSON_2)) } - if !bytes.Equal(gotIndex, index) { - t.Errorf("Repository.Tag() = %v, want %v", gotIndex, index) + if !manifestDeleted { + t.Errorf("manifestDeleted = %v, want %v", manifestDeleted, true) } -} - -func Test_ManifestStore_PushReference(t *testing.T) { - index := []byte(`{"manifests":[]}`) - indexDesc := ocispec.Descriptor{ - MediaType: ocispec.MediaTypeImageIndex, - Digest: digest.FromBytes(index), - Size: int64(len(index)), + if state := repo.loadReferrersState(); state != referrersStateUnsupported { + t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) } - var gotIndex []byte - ref := "foobar" - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + // test push image manifest with subject again, referrers list should not be changed + ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { - case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+ref: - if contentType := r.Header.Get("Content-Type"); contentType != indexDesc.MediaType { + case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+manifestDesc.Digest.String(): + if contentType := r.Header.Get("Content-Type"); contentType != manifestDesc.MediaType { w.WriteHeader(http.StatusBadRequest) break } @@ -3224,34 +3945,46 @@ func Test_ManifestStore_PushReference(t *testing.T) { if _, err := buf.ReadFrom(r.Body); err != nil { t.Errorf("fail to read: %v", err) } - gotIndex = buf.Bytes() - w.Header().Set("Docker-Content-Digest", indexDesc.Digest.String()) + gotManifest = buf.Bytes() + w.Header().Set("Docker-Content-Digest", manifestDesc.Digest.String()) w.WriteHeader(http.StatusCreated) - return + case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+subjectDesc.Digest.String(): + w.WriteHeader(http.StatusNotFound) + case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+referrersTag: + w.Write(indexJSON_2) default: t.Errorf("unexpected access: %s %s", r.Method, r.URL) - w.WriteHeader(http.StatusForbidden) + w.WriteHeader(http.StatusNotFound) } })) defer ts.Close() - uri, err := url.Parse(ts.URL) + uri, err = url.Parse(ts.URL) if err != nil { t.Fatalf("invalid test http server: %v", err) } - repo, err := NewRepository(uri.Host + "/test") + ctx = context.Background() + repo, err = NewRepository(uri.Host + "/test") if err != nil { t.Fatalf("NewRepository() error = %v", err) } - store := repo.Manifests() repo.PlainHTTP = true - ctx := context.Background() - err = store.PushReference(ctx, indexDesc, bytes.NewReader(index), ref) + if state := repo.loadReferrersState(); state != referrersStateUnknown { + t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) + } + err = repo.Push(ctx, manifestDesc, bytes.NewReader(manifestJSON)) if err != nil { - t.Fatalf("Repository.PushReference() error = %v", err) + t.Fatalf("Manifests.Push() error = %v", err) } - if !bytes.Equal(gotIndex, index) { - t.Errorf("Repository.PushReference() = %v, want %v", gotIndex, index) + if !bytes.Equal(gotManifest, manifestJSON) { + t.Errorf("Manifests.Push() = %v, want %v", string(gotManifest), string(manifestJSON)) + } + // referrers list should not be changed + if !bytes.Equal(gotReferrerIndex, indexJSON_2) { + t.Errorf("got referrers index = %v, want %v", string(gotReferrerIndex), string(indexJSON_2)) + } + if state := repo.loadReferrersState(); state != referrersStateUnsupported { + t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) } } @@ -4010,3 +4743,230 @@ func Test_getReferrersTag(t *testing.T) { }) } } + +func Test_generateIndex(t *testing.T) { + referrer_1 := ocispec.Artifact{ + MediaType: ocispec.MediaTypeArtifactManifest, + ArtifactType: "foo", + } + referrerJSON_1, err := json.Marshal(referrer_1) + if err != nil { + t.Fatal("failed to marshal manifest:", err) + } + referrer_2 := ocispec.Artifact{ + MediaType: ocispec.MediaTypeArtifactManifest, + ArtifactType: "bar", + } + referrerJSON_2, err := json.Marshal(referrer_2) + if err != nil { + t.Fatal("failed to marshal manifest:", err) + } + referrers := []ocispec.Descriptor{ + content.NewDescriptorFromBytes(referrer_1.MediaType, referrerJSON_1), + content.NewDescriptorFromBytes(referrer_2.MediaType, referrerJSON_2), + } + + wantIndex := ocispec.Index{ + Versioned: specs.Versioned{ + SchemaVersion: 2, // historical value. does not pertain to OCI or docker version + }, + MediaType: ocispec.MediaTypeImageIndex, + Manifests: referrers, + } + wantIndexJSON, err := json.Marshal(wantIndex) + if err != nil { + t.Fatal("failed to marshal index:", err) + } + wantIndexDesc := content.NewDescriptorFromBytes(wantIndex.MediaType, wantIndexJSON) + + wantEmptyIndex := ocispec.Index{ + Versioned: specs.Versioned{ + SchemaVersion: 2, // historical value. does not pertain to OCI or docker version + }, + MediaType: ocispec.MediaTypeImageIndex, + Manifests: []ocispec.Descriptor{}, + } + wantEmptyIndexJSON, err := json.Marshal(wantEmptyIndex) + if err != nil { + t.Fatal("failed to marshal index:", err) + } + wantEmptyIndexDesc := content.NewDescriptorFromBytes(wantEmptyIndex.MediaType, wantEmptyIndexJSON) + + tests := []struct { + name string + manifests []ocispec.Descriptor + wantDesc ocispec.Descriptor + wantBytes []byte + wantErr bool + }{ + { + name: "non-empty referrers list", + manifests: referrers, + wantDesc: wantIndexDesc, + wantBytes: wantIndexJSON, + wantErr: false, + }, + { + name: "nil referrers list", + manifests: nil, + wantDesc: wantEmptyIndexDesc, + wantBytes: wantEmptyIndexJSON, + wantErr: false, + }, + { + name: "empty referrers list", + manifests: nil, + wantDesc: wantEmptyIndexDesc, + wantBytes: wantEmptyIndexJSON, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, got1, err := generateIndex(tt.manifests) + if (err != nil) != tt.wantErr { + t.Errorf("generateReferrersIndex() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.wantDesc) { + t.Errorf("generateReferrersIndex() got = %v, want %v", got, tt.wantDesc) + } + if !reflect.DeepEqual(got1, tt.wantBytes) { + t.Errorf("generateReferrersIndex() got1 = %v, want %v", got1, tt.wantBytes) + } + }) + } +} + +func TestRepository_isReferrersAPIAvailable(t *testing.T) { + manifest := []byte(`{"layers":[]}`) + manifestDesc := ocispec.Descriptor{ + MediaType: ocispec.MediaTypeImageManifest, + Digest: digest.FromBytes(manifest), + Size: int64(len(manifest)), + } + + // referrers available + count := 0 + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+manifestDesc.Digest.String(): + count++ + w.WriteHeader(http.StatusOK) + default: + t.Errorf("unexpected access: %s %s", r.Method, r.URL) + w.WriteHeader(http.StatusNotFound) + } + + })) + defer ts.Close() + uri, err := url.Parse(ts.URL) + if err != nil { + t.Fatalf("invalid test http server: %v", err) + } + + ctx := context.Background() + repo, err := NewRepository(uri.Host + "/test") + if err != nil { + t.Fatalf("NewRepository() error = %v", err) + } + repo.PlainHTTP = true + + // 1st call + if state := repo.loadReferrersState(); state != referrersStateUnknown { + t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) + } + got, err := repo.isReferrersAPIAvailable(ctx, manifestDesc) + if err != nil { + t.Errorf("Repository.isReferrersAPIAvailable() error = %v, wantErr %v", err, nil) + } + if got != true { + t.Errorf("Repository.isReferrersAPIAvailable() = %v, want %v", got, true) + } + if state := repo.loadReferrersState(); state != referrersStateSupported { + t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported) + } + if count != 1 { + t.Errorf("count(Repository.isReferrersAPIAvailable()) = %v, want %v", count, 1) + } + + // 2nd call + if state := repo.loadReferrersState(); state != referrersStateSupported { + t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported) + } + got, err = repo.isReferrersAPIAvailable(ctx, manifestDesc) + if err != nil { + t.Errorf("Repository.isReferrersAPIAvailable() error = %v, wantErr %v", err, nil) + } + if got != true { + t.Errorf("Repository.isReferrersAPIAvailable() = %v, want %v", got, true) + } + if state := repo.loadReferrersState(); state != referrersStateSupported { + t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported) + } + if count != 1 { + t.Errorf("count(Repository.isReferrersAPIAvailable()) = %v, want %v", count, 1) + } + + // referrers unavailable + count = 0 + ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+manifestDesc.Digest.String(): + count++ + w.WriteHeader(http.StatusNotFound) + default: + t.Errorf("unexpected access: %s %s", r.Method, r.URL) + w.WriteHeader(http.StatusNotFound) + } + + })) + defer ts.Close() + uri, err = url.Parse(ts.URL) + if err != nil { + t.Fatalf("invalid test http server: %v", err) + } + + ctx = context.Background() + repo, err = NewRepository(uri.Host + "/test") + if err != nil { + t.Fatalf("NewRepository() error = %v", err) + } + repo.PlainHTTP = true + + // 1st call + if state := repo.loadReferrersState(); state != referrersStateUnknown { + t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) + } + got, err = repo.isReferrersAPIAvailable(ctx, manifestDesc) + if err != nil { + t.Errorf("Repository.isReferrersAPIAvailable() error = %v, wantErr %v", err, nil) + } + if got != false { + t.Errorf("Repository.isReferrersAPIAvailable() = %v, want %v", got, false) + } + if state := repo.loadReferrersState(); state != referrersStateUnsupported { + t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) + } + if count != 1 { + t.Errorf("count(Repository.isReferrersAPIAvailable()) = %v, want %v", count, 1) + } + + // 2nd call + if state := repo.loadReferrersState(); state != referrersStateUnsupported { + t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) + } + got, err = repo.isReferrersAPIAvailable(ctx, manifestDesc) + if err != nil { + t.Errorf("Repository.isReferrersAPIAvailable() error = %v, wantErr %v", err, nil) + } + if got != false { + t.Errorf("Repository.isReferrersAPIAvailable() = %v, want %v", got, false) + } + if state := repo.loadReferrersState(); state != referrersStateUnsupported { + t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) + } + if count != 1 { + t.Errorf("count(Repository.isReferrersAPIAvailable()) = %v, want %v", count, 1) + } +} diff --git a/registry/remote/utils.go b/registry/remote/utils.go index 7564cca9..797169f4 100644 --- a/registry/remote/utils.go +++ b/registry/remote/utils.go @@ -74,7 +74,11 @@ func limitSize(desc ocispec.Descriptor, n int64) error { n = defaultMaxMetadataBytes } if desc.Size > n { - return errdef.ErrSizeExceedsLimit + return fmt.Errorf( + "content size %v exceeds MaxMetadataBytes %v: %w", + desc.Size, + n, + errdef.ErrSizeExceedsLimit) } return nil }