mirror of
https://github.com/distribution/distribution.git
synced 2025-08-15 21:44:13 +00:00
Merge a029881776
into e827ce2772
This commit is contained in:
commit
5610c12b09
@ -59,9 +59,16 @@ type ImageIndex struct {
|
|||||||
// MediaType is the media type of this schema.
|
// MediaType is the media type of this schema.
|
||||||
MediaType string `json:"mediaType,omitempty"`
|
MediaType string `json:"mediaType,omitempty"`
|
||||||
|
|
||||||
|
// ArtifactType is the type of an artifact when the manifest is used for an
|
||||||
|
// artifact.
|
||||||
|
ArtifactType string `json:"artifactType,omitempty"`
|
||||||
|
|
||||||
// Manifests references a list of manifests
|
// Manifests references a list of manifests
|
||||||
Manifests []v1.Descriptor `json:"manifests"`
|
Manifests []v1.Descriptor `json:"manifests"`
|
||||||
|
|
||||||
|
// Subject is the descriptor of a manifest referred to by this manifest.
|
||||||
|
Subject *v1.Descriptor `json:"subject,omitempty"`
|
||||||
|
|
||||||
// Annotations is an optional field that contains arbitrary metadata for the
|
// Annotations is an optional field that contains arbitrary metadata for the
|
||||||
// image index
|
// image index
|
||||||
Annotations map[string]string `json:"annotations,omitempty"`
|
Annotations map[string]string `json:"annotations,omitempty"`
|
||||||
@ -87,17 +94,22 @@ type DeserializedImageIndex struct {
|
|||||||
// and its JSON representation. If annotations is nil or empty then the
|
// and its JSON representation. If annotations is nil or empty then the
|
||||||
// annotations property will be omitted from the JSON representation.
|
// annotations property will be omitted from the JSON representation.
|
||||||
func FromDescriptors(descriptors []v1.Descriptor, annotations map[string]string) (*DeserializedImageIndex, error) {
|
func FromDescriptors(descriptors []v1.Descriptor, annotations map[string]string) (*DeserializedImageIndex, error) {
|
||||||
return fromDescriptorsWithMediaType(descriptors, annotations, v1.MediaTypeImageIndex)
|
return fromDescriptorsWithMediaType(descriptors, nil, annotations, v1.MediaTypeImageIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
// fromDescriptorsWithMediaType is for testing purposes, it's useful to be able to specify the media type explicitly
|
// fromDescriptorsWithMediaType is for testing purposes, it's useful to be able to specify the media type explicitly
|
||||||
func fromDescriptorsWithMediaType(descriptors []v1.Descriptor, annotations map[string]string, mediaType string) (_ *DeserializedImageIndex, err error) {
|
func fromDescriptorsWithMediaType(descriptors []v1.Descriptor, subject *v1.Descriptor, annotations map[string]string, mediaType string) (_ *DeserializedImageIndex, err error) {
|
||||||
m := ImageIndex{
|
m := ImageIndex{
|
||||||
Versioned: specs.Versioned{SchemaVersion: 2},
|
Versioned: specs.Versioned{SchemaVersion: 2},
|
||||||
MediaType: mediaType,
|
MediaType: mediaType,
|
||||||
|
Subject: subject,
|
||||||
Annotations: annotations,
|
Annotations: annotations,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if subject != nil {
|
||||||
|
m.ArtifactType = "application/text"
|
||||||
|
}
|
||||||
|
|
||||||
m.Manifests = make([]v1.Descriptor, len(descriptors))
|
m.Manifests = make([]v1.Descriptor, len(descriptors))
|
||||||
copy(m.Manifests, descriptors)
|
copy(m.Manifests, descriptors)
|
||||||
|
|
||||||
@ -162,3 +174,17 @@ func validateIndex(b []byte) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Subject returns a pointer to the subject of this manifest or nil if there is
|
||||||
|
// none
|
||||||
|
func (m *DeserializedImageIndex) Subject() *distribution.Descriptor {
|
||||||
|
return m.ImageIndex.Subject
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the artifactType of the manifest
|
||||||
|
func (m *DeserializedImageIndex) Type() string {
|
||||||
|
if m.ArtifactType == "" {
|
||||||
|
return m.ImageIndex.MediaType
|
||||||
|
}
|
||||||
|
return m.ArtifactType
|
||||||
|
}
|
||||||
|
@ -85,7 +85,7 @@ func makeTestOCIImageIndex(t *testing.T, mediaType string) ([]v1.Descriptor, *De
|
|||||||
"com.example.locale": "en_GB",
|
"com.example.locale": "en_GB",
|
||||||
}
|
}
|
||||||
|
|
||||||
deserialized, err := fromDescriptorsWithMediaType(manifestDescriptors, annotations, mediaType)
|
deserialized, err := fromDescriptorsWithMediaType(manifestDescriptors, nil, annotations, mediaType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("error creating DeserializedManifestList: %v", err)
|
t.Fatalf("error creating DeserializedManifestList: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -58,10 +58,17 @@ type Manifest struct {
|
|||||||
// Config references the image configuration as a blob.
|
// Config references the image configuration as a blob.
|
||||||
Config v1.Descriptor `json:"config"`
|
Config v1.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
|
// Layers lists descriptors for the layers referenced by the
|
||||||
// configuration.
|
// configuration.
|
||||||
Layers []v1.Descriptor `json:"layers"`
|
Layers []v1.Descriptor `json:"layers"`
|
||||||
|
|
||||||
|
// Subject is the descriptor of a manifest referred to by this manifest.
|
||||||
|
Subject *v1.Descriptor `json:"subject,omitempty"`
|
||||||
|
|
||||||
// Annotations contains arbitrary metadata for the image manifest.
|
// Annotations contains arbitrary metadata for the image manifest.
|
||||||
Annotations map[string]string `json:"annotations,omitempty"`
|
Annotations map[string]string `json:"annotations,omitempty"`
|
||||||
}
|
}
|
||||||
@ -116,6 +123,21 @@ func (m *DeserializedManifest) UnmarshalJSON(b []byte) error {
|
|||||||
v1.MediaTypeImageManifest, mfst.MediaType)
|
v1.MediaTypeImageManifest, mfst.MediaType)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if mfst.Config.MediaType == v1.MediaTypeEmptyJSON && mfst.ArtifactType == "" {
|
||||||
|
return fmt.Errorf("if config.mediaType is '%s' then artifactType must be set", v1.MediaTypeEmptyJSON)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
m.Manifest = mfst
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -137,6 +159,20 @@ func (m *DeserializedManifest) Payload() (string, []byte, error) {
|
|||||||
return v1.MediaTypeImageManifest, m.canonical, nil
|
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
|
||||||
|
}
|
||||||
|
|
||||||
// validateManifest returns an error if the byte slice is invalid JSON or if it
|
// validateManifest returns an error if the byte slice is invalid JSON or if it
|
||||||
// contains fields that belong to a index
|
// contains fields that belong to a index
|
||||||
func validateManifest(b []byte) error {
|
func validateManifest(b []byte) error {
|
||||||
|
@ -6,6 +6,8 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/distribution/distribution/v3/manifest/schema2"
|
||||||
|
|
||||||
"github.com/distribution/distribution/v3"
|
"github.com/distribution/distribution/v3"
|
||||||
"github.com/distribution/distribution/v3/manifest/manifestlist"
|
"github.com/distribution/distribution/v3/manifest/manifestlist"
|
||||||
"github.com/opencontainers/go-digest"
|
"github.com/opencontainers/go-digest"
|
||||||
@ -39,6 +41,14 @@ const expectedManifestSerialization = `{
|
|||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
|
var (
|
||||||
|
emptyJsonDescriptor = distribution.Descriptor{
|
||||||
|
MediaType: v1.DescriptorEmptyJSON.MediaType,
|
||||||
|
Size: v1.DescriptorEmptyJSON.Size,
|
||||||
|
Digest: v1.DescriptorEmptyJSON.Digest,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
func makeTestManifest(mediaType string) Manifest {
|
func makeTestManifest(mediaType string) Manifest {
|
||||||
return Manifest{
|
return Manifest{
|
||||||
Versioned: specs.Versioned{SchemaVersion: 2},
|
Versioned: specs.Versioned{SchemaVersion: 2},
|
||||||
@ -243,3 +253,210 @@ 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: specs.Versioned{
|
||||||
|
SchemaVersion: 2,
|
||||||
|
},
|
||||||
|
MediaType: v1.MediaTypeImageManifest,
|
||||||
|
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: specs.Versioned{
|
||||||
|
SchemaVersion: 2,
|
||||||
|
},
|
||||||
|
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: specs.Versioned{
|
||||||
|
SchemaVersion: 2,
|
||||||
|
},
|
||||||
|
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: specs.Versioned{
|
||||||
|
SchemaVersion: 2,
|
||||||
|
},
|
||||||
|
ArtifactType: "application/vnd.example.catgif",
|
||||||
|
Config: emptyJsonDescriptor,
|
||||||
|
Layers: []distribution.Descriptor{
|
||||||
|
{
|
||||||
|
MediaType: "image/gif",
|
||||||
|
Size: 23423,
|
||||||
|
Digest: "sha256:ff1b4a27562d8ffc821b4d7368818ad7c759cfc2068b7adf0d2712315d67359a",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectValid: true,
|
||||||
|
expectedArtifactType: "application/vnd.example.catgif",
|
||||||
|
},
|
||||||
|
"invalid_artifact": {
|
||||||
|
manifest: Manifest{
|
||||||
|
Versioned: specs.Versioned{
|
||||||
|
SchemaVersion: 2,
|
||||||
|
},
|
||||||
|
Config: emptyJsonDescriptor, // This MUST have an artifactType
|
||||||
|
Layers: []distribution.Descriptor{
|
||||||
|
{
|
||||||
|
MediaType: "image/gif",
|
||||||
|
Size: 23423,
|
||||||
|
Digest: "sha256:ff1b4a27562d8ffc821b4d7368818ad7c759cfc2068b7adf0d2712315d67359a",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectValid: false,
|
||||||
|
},
|
||||||
|
"annotation_artifact": {
|
||||||
|
manifest: Manifest{
|
||||||
|
Versioned: specs.Versioned{
|
||||||
|
SchemaVersion: 2.,
|
||||||
|
},
|
||||||
|
ArtifactType: "application/vnd.example.comment",
|
||||||
|
Config: emptyJsonDescriptor,
|
||||||
|
Layers: []distribution.Descriptor{
|
||||||
|
emptyJsonDescriptor,
|
||||||
|
},
|
||||||
|
Annotations: map[string]string{
|
||||||
|
"com.example.data": "payload",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectValid: true,
|
||||||
|
expectedArtifactType: "application/vnd.example.comment",
|
||||||
|
},
|
||||||
|
"valid_subject": {
|
||||||
|
manifest: Manifest{
|
||||||
|
Versioned: specs.Versioned{
|
||||||
|
SchemaVersion: 2,
|
||||||
|
},
|
||||||
|
ArtifactType: "application/vnd.example.comment",
|
||||||
|
Config: emptyJsonDescriptor,
|
||||||
|
Layers: []distribution.Descriptor{
|
||||||
|
emptyJsonDescriptor,
|
||||||
|
},
|
||||||
|
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: specs.Versioned{
|
||||||
|
SchemaVersion: 2,
|
||||||
|
},
|
||||||
|
ArtifactType: "application/vnd.example.comment",
|
||||||
|
Config: emptyJsonDescriptor,
|
||||||
|
Layers: []distribution.Descriptor{
|
||||||
|
emptyJsonDescriptor,
|
||||||
|
},
|
||||||
|
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: specs.Versioned{
|
||||||
|
SchemaVersion: 2,
|
||||||
|
},
|
||||||
|
ArtifactType: "application/vnd.example.comment",
|
||||||
|
Config: emptyJsonDescriptor,
|
||||||
|
Layers: []distribution.Descriptor{
|
||||||
|
emptyJsonDescriptor,
|
||||||
|
},
|
||||||
|
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
manifests.go
18
manifests.go
@ -27,6 +27,18 @@ type Manifest interface {
|
|||||||
Payload() (mediaType string, payload []byte, err error)
|
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
|
||||||
|
}
|
||||||
|
|
||||||
// ManifestService describes operations on manifests.
|
// ManifestService describes operations on manifests.
|
||||||
type ManifestService interface {
|
type ManifestService interface {
|
||||||
// Exists returns true if the manifest exists.
|
// Exists returns true if the manifest exists.
|
||||||
@ -68,6 +80,12 @@ func ManifestMediaTypes() (mediaTypes []string) {
|
|||||||
return
|
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
|
// UnmarshalFunc implements manifest unmarshalling a given MediaType
|
||||||
type UnmarshalFunc func([]byte) (Manifest, v1.Descriptor, error)
|
type UnmarshalFunc func([]byte) (Manifest, v1.Descriptor, error)
|
||||||
|
|
||||||
|
@ -224,6 +224,16 @@ var (
|
|||||||
the maximum allowed.`,
|
the maximum allowed.`,
|
||||||
HTTPStatusCode: http.StatusBadRequest,
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// ErrorCodeManifestNotAcceptable is returned when the manifest found is not
|
||||||
|
// acceptable according to the client's Accept header
|
||||||
|
ErrorCodeManifestNotAcceptable = register(errGroup, 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,
|
||||||
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -19,6 +19,9 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/distribution/distribution/v3/manifest/ocischema"
|
||||||
|
v1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
|
|
||||||
"github.com/distribution/distribution/v3"
|
"github.com/distribution/distribution/v3"
|
||||||
"github.com/distribution/distribution/v3/configuration"
|
"github.com/distribution/distribution/v3/configuration"
|
||||||
"github.com/distribution/distribution/v3/manifest/manifestlist"
|
"github.com/distribution/distribution/v3/manifest/manifestlist"
|
||||||
@ -33,12 +36,18 @@ import (
|
|||||||
"github.com/gorilla/handlers"
|
"github.com/gorilla/handlers"
|
||||||
"github.com/opencontainers/go-digest"
|
"github.com/opencontainers/go-digest"
|
||||||
"github.com/opencontainers/image-spec/specs-go"
|
"github.com/opencontainers/image-spec/specs-go"
|
||||||
v1 "github.com/opencontainers/image-spec/specs-go/v1"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var headerConfig = http.Header{
|
var (
|
||||||
"X-Content-Type-Options": []string{"nosniff"},
|
headerConfig = http.Header{
|
||||||
}
|
"X-Content-Type-Options": []string{"nosniff"},
|
||||||
|
}
|
||||||
|
emptyJsonDescriptor = distribution.Descriptor{
|
||||||
|
MediaType: v1.DescriptorEmptyJSON.MediaType,
|
||||||
|
Size: v1.DescriptorEmptyJSON.Size,
|
||||||
|
Digest: v1.DescriptorEmptyJSON.Digest,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// digestSha256EmptyTar is the canonical sha256 digest of empty data
|
// digestSha256EmptyTar is the canonical sha256 digest of empty data
|
||||||
@ -2856,3 +2865,471 @@ func TestProxyManifestGetByTag(t *testing.T) {
|
|||||||
"Docker-Content-Digest": []string{newDigest.String()},
|
"Docker-Content-Digest": []string{newDigest.String()},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestArtifactManifest(t *testing.T) {
|
||||||
|
for name, test := range map[string]struct {
|
||||||
|
manifest func(*testing.T, *testEnv, reference.Named) distribution.Manifest
|
||||||
|
deleteSubject bool
|
||||||
|
}{
|
||||||
|
// 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, "schema2tag")
|
||||||
|
_, payload, err := args.manifest.Payload()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get subject payload: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pushScratch(t, testEnv, repo)
|
||||||
|
|
||||||
|
manifest, err := ocischema.FromStruct(ocischema.Manifest{
|
||||||
|
Versioned: specs.Versioned{SchemaVersion: 2},
|
||||||
|
ArtifactType: "application/vnd.example.sbom.v1",
|
||||||
|
Config: emptyJsonDescriptor,
|
||||||
|
Subject: &distribution.Descriptor{
|
||||||
|
MediaType: args.mediaType,
|
||||||
|
Digest: args.dgst,
|
||||||
|
Size: int64(len(payload)),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create manifest: %s", err)
|
||||||
|
}
|
||||||
|
return manifest
|
||||||
|
},
|
||||||
|
deleteSubject: true,
|
||||||
|
},
|
||||||
|
// When an OCI Image Manifest with a subject field is PUT before its
|
||||||
|
// subject, the subject's referrers link will be made in advance.
|
||||||
|
"image_manifest_with_subject": {
|
||||||
|
manifest: func(t *testing.T, testEnv *testEnv, repo reference.Named) distribution.Manifest {
|
||||||
|
config, configDigest, 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, configDigest, url, config)
|
||||||
|
manifest, err := ocischema.FromStruct(ocischema.Manifest{
|
||||||
|
Versioned: specs.Versioned{SchemaVersion: 2},
|
||||||
|
Config: distribution.Descriptor{
|
||||||
|
MediaType: v1.MediaTypeImageConfig,
|
||||||
|
Digest: configDigest,
|
||||||
|
},
|
||||||
|
Subject: &distribution.Descriptor{
|
||||||
|
MediaType: v1.MediaTypeImageManifest,
|
||||||
|
Digest: "sha256:ebe054f08821294feee7bc442014fdd38b4836d83781d8ba99d38eb50d0c9d85",
|
||||||
|
Size: 99,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create manifest: %s", err)
|
||||||
|
}
|
||||||
|
return manifest
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
testEnv := newTestEnv(t, true)
|
||||||
|
defer testEnv.Shutdown()
|
||||||
|
|
||||||
|
repo, err := reference.WithName("myrepo/myimage")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to make repo: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
manifest := test.manifest(t, testEnv, repo)
|
||||||
|
|
||||||
|
contentType, payload, err := manifest.Payload()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get raw manifest: %s", err)
|
||||||
|
}
|
||||||
|
ref, err := reference.WithDigest(repo, digest.FromBytes(payload))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to make reference: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
manifestURL, err := testEnv.builder.BuildManifestURL(ref)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to build manifest URL: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodPut, manifestURL, bytes.NewReader(payload))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create artifact PUT request: %s", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", contentType)
|
||||||
|
|
||||||
|
res, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to PUT manifest: %s", err)
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
if res.StatusCode != http.StatusCreated {
|
||||||
|
t.Fatalf("Incorrect status code for manifest PUT: %d, expected: %d", res.StatusCode, http.StatusCreated)
|
||||||
|
}
|
||||||
|
if res.Header.Get("Docker-Content-Digest") != ref.Digest().String() {
|
||||||
|
t.Errorf("Incorrect Docker-Content-Digest header: %q, expected %q", res.Header.Get("Docker-Content-Digest"), ref.Digest().String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(brackendawson): We should now try to get referrers for the subject
|
||||||
|
// (which should also eventually exist for that to work), but those APIs
|
||||||
|
// don't exist yet so for now just check the link was made.
|
||||||
|
referrer, ok := manifest.(distribution.Referrer)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("Manifest should implement distribution.Referrer: %T", manifest)
|
||||||
|
}
|
||||||
|
link, err := testEnv.app.driver.GetContent(context.Background(), fmt.Sprintf("/docker/registry/v2/repositories/%s/_manifests/revisions/sha256/%s/_referrers/_%s/sha256/%s/link",
|
||||||
|
repo.Name(), referrer.Subject().Digest.Hex(), url.QueryEscape(referrer.Type()), ref.Digest().Hex()))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get expected referrers link from subject with error: %s", err)
|
||||||
|
}
|
||||||
|
if string(link) != ref.Digest().String() {
|
||||||
|
t.Errorf("Subject's referrers link has incorrect content:\n%s\nexpected:\n%s", string(link), ref.Digest().String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// When an artifact manifest has been PUT it can be retrieved with GET.
|
||||||
|
location := res.Header.Get("Location")
|
||||||
|
req, err = http.NewRequest(http.MethodGet, location, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create artifact GET request: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err = http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to GET manifest: %s", err)
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
if res.StatusCode != http.StatusNotAcceptable {
|
||||||
|
t.Fatalf("Incorrect status code for manifest GET: %d, expected: %d", res.StatusCode, http.StatusNotAcceptable)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Accept", contentType)
|
||||||
|
res, err = http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to GET manifest: %s", err)
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
if res.StatusCode != http.StatusOK {
|
||||||
|
t.Fatalf("Incorrect status code for manifest GET: %d, expected: %d", res.StatusCode, http.StatusOK)
|
||||||
|
}
|
||||||
|
if res.Header.Get("Content-Type") != contentType {
|
||||||
|
t.Errorf("Incorrect mediaType for manifest GET: %q, expected: %q", res.Header.Get("Content-Type"), contentType)
|
||||||
|
}
|
||||||
|
gotManifest, err := io.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to read manifest GET body: %s", err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(gotManifest, payload) {
|
||||||
|
t.Errorf("Pulled manifest does not match pushed manifest, got:\n%s\nexpected:\n%s", string(gotManifest), string(payload))
|
||||||
|
}
|
||||||
|
|
||||||
|
if test.deleteSubject {
|
||||||
|
// When a subject is deleted, it's referrers link remains
|
||||||
|
subjectRef, err := reference.WithDigest(repo, referrer.Subject().Digest)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to build subject reference: %s", err)
|
||||||
|
}
|
||||||
|
subjectURL, err := testEnv.builder.BuildManifestURL(subjectRef)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Failed to build subject URL: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodDelete, subjectURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create subject DELETE request: %s", err)
|
||||||
|
}
|
||||||
|
res, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to DELETE subject: %s", err)
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
if res.StatusCode != http.StatusAccepted {
|
||||||
|
t.Fatalf("Incorrect status code for subject DELETE: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(brackendawson): We should now try to get referrers for the subject
|
||||||
|
// (which should also eventually exist for that to work), but those APIs
|
||||||
|
// don't exist yet so for now just check the link still exists.
|
||||||
|
link, err := testEnv.app.driver.GetContent(context.Background(), fmt.Sprintf("/docker/registry/v2/repositories/%s/_manifests/revisions/sha256/%s/_referrers/_%s/sha256/%s/link",
|
||||||
|
repo.Name(), referrer.Subject().Digest.Hex(), url.QueryEscape(referrer.Type()), ref.Digest().Hex()))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get expected referrers link from subject with error: %s", err)
|
||||||
|
}
|
||||||
|
if string(link) != ref.Digest().String() {
|
||||||
|
t.Errorf("Subject's referrers link has incorrect content:\n%s\nexpected:\n%s", string(link), ref.Digest().String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// When an artifact manifest is DELETEd then it will not be found if you GET
|
||||||
|
// it. Its subject's referrer link will be left dangling.
|
||||||
|
req, err = http.NewRequest(http.MethodDelete, location, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create artifact DELETE request: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err = http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to DELETE manifest: %s", err)
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
if res.StatusCode != http.StatusAccepted {
|
||||||
|
t.Fatalf("Incorrect status code for manifest DELETE: %d, expected: %d", res.StatusCode, http.StatusAccepted)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err = http.NewRequest(http.MethodGet, location, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create artifact GET request: %s", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Accept", v1.MediaTypeImageManifest)
|
||||||
|
res, err = http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to GET manifest: %s", err)
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
if res.StatusCode != http.StatusNotFound {
|
||||||
|
t.Fatalf("Incorrect status code for manifest GET: %d, expected: %d", res.StatusCode, http.StatusNotFound)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDockerManifestWithSubject(t *testing.T) {
|
||||||
|
// When a docker image manifest containing a "subject" field is uploaded
|
||||||
|
// then no referrer links are made for that invalid subject.
|
||||||
|
testEnv := newTestEnv(t, true)
|
||||||
|
defer testEnv.Shutdown()
|
||||||
|
|
||||||
|
repo, err := reference.WithName("test/repo")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to build repo: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
config, configDigest, err := testutil.CreateRandomTarFile()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to make config blob: %s", err)
|
||||||
|
}
|
||||||
|
url, _ := startPushLayer(t, testEnv, repo)
|
||||||
|
pushLayer(t, testEnv.builder, repo, configDigest, url, config)
|
||||||
|
layer, layerDigest, err := testutil.CreateRandomTarFile()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to make layer blob: %s", err)
|
||||||
|
}
|
||||||
|
url, _ = startPushLayer(t, testEnv, repo)
|
||||||
|
pushLayer(t, testEnv.builder, repo, layerDigest, url, layer)
|
||||||
|
|
||||||
|
manifest, err := schema2.FromStruct(schema2.Manifest{
|
||||||
|
Versioned: specs.Versioned{
|
||||||
|
SchemaVersion: 2,
|
||||||
|
},
|
||||||
|
MediaType: schema2.MediaTypeManifest,
|
||||||
|
Config: distribution.Descriptor{
|
||||||
|
MediaType: schema2.MediaTypeImageConfig,
|
||||||
|
Digest: configDigest,
|
||||||
|
Size: testutil.MustSeekerLen(config),
|
||||||
|
},
|
||||||
|
Layers: []distribution.Descriptor{
|
||||||
|
{
|
||||||
|
MediaType: schema2.MediaTypeLayer,
|
||||||
|
Digest: layerDigest,
|
||||||
|
Size: testutil.MustSeekerLen(layer),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to make base manifest: %s", err)
|
||||||
|
}
|
||||||
|
var manifestFields map[string]interface{}
|
||||||
|
_, payload, err := manifest.Payload()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get base manifest payload: %s", err)
|
||||||
|
}
|
||||||
|
if err = json.Unmarshal(payload, &manifestFields); err != nil {
|
||||||
|
t.Fatalf("Failed to unmarshal base manifest: %s", err)
|
||||||
|
}
|
||||||
|
manifestFields["subject"] = distribution.Descriptor{
|
||||||
|
MediaType: schema2.MediaTypeManifest,
|
||||||
|
Digest: "sha256:118011bef6c697f7107cc0d788664a0f8c7d0316ce8d17673634155f5ecdba39",
|
||||||
|
Size: 56,
|
||||||
|
}
|
||||||
|
rawManifest, _ := json.Marshal(manifestFields)
|
||||||
|
if err = manifest.UnmarshalJSON(rawManifest); err != nil {
|
||||||
|
t.Fatalf("Failed to re-build manifest: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ref, err := reference.WithTag(repo, "latest")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to build reference: %s", err)
|
||||||
|
}
|
||||||
|
if url, err = testEnv.builder.BuildManifestURL(ref); err != nil {
|
||||||
|
t.Fatalf("Failed to build manifest url: %s", err)
|
||||||
|
}
|
||||||
|
res := putManifest(t, "putting manifest", url, schema2.MediaTypeManifest, manifest)
|
||||||
|
if res.StatusCode != http.StatusCreated {
|
||||||
|
t.Fatalf("Incorrect status code from manifest PUT: %d, expected: %d", res.StatusCode, http.StatusCreated)
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
|
||||||
|
// TODO(brackendawson): We should now try to get referrers for the subject
|
||||||
|
// (which should also eventually exist for that to work), but those APIs
|
||||||
|
// don't exist yet so for now just check the link was not made.
|
||||||
|
_, err = testEnv.app.driver.Stat(context.Background(), fmt.Sprintf("/docker/registry/v2/repositories/%s/_manifests/revisions/sha256/%s/_referrers",
|
||||||
|
repo.Name(), res.Header.Get("Docker-Content-Digest")))
|
||||||
|
var expectedErr storagedriver.InvalidPathError
|
||||||
|
if !errors.As(err, &expectedErr) {
|
||||||
|
t.Fatalf("Should have got invalid path error for referrer directory: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestArtifactManifestValidation(t *testing.T) {
|
||||||
|
for name, test := range map[string]struct {
|
||||||
|
config func(*testing.T, *testEnv, reference.Named) distribution.Descriptor
|
||||||
|
layers func(*testing.T, *testEnv, reference.Named) []distribution.Descriptor
|
||||||
|
wantCode int
|
||||||
|
}{
|
||||||
|
"layers_must_exist": {
|
||||||
|
config: func(t *testing.T, testEnv *testEnv, repo reference.Named) distribution.Descriptor {
|
||||||
|
pushScratch(t, testEnv, repo)
|
||||||
|
return emptyJsonDescriptor
|
||||||
|
},
|
||||||
|
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,
|
||||||
|
Digest: "sha256:7688b6ef52555962d008fff894223582c484517cea7da49ee67800adc7fc8866",
|
||||||
|
Size: 56,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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 emptyJsonDescriptor // 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": {
|
||||||
|
config: func(t *testing.T, testEnv *testEnv, repo reference.Named) distribution.Descriptor {
|
||||||
|
pushScratch(t, testEnv, repo)
|
||||||
|
return emptyJsonDescriptor
|
||||||
|
},
|
||||||
|
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: 201,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
testEnv := newTestEnv(t, true)
|
||||||
|
defer testEnv.Shutdown()
|
||||||
|
|
||||||
|
repo, err := reference.WithName("myrepo/myimage")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to make repo: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
manifest, err := ocischema.FromStruct(ocischema.Manifest{
|
||||||
|
Versioned: specs.Versioned{SchemaVersion: 2},
|
||||||
|
Config: test.config(t, testEnv, repo),
|
||||||
|
ArtifactType: "application/vnd.example.sbom.v1",
|
||||||
|
Layers: test.layers(t, testEnv, repo),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to make manifest: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
contentType, payload, err := manifest.Payload()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get raw manifest: %s", err)
|
||||||
|
}
|
||||||
|
ref, err := reference.WithDigest(repo, digest.FromBytes(payload))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to make reference: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
manifestURL, err := testEnv.builder.BuildManifestURL(ref)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to build manifest URL: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodPut, manifestURL, bytes.NewReader(payload))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create artifact PUT request: %s", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", contentType)
|
||||||
|
|
||||||
|
res, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to PUT manifest: %s", err)
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
if res.StatusCode != test.wantCode {
|
||||||
|
t.Fatalf("Incorrect status code for manifest PUT: %d, expected: %d", res.StatusCode, test.wantCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func pushScratch(t *testing.T, testEnv *testEnv, repo reference.Named) {
|
||||||
|
url, _ := startPushLayer(t, testEnv, repo)
|
||||||
|
pushLayer(t, testEnv.builder, repo, v1.DescriptorEmptyJSON.Digest, url, bytes.NewBuffer(v1.DescriptorEmptyJSON.Data))
|
||||||
|
}
|
||||||
|
@ -161,11 +161,11 @@ func (imh *manifestHandler) GetManifest(w http.ResponseWriter, r *http.Request)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if manifestType == ociSchema && !supports[ociSchema] {
|
if manifestType == ociSchema && !supports[ociSchema] {
|
||||||
imh.Errors = append(imh.Errors, errcode.ErrorCodeManifestUnknown.WithMessage("OCI manifest found, but accept header does not support OCI manifests"))
|
imh.Errors = append(imh.Errors, errcode.ErrorCodeManifestNotAcceptable.WithMessage("OCI manifest found, but accept header does not support OCI manifests"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if manifestType == ociImageIndexSchema && !supports[ociImageIndexSchema] {
|
if manifestType == ociImageIndexSchema && !supports[ociImageIndexSchema] {
|
||||||
imh.Errors = append(imh.Errors, errcode.ErrorCodeManifestUnknown.WithMessage("OCI index found, but accept header does not support OCI indexes"))
|
imh.Errors = append(imh.Errors, errcode.ErrorCodeManifestNotAcceptable.WithMessage("OCI index found, but accept header does not support OCI indexes"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -134,9 +134,9 @@ type FileWriter interface {
|
|||||||
// PathRegexp is the regular expression which each file path must match. A
|
// PathRegexp is the regular expression which each file path must match. A
|
||||||
// file path is absolute, beginning with a slash and containing a positive
|
// file path is absolute, beginning with a slash and containing a positive
|
||||||
// number of path components separated by slashes, where each component is
|
// number of path components separated by slashes, where each component is
|
||||||
// restricted to alphanumeric characters or a period, underscore, or
|
// restricted to alphanumeric characters or a period, underscore, hyphen or
|
||||||
// hyphen.
|
// percent.
|
||||||
var PathRegexp = regexp.MustCompile(`^(/[A-Za-z0-9._-]+)+$`)
|
var PathRegexp = regexp.MustCompile(`^(/[A-Za-z0-9._%-]+)+$`)
|
||||||
|
|
||||||
// ErrUnsupportedMethod may be returned in the case where a StorageDriver implementation does not support an optional method.
|
// ErrUnsupportedMethod may be returned in the case where a StorageDriver implementation does not support an optional method.
|
||||||
type ErrUnsupportedMethod struct {
|
type ErrUnsupportedMethod struct {
|
||||||
|
@ -18,6 +18,7 @@ type ocischemaManifestHandler struct {
|
|||||||
blobStore distribution.BlobStore
|
blobStore distribution.BlobStore
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
manifestURLs manifestURLs
|
manifestURLs manifestURLs
|
||||||
|
references ReferenceService
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ ManifestHandler = &ocischemaManifestHandler{}
|
var _ ManifestHandler = &ocischemaManifestHandler{}
|
||||||
@ -45,11 +46,19 @@ func (ms *ocischemaManifestHandler) Put(ctx context.Context, manifest distributi
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
mt, payload, err := m.Payload()
|
mt, payload, err := manifest.Payload()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if referrer, ok := manifest.(distribution.Referrer); ok {
|
||||||
|
if subject := referrer.Subject(); subject != nil {
|
||||||
|
if err := ms.references.Link(ctx, referrer.Type(), digest.FromBytes(payload), subject.Digest); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
revision, err := ms.blobStore.Put(ctx, mt, payload)
|
revision, err := ms.blobStore.Put(ctx, mt, payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
dcontext.GetLogger(ctx).Errorf("error putting payload into blobstore: %v", err)
|
dcontext.GetLogger(ctx).Errorf("error putting payload into blobstore: %v", err)
|
||||||
@ -88,7 +97,7 @@ func (ms *ocischemaManifestHandler) verifyManifest(ctx context.Context, mnfst oc
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch descriptor.MediaType {
|
switch descriptor.MediaType {
|
||||||
case v1.MediaTypeImageLayer, v1.MediaTypeImageLayerGzip, v1.MediaTypeImageLayerNonDistributable, v1.MediaTypeImageLayerNonDistributableGzip: //nolint:staticcheck // ignore A1019: v1.MediaTypeImageLayerNonDistributable is deprecated: Non-distributable layers are deprecated, and not recommended for future use.
|
case v1.MediaTypeImageLayer, v1.MediaTypeImageLayerGzip, v1.MediaTypeImageLayerNonDistributable, v1.MediaTypeImageLayerNonDistributableGzip: //nolint:staticcheck // Ignore SA1019 v1.MediaTypeImageLayerNonDistributable is deprecated, it is used for backwards compatibility
|
||||||
allow := ms.manifestURLs.allow
|
allow := ms.manifestURLs.allow
|
||||||
deny := ms.manifestURLs.deny
|
deny := ms.manifestURLs.deny
|
||||||
for _, u := range descriptor.URLs {
|
for _, u := range descriptor.URLs {
|
||||||
|
@ -36,7 +36,7 @@ func TestVerifyOCIManifestNonDistributableLayer(t *testing.T) {
|
|||||||
nonDistributableLayer := v1.Descriptor{
|
nonDistributableLayer := v1.Descriptor{
|
||||||
Digest: "sha256:463435349086340864309863409683460843608348608934092322395278926a",
|
Digest: "sha256:463435349086340864309863409683460843608348608934092322395278926a",
|
||||||
Size: 6323,
|
Size: 6323,
|
||||||
MediaType: v1.MediaTypeImageLayerNonDistributableGzip, //nolint:staticcheck // ignore A1019: v1.MediaTypeImageLayerNonDistributableGzip is deprecated: Non-distributable layers are deprecated, and not recommended for future use
|
MediaType: v1.MediaTypeImageLayerNonDistributableGzip, //nolint:staticcheck // Ignore SA1019 v1.MediaTypeImageLayerNonDistributable is deprecated, it is used for backwards compatibility
|
||||||
}
|
}
|
||||||
|
|
||||||
emptyLayer := v1.Descriptor{
|
emptyLayer := v1.Descriptor{
|
||||||
|
@ -2,6 +2,7 @@ package storage
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/url"
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@ -114,6 +115,10 @@ const (
|
|||||||
// blobPathSpec: <root>/v2/blobs/<algorithm>/<first two hex bytes of digest>/<hex digest>
|
// blobPathSpec: <root>/v2/blobs/<algorithm>/<first two hex bytes of digest>/<hex digest>
|
||||||
// blobDataPathSpec: <root>/v2/blobs/<algorithm>/<first two hex bytes of digest>/<hex digest>/data
|
// blobDataPathSpec: <root>/v2/blobs/<algorithm>/<first two hex bytes of digest>/<hex digest>/data
|
||||||
//
|
//
|
||||||
|
// Artifacts:
|
||||||
|
//
|
||||||
|
// subjectReferrerPathSpec <root>/v2/repositories/<name>/_manifests/revisions/<algorithm>/<subject hex digest>/_referrers/_<artifactType>/<algorithm>/<referrer hex digest>/link
|
||||||
|
//
|
||||||
// For more information on the semantic meaning of each path and their
|
// For more information on the semantic meaning of each path and their
|
||||||
// contents, please see the path spec documentation.
|
// contents, please see the path spec documentation.
|
||||||
func pathFor(spec pathSpec) (string, error) {
|
func pathFor(spec pathSpec) (string, error) {
|
||||||
@ -250,6 +255,19 @@ func pathFor(spec pathSpec) (string, error) {
|
|||||||
return path.Join(append(repoPrefix, v.name, "_uploads", v.id, "hashstates", string(v.alg), offset)...), nil
|
return path.Join(append(repoPrefix, v.name, "_uploads", v.id, "hashstates", string(v.alg), offset)...), nil
|
||||||
case repositoriesRootPathSpec:
|
case repositoriesRootPathSpec:
|
||||||
return path.Join(repoPrefix...), nil
|
return path.Join(repoPrefix...), nil
|
||||||
|
case subjectReferrerLinkPathSpec:
|
||||||
|
manifestPath, err := pathFor(manifestRevisionPathSpec{name: v.name, revision: v.subject})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
artifactType := "_" + url.QueryEscape(v.mediaType)
|
||||||
|
components, err := digestPathComponents(v.referrer, false)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return path.Join(manifestPath, "_referrers", artifactType, path.Join(components...), "link"), nil
|
||||||
default:
|
default:
|
||||||
// TODO(sday): This is an internal error. Ensure it doesn't escape (panic?).
|
// TODO(sday): This is an internal error. Ensure it doesn't escape (panic?).
|
||||||
return "", fmt.Errorf("unknown path spec: %#v", v)
|
return "", fmt.Errorf("unknown path spec: %#v", v)
|
||||||
@ -450,6 +468,17 @@ type repositoriesRootPathSpec struct{}
|
|||||||
|
|
||||||
func (repositoriesRootPathSpec) pathSpec() {}
|
func (repositoriesRootPathSpec) pathSpec() {}
|
||||||
|
|
||||||
|
// subjectReferrerLinkPathSpec returns the describes the link of a subject to
|
||||||
|
// its referrers
|
||||||
|
type subjectReferrerLinkPathSpec struct {
|
||||||
|
name string
|
||||||
|
referrer digest.Digest
|
||||||
|
subject digest.Digest
|
||||||
|
mediaType string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (subjectReferrerLinkPathSpec) pathSpec() {}
|
||||||
|
|
||||||
// digestPathComponents provides a consistent path breakdown for a given
|
// digestPathComponents provides a consistent path breakdown for a given
|
||||||
// digest. For a generic digest, it will be as follows:
|
// digest. For a generic digest, it will be as follows:
|
||||||
//
|
//
|
||||||
|
35
registry/storage/references.go
Normal file
35
registry/storage/references.go
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/distribution/distribution/v3"
|
||||||
|
"github.com/opencontainers/go-digest"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReferenceService is a service to manage internal links from subjects back to
|
||||||
|
// their referrers.
|
||||||
|
type ReferenceService interface {
|
||||||
|
// Link creates a link from a subject back to a referrer
|
||||||
|
Link(ctx context.Context, mediaType string, referrer, subject digest.Digest) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type referenceHandler struct {
|
||||||
|
*blobStore
|
||||||
|
repository distribution.Repository
|
||||||
|
pathFn func(name, mediaType string, reference, artifact_subject_must_be_manifest digest.Digest) (string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *referenceHandler) Link(ctx context.Context, artifactType string, referrer, subject digest.Digest) error {
|
||||||
|
path, err := r.pathFn(r.repository.Named().Name(), artifactType, referrer, subject)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.blobStore.link(ctx, path, referrer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// subjectReferrerLinkPath provides the path to the subject's referrer link
|
||||||
|
func subjectReferrerLinkPath(name, mediaType string, referrer, subject digest.Digest) (string, error) {
|
||||||
|
return pathFor(subjectReferrerLinkPathSpec{name: name, mediaType: mediaType, referrer: referrer, subject: subject})
|
||||||
|
}
|
@ -305,6 +305,11 @@ func (repo *repository) Manifests(ctx context.Context, options ...distribution.M
|
|||||||
repository: repo,
|
repository: repo,
|
||||||
blobStore: blobStore,
|
blobStore: blobStore,
|
||||||
manifestURLs: repo.registry.manifestURLs,
|
manifestURLs: repo.registry.manifestURLs,
|
||||||
|
references: &referenceHandler{
|
||||||
|
blobStore: repo.blobStore,
|
||||||
|
repository: repo,
|
||||||
|
pathFn: subjectReferrerLinkPath,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
ocischemaIndexHandler: &ocischemaIndexHandler{
|
ocischemaIndexHandler: &ocischemaIndexHandler{
|
||||||
manifestListHandler: manifestListHandler,
|
manifestListHandler: manifestListHandler,
|
||||||
|
@ -114,3 +114,29 @@ func UploadBlobs(repository distribution.Repository, layers map[digest.Digest]io
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SeekerLen returns the apparent size of s
|
||||||
|
func SeekerLen(s io.Seeker) (int64, error) {
|
||||||
|
offset, err := s.Seek(0, io.SeekCurrent)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("failed to read initial offset: %w", err)
|
||||||
|
}
|
||||||
|
size, err := s.Seek(0, io.SeekEnd)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("failed to read size: %w", err)
|
||||||
|
}
|
||||||
|
_, err = s.Seek(offset, io.SeekStart)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("failed to restore initial offset: %w", err)
|
||||||
|
}
|
||||||
|
return size, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustSeekerLen returns the apparent size of s or panics
|
||||||
|
func MustSeekerLen(s io.Seeker) int64 {
|
||||||
|
size, err := SeekerLen(s)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return size
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user