package storage import ( "bytes" "context" "io" "reflect" "testing" "github.com/distribution/distribution/v3" "github.com/distribution/distribution/v3/manifest" "github.com/distribution/distribution/v3/manifest/manifestlist" "github.com/distribution/distribution/v3/manifest/ocischema" "github.com/distribution/distribution/v3/manifest/schema1" //nolint:staticcheck // Ignore SA1019: "github.com/distribution/distribution/v3/manifest/schema1" is deprecated, as it's used for backward compatibility. "github.com/distribution/distribution/v3/reference" "github.com/distribution/distribution/v3/registry/storage/cache/memory" "github.com/distribution/distribution/v3/registry/storage/driver" "github.com/distribution/distribution/v3/registry/storage/driver/inmemory" "github.com/distribution/distribution/v3/testutil" "github.com/docker/libtrust" "github.com/opencontainers/go-digest" v1 "github.com/opencontainers/image-spec/specs-go/v1" ) type manifestStoreTestEnv struct { ctx context.Context driver driver.StorageDriver registry distribution.Namespace repository distribution.Repository name reference.Named tag string } func newManifestStoreTestEnv(t *testing.T, name reference.Named, tag string, options ...RegistryOption) *manifestStoreTestEnv { ctx := context.Background() drvr := inmemory.New() registry, err := NewRegistry(ctx, drvr, options...) if err != nil { t.Fatalf("error creating registry: %v", err) } repo, err := registry.Repository(ctx, name) if err != nil { t.Fatalf("unexpected error getting repo: %v", err) } return &manifestStoreTestEnv{ ctx: ctx, driver: drvr, registry: registry, repository: repo, name: name, tag: tag, } } func TestManifestStorage(t *testing.T) { k, err := libtrust.GenerateECP256PrivateKey() if err != nil { t.Fatal(err) } testManifestStorage(t, true, BlobDescriptorCacheProvider(memory.NewInMemoryBlobDescriptorCacheProvider(memory.UnlimitedSize)), EnableDelete, EnableRedirect, Schema1SigningKey(k), EnableSchema1) } func TestManifestStorageV1Unsupported(t *testing.T) { k, err := libtrust.GenerateECP256PrivateKey() if err != nil { t.Fatal(err) } testManifestStorage(t, false, BlobDescriptorCacheProvider(memory.NewInMemoryBlobDescriptorCacheProvider(memory.UnlimitedSize)), EnableDelete, EnableRedirect, Schema1SigningKey(k)) } func testManifestStorage(t *testing.T, schema1Enabled bool, options ...RegistryOption) { repoName, _ := reference.WithName("foo/bar") env := newManifestStoreTestEnv(t, repoName, "thetag", options...) ctx := context.Background() ms, err := env.repository.Manifests(ctx) if err != nil { t.Fatal(err) } m := schema1.Manifest{ //nolint:staticcheck // Ignore SA1019: "github.com/distribution/distribution/v3/manifest/schema1" is deprecated, as it's used for backward compatibility. Versioned: manifest.Versioned{ SchemaVersion: 1, }, Name: env.name.Name(), Tag: env.tag, } // 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, dgst, err := testutil.CreateRandomTarFile() if err != nil { t.Fatalf("unexpected error generating test layer file") } testLayers[dgst] = rs m.FSLayers = append(m.FSLayers, schema1.FSLayer{ //nolint:staticcheck // Ignore SA1019: "github.com/distribution/distribution/v3/manifest/schema1" is deprecated, as it's used for backward compatibility. BlobSum: dgst, }) m.History = append(m.History, schema1.History{ //nolint:staticcheck // Ignore SA1019: "github.com/distribution/distribution/v3/manifest/schema1" is deprecated, as it's used for backward compatibility. V1Compatibility: "", }) } pk, err := libtrust.GenerateECP256PrivateKey() if err != nil { t.Fatalf("unexpected error generating private key: %v", err) } sm, merr := schema1.Sign(&m, pk) //nolint:staticcheck // Ignore SA1019: "github.com/distribution/distribution/v3/manifest/schema1" is deprecated, as it's used for backward compatibility. if merr != nil { t.Fatalf("error signing manifest: %v", err) } _, err = ms.Put(ctx, sm) if err == nil { t.Fatalf("expected errors putting manifest with full verification") } // If schema1 is not enabled, do a short version of this test, just checking // if we get the right error when we Put if !schema1Enabled { if err != distribution.ErrSchemaV1Unsupported { t.Fatalf("got the wrong error when schema1 is disabled: %s", err) } return } switch err := err.(type) { case distribution.ErrManifestVerification: if len(err) != 2 { t.Fatalf("expected 2 verification errors: %#v", err) } for _, err := range err { if _, ok := err.(distribution.ErrManifestBlobUnknown); !ok { t.Fatalf("unexpected error type: %v", err) } } default: t.Fatalf("unexpected error verifying manifest: %v", err) } // Now, upload the layers that were missing! for dgst, rs := range testLayers { wr, err := env.repository.Blobs(env.ctx).Create(env.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(env.ctx, distribution.Descriptor{Digest: dgst}); err != nil { t.Fatalf("unexpected error finishing upload: %v", err) } } var manifestDigest digest.Digest if manifestDigest, err = ms.Put(ctx, sm); err != nil { t.Fatalf("unexpected error putting manifest: %v", err) } exists, err := ms.Exists(ctx, manifestDigest) if err != nil { t.Fatalf("unexpected error checking manifest existence: %#v", err) } if !exists { t.Fatalf("manifest should exist") } fromStore, err := ms.Get(ctx, manifestDigest) if err != nil { t.Fatalf("unexpected error fetching manifest: %v", err) } fetchedManifest, ok := fromStore.(*schema1.SignedManifest) //nolint:staticcheck // Ignore SA1019: "github.com/distribution/distribution/v3/manifest/schema1" is deprecated, as it's used for backward compatibility. if !ok { t.Fatalf("unexpected manifest type from signedstore") } if !bytes.Equal(fetchedManifest.Canonical, sm.Canonical) { t.Fatalf("fetched payload does not match original payload: %q != %q", fetchedManifest.Canonical, sm.Canonical) } _, pl, err := fetchedManifest.Payload() //nolint:staticcheck // Ignore SA1019: "github.com/distribution/distribution/v3/manifest/schema1" is deprecated, as it's used for backward compatibility. if err != nil { t.Fatalf("error getting payload %#v", err) } fetchedJWS, err := libtrust.ParsePrettySignature(pl, "signatures") if err != nil { t.Fatalf("unexpected error parsing jws: %v", err) } payload, err := fetchedJWS.Payload() if err != nil { t.Fatalf("unexpected error extracting payload: %v", err) } // Now that we have a payload, take a moment to check that the manifest is // return by the payload digest. dgst := digest.FromBytes(payload) exists, err = ms.Exists(ctx, dgst) if err != nil { t.Fatalf("error checking manifest existence by digest: %v", err) } if !exists { t.Fatalf("manifest %s should exist", dgst) } fetchedByDigest, err := ms.Get(ctx, dgst) if err != nil { t.Fatalf("unexpected error fetching manifest by digest: %v", err) } byDigestManifest, ok := fetchedByDigest.(*schema1.SignedManifest) //nolint:staticcheck // Ignore SA1019: "github.com/distribution/distribution/v3/manifest/schema1" is deprecated, as it's used for backward compatibility. if !ok { t.Fatalf("unexpected manifest type from signedstore") } if !bytes.Equal(byDigestManifest.Canonical, fetchedManifest.Canonical) { t.Fatalf("fetched manifest not equal: %q != %q", byDigestManifest.Canonical, fetchedManifest.Canonical) } sigs, err := fetchedJWS.Signatures() if err != nil { t.Fatalf("unable to extract signatures: %v", err) } if len(sigs) != 1 { t.Fatalf("unexpected number of signatures: %d != %d", len(sigs), 1) } // Now, push the same manifest with a different key pk2, err := libtrust.GenerateECP256PrivateKey() if err != nil { t.Fatalf("unexpected error generating private key: %v", err) } sm2, err := schema1.Sign(&m, pk2) //nolint:staticcheck // Ignore SA1019: "github.com/distribution/distribution/v3/manifest/schema1" is deprecated, as it's used for backward compatibility. if err != nil { t.Fatalf("unexpected error signing manifest: %v", err) } _, pl, err = sm2.Payload() //nolint:staticcheck // Ignore SA1019: "github.com/distribution/distribution/v3/manifest/schema1" is deprecated, as it's used for backward compatibility. if err != nil { t.Fatalf("error getting payload %#v", err) } jws2, err := libtrust.ParsePrettySignature(pl, "signatures") if err != nil { t.Fatalf("error parsing signature: %v", err) } sigs2, err := jws2.Signatures() if err != nil { t.Fatalf("unable to extract signatures: %v", err) } if len(sigs2) != 1 { t.Fatalf("unexpected number of signatures: %d != %d", len(sigs2), 1) } if manifestDigest, err = ms.Put(ctx, sm2); err != nil { t.Fatalf("unexpected error putting manifest: %v", err) } fromStore, err = ms.Get(ctx, manifestDigest) if err != nil { t.Fatalf("unexpected error fetching manifest: %v", err) } fetched, ok := fromStore.(*schema1.SignedManifest) //nolint:staticcheck // Ignore SA1019: "github.com/distribution/distribution/v3/manifest/schema1" is deprecated, as it's used for backward compatibility. if !ok { t.Fatalf("unexpected type from signed manifeststore : %T", fetched) } if _, err := schema1.Verify(fetched); err != nil { //nolint:staticcheck // Ignore SA1019: "github.com/distribution/distribution/v3/manifest/schema1" is deprecated, as it's used for backward compatibility. t.Fatalf("unexpected error verifying manifest: %v", err) } _, pl, err = fetched.Payload() //nolint:staticcheck // Ignore SA1019: "github.com/distribution/distribution/v3/manifest/schema1" is deprecated, as it's used for backward compatibility. if err != nil { t.Fatalf("error getting payload %#v", err) } receivedJWS, err := libtrust.ParsePrettySignature(pl, "signatures") if err != nil { t.Fatalf("unexpected error parsing jws: %v", err) } receivedPayload, err := receivedJWS.Payload() if err != nil { t.Fatalf("unexpected error extracting received payload: %v", err) } if !bytes.Equal(receivedPayload, payload) { t.Fatalf("payloads are not equal") } // Test deleting manifests err = ms.Delete(ctx, dgst) if err != nil { t.Fatalf("unexpected an error deleting manifest by digest: %v", err) } exists, err = ms.Exists(ctx, dgst) if err != nil { t.Fatalf("Error querying manifest existence") } if exists { t.Errorf("Deleted manifest should not exist") } deletedManifest, err := ms.Get(ctx, dgst) if err == nil { t.Errorf("Unexpected success getting deleted manifest") } switch err.(type) { case distribution.ErrManifestUnknownRevision: break default: t.Errorf("Unexpected error getting deleted manifest: %s", reflect.ValueOf(err).Type()) } if deletedManifest != nil { t.Errorf("Deleted manifest get returned non-nil") } // Re-upload should restore manifest to a good state _, err = ms.Put(ctx, sm) if err != nil { t.Errorf("Error re-uploading deleted manifest") } exists, err = ms.Exists(ctx, dgst) if err != nil { t.Fatalf("Error querying manifest existence") } if !exists { t.Errorf("Restored manifest should exist") } deletedManifest, err = ms.Get(ctx, dgst) if err != nil { t.Errorf("Unexpected error getting manifest") } if deletedManifest == nil { t.Errorf("Deleted manifest get returned non-nil") } r, err := NewRegistry(ctx, env.driver, BlobDescriptorCacheProvider(memory.NewInMemoryBlobDescriptorCacheProvider(memory.UnlimitedSize)), EnableRedirect) if err != nil { t.Fatalf("error creating registry: %v", err) } repo, err := r.Repository(ctx, env.name) if err != nil { t.Fatalf("unexpected error getting repo: %v", err) } ms, err = repo.Manifests(ctx) if err != nil { t.Fatal(err) } err = ms.Delete(ctx, dgst) if err == nil { t.Errorf("Unexpected success deleting while disabled") } } func TestOCIManifestStorage(t *testing.T) { testOCIManifestStorage(t, "includeMediaTypes=true", true) testOCIManifestStorage(t, "includeMediaTypes=false", false) } func testOCIManifestStorage(t *testing.T, testname string, includeMediaTypes bool) { var imageMediaType string var indexMediaType string if includeMediaTypes { imageMediaType = v1.MediaTypeImageManifest indexMediaType = v1.MediaTypeImageIndex } else { imageMediaType = "" indexMediaType = "" } repoName, _ := reference.WithName("foo/bar") env := newManifestStoreTestEnv(t, repoName, "thetag", BlobDescriptorCacheProvider(memory.NewInMemoryBlobDescriptorCacheProvider(memory.UnlimitedSize)), EnableDelete, EnableRedirect) ctx := context.Background() ms, err := env.repository.Manifests(ctx) if err != nil { t.Fatal(err) } // Build a manifest and store it and its layers in the registry blobStore := env.repository.Blobs(ctx) builder := ocischema.NewManifestBuilder(blobStore, []byte{}, map[string]string{}) err = builder.(*ocischema.Builder).SetMediaType(imageMediaType) if err != nil { t.Fatal(err) } // Add some layers for i := 0; i < 2; i++ { rs, dgst, err := testutil.CreateRandomTarFile() if err != nil { t.Fatalf("%s: unexpected error generating test layer file", testname) } wr, err := env.repository.Blobs(env.ctx).Create(env.ctx) if err != nil { t.Fatalf("%s: unexpected error creating test upload: %v", testname, err) } if _, err := io.Copy(wr, rs); err != nil { t.Fatalf("%s: unexpected error copying to upload: %v", testname, err) } if _, err := wr.Commit(env.ctx, distribution.Descriptor{Digest: dgst}); err != nil { t.Fatalf("%s: unexpected error finishing upload: %v", testname, err) } builder.AppendReference(distribution.Descriptor{Digest: dgst}) } mfst, err := builder.Build(ctx) if err != nil { t.Fatalf("%s: unexpected error generating manifest: %v", testname, err) } // before putting the manifest test for proper handling of SchemaVersion if mfst.(*ocischema.DeserializedManifest).Manifest.SchemaVersion != 2 { t.Fatalf("%s: unexpected error generating default version for oci manifest", testname) } mfst.(*ocischema.DeserializedManifest).Manifest.SchemaVersion = 0 var manifestDigest digest.Digest if manifestDigest, err = ms.Put(ctx, mfst); err != nil { if err.Error() != "unrecognized manifest schema version 0" { t.Fatalf("%s: unexpected error putting manifest: %v", testname, err) } mfst.(*ocischema.DeserializedManifest).Manifest.SchemaVersion = 2 if manifestDigest, err = ms.Put(ctx, mfst); err != nil { t.Fatalf("%s: unexpected error putting manifest: %v", testname, err) } } // Also create an image index that contains the manifest descriptor, err := env.registry.BlobStatter().Stat(ctx, manifestDigest) if err != nil { t.Fatalf("%s: unexpected error getting manifest descriptor", testname) } descriptor.MediaType = v1.MediaTypeImageManifest platformSpec := manifestlist.PlatformSpec{ Architecture: "atari2600", OS: "CP/M", } manifestDescriptors := []manifestlist.ManifestDescriptor{ { Descriptor: descriptor, Platform: platformSpec, }, } imageIndex, err := manifestlist.FromDescriptorsWithMediaType(manifestDescriptors, indexMediaType) if err != nil { t.Fatalf("%s: unexpected error creating image index: %v", testname, err) } var indexDigest digest.Digest if indexDigest, err = ms.Put(ctx, imageIndex); err != nil { t.Fatalf("%s: unexpected error putting image index: %v", testname, err) } // Now check that we can retrieve the manifest fromStore, err := ms.Get(ctx, manifestDigest) if err != nil { t.Fatalf("%s: unexpected error fetching manifest: %v", testname, err) } fetchedManifest, ok := fromStore.(*ocischema.DeserializedManifest) if !ok { t.Fatalf("%s: unexpected type for fetched manifest", testname) } if fetchedManifest.MediaType != imageMediaType { t.Fatalf("%s: unexpected MediaType for result, %s", testname, fetchedManifest.MediaType) } if fetchedManifest.SchemaVersion != ocischema.SchemaVersion.SchemaVersion { t.Fatalf("%s: unexpected schema version for result, %d", testname, fetchedManifest.SchemaVersion) } payloadMediaType, _, err := fromStore.Payload() if err != nil { t.Fatalf("%s: error getting payload %v", testname, err) } if payloadMediaType != v1.MediaTypeImageManifest { t.Fatalf("%s: unexpected MediaType for manifest payload, %s", testname, payloadMediaType) } // and the image index fromStore, err = ms.Get(ctx, indexDigest) if err != nil { t.Fatalf("%s: unexpected error fetching image index: %v", testname, err) } fetchedIndex, ok := fromStore.(*manifestlist.DeserializedManifestList) if !ok { t.Fatalf("%s: unexpected type for fetched manifest", testname) } if fetchedIndex.MediaType != indexMediaType { t.Fatalf("%s: unexpected MediaType for result, %s", testname, fetchedIndex.MediaType) } payloadMediaType, _, err = fromStore.Payload() if err != nil { t.Fatalf("%s: error getting payload %v", testname, err) } if payloadMediaType != v1.MediaTypeImageIndex { t.Fatalf("%s: unexpected MediaType for index payload, %s", testname, payloadMediaType) } } // TestLinkPathFuncs ensures that the link path functions behavior are locked // down and implemented as expected. func TestLinkPathFuncs(t *testing.T) { for _, testcase := range []struct { repo string digest digest.Digest linkPathFn linkPathFunc expected string }{ { repo: "foo/bar", digest: "sha256:deadbeaf98fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", linkPathFn: blobLinkPath, expected: "/docker/registry/v2/repositories/foo/bar/_layers/sha256/deadbeaf98fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855/link", }, { repo: "foo/bar", digest: "sha256:deadbeaf98fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", linkPathFn: manifestRevisionLinkPath, expected: "/docker/registry/v2/repositories/foo/bar/_manifests/revisions/sha256/deadbeaf98fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855/link", }, } { p, err := testcase.linkPathFn(testcase.repo, testcase.digest) if err != nil { t.Fatalf("unexpected error calling linkPathFn(pm, %q, %q): %v", testcase.repo, testcase.digest, err) } if p != testcase.expected { t.Fatalf("incorrect path returned: %q != %q", p, testcase.expected) } } }