mirror of
https://github.com/distribution/distribution.git
synced 2025-08-15 13:33:25 +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 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 []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
|
||||
// image index
|
||||
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
|
||||
// annotations property will be omitted from the JSON representation.
|
||||
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
|
||||
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{
|
||||
Versioned: specs.Versioned{SchemaVersion: 2},
|
||||
MediaType: mediaType,
|
||||
Subject: subject,
|
||||
Annotations: annotations,
|
||||
}
|
||||
|
||||
if subject != nil {
|
||||
m.ArtifactType = "application/text"
|
||||
}
|
||||
|
||||
m.Manifests = make([]v1.Descriptor, len(descriptors))
|
||||
copy(m.Manifests, descriptors)
|
||||
|
||||
@ -162,3 +174,17 @@ func validateIndex(b []byte) error {
|
||||
}
|
||||
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",
|
||||
}
|
||||
|
||||
deserialized, err := fromDescriptorsWithMediaType(manifestDescriptors, annotations, mediaType)
|
||||
deserialized, err := fromDescriptorsWithMediaType(manifestDescriptors, nil, annotations, mediaType)
|
||||
if err != nil {
|
||||
t.Fatalf("error creating DeserializedManifestList: %v", err)
|
||||
}
|
||||
|
@ -58,10 +58,17 @@ type Manifest struct {
|
||||
// Config references the image configuration as a blob.
|
||||
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
|
||||
// configuration.
|
||||
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 map[string]string `json:"annotations,omitempty"`
|
||||
}
|
||||
@ -116,6 +123,21 @@ func (m *DeserializedManifest) UnmarshalJSON(b []byte) error {
|
||||
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
|
||||
|
||||
return nil
|
||||
@ -137,6 +159,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
|
||||
}
|
||||
|
||||
// validateManifest returns an error if the byte slice is invalid JSON or if it
|
||||
// contains fields that belong to a index
|
||||
func validateManifest(b []byte) error {
|
||||
|
@ -6,6 +6,8 @@ import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/distribution/distribution/v3/manifest/schema2"
|
||||
|
||||
"github.com/distribution/distribution/v3"
|
||||
"github.com/distribution/distribution/v3/manifest/manifestlist"
|
||||
"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 {
|
||||
return Manifest{
|
||||
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)
|
||||
}
|
||||
|
||||
// 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.
|
||||
type ManifestService interface {
|
||||
// Exists returns true if the manifest exists.
|
||||
@ -68,6 +80,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, v1.Descriptor, error)
|
||||
|
||||
|
@ -224,6 +224,16 @@ 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 = 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 (
|
||||
|
@ -19,6 +19,9 @@ import (
|
||||
"strings"
|
||||
"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/configuration"
|
||||
"github.com/distribution/distribution/v3/manifest/manifestlist"
|
||||
@ -33,12 +36,18 @@ import (
|
||||
"github.com/gorilla/handlers"
|
||||
"github.com/opencontainers/go-digest"
|
||||
"github.com/opencontainers/image-spec/specs-go"
|
||||
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"},
|
||||
}
|
||||
emptyJsonDescriptor = distribution.Descriptor{
|
||||
MediaType: v1.DescriptorEmptyJSON.MediaType,
|
||||
Size: v1.DescriptorEmptyJSON.Size,
|
||||
Digest: v1.DescriptorEmptyJSON.Digest,
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
// digestSha256EmptyTar is the canonical sha256 digest of empty data
|
||||
@ -2856,3 +2865,471 @@ func TestProxyManifestGetByTag(t *testing.T) {
|
||||
"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] {
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -134,9 +134,9 @@ type FileWriter interface {
|
||||
// PathRegexp is the regular expression which each file path must match. A
|
||||
// file path is absolute, beginning with a slash and containing a positive
|
||||
// number of path components separated by slashes, where each component is
|
||||
// restricted to alphanumeric characters or a period, underscore, or
|
||||
// hyphen.
|
||||
var PathRegexp = regexp.MustCompile(`^(/[A-Za-z0-9._-]+)+$`)
|
||||
// restricted to alphanumeric characters or a period, underscore, hyphen or
|
||||
// percent.
|
||||
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.
|
||||
type ErrUnsupportedMethod struct {
|
||||
|
@ -18,6 +18,7 @@ type ocischemaManifestHandler struct {
|
||||
blobStore distribution.BlobStore
|
||||
ctx context.Context
|
||||
manifestURLs manifestURLs
|
||||
references ReferenceService
|
||||
}
|
||||
|
||||
var _ ManifestHandler = &ocischemaManifestHandler{}
|
||||
@ -45,11 +46,19 @@ func (ms *ocischemaManifestHandler) Put(ctx context.Context, manifest distributi
|
||||
return "", err
|
||||
}
|
||||
|
||||
mt, payload, err := m.Payload()
|
||||
mt, payload, err := manifest.Payload()
|
||||
if err != nil {
|
||||
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)
|
||||
if err != nil {
|
||||
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 {
|
||||
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
|
||||
deny := ms.manifestURLs.deny
|
||||
for _, u := range descriptor.URLs {
|
||||
|
@ -36,7 +36,7 @@ func TestVerifyOCIManifestNonDistributableLayer(t *testing.T) {
|
||||
nonDistributableLayer := v1.Descriptor{
|
||||
Digest: "sha256:463435349086340864309863409683460843608348608934092322395278926a",
|
||||
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{
|
||||
|
@ -2,6 +2,7 @@ package storage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
@ -114,6 +115,10 @@ const (
|
||||
// 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
|
||||
//
|
||||
// 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
|
||||
// contents, please see the path spec documentation.
|
||||
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
|
||||
case repositoriesRootPathSpec:
|
||||
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:
|
||||
// TODO(sday): This is an internal error. Ensure it doesn't escape (panic?).
|
||||
return "", fmt.Errorf("unknown path spec: %#v", v)
|
||||
@ -450,6 +468,17 @@ type repositoriesRootPathSpec struct{}
|
||||
|
||||
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
|
||||
// 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,
|
||||
blobStore: blobStore,
|
||||
manifestURLs: repo.registry.manifestURLs,
|
||||
references: &referenceHandler{
|
||||
blobStore: repo.blobStore,
|
||||
repository: repo,
|
||||
pathFn: subjectReferrerLinkPath,
|
||||
},
|
||||
},
|
||||
ocischemaIndexHandler: &ocischemaIndexHandler{
|
||||
manifestListHandler: manifestListHandler,
|
||||
|
@ -114,3 +114,29 @@ func UploadBlobs(repository distribution.Repository, layers map[digest.Digest]io
|
||||
}
|
||||
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