8000 Support OCI Image Manifests with artifactType and subject properties by brackendawson · Pull Request #3834 · distribution/distribution · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Support OCI Image Manifests with artifactType and subject properties #3834

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
30015b3
Parsing and storing of OCI Artifact Manifests
brackendawson Feb 1, 2023
758f8ae
Add failing test for artifact referrers links
brackendawson Feb 2, 2023
608a570
Artifact subject should be a pointer field
brackendawson Feb 2, 2023
cd0c26c
Implement the Referrers interface on Artifact
brackendawson Feb 2, 2023
a9fafba
Create referrer links on subjects of artifacts
brackendawson Feb 2, 2023
3b4919b
Artifact manifest package should ba named OCI
brackendawson Feb 2, 2023
d655919
Implement GET of artifact manifests
brackendawson Feb 2, 2023
19033a3 8000
Test artifact manifest deletion
brackendawson Feb 2, 2023
92a5cff
Test subject field in a manifest that can't refer
brackendawson Feb 2, 2023
baa392b
Test additional artifact manifest cases
brackendawson Feb 2, 2023
2f078b7
Remove stutter from artifact filenames
brackendawson Feb 2, 2023
d9b2869
Remove duplication of manifest.Unversioned
brackendawson Feb 2, 2023
35c22d3
Remove unlinker for now
brackendawson Feb 2, 2023
27b6905
Document the reason for unversioned
brackendawson Feb 2, 2023
db324a4
Algorithm in referrer link path is a parameter
brackendawson Feb 2, 2023
35eae60
Include a test for an artifact with blobs
brackendawson Feb 2, 2023
eb2f108
OCI manifest verification error should be generic
brackendawson Feb 2, 2023
3b1947a
Support OCI Image Manifests with subject property
brackendawson Feb 2, 2023
27a9632
Changes for OCI 1.1RC3
brackendawson May 3, 2023
a883f79
Remove duplicated text from comment
brackendawson May 23, 2023
28e95f7
OCI image-spec v1.1RC3 deprecated non-dist layers
brackendawson May 23, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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.0.2
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
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM=
github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
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=
Expand Down
36 changes: 36 additions & 0 deletions manifest/ocischema/manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,17 @@ 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"`

// Subject is the descriptor of a manifest referred to by this manifest.
Subject *distribution.Descriptor `json:"subject,omitempty"`

// Annotations contains arbitrary metadata for the image manifest.
Annotations map[string]string `json:"annotations,omitempty"`
}
Expand Down Expand Up @@ -103,6 +110,21 @@ func (m *DeserializedManifest) UnmarshalJSON(b []byte) error {
v1.MediaTypeImageManifest, mfst.MediaType)
}

if mfst.Config.MediaType == v1.MediaTypeScratch && mfst.ArtifactType == &q E864 uot;" {
return fmt.Errorf("if config.mediaType is '%s' then artifactType must be set", v1.MediaTypeScratch)
}

// The subject if specified 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
Expand All @@ -124,6 +146,20 @@ func (m DeserializedManifest) Payload() (string, []byte, error) {
return v1.MediaTypeImageManifest, m.canonical, nil
}

// 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 the manifest
func (m *DeserializedManifest) Type() string {
if m.ArtifactType == "" {
return m.Config.MediaType
}
return m.ArtifactType
}

// unknownDocument represents a manifest, manifest list, or index that has not
// yet been validated
type unknownDocument struct {
Expand Down
197 changes: 197 additions & 0 deletions manifest/ocischema/manifest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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)
}
})
}
}
18 changes: 18 additions & 0 deletions manifests.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,18 @@ type Manifest interface {
Payload() (mediaType string, payload []byte, err error)
}

// Referrer represents a Manifest which can refer to a subject.
type Referrer interface {
// Subject returns a pointer to a Descriptor representing the manifest which
// this manifest refers to or nil if this manifest does not refer to a
// subject.
Subject() *Descriptor

// Type returns the type of the referrer if there is one, otherwise it
// returns empty string
Type() string
}

// ManifestBuilder creates a manifest allowing one to include dependencies.
// Instances can be obtained from a version-specific manifest package. Manifest
// specific data is passed into the function which creates the builder.
Expand Down Expand Up @@ -84,6 +96,12 @@ func ManifestMediaTypes() (mediaTypes []string) {
return
}

// ManifestMediaTypeSupported returns true if the given mediaType is supported.
func ManifestMediaTypeSupported(mediaType string) bool {
_, ok := mappings[mediaType]
return ok
}

// UnmarshalFunc implements manifest unmarshalling a given MediaType
type UnmarshalFunc func([]byte) (Manifest, Descriptor, error)

Expand Down
10 changes: 10 additions & 0 deletions registry/api/v2/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,4 +155,14 @@ var (
the maximum allowed.`,
HTTPStatusCode: http.StatusBadRequest,
})

// ErrorCodeManifestNotAcceptable is returned when the manifest found is not
// acceptable according to the client's Accept header
ErrorCodeManifestNotAcceptable = errcode.Register(errGroup, errcode.ErrorDescriptor{
Value: "MANIFEST_NOT_ACCEPTABLE",
Message: "manifest does not match Accept header",
Description: `This is returned if the manifest known to the registry
has a different mediaType then the client's Accept header.`,
HTTPStatusCode: http.StatusNotAcceptable,
})
)
Loading
0