From 27a96320b94dcda78d3080c0aac0242ef696c509 Mon Sep 17 00:00:00 2001 From: Bracken Dawson Date: Wed, 3 May 2023 11:27:48 +0100 Subject: [PATCH] Changes for OCI 1.1RC3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Artifact Manifest was deleted ¯\_(ツ)_/¯, Image manifest keeps the subject field, gains an artifactType field, and the rules for what the artifactType actually is change. Keeping a lot of the framework that came of the Artifact Manifest because I think it lead to some worthwhile abstractions. Signed-off-by: Bracken Dawson --- go.mod | 2 +- go.sum | 4 +- .../ociartifact/fixtures/blob-subject.json | 20 -- manifest/ociartifact/fixtures/manifest.json | 20 -- .../ociartifact/fixtures/no-media-type.json | 19 -- manifest/ociartifact/fixtures/no-subject.json | 15 -- manifest/ociartifact/fixtures_test.go | 92 -------- manifest/ociartifact/manifest.go | 129 ------------ manifest/ociartifact/manifest_test.go | 95 --------- manifest/ocischema/manifest.go | 27 ++- manifest/ocischema/manifest_test.go | 197 ++++++++++++++++++ manifest/versioned.go | 7 - registry/handlers/api_test.go | 157 ++++++++------ registry/handlers/manifests.go | 16 +- .../storage/fixtures/naughtiest-artifact.json | 4 - registry/storage/manifeststore.go | 18 +- registry/storage/ocimanifesthandler.go | 43 +--- registry/storage/ocimanifesthandler_test.go | 84 -------- .../image-spec/specs-go/v1/annotations.go | 3 - .../image-spec/specs-go/v1/artifact.go | 34 --- .../image-spec/specs-go/v1/config.go | 29 ++- .../image-spec/specs-go/v1/manifest.go | 11 + .../image-spec/specs-go/v1/mediatype.go | 19 +- .../image-spec/specs-go/version.go | 2 +- vendor/modules.txt | 4 +- 25 files changed, 372 insertions(+), 679 deletions(-) delete mode 100644 manifest/ociartifact/fixtures/blob-subject.json delete mode 100644 manifest/ociartifact/fixtures/manifest.json delete mode 100644 manifest/ociartifact/fixtures/no-media-type.json delete mode 100644 manifest/ociartifact/fixtures/no-subject.json delete mode 100644 manifest/ociartifact/fixtures_test.go delete mode 100644 manifest/ociartifact/manifest.go delete mode 100644 manifest/ociartifact/manifest_test.go delete mode 100644 registry/storage/fixtures/naughtiest-artifact.json delete mode 100644 vendor/github.com/opencontainers/image-spec/specs-go/v1/artifact.go diff --git a/go.mod b/go.mod index d2f1a9af3..e67031109 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,7 @@ require ( github.com/mitchellh/mapstructure v1.1.2 github.com/ncw/swift v1.0.47 github.com/opencontainers/go-digest v1.0.0 - github.com/opencontainers/image-spec v1.1.0-rc2 + github.com/opencontainers/image-spec v1.1.0-rc3 github.com/prometheus/client_golang v1.12.1 // indirect; updated to latest github.com/sirupsen/logrus v1.8.1 github.com/spf13/cobra v1.6.1 diff --git a/go.sum b/go.sum index 33be5505c..944239093 100644 --- a/go.sum +++ b/go.sum @@ -227,8 +227,8 @@ github.com/ncw/swift v1.0.47 h1:4DQRPj35Y41WogBxyhOXlrI37nzGlyEcsforeudyYPQ= github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.1.0-rc2 h1:2zx/Stx4Wc5pIPDvIxHXvXtQFW/7XWJGmnM7r3wg034= -github.com/opencontainers/image-spec v1.1.0-rc2/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ= +github.com/opencontainers/image-spec v1.1.0-rc3 h1:fzg1mXZFj8YdPeNkRXMg+zb88BFV0Ys52cJydRwBkb8= +github.com/opencontainers/image-spec v1.1.0-rc3/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8= github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4 h1:Qj1ukM4GlMWXNdMBuXcXfz/Kw9s1qm0CLY32QxuSImI= github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4/go.mod h1:N6UoU20jOqggOuDwUaBQpluzLNDqif3kq9z2wpdYEfQ= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= diff --git a/manifest/ociartifact/fixtures/blob-subject.json b/manifest/ociartifact/fixtures/blob-subject.json deleted file mode 100644 index 6935f2f8c..000000000 --- a/manifest/ociartifact/fixtures/blob-subject.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "mediaType": "application/vnd.oci.artifact.manifest.v1+json", - "artifactType": "application/vnd.example.sbom.v1", - "blobs": [ - { - "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", - "digest": "sha256:b093528a5eabd2ce6c954c0ecad0509f95536744c176f181c2640a8b126cdbcf", - "size": 29876998 - } - ], - "subject": { - "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", - "size": 343, - "digest": "sha256:dcfff5eb40423f055a4cd0a8d7ed39ff6cb9816868f5766b4088b9e9906961b9" - }, - "annotations": { - "org.opencontainers.artifact.created": "2023-01-16T14:40:01Z", - "org.example.sbom.format": "json" - } -} diff --git a/manifest/ociartifact/fixtures/manifest.json b/manifest/ociartifact/fixtures/manifest.json deleted file mode 100644 index e73299c09..000000000 --- a/manifest/ociartifact/fixtures/manifest.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "mediaType": "application/vnd.oci.artifact.manifest.v1+json", - "artifactType": "application/vnd.example.sbom.v1", - "blobs": [ - { - "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", - "digest": "sha256:b093528a5eabd2ce6c954c0ecad0509f95536744c176f181c2640a8b126cdbcf", - "size": 29876998 - } - ], - "subject": { - "mediaType": "application/vnd.oci.image.manifest.v1+json", - "size": 549, - "digest": "sha256:f756842dc7541130d3a327a870a38aa9521233fc076d0ee2cea895c8c0a1e388" - }, - "annotations": { - "org.opencontainers.artifact.created": "2023-01-16T14:40:01Z", - "org.example.sbom.format": "json" - } -} diff --git a/manifest/ociartifact/fixtures/no-media-type.json b/manifest/ociartifact/fixtures/no-media-type.json deleted file mode 100644 index 572273bd4..000000000 --- a/manifest/ociartifact/fixtures/no-media-type.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "artifactType": "application/vnd.example.sbom.v1", - "blobs": [ - { - "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", - "digest": "sha256:b093528a5eabd2ce6c954c0ecad0509f95536744c176f181c2640a8b126cdbcf", - "size": 29876998 - } - ], - "subject": { - "mediaType": "application/vnd.oci.image.manifest.v1+json", - "size": 549, - "digest": "sha256:f756842dc7541130d3a327a870a38aa9521233fc076d0ee2cea895c8c0a1e388" - }, - "annotations": { - "org.opencontainers.artifact.created": "2023-01-16T14:40:01Z", - "org.example.sbom.format": "json" - } -} diff --git a/manifest/ociartifact/fixtures/no-subject.json b/manifest/ociartifact/fixtures/no-subject.json deleted file mode 100644 index a601f3576..000000000 --- a/manifest/ociartifact/fixtures/no-subject.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "mediaType": "application/vnd.oci.artifact.manifest.v1+json", - "artifactType": "application/vnd.example.sbom.v1", - "blobs": [ - { - "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", - "digest": "sha256:b093528a5eabd2ce6c954c0ecad0509f95536744c176f181c2640a8b126cdbcf", - "size": 29876998 - } - ], - "annotations": { - "org.opencontainers.artifact.created": "2023-01-16T14:40:01Z", - "org.example.sbom.format": "json" - } -} diff --git a/manifest/ociartifact/fixtures_test.go b/manifest/ociartifact/fixtures_test.go deleted file mode 100644 index 0a8d4ecda..000000000 --- a/manifest/ociartifact/fixtures_test.go +++ /dev/null @@ -1,92 +0,0 @@ -package ociartifact - -import ( - _ "embed" - - "github.com/distribution/distribution/v3" - "github.com/distribution/distribution/v3/manifest" - _ "github.com/distribution/distribution/v3/manifest/ocischema" - v1 "github.com/opencontainers/image-spec/specs-go/v1" -) - -var ( - // ManifestBytes is an example of a valid artifact manifest - //go:embed fixtures/manifest.json - ManifestBytes []byte - ManifestDescriptor = distribution.Descriptor{ - MediaType: v1.MediaTypeArtifactManifest, - Size: 647, - Digest: "sha256:dadc6d0e6ccdcb629c78d1d13ee4e857b0fad468371e67839e777f2e1b9e33c4", - } - ManifestDeserialized = &DeserializedManifest{ - Manifest: Manifest{ - Unversioned: manifest.Unversioned{ - MediaType: v1.MediaTypeArtifactManifest, - }, - ArtifactType: "application/vnd.example.sbom.v1", - Blobs: []distribution.Descriptor{ - { - MediaType: "application/vnd.oci.image.layer.v1.tar+gzip", - Digest: "sha256:b093528a5eabd2ce6c954c0ecad0509f95536744c176f181c2640a8b126cdbcf", - Size: 29876998, - }, - }, - Subject: &distribution.Descriptor{ - MediaType: v1.MediaTypeImageManifest, - Digest: "sha256:f756842dc7541130d3a327a870a38aa9521233fc076d0ee2cea895c8c0a1e388", - Size: 549, - }, - Annotations: map[string]string{ - "org.opencontainers.artifact.created": "2023-01-16T14:40:01Z", - "org.example.sbom.format": "json", - }, - }, - canonical: ManifestBytes, - } -) - -var ( - // ManifestNoMediaType is an invalid artifact manifest because there is no - // mediaType field, unlike image manifests the fils is mandatory. - //go:embed fixtures/no-media-type.json - ManifestNoMediaType []byte -) - -var ( - // ManifestNoSubjectBytes is an example of a valid artifact manifest that has no - // subject. - //go:embed fixtures/no-subject.json - ManifestNoSubjectBytes []byte - ManifestNoSubjectDescriptor = distribution.Descriptor{ - MediaType: v1.MediaTypeArtifactManifest, - Size: 459, - Digest: "sha256:89d65c08998b0b94515414eb060b6c28e15f76b2c3a51efe0b2b541a53babe55", - } - ManifestNoSubjectDeserialized = &DeserializedManifest{ - Manifest: Manifest{ - Unversioned: manifest.Unversioned{ - MediaType: v1.MediaTypeArtifactManifest, - }, - ArtifactType: "application/vnd.example.sbom.v1", - Blobs: []distribution.Descriptor{ - { - MediaType: "application/vnd.oci.image.layer.v1.tar+gzip", - Digest: "sha256:b093528a5eabd2ce6c954c0ecad0509f95536744c176f181c2640a8b126cdbcf", - Size: 29876998, - }, - }, - Annotations: map[string]string{ - "org.opencontainers.artifact.created": "2023-01-16T14:40:01Z", - "org.example.sbom.format": "json", - }, - }, - canonical: ManifestNoSubjectBytes, - } -) - -var ( - // ManifestBlobSubjectBytes is an example of an invalid artifact manifest - // because the subject must be another manifest. - //go:embed fixtures/blob-subject.json - ManifestBlobSubjectBytes []byte -) diff --git a/manifest/ociartifact/manifest.go b/manifest/ociartifact/manifest.go deleted file mode 100644 index 265074073..000000000 --- a/manifest/ociartifact/manifest.go +++ /dev/null @@ -1,129 +0,0 @@ -package ociartifact - -import ( - "encoding/json" - "errors" - "fmt" - - "github.com/distribution/distribution/v3" - "github.com/distribution/distribution/v3/manifest" - "github.com/opencontainers/go-digest" - v1 "github.com/opencontainers/image-spec/specs-go/v1" -) - -func init() { - artifactFunc := func(b []byte) (distribution.Manifest, distribution.Descriptor, error) { - m := &DeserializedManifest{} - err := m.UnmarshalJSON(b) - if err != nil { - return nil, distribution.Descriptor{}, err - } - - dgst := digest.FromBytes(b) - return m, distribution.Descriptor{Digest: dgst, Size: int64(len(b)), MediaType: v1.MediaTypeArtifactManifest}, nil - } - if err := distribution.RegisterManifestSchema(v1.MediaTypeArtifactManifest, artifactFunc); err != nil { - panic(fmt.Sprintf("Unable to register manifest: %s", err)) - } -} - -// Manifest defines an oci artifact manifest. -type Manifest struct { - manifest.Unversioned - - // ArtifactType is the media type of the artifact referenced by this - // artifact manifest. - ArtifactType string `json:"artifactType,omitempty"` - - // Blobs lists the descriptors for the files making up the artifact - // referenced by this this artifact manifest. - Blobs []distribution.Descriptor `json:"blobs,omitempty"` - - // Subject is the descriptor of a manifest referred to by this artifact. - Subject *distribution.Descriptor `json:"subject,omitempty"` - - // Annotations contain arbitrary metadata for the image manifest - Annotations map[string]string `json:"annotations,omitempty"` -} - -// FromStruct takes a Manifest structure, marshals it to JSON, and returns a -// DeserializedManifest which contains the manifest and its JSON representation. -func FromStruct(m Manifest) (*DeserializedManifest, error) { - var deserialized DeserializedManifest - deserialized.Manifest = m - - var err error - deserialized.canonical, err = json.MarshalIndent(&m, "", " ") - return &deserialized, err -} - -// DeserializedManifest wraps Manifest with a copy of the original JSON. -// It satisfies the distribution.Manifest interface. -type DeserializedManifest struct { - Manifest - - // canonical is the canonical byte representation of the Manifest. - canonical []byte -} - -func (m *DeserializedManifest) UnmarshalJSON(b []byte) error { - m.canonical = make([]byte, len(b)) - copy(m.canonical, b) - - var manifest Manifest - if err := json.Unmarshal(m.canonical, &manifest); err != nil { - return err - } - - if manifest.MediaType != v1.MediaTypeArtifactManifest { - return fmt.Errorf("mediaType in manifest must be '%q' not '%s'", - v1.MediaTypeArtifactManifest, manifest.MediaType) - } - - // The subject if specified must be a must be a manifest. This is validated - // here rather then in the storage manifest Put handler because the subject - // does not have to exist, so there is nothing to validate in the manifest - // store. If a non-compliant client provided the digest of a blob then the - // registry would still indicate that the referred manifest does not exist. - if manifest.Subject != nil { - if !distribution.ManifestMediaTypeSupported(manifest.Subject.MediaType) { - return fmt.Errorf("subject.mediaType must be a manifest, not '%s'", manifest.Subject.MediaType) - } - } - - m.Manifest = manifest - return nil -} - -// MarshalJSON returns the contents of canonical. If canonical is empty, -// returns an error. -func (m *DeserializedManifest) MarshalJSON() ([]byte, error) { - if len(m.canonical) > 0 { - return m.canonical, nil - } - - return nil, errors.New("JSON representation not initialized in DeserializedManifest") -} - -// Payload returns the raw content of the manifest. The contents can be used to -// calculate the content identifier. -func (m *DeserializedManifest) Payload() (string, []byte, error) { - return v1.MediaTypeArtifactManifest, m.canonical, nil -} - -// References returns the descriptors of this manifest's blobs only. -func (m *DeserializedManifest) References() []distribution.Descriptor { - return m.Blobs -} - -// Subject returns a pointer to the subject of this manifest or nil if there is -// none -func (m *DeserializedManifest) Subject() *distribution.Descriptor { - return m.Manifest.Subject -} - -// Type returns the artifactType of this manifest if there is one, otherwise it -// returns empty string. -func (m *DeserializedManifest) Type() string { - return m.Manifest.ArtifactType -} diff --git a/manifest/ociartifact/manifest_test.go b/manifest/ociartifact/manifest_test.go deleted file mode 100644 index 02d13e4c8..000000000 --- a/manifest/ociartifact/manifest_test.go +++ /dev/null @@ -1,95 +0,0 @@ -package ociartifact_test - -import ( - "reflect" - "testing" - - "github.com/distribution/distribution/v3" - "github.com/distribution/distribution/v3/manifest/ociartifact" - v1 "github.com/opencontainers/image-spec/specs-go/v1" -) - -func TestArtifactFunc(t *testing.T) { - for name, test := range map[string]struct { - rawManifest []byte - expectError bool - expectedDescriptor distribution.Descriptor - expectedManifest distribution.Manifest - }{ - "valid_artifact": { - rawManifest: ociartifact.ManifestBytes, - expectedDescriptor: ociartifact.ManifestDescriptor, - expectedManifest: ociartifact.ManifestDeserialized, - }, - "artifact_must_have_mediaType": { - rawManifest: ociartifact.ManifestNoMediaType, - expectError: true, - }, - "artifact_can_have_no_subject": { - rawManifest: ociartifact.ManifestNoSubjectBytes, - expectedDescriptor: ociartifact.ManifestNoSubjectDescriptor, - expectedManifest: ociartifact.ManifestNoSubjectDeserialized, - }, - "artifact_subject_must_be_manifest": { - rawManifest: ociartifact.ManifestBlobSubjectBytes, - expectError: true, - }, - } { - t.Run(name, func(t *testing.T) { - test := test - t.Parallel() - - manifest, descriptor, err := distribution.UnmarshalManifest(v1.MediaTypeArtifactManifest, test.rawManifest) - - if err != nil != test.expectError { - t.Fatalf("Unexpected error value: %s, expected error=%t", err, test.expectError) - } - if !reflect.DeepEqual(descriptor, test.expectedDescriptor) { - t.Errorf("Descriptor incorrect:\n%v\nexpected:\n%v", descriptor, test.expectedDescriptor) - } - if !reflect.DeepEqual(manifest, test.expectedManifest) { - t.Errorf("Manifest incorrect:\n%v\nexpected:\n%v", manifest, test.expectedManifest) - } - }) - } -} - -func TestPayload(t *testing.T) { - t.Parallel() - - manifest, _, err := distribution.UnmarshalManifest(v1.MediaTypeArtifactManifest, ociartifact.ManifestBytes) - if err != nil { - t.Fatalf("Failed to unmarshal manifest: %s", err) - } - - mediaType, payload, err := manifest.Payload() - - if err != nil { - t.Fatalf("Unexpected error: %s", err) - } - if mediaType != v1.MediaTypeArtifactManifest { - t.Errorf("Unexpected mediaType %q, should be %q", mediaType, v1.MediaTypeArtifactManifest) - } - if !reflect.DeepEqual(payload, ociartifact.ManifestBytes) { - t.Errorf("Unexpected payload, should exactly match inputted manifest.") - } -} - -func TestReferences(t *testing.T) { - // References only returns the blobs of this artifact and not also it's - // subject because the subject does not have to exist when the artifact is - // pushed. Unlike when an image index returns the references image manifests - // because those do have to exist before the image index is pushed. - t.Parallel() - - manifest, _, err := distribution.UnmarshalManifest(v1.MediaTypeArtifactManifest, ociartifact.ManifestBytes) - if err != nil { - t.Fatalf("Failed to unmarshal manifest: %s", err) - } - - references := manifest.References() - - if !reflect.DeepEqual(references, ociartifact.ManifestDeserialized.Blobs) { - t.Errorf("Unexpected references:\n%v\nexpected:\n%v", references, ociartifact.ManifestDeserialized.Blobs) - } -} diff --git a/manifest/ocischema/manifest.go b/manifest/ocischema/manifest.go index 9bdef0e64..c535f62a0 100644 --- a/manifest/ocischema/manifest.go +++ b/manifest/ocischema/manifest.go @@ -45,6 +45,10 @@ type Manifest struct { // Config references the image configuration as a blob. Config distribution.Descriptor `json:"config"` + // ArtifactType is the type of an artifact when the manifest is used for an + // artifact. + ArtifactType string `json:"artifactType,omitempty"` + // Layers lists descriptors for the layers referenced by the // configuration. Layers []distribution.Descriptor `json:"layers"` @@ -106,6 +110,21 @@ func (m *DeserializedManifest) UnmarshalJSON(b []byte) error { v1.MediaTypeImageManifest, mfst.MediaType) } + if mfst.Config.MediaType == v1.MediaTypeScratch && mfst.ArtifactType == "" { + return fmt.Errorf("if config.mediaType is '%s' then artifactType must be set", v1.MediaTypeScratch) + } + + // The subject if specified must be a must be a manifest. This is validated + // here rather than in the storage manifest Put handler because the subject + // does not have to exist, so there is nothing to validate in the manifest + // store. If a non-compliant client provided the digest of a blob then this + // registry would still indicate that the referred manifest does not exist. + if mfst.Subject != nil { + if !distribution.ManifestMediaTypeSupported(mfst.Subject.MediaType) { + return fmt.Errorf("subject.mediaType must be a manifest, not '%s'", mfst.Subject.MediaType) + } + } + m.Manifest = mfst return nil @@ -133,10 +152,12 @@ func (m *DeserializedManifest) Subject() *distribution.Descriptor { return m.Manifest.Subject } -// Type returns empty string because image manifests with subjects cannot have -// an artifact type. +// Type returns the artifactType of the manifest func (m *DeserializedManifest) Type() string { - return "" + if m.ArtifactType == "" { + return m.Config.MediaType + } + return m.ArtifactType } // unknownDocument represents a manifest, manifest list, or index that has not diff --git a/manifest/ocischema/manifest_test.go b/manifest/ocischema/manifest_test.go index a906eab9e..a557aa532 100644 --- a/manifest/ocischema/manifest_test.go +++ b/manifest/ocischema/manifest_test.go @@ -9,6 +9,7 @@ import ( "github.com/distribution/distribution/v3" "github.com/distribution/distribution/v3/manifest" "github.com/distribution/distribution/v3/manifest/manifestlist" + "github.com/distribution/distribution/v3/manifest/schema2" v1 "github.com/opencontainers/image-spec/specs-go/v1" ) @@ -39,6 +40,14 @@ const expectedManifestSerialization = `{ } }` +var ( + scratchDescriptor = distribution.Descriptor{ + MediaType: v1.ScratchDescriptor.MediaType, + Size: v1.ScratchDescriptor.Size, + Digest: v1.ScratchDescriptor.Digest, + } +) + func makeTestManifest(mediaType string) Manifest { return Manifest{ Versioned: manifest.Versioned{ @@ -214,3 +223,191 @@ func TestValidateManifest(t *testing.T) { } }) } + +func TestArtifactManifest(t *testing.T) { + for name, test := range map[string]struct { + manifest Manifest + expectValid bool + expectedArtifactType string + }{ + "not_artifact": { + manifest: Manifest{ + Versioned: SchemaVersion, + Config: distribution.Descriptor{ + MediaType: v1.MediaTypeImageConfig, + Size: 200, + Digest: "sha256:4de6702c739d8c9ed907f4c031fd0abc54ee1bf372603a585e139730772cc0b8", + }, + Layers: []distribution.Descriptor{ + { + MediaType: v1.MediaTypeImageLayerGzip, + Size: 23423, + Digest: "sha256:ff1b4a27562d8ffc821b4d7368818ad7c759cfc2068b7adf0d2712315d67359a", + }, + }, + }, + expectValid: true, + expectedArtifactType: v1.MediaTypeImageConfig, + }, + "typical_artifact": { + manifest: Manifest{ + Versioned: SchemaVersion, + Config: distribution.Descriptor{ + MediaType: "application/vnd.example.thing", + Size: 200, + Digest: "sha256:4de6702c739d8c9ed907f4c031fd0abc54ee1bf372603a585e139730772cc0b8", + }, + Layers: []distribution.Descriptor{ + { + MediaType: v1.MediaTypeImageLayerGzip, + Size: 23423, + Digest: "sha256:ff1b4a27562d8ffc821b4d7368818ad7c759cfc2068b7adf0d2712315d67359a", + }, + }, + }, + expectValid: true, + expectedArtifactType: "application/vnd.example.thing", + }, + "also_typical_artifact": { + manifest: Manifest{ + Versioned: SchemaVersion, + ArtifactType: "application/vnd.example.sbom", + Config: distribution.Descriptor{ + MediaType: v1.MediaTypeImageConfig, + Size: 200, + Digest: "sha256:4de6702c739d8c9ed907f4c031fd0abc54ee1bf372603a585e139730772cc0b8", + }, + Layers: []distribution.Descriptor{ + { + MediaType: v1.MediaTypeImageLayerGzip, + Size: 23423, + Digest: "sha256:ff1b4a27562d8ffc821b4d7368818ad7c759cfc2068b7adf0d2712315d67359a", + }, + }, + }, + expectValid: true, + expectedArtifactType: "application/vnd.example.sbom", + }, + "configless_artifact": { + manifest: Manifest{ + Versioned: SchemaVersion, + ArtifactType: "application/vnd.example.catgif", + Config: scratchDescriptor, + Layers: []distribution.Descriptor{ + { + MediaType: "image/gif", + Size: 23423, + Digest: "sha256:ff1b4a27562d8ffc821b4d7368818ad7c759cfc2068b7adf0d2712315d67359a", + }, + }, + }, + expectValid: true, + expectedArtifactType: "application/vnd.example.catgif", + }, + "invalid_artifact": { + manifest: Manifest{ + Versioned: SchemaVersion, + Config: scratchDescriptor, // This MUST have an artifactType + Layers: []distribution.Descriptor{ + { + MediaType: "image/gif", + Size: 23423, + Digest: "sha256:ff1b4a27562d8ffc821b4d7368818ad7c759cfc2068b7adf0d2712315d67359a", + }, + }, + }, + expectValid: false, + }, + "annotation_artifact": { + manifest: Manifest{ + Versioned: SchemaVersion, + ArtifactType: "application/vnd.example.comment", + Config: scratchDescriptor, + Layers: []distribution.Descriptor{ + scratchDescriptor, + }, + Annotations: map[string]string{ + "com.example.data": "payload", + }, + }, + expectValid: true, + expectedArtifactType: "application/vnd.example.comment", + }, + "valid_subject": { + manifest: Manifest{ + Versioned: SchemaVersion, + ArtifactType: "application/vnd.example.comment", + Config: scratchDescriptor, + Layers: []distribution.Descriptor{ + scratchDescriptor, + }, + Subject: &distribution.Descriptor{ + MediaType: v1.MediaTypeImageManifest, + Size: 365, + Digest: "sha256:05b3abf2579a5eb66403cd78be557fd860633a1fe2103c7642030defe32c657f", + }, + Annotations: map[string]string{ + "com.example.data": "payload", + }, + }, + expectValid: true, + expectedArtifactType: "application/vnd.example.comment", + }, + "invalid_subject": { + manifest: Manifest{ + Versioned: SchemaVersion, + ArtifactType: "application/vnd.example.comment", + Config: scratchDescriptor, + Layers: []distribution.Descriptor{ + scratchDescriptor, + }, + Subject: &distribution.Descriptor{ + MediaType: v1.MediaTypeImageLayerGzip, // The subject is a manifest + Size: 365, + Digest: "sha256:05b3abf2579a5eb66403cd78be557fd860633a1fe2103c7642030defe32c657f", + }, + Annotations: map[string]string{ + "com.example.data": "payload", + }, + }, + expectValid: false, + }, + "docker_manifest_valid_as_subject": { + manifest: Manifest{ + Versioned: SchemaVersion, + ArtifactType: "application/vnd.example.comment", + Config: scratchDescriptor, + Layers: []distribution.Descriptor{ + scratchDescriptor, + }, + Subject: &distribution.Descriptor{ + MediaType: schema2.MediaTypeManifest, + Size: 365, + Digest: "sha256:05b3abf2579a5eb66403cd78be557fd860633a1fe2103c7642030defe32c657f", + }, + Annotations: map[string]string{ + "com.example.data": "payload", + }, + }, + expectValid: true, + expectedArtifactType: "application/vnd.example.comment", + }, + } { + t.Run(name, func(t *testing.T) { + dm, err := FromStruct(test.manifest) + if err != nil { + t.Fatalf("Error making DeserializedManifest from struct: %s", err) + } + m, _, err := distribution.UnmarshalManifest(v1.MediaTypeImageManifest, dm.canonical) + if test.expectValid != (nil == err) { + t.Fatalf("expectValid=%t but got err=%v", test.expectValid, err) + } + if err != nil { + return + } + if artifactType := m.(distribution.Referrer).Type(); artifactType != test.expectedArtifactType { + t.Errorf("Expected artifactType to be %q but got %q", test.expectedArtifactType, artifactType) + } + }) + } +} diff --git a/manifest/versioned.go b/manifest/versioned.go index 25222d73d..caa6b14e8 100644 --- a/manifest/versioned.go +++ b/manifest/versioned.go @@ -10,10 +10,3 @@ type Versioned struct { // MediaType is the media type of this schema. MediaType string `json:"mediaType,omitempty"` } - -// Unversioned provides a struct with the mediaType only. Incoming content with -// unknown mediaType can be decoded against this struct to check the type. -type Unversioned struct { - // MediaType is the media type of this schema. - MediaType string `json:"mediaType,omitempty"` -} diff --git a/registry/handlers/api_test.go b/registry/handlers/api_test.go index cf511f207..48324eff3 100644 --- a/registry/handlers/api_test.go +++ b/registry/handlers/api_test.go @@ -23,7 +23,6 @@ import ( "github.com/distribution/distribution/v3/configuration" "github.com/distribution/distribution/v3/manifest" "github.com/distribution/distribution/v3/manifest/manifestlist" - "github.com/distribution/distribution/v3/manifest/ociartifact" "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/manifest/schema2" @@ -40,9 +39,16 @@ import ( v1 "github.com/opencontainers/image-spec/specs-go/v1" ) -var headerConfig = http.Header{ - "X-Content-Type-Options": []string{"nosniff"}, -} +var ( + headerConfig = http.Header{ + "X-Content-Type-Options": []string{"nosniff"}, + } + scratchDescriptor = distribution.Descriptor{ + MediaType: v1.ScratchDescriptor.MediaType, + Size: v1.ScratchDescriptor.Size, + Digest: v1.ScratchDescriptor.Digest, + } +) const ( // digestSha256EmptyTar is the canonical sha256 digest of empty data @@ -3163,50 +3169,8 @@ func TestArtifactManifest(t *testing.T) { manifest func(*testing.T, *testEnv, reference.Named) distribution.Manifest deleteSubject bool }{ - // When an OCI artifact is PUT before its subject, the subject's referrers - // link will be made in advance. - "valid": { - manifest: func(t *testing.T, testEnv *testEnv, repo reference.Named) distribution.Manifest { - manifest, err := ociartifact.FromStruct(ociartifact.Manifest{ - Unversioned: manifest.Unversioned{ - MediaType: v1.MediaTypeArtifactManifest, - }, - ArtifactType: "application/vnd.example.sbom.v1", - Subject: &distribution.Descriptor{ - MediaType: v1.MediaTypeImageManifest, - Digest: "sha256:ebe054f08821294feee7bc442014fdd38b4836d83781d8ba99d38eb50d0c9d85", - Size: 99, - }, - }) - if err != nil { - t.Fatalf("Failed to create manifest: %s", err) - } - return manifest - }, - }, - // An artifact can have no artifactType - "no_type": { - manifest: func(t *testing.T, testEnv *testEnv, repo reference.Named) distribution.Manifest { - manifest, err := ociartifact.FromStruct(ociartifact.Manifest{ - Unversioned: manifest.Unversioned{ - MediaType: v1.MediaTypeArtifactManifest, - }, - ArtifactType: "", - Subject: &distribution.Descriptor{ - MediaType: v1.MediaTypeImageManifest, - Digest: "sha256:ebe054f08821294feee7bc442014fdd38b4836d83781d8ba99d38eb50d0c9d85", - Size: 99, - }, - }) - if err != nil { - t.Fatalf("Failed to create manifest: %s", err) - } - return manifest - }, - }, - - // The link is also made when the subject already exists and is kept if - // the subject is deleted + // The link is made when the subject already exists and is kept if the + // subject is deleted "subject_exists": { manifest: func(t *testing.T, testEnv *testEnv, repo reference.Named) distribution.Manifest { args := testManifestAPISchema2(t, testEnv, repo) @@ -3215,11 +3179,12 @@ func TestArtifactManifest(t *testing.T) { t.Fatalf("Failed to get subject payload: %s", err) } - manifest, err := ociartifact.FromStruct(ociartifact.Manifest{ - Unversioned: manifest.Unversioned{ - MediaType: v1.MediaTypeArtifactManifest, - }, + pushScratch(t, testEnv, repo) + + manifest, err := ocischema.FromStruct(ocischema.Manifest{ + Versioned: ocischema.SchemaVersion, ArtifactType: "application/vnd.example.sbom.v1", + Config: scratchDescriptor, Subject: &distribution.Descriptor{ MediaType: args.mediaType, Digest: args.dgst, @@ -3244,10 +3209,7 @@ func TestArtifactManifest(t *testing.T) { url, _ := startPushLayer(t, testEnv, repo) pushLayer(t, testEnv.builder, repo, configDigest, url, config) manifest, err := ocischema.FromStruct(ocischema.Manifest{ - Versioned: manifest.Versioned{ - SchemaVersion: 2, - MediaType: v1.MediaTypeImageManifest, - }, + Versioned: ocischema.SchemaVersion, Config: distribution.Descriptor{ MediaType: v1.MediaTypeImageConfig, Digest: configDigest, @@ -3412,7 +3374,7 @@ func TestArtifactManifest(t *testing.T) { if err != nil { t.Fatalf("Failed to create artifact GET request: %s", err) } - req.Header.Set("Accept", v1.MediaTypeArtifactManifest) + req.Header.Set("Accept", v1.MediaTypeImageManifest) res, err = http.DefaultClient.Do(req) if err != nil { t.Fatalf("Failed to GET manifest: %s", err) @@ -3512,12 +3474,17 @@ func TestDockerManifestWithSubject(t *testing.T) { func TestArtifactManifestValidation(t *testing.T) { for name, test := range map[string]struct { - blobs func(*testing.T, *testEnv, reference.Named) []distribution.Descriptor + config func(*testing.T, *testEnv, reference.Named) distribution.Descriptor + layers func(*testing.T, *testEnv, reference.Named) []distribution.Descriptor wantCode int }{ - "blobs_must_exist": { - blobs: func(t *testing.T, te *testEnv, n reference.Named) []distribution.Descriptor { - // a blob which has not been uploaded + "layers_must_exist": { + config: func(t *testing.T, testEnv *testEnv, repo reference.Named) distribution.Descriptor { + pushScratch(t, testEnv, repo) + return scratchDescriptor + }, + layers: func(t *testing.T, te *testEnv, n reference.Named) []distribution.Descriptor { + // a layer which has not been uploaded return []distribution.Descriptor{ { MediaType: v1.MediaTypeImageLayer, @@ -3528,8 +3495,60 @@ func TestArtifactManifestValidation(t *testing.T) { }, wantCode: 400, }, + "config_must_be_set": { + config: func(t *testing.T, testEnv *testEnv, repo reference.Named) distribution.Descriptor { + return distribution.Descriptor{} // zero value + }, + layers: func(t *testing.T, testEnv *testEnv, repo reference.Named) []distribution.Descriptor { + layers := int64(10) + digests := make([]distribution.Descriptor, layers) + for i := int64(0); i < layers; i++ { + rs, digest, err := testutil.CreateRandomTarFile() + if err != nil { + t.Fatalf("Failed to create test blob: %s", err) + } + url, _ := startPushLayer(t, testEnv, repo) + pushLayer(t, testEnv.builder, repo, digest, url, rs) + digests[i] = distribution.Descriptor{ + MediaType: v1.MediaTypeImageLayer, + Digest: digest, + Size: testutil.MustSeekerLen(rs), + } + } + return digests + }, + wantCode: 400, + }, + "config_must_exist": { + config: func(t *testing.T, testEnv *testEnv, repo reference.Named) distribution.Descriptor { + return scratchDescriptor // not uploaded + }, + layers: func(t *testing.T, testEnv *testEnv, repo reference.Named) []distribution.Descriptor { + layers := int64(10) + digests := make([]distribution.Descriptor, layers) + for i := int64(0); i < layers; i++ { + rs, digest, err := testutil.CreateRandomTarFile() + if err != nil { + t.Fatalf("Failed to create test blob: %s", err) + } + url, _ := startPushLayer(t, testEnv, repo) + pushLayer(t, testEnv.builder, repo, digest, url, rs) + digests[i] = distribution.Descriptor{ + MediaType: v1.MediaTypeImageLayer, + Digest: digest, + Size: testutil.MustSeekerLen(rs), + } + } + return digests + }, + wantCode: 400, + }, "valid_blobs": { - blobs: func(t *testing.T, testEnv *testEnv, repo reference.Named) []distribution.Descriptor { + config: func(t *testing.T, testEnv *testEnv, repo reference.Named) distribution.Descriptor { + pushScratch(t, testEnv, repo) + return scratchDescriptor + }, + layers: func(t *testing.T, testEnv *testEnv, repo reference.Named) []distribution.Descriptor { layers := int64(10) digests := make([]distribution.Descriptor, layers) for i := int64(0); i < layers; i++ { @@ -3559,12 +3578,11 @@ func TestArtifactManifestValidation(t *testing.T) { t.Fatalf("failed to make repo: %s", err) } - manifest, err := ociartifact.FromStruct(ociartifact.Manifest{ - Unversioned: manifest.Unversioned{ - MediaType: v1.MediaTypeArtifactManifest, - }, + manifest, err := ocischema.FromStruct(ocischema.Manifest{ + Versioned: ocischema.SchemaVersion, + Config: test.config(t, testEnv, repo), ArtifactType: "application/vnd.example.sbom.v1", - Blobs: test.blobs(t, testEnv, repo), + Layers: test.layers(t, testEnv, repo), }) if err != nil { t.Fatalf("Failed to make manifest: %s", err) @@ -3600,3 +3618,8 @@ func TestArtifactManifestValidation(t *testing.T) { }) } } + +func pushScratch(t *testing.T, testEnv *testEnv, repo reference.Named) { + url, _ := startPushLayer(t, testEnv, repo) + pushLayer(t, testEnv.builder, repo, v1.ScratchDescriptor.Digest, url, bytes.NewBuffer(v1.ScratchDescriptor.Data)) +} diff --git a/registry/handlers/manifests.go b/registry/handlers/manifests.go index 4a3d4d39d..1e1459715 100644 --- a/registry/handlers/manifests.go +++ b/registry/handlers/manifests.go @@ -10,7 +10,6 @@ import ( "github.com/distribution/distribution/v3" dcontext "github.com/distribution/distribution/v3/context" "github.com/distribution/distribution/v3/manifest/manifestlist" - "github.com/distribution/distribution/v3/manifest/ociartifact" "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/manifest/schema2" @@ -41,8 +40,7 @@ const ( manifestlistSchema // 2 ociSchema // 3 ociImageIndexSchema // 4 - ociArtifact // 5 - numStorageTypes // 6 + numStorageTypes // 5 ) // manifestDispatcher takes the request context and builds the @@ -117,9 +115,6 @@ func (imh *manifestHandler) GetManifest(w http.ResponseWriter, r *http.Request) if mediaType == v1.MediaTypeImageIndex { supports[ociImageIndexSchema] = true } - if mediaType == v1.MediaTypeArtifactManifest { - supports[ociArtifact] = true - } } } @@ -163,8 +158,6 @@ func (imh *manifestHandler) GetManifest(w http.ResponseWriter, r *http.Request) manifestType = manifestSchema2 } else if _, isOCImanifest := manifest.(*ocischema.DeserializedManifest); isOCImanifest { manifestType = ociSchema - } else if _, isOCIArtifact := manifest.(*ociartifact.DeserializedManifest); isOCIArtifact { - manifestType = ociArtifact } else if isManifestList { if manifestList.MediaType == manifestlist.MediaTypeManifestList { manifestType = manifestlistSchema @@ -181,10 +174,6 @@ func (imh *manifestHandler) GetManifest(w http.ResponseWriter, r *http.Request) imh.Errors = append(imh.Errors, v2.ErrorCodeManifestNotAcceptable.WithMessage("OCI index found, but accept header does not support OCI indexes")) return } - if manifestType == ociArtifact && !supports[ociArtifact] { - imh.Errors = append(imh.Errors, v2.ErrorCodeManifestNotAcceptable.WithMessage("OCI artifact found, but accept header does not support OCI artifacts")) - return - } // Only rewrite schema2 manifests when they are being fetched by tag. // If they are being fetched by digest, we can't return something not // matching the digest. @@ -333,8 +322,7 @@ func (imh *manifestHandler) PutManifest(w http.ResponseWriter, r *http.Request) return } - isAnOCIManifest := mediaType == v1.MediaTypeImageManifest || mediaType == v1.MediaTypeImageIndex || - mediaType == v1.MediaTypeArtifactManifest + isAnOCIManifest := mediaType == v1.MediaTypeImageManifest || mediaType == v1.MediaTypeImageIndex if isAnOCIManifest { dcontext.GetLogger(imh).Debug("Putting an OCI Manifest!") diff --git a/registry/storage/fixtures/naughtiest-artifact.json b/registry/storage/fixtures/naughtiest-artifact.json deleted file mode 100644 index 7b166f173..000000000 --- a/registry/storage/fixtures/naughtiest-artifact.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "schemaVersion": "this is allowed, I can do this if I want", - "mediaType": "application/vnd.oci.artifact.manifest.v1+json" -} diff --git a/registry/storage/manifeststore.go b/registry/storage/manifeststore.go index 542c4021e..6b2f4e58f 100644 --- a/registry/storage/manifeststore.go +++ b/registry/storage/manifeststore.go @@ -9,7 +9,6 @@ import ( dcontext "github.com/distribution/distribution/v3/context" "github.com/distribution/distribution/v3/manifest" "github.com/distribution/distribution/v3/manifest/manifestlist" - "github.com/distribution/distribution/v3/manifest/ociartifact" "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/manifest/schema2" @@ -90,21 +89,6 @@ func (ms *manifestStore) Get(ctx context.Context, dgst digest.Digest, options .. return nil, err } - // The content is unmarshalled into Unversioned first because it's possible - // for a spec compliant OCI artifact manifest to fail to unmarshal into - // Versioned due to the schemaVersion property being unknown. We MUST ignore - // this unknown property, and unmarshalling into Versioned would implicitly - // constrain that property to a number type. - var unversioned manifest.Unversioned - if err = json.Unmarshal(content, &unversioned); err != nil { - return nil, err - } - - switch unversioned.MediaType { - case v1.MediaTypeArtifactManifest: - return ms.ocischemaHandler.Unmarshal(ctx, dgst, content) - } - var versioned manifest.Versioned if err = json.Unmarshal(content, &versioned); err != nil { return nil, err @@ -150,7 +134,7 @@ func (ms *manifestStore) Put(ctx context.Context, manifest distribution.Manifest return ms.schema1Handler.Put(ctx, manifest, ms.skipDependencyVerification) case *schema2.DeserializedManifest: return ms.schema2Handler.Put(ctx, manifest, ms.skipDependencyVerification) - case *ocischema.DeserializedManifest, *ociartifact.DeserializedManifest: + case *ocischema.DeserializedManifest: return ms.ocischemaHandler.Put(ctx, manifest, ms.skipDependencyVerification) case *manifestlist.DeserializedManifestList: return ms.manifestListHandler.Put(ctx, manifest, ms.skipDependencyVerification) diff --git a/registry/storage/ocimanifesthandler.go b/registry/storage/ocimanifesthandler.go index ab29e9dba..468e6cc50 100644 --- a/registry/storage/ocimanifesthandler.go +++ b/registry/storage/ocimanifesthandler.go @@ -2,14 +2,11 @@ package storage import ( "context" - "encoding/json" "fmt" "net/url" "github.com/distribution/distribution/v3" dcontext "github.com/distribution/distribution/v3/context" - "github.com/distribution/distribution/v3/manifest" - "github.com/distribution/distribution/v3/manifest/ociartifact" "github.com/distribution/distribution/v3/manifest/ocischema" "github.com/opencontainers/go-digest" v1 "github.com/opencontainers/image-spec/specs-go/v1" @@ -30,25 +27,8 @@ var _ ManifestHandler = &ocischemaManifestHandler{} func (ms *ocischemaManifestHandler) Unmarshal(ctx context.Context, dgst digest.Digest, content []byte) (distribution.Manifest, error) { dcontext.GetLogger(ms.ctx).Debug("(*ocischemaManifestHandler).Unmarshal") - var unversioned manifest.Unversioned - if err := json.Unmarshal(content, &unversioned); err != nil { - return nil, err - } - var m distribution.Manifest - switch unversioned.MediaType { - case "", v1.MediaTypeImageManifest: - m = &ocischema.DeserializedManifest{} - case v1.MediaTypeArtifactManifest: - m = &ociartifact.DeserializedManifest{} - default: - return nil, fmt.Errorf("if present, mediaType should be '%s' or '%s', not '%s'", - v1.MediaTypeImageManifest, - v1.MediaTypeArtifactManifest, - unversioned.MediaType, - ) - } - - if err := m.(json.Unmarshaler).UnmarshalJSON(content); err != nil { + m := &ocischema.DeserializedManifest{} + if err := m.UnmarshalJSON(content); err != nil { return nil, err } @@ -58,7 +38,12 @@ func (ms *ocischemaManifestHandler) Unmarshal(ctx context.Context, dgst digest.D func (ms *ocischemaManifestHandler) Put(ctx context.Context, manifest distribution.Manifest, skipDependencyVerification bool) (digest.Digest, error) { dcontext.GetLogger(ms.ctx).Debug("(*ocischemaManifestHandler).Put") - if err := ms.verifyManifest(ms.ctx, manifest, skipDependencyVerification); err != nil { + m, ok := manifest.(*ocischema.DeserializedManifest) + if !ok { + return "", fmt.Errorf("non-ocischema manifest put to ocischemaManifestHandler: %T", manifest) + } + + if err := ms.verifyManifest(ms.ctx, *m, skipDependencyVerification); err != nil { return "", err } @@ -87,17 +72,11 @@ func (ms *ocischemaManifestHandler) Put(ctx context.Context, manifest distributi // verifyManifest ensures that the manifest content is valid from the // perspective of the registry. As a policy, the registry only tries to store // valid content, leaving trust policies of that content up to consumers. -func (ms *ocischemaManifestHandler) verifyManifest(ctx context.Context, mnfst distribution.Manifest, skipDependencyVerification bool) error { +func (ms *ocischemaManifestHandler) verifyManifest(ctx context.Context, mnfst ocischema.DeserializedManifest, skipDependencyVerification bool) error { var errs distribution.ErrManifestVerification - switch m := mnfst.(type) { - case *ocischema.DeserializedManifest: - if m.Manifest.SchemaVersion != 2 { - return fmt.Errorf("unrecognized manifest schema version %d", m.Manifest.SchemaVersion) - } - case *ociartifact.DeserializedManifest: - default: - return fmt.Errorf("unrecognized manifest type: %T", mnfst) + if mnfst.Manifest.SchemaVersion != 2 { + return fmt.Errorf("unrecognized manifest schema version %d", mnfst.Manifest.SchemaVersion) } if skipDependencyVerification { diff --git a/registry/storage/ocimanifesthandler_test.go b/registry/storage/ocimanifesthandler_test.go index ecaccb844..5eab9b9f6 100644 --- a/registry/storage/ocimanifesthandler_test.go +++ b/registry/storage/ocimanifesthandler_test.go @@ -2,24 +2,18 @@ package storage import ( "context" - _ "embed" - "reflect" "regexp" "strings" "testing" "github.com/distribution/distribution/v3" "github.com/distribution/distribution/v3/manifest" - "github.com/distribution/distribution/v3/manifest/ociartifact" "github.com/distribution/distribution/v3/manifest/ocischema" "github.com/distribution/distribution/v3/registry/storage/driver/inmemory" "github.com/opencontainers/go-digest" v1 "github.com/opencontainers/image-spec/specs-go/v1" ) -//go:embed fixtures/naughtiest-artifact.json -var naughtiestArtifact []byte - func TestVerifyOCIManifestNonDistributableLayer(t *testing.T) { ctx := context.Background() inmemoryDriver := inmemory.New() @@ -338,81 +332,3 @@ func TestVerifyOCIManifestBlobLayerAndConfig(t *testing.T) { checkFn(m, c.Err) } } - -func TestPutArtifact(t *testing.T) { - t.Parallel() - - driver := inmemory.New() - registry := createRegistry(t, driver) - repo := makeRepository(t, registry, "test") - manifestService := makeManifestService(t, repo) - - ctx := context.Background() - blob, err := repo.Blobs(ctx).Put(ctx, v1.MediaTypeImageLayerGzip, []byte("I am an artifact")) - if err != nil { - t.Fatal(err) - } - - template := ociartifact.Manifest{ - Unversioned: manifest.Unversioned{ - MediaType: v1.MediaTypeArtifactManifest, - }, - ArtifactType: "application/vnd.example.sbom.v1", - Blobs: []distribution.Descriptor{ - blob, - }, - Subject: &distribution.Descriptor{ - MediaType: v1.MediaTypeImageManifest, - Digest: "sha256:195ce2d6ff471aa95e91f3ea1e95a27d474a452f040d4c18f6eb29f3ca42a821", - Size: 21, - }, - } - - manifest, err := ociartifact.FromStruct(template) - if err != nil { - t.Fatalf("Failed to construct artifact manifest: %s", err) - } - - _, err = manifestService.Put(ctx, manifest) - - if err != nil { - t.Fatalf("Unexpected error: %s", err) - } -} - -func TestGetArtifact(t *testing.T) { - t.Parallel() - - driver := inmemory.New() - registry := createRegistry(t, driver) - repo := makeRepository(t, registry, "test") - manifestService := makeManifestService(t, repo) - - manifest := &ociartifact.DeserializedManifest{} - err := manifest.UnmarshalJSON(naughtiestArtifact) - if err != nil { - t.Fatalf("Failed to parse valid artifact manifest: %s", err) - } - - ctx := context.Background() - dgst, err := manifestService.Put(ctx, manifest) - if err != nil { - t.Fatalf("Failed to Put valid artifact manifest: %s", err) - } - - gotManifest, err := manifestService.Get(ctx, dgst) - if err != nil { - t.Fatalf("Failed to get valid artifact manifest: %s", err) - } - _, gotPayload, err := gotManifest.Payload() - if err != nil { - t.Fatalf("Failed ot get returned manifest payload: %s", err) - } - _, expectedPayload, err := manifest.Payload() - if err != nil { - t.Fatalf("Failed ot get valid manifest payload: %s", err) - } - if !reflect.DeepEqual(gotPayload, expectedPayload) { - t.Errorf("Pulled manifest does not match put manifest, got:\n%s\nexpected:\n%s", gotPayload, expectedPayload) - } -} diff --git a/vendor/github.com/opencontainers/image-spec/specs-go/v1/annotations.go b/vendor/github.com/opencontainers/image-spec/specs-go/v1/annotations.go index 6f9e6fd3a..e62892046 100644 --- a/vendor/github.com/opencontainers/image-spec/specs-go/v1/annotations.go +++ b/vendor/github.com/opencontainers/image-spec/specs-go/v1/annotations.go @@ -65,7 +65,4 @@ const ( // AnnotationArtifactDescription is the annotation key for the human readable description for the artifact. AnnotationArtifactDescription = "org.opencontainers.artifact.description" - - // AnnotationReferrersFiltersApplied is the annotation key for the comma separated list of filters applied by the registry in the referrers listing. - AnnotationReferrersFiltersApplied = "org.opencontainers.referrers.filtersApplied" ) diff --git a/vendor/github.com/opencontainers/image-spec/specs-go/v1/artifact.go b/vendor/github.com/opencontainers/image-spec/specs-go/v1/artifact.go deleted file mode 100644 index 03d76ce43..000000000 --- a/vendor/github.com/opencontainers/image-spec/specs-go/v1/artifact.go +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright 2022 The Linux Foundation -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package v1 - -// Artifact describes an artifact manifest. -// This structure provides `application/vnd.oci.artifact.manifest.v1+json` mediatype when marshalled to JSON. -type Artifact struct { - // MediaType is the media type of the object this schema refers to. - MediaType string `json:"mediaType"` - - // ArtifactType is the IANA media type of the artifact this schema refers to. - ArtifactType string `json:"artifactType"` - - // Blobs is a collection of blobs referenced by this manifest. - Blobs []Descriptor `json:"blobs,omitempty"` - - // Subject (reference) is an optional link from the artifact to another manifest forming an association between the artifact and the other manifest. - Subject *Descriptor `json:"subject,omitempty"` - - // Annotations contains arbitrary metadata for the artifact manifest. - Annotations map[string]string `json:"annotations,omitempty"` -} diff --git a/vendor/github.com/opencontainers/image-spec/specs-go/v1/config.go b/vendor/github.com/opencontainers/image-spec/specs-go/v1/config.go index ffff4b6d1..36b0aeb8f 100644 --- a/vendor/github.com/opencontainers/image-spec/specs-go/v1/config.go +++ b/vendor/github.com/opencontainers/image-spec/specs-go/v1/config.go @@ -48,6 +48,17 @@ type ImageConfig struct { // StopSignal contains the system call signal that will be sent to the container to exit. StopSignal string `json:"StopSignal,omitempty"` + + // ArgsEscaped + // + // Deprecated: This field is present only for legacy compatibility with + // Docker and should not be used by new image builders. It is used by Docker + // for Windows images to indicate that the `Entrypoint` or `Cmd` or both, + // contains only a single element array, that is a pre-escaped, and combined + // into a single string `CommandLine`. If `true` the value in `Entrypoint` or + // `Cmd` should be used as-is to avoid double escaping. + // https://github.com/opencontainers/image-spec/pull/892 + ArgsEscaped bool `json:"ArgsEscaped,omitempty"` } // RootFS describes a layer content addresses @@ -86,22 +97,8 @@ type Image struct { // Author defines the name and/or email address of the person or entity which created and is responsible for maintaining the image. Author string `json:"author,omitempty"` - // Architecture is the CPU architecture which the binaries in this image are built to run on. - Architecture string `json:"architecture"` - - // Variant is the variant of the specified CPU architecture which image binaries are intended to run on. - Variant string `json:"variant,omitempty"` - - // OS is the name of the operating system which the image is built to run on. - OS string `json:"os"` - - // OSVersion is an optional field specifying the operating system - // version, for example on Windows `10.0.14393.1066`. - OSVersion string `json:"os.version,omitempty"` - - // OSFeatures is an optional field specifying an array of strings, - // each listing a required OS feature (for example on Windows `win32k`). - OSFeatures []string `json:"os.features,omitempty"` + // Platform describes the platform which the image in the manifest runs on. + Platform // Config defines the execution parameters which should be used as a base when running a container using the image. Config ImageConfig `json:"config,omitempty"` diff --git a/vendor/github.com/opencontainers/image-spec/specs-go/v1/manifest.go b/vendor/github.com/opencontainers/image-spec/specs-go/v1/manifest.go index 730a09359..4ce7b54cc 100644 --- a/vendor/github.com/opencontainers/image-spec/specs-go/v1/manifest.go +++ b/vendor/github.com/opencontainers/image-spec/specs-go/v1/manifest.go @@ -23,6 +23,9 @@ type Manifest struct { // MediaType specifies the type of this document data structure e.g. `application/vnd.oci.image.manifest.v1+json` MediaType string `json:"mediaType,omitempty"` + // ArtifactType specifies the IANA media type of artifact when the manifest is used for an artifact. + ArtifactType string `json:"artifactType,omitempty"` + // Config references a configuration object for a container, by digest. // The referenced configuration object is a JSON blob that the runtime uses to set up the container. Config Descriptor `json:"config"` @@ -36,3 +39,11 @@ type Manifest struct { // Annotations contains arbitrary metadata for the image manifest. Annotations map[string]string `json:"annotations,omitempty"` } + +// ScratchDescriptor is the descriptor of a blob with content of `{}`. +var ScratchDescriptor = Descriptor{ + MediaType: MediaTypeScratch, + Digest: `sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a`, + Size: 2, + Data: []byte(`{}`), +} diff --git a/vendor/github.com/opencontainers/image-spec/specs-go/v1/mediatype.go b/vendor/github.com/opencontainers/image-spec/specs-go/v1/mediatype.go index 935b481e3..5dd31255e 100644 --- a/vendor/github.com/opencontainers/image-spec/specs-go/v1/mediatype.go +++ b/vendor/github.com/opencontainers/image-spec/specs-go/v1/mediatype.go @@ -40,21 +40,36 @@ const ( // MediaTypeImageLayerNonDistributable is the media type for layers referenced by // the manifest but with distribution restrictions. + // + // Deprecated: Non-distributable layers are deprecated, and not recommended + // for future use. Implementations SHOULD NOT produce new non-distributable + // layers. + // https://github.com/opencontainers/image-spec/pull/965 MediaTypeImageLayerNonDistributable = "application/vnd.oci.image.layer.nondistributable.v1.tar" // MediaTypeImageLayerNonDistributableGzip is the media type for // gzipped layers referenced by the manifest but with distribution // restrictions. + // + // Deprecated: Non-distributable layers are deprecated, and not recommended + // for future use. Implementations SHOULD NOT produce new non-distributable + // layers. + // https://github.com/opencontainers/image-spec/pull/965 MediaTypeImageLayerNonDistributableGzip = "application/vnd.oci.image.layer.nondistributable.v1.tar+gzip" // MediaTypeImageLayerNonDistributableZstd is the media type for zstd // compressed layers referenced by the manifest but with distribution // restrictions. + // + // Deprecated: Non-distributable layers are deprecated, and not recommended + // for future use. Implementations SHOULD NOT produce new non-distributable + // layers. + // https://github.com/opencontainers/image-spec/pull/965 MediaTypeImageLayerNonDistributableZstd = "application/vnd.oci.image.layer.nondistributable.v1.tar+zstd" // MediaTypeImageConfig specifies the media type for the image configuration. MediaTypeImageConfig = "application/vnd.oci.image.config.v1+json" - // MediaTypeArtifactManifest specifies the media type for a content descriptor. - MediaTypeArtifactManifest = "application/vnd.oci.artifact.manifest.v1+json" + // MediaTypeScratch specifies the media type for an unused blob containing the value `{}` + MediaTypeScratch = "application/vnd.oci.scratch.v1+json" ) diff --git a/vendor/github.com/opencontainers/image-spec/specs-go/version.go b/vendor/github.com/opencontainers/image-spec/specs-go/version.go index d27903579..3d4119b44 100644 --- a/vendor/github.com/opencontainers/image-spec/specs-go/version.go +++ b/vendor/github.com/opencontainers/image-spec/specs-go/version.go @@ -25,7 +25,7 @@ const ( VersionPatch = 0 // VersionDev indicates development branch. Releases will be empty string. - VersionDev = "-rc2" + VersionDev = "-rc.3" ) // Version is the specification version that the package types support. diff --git a/vendor/modules.txt b/vendor/modules.txt index 3189b7a57..c5fd3866a 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -231,8 +231,8 @@ github.com/ncw/swift/swifttest ## explicit; go 1.13 github.com/opencontainers/go-digest github.com/opencontainers/go-digest/digestset -# github.com/opencontainers/image-spec v1.1.0-rc2 -## explicit; go 1.17 +# github.com/opencontainers/image-spec v1.1.0-rc3 +## explicit; go 1.18 github.com/opencontainers/image-spec/specs-go github.com/opencontainers/image-spec/specs-go/v1 # github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4