diff --git a/.github/workflows/oras-release.yml b/.github/workflows/oras-release.yml
new file mode 100644
index 00000000000..bf95829d7e1
--- /dev/null
+++ b/.github/workflows/oras-release.yml
@@ -0,0 +1,59 @@
+name: Release oras-project/registry container image
+
+on:
+ push:
+ tags:
+ - "v[0-9]+.[0-9]+.[0-9]+-alpha"
+
+jobs:
+ publish:
+ name: Build and publish container image
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: write
+ env:
+ REGISTRY: ghcr.io
+ REPOSITORY: ${{ format('{0}/registry', github.repository_owner) }}
+ DOCKER_BUILDTAGS: "include_oss include_gcs"
+ CGO_ENABLED: 1
+ GO111MODULE: "auto"
+ GOPATH: ${{ github.workspace }}
+ GOOS: linux
+ COMMIT_RANGE: ${{ github.event_name == 'pull_request' && format('{0}..{1}',github.event.pull_request.base.sha, github.event.pull_request.head.sha) || format('{0}..{1}', github.event.before, github.event.after) }}
+
+ steps:
+ - name: Get git tag
+ id: get_git_tag
+ run: echo ::set-output name=git_tag::${GITHUB_REF#refs/tags/}
+
+ - name: Check out source code
+ if: ${{ success() }}
+ uses: actions/checkout@v2
+ with:
+ ref: ${{ steps.get_git_tag.outputs.git_tag }}
+
+ - name: Set docker image tag
+ env:
+ GIT_TAG: ${{ steps.get_git_tag.outputs.git_tag }}
+ id: get_image_tag
+ run: echo ::set-output name=docker_tag::${GIT_TAG}
+
+ - name: Login to GitHub Container Registry
+ uses: docker/login-action@v1
+ with:
+ registry: ${{ env.REGISTRY }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Build and push
+ if: ${{ success() }}
+ uses: docker/build-push-action@v2
+ with:
+ context: .
+ file: ./Dockerfile
+ platforms: linux/amd64
+ push: true
+ tags: |
+ ${{ env.REGISTRY }}/${{ env.REPOSITORY }}:${{ steps.get_image_tag.outputs.docker_tag }}
+ ${{ env.REGISTRY }}/${{ env.REPOSITORY }}:latest
diff --git a/README.md b/README.md
index 3af0093ec49..89a1b2f4d48 100644
--- a/README.md
+++ b/README.md
@@ -1,73 +1,160 @@
# Distribution
-The toolset to pack, ship, store, and deliver content.
-
-This repository's main product is the Open Source Registry implementation
-for storing and distributing container images using the
-[OCI Distribution Specification](https://github.com/opencontainers/distribution-spec).
-The goal of this project is to provide a simple, secure, and scalable base
-for building a large scale registry solution or running a simple private registry.
-It is a core library for many registry operators including Docker Hub, GitHub Container Registry,
-GitLab Container Registry and DigitalOcean Container Registry, as well as the CNCF Harbor
-Project, and VMware Harbor Registry.
-
-
-
-[](https://github.com/distribution/distribution/actions?query=workflow%3ACI)
-[](https://pkg.go.dev/github.com/distribution/distribution)
-[](LICENSE)
-[](https://codecov.io/gh/distribution/distribution)
-[](https://app.fossa.com/projects/custom%2B162%2Fgithub.com%2Fdistribution%2Fdistribution?ref=badge_shield)
-[](https://github.com/distribution/distribution/actions?query=workflow%3Aconformance)
-
-This repository contains the following components:
-
-|**Component** |Description |
-|--------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| **registry** | An implementation of the [OCI Distribution Specification](https://github.com/opencontainers/distribution-spec). |
-| **libraries** | A rich set of libraries for interacting with distribution components. Please see [godoc](https://pkg.go.dev/github.com/distribution/distribution) for details. **Note**: The interfaces for these libraries are **unstable**. |
-| **documentation** | Docker's full documentation set is available at [docs.docker.com](https://docs.docker.com). This repository [contains the subset](docs/) related just to the registry. |
-
-### How does this integrate with Docker, containerd, and other OCI client?
-
-Clients implement against the OCI specification and communicate with the
-registry using HTTP. This project contains a client implementation which
-is currently in use by Docker, however, it is deprecated for the
-[implementation in containerd](https://github.com/containerd/containerd/tree/master/remotes/docker)
-and will not support new features.
-
-### What are the long term goals of the Distribution project?
-
-The _Distribution_ project has the further long term goal of providing a
-secure tool chain for distributing content. The specifications, APIs and tools
-should be as useful with Docker as they are without.
-
-Our goal is to design a professional grade and extensible content distribution
-system that allow users to:
-
-* Enjoy an efficient, secured and reliable way to store, manage, package and
- exchange content
-* Hack/roll their own on top of healthy open-source components
-* Implement their own home made solution through good specs, and solid
- extensions mechanism.
-
-## Contribution
-
-Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to contribute
-issues, fixes, and patches to this project. If you are contributing code, see
-the instructions for [building a development environment](BUILDING.md).
-
-## Communication
-
-For async communication and long running discussions please use issues and pull requests on the github repo.
-This will be the best place to discuss design and implementation.
-
-For sync communication we have a #distribution channel in the [CNCF Slack](https://slack.cncf.io/)
-that everyone is welcome to join and chat about development.
-
-## Licenses
-
-The distribution codebase is released under the [Apache 2.0 license](LICENSE).
-The README.md file, and files in the "docs" folder are licensed under the
-Creative Commons Attribution 4.0 International License. You may obtain a
-copy of the license, titled CC-BY-4.0, at http://creativecommons.org/licenses/by/4.0/.
+This fork of [distribution/distribution](distribution-distribution) provides
+an experimental implementation of [reference types](reference-types).
+
+Features supported:
+
+- :heavy_check_mark: PUT ORAS Artifact Manifest
+- :heavy_check_mark: GET ORAS Artifact Manifest
+- :heavy_check_mark: LIST referrers
+ - [ ] Pagination support
+- [ ] Garbage Collection of reference types
+
+To power the `/referrers` API, the implementation creates and uses an index.
+See [referrers.md](docs/referrers.md) for details.
+
+## Usage - Push, Discover, Pull
+
+The following steps illustrate how ORAS artifacts can be stored and retrieved
+from a registry. The artifact in this example is a Notary V2
+[signature](signature).
+
+### Prerequisites
+
+- Local registry prototype instance
+- [docker-generate](https://github.com/shizhMSFT/docker-generate)
+- [nv2](https://github.com/notaryproject/nv2)
+- `curl`
+- `jq`
+
+### Push an image to your registry
+
+```shell
+# Initialize local registry variables
+regIp="127.0.0.1" && \
+ regPort="5000" && \
+ registry="$regIp:$regPort" && \
+ repo="busybox" && \
+ tag="latest" && \
+ image="$repo:$tag" && \
+ reference="$registry/$image"
+
+# Pull an image from docker hub and push to local registry
+docker pull $image && \
+ docker tag $image $reference && \
+ docker push $reference
+```
+
+### Generate image manifest and sign it
+
+```shell
+# Generate self-signed certificates
+openssl req \
+ -x509 \
+ -sha256 \
+ -nodes \
+ -newkey rsa:2048 \
+ -days 365 \
+ -subj "/CN=$regIp/O=example inc/C=IN/ST=Haryana/L=Gurgaon" \
+ -addext "subjectAltName=IP:$regIp" \
+ -keyout example.key \
+ -out example.crt
+
+# Generate image manifest
+manifestFile="manifest-to-sign.json" && \
+ docker generate manifest $image > $manifestFile
+
+# Sign manifest
+signatureFile="manifest-signature.jwt" && \
+ nv2 sign --method x509 \
+ -k example.key \
+ -c example.crt \
+ -r $reference \
+ -o $signatureFile \
+ file:$manifestFile
+```
+
+### Obtain manifest and signature digests
+
+```shell
+manifestDigest="sha256:$(sha256sum $manifestFile | cut -d " " -f 1)" && \
+ signatureDigest="sha256:$(sha256sum $signatureFile | cut -d " " -f 1)"
+```
+
+### Create an Artifact file referencing the manifest that was signed and its signature as blob
+
+```shell
+artifactFile="artifact.json" && \
+ artifactMediaType="application/vnd.cncf.oras.artifact.manifest.v1+json" && \
+ artifactType="application/vnd.cncf.notary.v2" && \
+ signatureMediaType="application/vnd.cncf.notary.signature.v2+jwt" && \
+ signatureFileSize=`wc -c < $signatureFile` && \
+ manifestMediaType="$(cat $manifestFile | jq -r '.mediaType')" && \
+ manifestFileSize=`wc -c < $manifestFile`
+
+cat < $artifactFile
+{
+ "mediaType": "$artifactMediaType",
+ "artifactType": "$artifactType",
+ "blobs": [
+ {
+ "mediaType": "$signatureMediaType",
+ "digest": "$signatureDigest",
+ "size": $signatureFileSize
+ }
+ ],
+ "subject": {
+ "mediaType": "$manifestMediaType",
+ "digest": "$manifestDigest",
+ "size": $manifestFileSize
+ }
+}
+EOF
+```
+
+### Obtain artifact digest
+
+```shell
+artifactDigest="sha256:$(sha256sum $artifactFile | cut -d " " -f 1)"
+```
+
+### Push signature and artifact
+
+```shell
+# Initiate blob upload and obtain PUT location
+blobPutLocation=`curl -I -X POST -s http://$registry/v2/$repo/blobs/uploads/ | grep "Location: " | sed -e "s/Location: //;s/$/\&digest=$signatureDigest/;s/\r//"`
+
+# Push signature blob
+curl -X PUT -H "Content-Type: application/octet-stream" --data-binary @"$signatureFile" $blobPutLocation
+
+# Push artifact
+curl -X PUT --data-binary @"$artifactFile" -H "Content-Type: $artifactMediaType" "http://$registry/v2/$repo/manifests/$artifactDigest"
+```
+
+### List referrers
+
+```shell
+# Retrieve referrers
+curl -s "http://$registry/oras/artifacts/v1/$repo/manifests/$manifestDigest/referrers?artifactType=$artifactType" | jq
+```
+
+### Verify signature
+
+```shell
+# Retrieve signature
+artifactDigest=`curl -s "http://$registry/oras/artifacts/v1/$repo/manifests/$manifestDigest/referrers?artifactType=$artifactType" | jq -r '.references[0].digest'` && \
+ signatureDigest=`curl -s "http://$registry/oras/artifacts/v1/$repo/manifests/$artifactDigest" | jq -r '.blobs[0].digest'` && \
+ retrievedSignatureFile="retrieved-signature.json" && \
+ curl -s http://$registry/v2/$repo/blobs/$signatureDigest > $retrievedSignatureFile
+
+# Verify signature
+nv2 verify \
+ -f $retrievedSignatureFile \
+ -c example.crt \
+ file:$manifestFile
+```
+
+[distribution-distribution]: https://github.com/distribution/distribution
+[reference-types]: https://github.com/oras-project/artifacts-spec
+[signature]: https://github.com/notaryproject/nv2/tree/prototype-2/docs/nv2
diff --git a/docs/referrers.md b/docs/referrers.md
new file mode 100644
index 00000000000..58ac9e8e2ce
--- /dev/null
+++ b/docs/referrers.md
@@ -0,0 +1,95 @@
+[[__TOC__]]
+
+# ORAS Artifacts Distribution
+
+This document describes an experimental prototype that implements the
+[ORAS Artifact Manifest](https://github.com/oras-project/artifacts-spec) spec.
+
+## Implementation
+
+To power the [/referrers](https://github.com/oras-project/artifacts-spec/blob/main/manifest-referrers-api.md) API, the
+referrers of a manifest are indexed in the repository store. The following example illustrates the creation of this
+index.
+
+The `nginx:v1` image is already persisted:
+
+- repository: `nginx`
+- digest: `sha256:111ma2d22ae5ef400769fa51c84717264cd1520ac8d93dc071374c1be49a111m`
+- tag: `v1.0`
+
+The repository store layout is represented as:
+
+```bash
+
+└── v2
+ └── repositories
+ └── nginx
+ └── _manifests
+ └── revisions
+ └── sha256
+ └── 111ma2d22ae5ef400769fa51c84717264cd1520ac8d93dc071374c1be49a111m
+ └── link
+```
+
+Push a signature as blob and an ORAS Artifact that contains a blobs property referencing the signature, with the
+following properties:
+
+- digest: `sha256:222ibbf80b44ce6be8234e6ff90a1ac34acbeb826903b02cfa0da11c82cb222i`
+- `subjectManifest` digest: `sha256:111ma2d22ae5ef400769fa51c84717264cd1520ac8d93dc071374c1be49a111m`
+- `artifactType`: `application/vnd.example.artifact`
+
+On `PUT`, the artifact appears as a manifest revision. Additionally, an index entry is created under
+the subject to facilitate a lookup to the referrer. The index path where the entry is added is
+`/ref/`, as shown below.
+
+```
+
+└── v2
+ └── repositories
+ └── nginx
+ └── _manifests
+ └── revisions
+ └── sha256
+ ├── 111ma2d22ae5ef400769fa51c84717264cd1520ac8d93dc071374c1be49a111m
+ │ ├── link
+ │ └── ref
+ │ └── digest(application/vnd.example.artifact)
+ │ └── sha256
+ │ └── 222ibbf80b44ce6be8234e6ff90a1ac34acbeb826903b02cfa0da11c82cb222i
+ │ └── link
+ └── 222ibbf80b44ce6be8234e6ff90a1ac34acbeb826903b02cfa0da11c82cb222i
+ └── link
+```
+
+Push another ORAS artifact with the following properties:
+
+- digest: `sha256:333ic0c33ebc4a74a0a554c86ac2b28ddf3454a5ad9cf90ea8cea9f9e75c333i`
+- `subjectManifest` digest: `sha256:111ma2d22ae5ef400769fa51c84717264cd1520ac8d93dc071374c1be49a111m`
+- `artifactType`: `application/vnd.another.example.artifact`
+
+This results in an addition to the index as shown below.
+
+```
+
+└── v2
+ └── repositories
+ └── nginx
+ └── _manifests
+ └── revisions
+ └── sha256
+ ├── 111ma2d22ae5ef400769fa51c84717264cd1520ac8d93dc071374c1be49a111m
+ │ ├── link
+ │ └── ref
+ │ ├── digest(application/vnd.example.artifact)
+ │ │ └── sha256
+ │ │ └── 222ibbf80b44ce6be8234e6ff90a1ac34acbeb826903b02cfa0da11c82cb222i
+ │ │ └── link
+ │ └── digest(application/vnd.another.example.artifact)
+ │ └── sha256
+ │ └── 333ic0c33ebc4a74a0a554c86ac2b28ddf3454a5ad9cf90ea8cea9f9e75c333i
+ │ └── link
+ ├── 222ibbf80b44ce6be8234e6ff90a1ac34acbeb826903b02cfa0da11c82cb222i
+ │ └── link
+ └── 333ic0c33ebc4a74a0a554c86ac2b28ddf3454a5ad9cf90ea8cea9f9e75c333i
+ └── link
+```
diff --git a/go.mod b/go.mod
index 89d044f1e56..33382bceb92 100644
--- a/go.mod
+++ b/go.mod
@@ -30,6 +30,7 @@ require (
github.com/ncw/swift v1.0.47
github.com/opencontainers/go-digest v1.0.0
github.com/opencontainers/image-spec v1.0.1
+ github.com/oras-project/artifacts-spec v0.0.0-20210910233110-813953a626ae
github.com/satori/go.uuid v1.2.0 // indirect
github.com/sirupsen/logrus v1.8.1
github.com/spf13/cobra v0.0.3
diff --git a/go.sum b/go.sum
index 50b314e7a3f..d65dec4dadf 100644
--- a/go.sum
+++ b/go.sum
@@ -94,6 +94,8 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI=
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
+github.com/oras-project/artifacts-spec v0.0.0-20210910233110-813953a626ae h1:vw1ccIckPxFFx3/wW8SaVLtvQ5viAduN+fuku8KbEfo=
+github.com/oras-project/artifacts-spec v0.0.0-20210910233110-813953a626ae/go.mod h1:Xch2aLzSwtkhbFFN6LUzTfLtukYvMMdXJ4oZ8O7BOdc=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
diff --git a/manifest/orasartifact/manifest.go b/manifest/orasartifact/manifest.go
new file mode 100644
index 00000000000..d3072d85c61
--- /dev/null
+++ b/manifest/orasartifact/manifest.go
@@ -0,0 +1,101 @@
+package orasartifact
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+
+ "github.com/distribution/distribution/v3"
+ "github.com/opencontainers/go-digest"
+ v1 "github.com/oras-project/artifacts-spec/specs-go/v1"
+)
+
+func init() {
+ unmarshalFunc := func(b []byte) (distribution.Manifest, distribution.Descriptor, error) {
+ d := new(DeserializedManifest)
+ err := d.UnmarshalJSON(b)
+ if err != nil {
+ return nil, distribution.Descriptor{}, err
+ }
+
+ dgst := digest.FromBytes(b)
+ return d, distribution.Descriptor{Digest: dgst, Size: int64(len(b)), MediaType: v1.MediaTypeArtifactManifest}, err
+ }
+ err := distribution.RegisterManifestSchema(v1.MediaTypeArtifactManifest, unmarshalFunc)
+ if err != nil {
+ panic(fmt.Sprintf("Unable to register ORAS artifact manifest: %s", err))
+ }
+}
+
+// Manifest describes ORAS artifact manifests.
+type Manifest struct {
+ inner v1.Manifest
+}
+
+// ArtifactType returns the artifactType of this ORAS artifact.
+func (a Manifest) ArtifactType() string {
+ return a.inner.ArtifactType
+}
+
+// References returns the distribution descriptors for the referenced blobs.
+func (a Manifest) References() []distribution.Descriptor {
+ blobs := make([]distribution.Descriptor, len(a.inner.Blobs))
+ for i := range a.inner.Blobs {
+ blobs[i] = distribution.Descriptor{
+ MediaType: a.inner.Blobs[i].MediaType,
+ Digest: a.inner.Blobs[i].Digest,
+ Size: a.inner.Blobs[i].Size,
+ }
+ }
+ return blobs
+}
+
+// Subject returns the the subject manifest this artifact references.
+func (a Manifest) Subject() distribution.Descriptor {
+ return distribution.Descriptor{
+ MediaType: a.inner.Subject.MediaType,
+ Digest: a.inner.Subject.Digest,
+ Size: a.inner.Subject.Size,
+ }
+}
+
+// DeserializedManifest wraps Manifest with a copy of the original JSON data.
+type DeserializedManifest struct {
+ Manifest
+
+ // raw is the raw byte representation of the ORAS artifact.
+ raw []byte
+}
+
+// UnmarshalJSON populates a new Manifest struct from JSON data.
+func (d *DeserializedManifest) UnmarshalJSON(b []byte) error {
+ d.raw = make([]byte, len(b))
+ copy(d.raw, b)
+
+ var man v1.Manifest
+ if err := json.Unmarshal(d.raw, &man); err != nil {
+ return err
+ }
+ if man.ArtifactType == "" {
+ return errors.New("artifactType cannot be empty")
+ }
+ d.inner = man
+
+ return nil
+}
+
+// MarshalJSON returns the raw content.
+func (d *DeserializedManifest) MarshalJSON() ([]byte, error) {
+ if len(d.raw) > 0 {
+ return d.raw, nil
+ }
+
+ return nil, errors.New("JSON representation not initialized in DeserializedManifest")
+}
+
+// Payload returns the raw content of the Artifact. The contents can be
+// used to calculate the content identifier.
+func (d DeserializedManifest) Payload() (string, []byte, error) {
+ // NOTE: This is a hack. The media type should be read from storage.
+ return v1.MediaTypeArtifactManifest, d.raw, nil
+}
diff --git a/manifests.go b/manifests.go
index 8f84a220a97..9bd97b7f711 100644
--- a/manifests.go
+++ b/manifests.go
@@ -6,6 +6,7 @@ import (
"mime"
"github.com/opencontainers/go-digest"
+ orasartifact "github.com/oras-project/artifacts-spec/specs-go/v1"
)
// Manifest represents a registry object specifying a set of
@@ -61,6 +62,10 @@ type ManifestService interface {
// Delete removes the manifest specified by the given digest. Deleting
// a manifest that doesn't exist will return ErrManifestNotFound
Delete(ctx context.Context, dgst digest.Digest) error
+
+ // Referrers returns a collection of manifests which reference the given manifest,
+ // filtered by artifactType.
+ Referrers(ctx context.Context, dgst digest.Digest, artifactType string) ([]orasartifact.Descriptor, error)
}
// ManifestEnumerator enables iterating over manifests
diff --git a/registry/api/oras/artifacts/v1/descriptors.go b/registry/api/oras/artifacts/v1/descriptors.go
new file mode 100644
index 00000000000..2f5841fef54
--- /dev/null
+++ b/registry/api/oras/artifacts/v1/descriptors.go
@@ -0,0 +1,128 @@
+package v1
+
+import (
+ "net/http"
+
+ "github.com/distribution/distribution/v3/reference"
+ v2 "github.com/distribution/distribution/v3/registry/api/v2"
+)
+
+var (
+ nameParameterDescriptor = v2.ParameterDescriptor{
+ Name: "name",
+ Type: "string",
+ Format: reference.NameRegexp.String(),
+ Required: true,
+ Description: `Name of the target repository.`,
+ }
+
+ digestParameterDescriptor = v2.ParameterDescriptor{
+ Name: "digest",
+ Type: "string",
+ Format: reference.DigestRegexp.String(),
+ Required: true,
+ Description: `Digest of the target manifest.`,
+ }
+
+ hostHeader = v2.ParameterDescriptor{
+ Name: "Host",
+ Type: "string",
+ Description: "Standard HTTP Host Header. Should be set to the registry host.",
+ Format: "",
+ Examples: []string{"registry-1.docker.io"},
+ }
+
+ authHeader = v2.ParameterDescriptor{
+ Name: "Authorization",
+ Type: "string",
+ Description: "An RFC7235 compliant authorization header.",
+ Format: " ",
+ Examples: []string{"Bearer dGhpcyBpcyBhIGZha2UgYmVhcmVyIHRva2VuIQ=="},
+ }
+)
+
+var routeDescriptors = []v2.RouteDescriptor{
+
+ {
+ Name: RouteNameReferrers,
+ Path: "/oras/artifacts/v1/{name:" + reference.NameRegexp.String() + "}/manifests/{digest:" + reference.DigestRegexp.String() + "}/referrers",
+ Entity: "Referrers",
+ Description: "Retrieve information about artifacts that reference this manifest.",
+ Methods: []v2.MethodDescriptor{
+ {
+ Method: "GET",
+ Description: "Fetch a list of referrers.",
+ Requests: []v2.RequestDescriptor{
+ {
+ Name: "Referrers",
+ Description: "Return a list of all referrers of the given manifest filtered by the given artifact type.",
+ Headers: []v2.ParameterDescriptor{
+ hostHeader,
+ authHeader,
+ },
+ PathParameters: []v2.ParameterDescriptor{
+ nameParameterDescriptor,
+ digestParameterDescriptor,
+ },
+ QueryParameters: []v2.ParameterDescriptor{
+ {
+ Name: "artifactType",
+ Type: "query",
+ Format: "",
+ Description: `Artifact type of the requested referrers.`,
+ },
+ {
+ Name: "n",
+ Type: "query",
+ Format: "",
+ Description: `Pagination parameter indicating the requested number of results`,
+ },
+ },
+ Successes: []v2.ResponseDescriptor{
+ {
+ StatusCode: http.StatusOK,
+ Description: "A list of referrers of the given manifest filtered by the given artifact type.",
+ Headers: []v2.ParameterDescriptor{
+ {
+ Name: "Content-Length",
+ Type: "integer",
+ Description: "Length of the JSON response body.",
+ Format: "",
+ },
+ {
+ Name: "Content-Type",
+ Type: "string",
+ Description: "Type of content in the response body.",
+ Format: "",
+ },
+ {
+ Name: "Link",
+ Type: "string",
+ Description: "RFC5988 Link header with next relationship. Used for pagination.",
+ Format: "",
+ },
+ },
+ Body: v2.BodyDescriptor{
+ ContentType: "application/json",
+ Format: `
+{
+ "references": [
+ {
+ "digest": "",
+ "mediaType": "",
+ "artifactType": "",
+ "size":
+ },
+ ...
+ ]
+}
+ `,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+}
diff --git a/registry/api/oras/artifacts/v1/doc.go b/registry/api/oras/artifacts/v1/doc.go
new file mode 100644
index 00000000000..da4d2a4ab8b
--- /dev/null
+++ b/registry/api/oras/artifacts/v1/doc.go
@@ -0,0 +1,3 @@
+// Package v1 describes routes, urls and the error codes used in ORAS
+// Artifacts V1 API.
+package v1
diff --git a/registry/api/oras/artifacts/v1/routes.go b/registry/api/oras/artifacts/v1/routes.go
new file mode 100644
index 00000000000..3922876151d
--- /dev/null
+++ b/registry/api/oras/artifacts/v1/routes.go
@@ -0,0 +1,23 @@
+package v1
+
+import "github.com/gorilla/mux"
+
+// The following are definitions of the name under which all V1 routes are
+// registered. These symbols can be used to look up a route based on the name.
+const (
+ RouteNameReferrers = "referrers"
+)
+
+// RouterWithPrefix builds a gorilla router with a configured prefix
+// on all routes.
+func AddPaths(router *mux.Router) *mux.Router {
+ if router == nil {
+ return nil
+ }
+
+ for _, descriptor := range routeDescriptors {
+ router.Path(descriptor.Path).Name(descriptor.Name)
+ }
+
+ return router
+}
diff --git a/registry/api/v2/errors.go b/registry/api/v2/errors.go
index 23a87f8c0b5..4c49daa4bae 100644
--- a/registry/api/v2/errors.go
+++ b/registry/api/v2/errors.go
@@ -109,7 +109,7 @@ var (
ErrorCodeManifestBlobUnknown = errcode.Register(errGroup, errcode.ErrorDescriptor{
Value: "MANIFEST_BLOB_UNKNOWN",
Message: "blob unknown to registry",
- Description: `This error may be returned when a manifest blob is
+ Description: `This error may be returned when a manifest blob is
unknown to the registry.`,
HTTPStatusCode: http.StatusBadRequest,
})
diff --git a/registry/client/repository.go b/registry/client/repository.go
index a3379c0a06c..b9b876a6dc8 100644
--- a/registry/client/repository.go
+++ b/registry/client/repository.go
@@ -21,6 +21,7 @@ import (
"github.com/distribution/distribution/v3/registry/storage/cache"
"github.com/distribution/distribution/v3/registry/storage/cache/memory"
"github.com/opencontainers/go-digest"
+ orasartifacts "github.com/oras-project/artifacts-spec/specs-go/v1"
)
// Registry provides an interface for calling Repositories, which returns a catalog of repositories.
@@ -397,6 +398,10 @@ type manifests struct {
etags map[string]string
}
+func (ms *manifests) Referrers(_ context.Context, _ digest.Digest, _ string) ([]orasartifacts.Descriptor, error) {
+ return nil, fmt.Errorf("not implemented")
+}
+
func (ms *manifests) Exists(ctx context.Context, dgst digest.Digest) (bool, error) {
ref, err := reference.WithDigest(ms.name, dgst)
if err != nil {
diff --git a/registry/handlers/app.go b/registry/handlers/app.go
index 212c79a709f..b1f85a9618d 100644
--- a/registry/handlers/app.go
+++ b/registry/handlers/app.go
@@ -24,6 +24,7 @@ import (
"github.com/distribution/distribution/v3/notifications"
"github.com/distribution/distribution/v3/reference"
"github.com/distribution/distribution/v3/registry/api/errcode"
+ v1 "github.com/distribution/distribution/v3/registry/api/oras/artifacts/v1"
v2 "github.com/distribution/distribution/v3/registry/api/v2"
"github.com/distribution/distribution/v3/registry/auth"
registrymiddleware "github.com/distribution/distribution/v3/registry/middleware/registry"
@@ -111,6 +112,10 @@ func NewApp(ctx context.Context, config *configuration.Configuration) *App {
app.register(v2.RouteNameBlobUpload, blobUploadDispatcher)
app.register(v2.RouteNameBlobUploadChunk, blobUploadDispatcher)
+ // Register ORAS artifact routes.
+ app.router = v1.AddPaths(app.router)
+ app.register(v1.RouteNameReferrers, referrersDispatcher)
+
// override the storage driver's UA string for registry outbound HTTP requests
storageParams := config.Storage.Parameters()
if storageParams == nil {
diff --git a/registry/handlers/referrers.go b/registry/handlers/referrers.go
new file mode 100644
index 00000000000..7011bce05c4
--- /dev/null
+++ b/registry/handlers/referrers.go
@@ -0,0 +1,89 @@
+package handlers
+
+import (
+ "encoding/json"
+ "net/http"
+
+ "github.com/distribution/distribution/v3"
+ dcontext "github.com/distribution/distribution/v3/context"
+ "github.com/distribution/distribution/v3/registry/api/errcode"
+ v2 "github.com/distribution/distribution/v3/registry/api/v2"
+ "github.com/gorilla/handlers"
+ "github.com/opencontainers/go-digest"
+ orasartifacts "github.com/oras-project/artifacts-spec/specs-go/v1"
+)
+
+// referrersDispatcher takes the request context and builds the
+// appropriate handler for handling manifest referrer requests.
+func referrersDispatcher(ctx *Context, r *http.Request) http.Handler {
+ handler := &referrersHandler{
+ Context: ctx,
+ }
+ handler.Digest, _ = getDigest(ctx)
+
+ mhandler := handlers.MethodHandler{
+ "GET": http.HandlerFunc(handler.Get),
+ }
+
+ return mhandler
+}
+
+// referrersResponse describes the response body of the referrers API.
+type referrersResponse struct {
+ Referrers []orasartifacts.Descriptor `json:"references"`
+}
+
+// referrersHandler handles http operations on manifest referrers.
+type referrersHandler struct {
+ *Context
+
+ // Digest is the target manifest's digest.
+ Digest digest.Digest
+}
+
+// Get gets the list of artifacts that reference the given manifest filtered by the artifact type
+// specified in the request.
+func (h *referrersHandler) Get(w http.ResponseWriter, r *http.Request) {
+ dcontext.GetLogger(h).Debug("Get")
+
+ // This can be empty
+ artifactType := r.FormValue("artifactType")
+
+ if h.Digest == "" {
+ h.Errors = append(h.Errors, v2.ErrorCodeManifestUnknown.WithDetail("digest not specified"))
+ return
+ }
+
+ ms, err := h.Repository.Manifests(h.Context)
+ if err != nil {
+ h.Errors = append(h.Errors, err)
+ return
+ }
+
+ referrers, err := ms.Referrers(h.Context, h.Digest, artifactType)
+ if err != nil {
+ if _, ok := err.(distribution.ErrManifestUnknownRevision); ok {
+ h.Errors = append(h.Errors, v2.ErrorCodeManifestUnknown.WithDetail(err))
+ } else {
+ h.Errors = append(h.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
+ }
+ return
+ }
+
+ if referrers == nil {
+ referrers = []orasartifacts.Descriptor{}
+ }
+
+ response := referrersResponse{}
+
+ for _, referrer := range referrers {
+ response.Referrers = append(response.Referrers, referrer)
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ enc := json.NewEncoder(w)
+ if err = enc.Encode(response); err != nil {
+ h.Errors = append(h.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
+ return
+ }
+}
diff --git a/registry/proxy/proxymanifeststore.go b/registry/proxy/proxymanifeststore.go
index 0b29386a758..6af93dfe290 100644
--- a/registry/proxy/proxymanifeststore.go
+++ b/registry/proxy/proxymanifeststore.go
@@ -9,6 +9,7 @@ import (
"github.com/distribution/distribution/v3/reference"
"github.com/distribution/distribution/v3/registry/proxy/scheduler"
"github.com/opencontainers/go-digest"
+ orasartifacts "github.com/oras-project/artifacts-spec/specs-go/v1"
)
// todo(richardscothern): from cache control header or config
@@ -25,6 +26,10 @@ type proxyManifestStore struct {
var _ distribution.ManifestService = &proxyManifestStore{}
+func (pms proxyManifestStore) Referrers(_ context.Context, _ digest.Digest, _ string) ([]orasartifacts.Descriptor, error) {
+ return nil, distribution.ErrUnsupported
+}
+
func (pms proxyManifestStore) Exists(ctx context.Context, dgst digest.Digest) (bool, error) {
exists, err := pms.localManifests.Exists(ctx, dgst)
if err != nil {
diff --git a/registry/proxy/proxymanifeststore_test.go b/registry/proxy/proxymanifeststore_test.go
index 8967e992c57..59f9521a2ae 100644
--- a/registry/proxy/proxymanifeststore_test.go
+++ b/registry/proxy/proxymanifeststore_test.go
@@ -19,6 +19,7 @@ import (
"github.com/distribution/distribution/v3/testutil"
"github.com/docker/libtrust"
"github.com/opencontainers/go-digest"
+ orasartifacts "github.com/oras-project/artifacts-spec/specs-go/v1"
)
type statsManifest struct {
@@ -61,6 +62,11 @@ func (sm statsManifest) Put(ctx context.Context, manifest distribution.Manifest,
return sm.manifests.Put(ctx, manifest)
}
+func (sm statsManifest) Referrers(ctx context.Context, dgst digest.Digest, referrerType string) ([]orasartifacts.Descriptor, error) {
+ sm.stats["referrers"]++
+ return sm.Referrers(ctx, dgst, referrerType)
+}
+
type mockChallenger struct {
sync.Mutex
count int
diff --git a/registry/storage/manifeststore.go b/registry/storage/manifeststore.go
index 0daa92dfe91..ad2f674b93c 100644
--- a/registry/storage/manifeststore.go
+++ b/registry/storage/manifeststore.go
@@ -10,12 +10,20 @@ import (
"github.com/distribution/distribution/v3/manifest"
"github.com/distribution/distribution/v3/manifest/manifestlist"
"github.com/distribution/distribution/v3/manifest/ocischema"
+ "github.com/distribution/distribution/v3/manifest/orasartifact"
"github.com/distribution/distribution/v3/manifest/schema1"
"github.com/distribution/distribution/v3/manifest/schema2"
+ "github.com/distribution/distribution/v3/registry/storage/driver"
"github.com/opencontainers/go-digest"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
+ orasartifactv1 "github.com/oras-project/artifacts-spec/specs-go/v1"
)
+// referrersStoreFunc describes a function that returns the referrers store
+// for the given manifest of the given artifactType.
+// A referrers store provides links to referrer manifests.
+type referrersStoreFunc func(ctx context.Context, revision digest.Digest, artifactType string) *linkedBlobStore
+
// A ManifestHandler gets and puts manifests of a particular type.
type ManifestHandler interface {
// Unmarshal unmarshals the manifest from a byte slice.
@@ -52,6 +60,9 @@ type manifestStore struct {
schema2Handler ManifestHandler
ocischemaHandler ManifestHandler
manifestListHandler ManifestHandler
+ orasArtifactHandler ManifestHandler
+
+ referrersStore referrersStoreFunc
}
var _ distribution.ManifestService = &manifestStore{}
@@ -121,9 +132,10 @@ func (ms *manifestStore) Get(ctx context.Context, dgst digest.Digest, options ..
default:
return nil, distribution.ErrManifestVerification{fmt.Errorf("unrecognized manifest content type %s", versioned.MediaType)}
}
+ default:
+ // Assume it's an ORAS artifact manifest.
+ return ms.orasArtifactHandler.Unmarshal(ctx, dgst, content)
}
-
- return nil, fmt.Errorf("unrecognized manifest schema version %d", versioned.SchemaVersion)
}
func (ms *manifestStore) Put(ctx context.Context, manifest distribution.Manifest, options ...distribution.ManifestServiceOption) (digest.Digest, error) {
@@ -138,11 +150,56 @@ func (ms *manifestStore) Put(ctx context.Context, manifest distribution.Manifest
return ms.ocischemaHandler.Put(ctx, manifest, ms.skipDependencyVerification)
case *manifestlist.DeserializedManifestList:
return ms.manifestListHandler.Put(ctx, manifest, ms.skipDependencyVerification)
+ case *orasartifact.DeserializedManifest:
+ return ms.orasArtifactHandler.Put(ctx, manifest, ms.skipDependencyVerification)
}
return "", fmt.Errorf("unrecognized manifest type %T", manifest)
}
+// Referrers returns referrer manifests filtered by the given referrerType.
+func (ms *manifestStore) Referrers(ctx context.Context, revision digest.Digest, referrerType string) ([]orasartifactv1.Descriptor, error) {
+ dcontext.GetLogger(ms.ctx).Debug("(*manifestStore).Referrers")
+
+ var referrers []orasartifactv1.Descriptor
+
+ err := ms.referrersStore(ctx, revision, referrerType).Enumerate(ctx, func(referrerRevision digest.Digest) error {
+ man, err := ms.Get(ctx, referrerRevision)
+ if err != nil {
+ return err
+ }
+
+ orasArtifactMan, ok := man.(*orasartifact.DeserializedManifest)
+ if !ok {
+ // The PUT handler would guard against this situation. Skip this manifest.
+ return nil
+ }
+
+ desc, err := ms.blobStore.Stat(ctx, referrerRevision)
+ if err != nil {
+ return err
+ }
+ desc.MediaType, _, _ = man.Payload()
+ referrers = append(referrers, orasartifactv1.Descriptor{
+ MediaType: desc.MediaType,
+ Size: desc.Size,
+ Digest: desc.Digest,
+ ArtifactType: orasArtifactMan.ArtifactType(),
+ })
+ return nil
+ })
+
+ if err != nil {
+ switch err.(type) {
+ case driver.PathNotFoundError:
+ return nil, nil
+ }
+ return nil, err
+ }
+
+ return referrers, nil
+}
+
// Delete removes the revision of the specified manifest.
func (ms *manifestStore) Delete(ctx context.Context, dgst digest.Digest) error {
dcontext.GetLogger(ms.ctx).Debug("(*manifestStore).Delete")
diff --git a/registry/storage/orasartifactmanifesthandler.go b/registry/storage/orasartifactmanifesthandler.go
new file mode 100644
index 00000000000..ffb8c65e1ee
--- /dev/null
+++ b/registry/storage/orasartifactmanifesthandler.go
@@ -0,0 +1,123 @@
+package storage
+
+import (
+ "context"
+ "errors"
+ "fmt"
+
+ "github.com/distribution/distribution/v3"
+ dcontext "github.com/distribution/distribution/v3/context"
+ "github.com/distribution/distribution/v3/manifest/orasartifact"
+ "github.com/opencontainers/go-digest"
+)
+
+// orasArtifactManifestHandler is a ManifestHandler that covers ORAS Artifacts.
+type orasArtifactManifestHandler struct {
+ repository distribution.Repository
+ blobStore distribution.BlobStore
+ ctx context.Context
+ referrersStore referrersStoreFunc
+}
+
+func (oamh *orasArtifactManifestHandler) Unmarshal(ctx context.Context, dgst digest.Digest, content []byte) (distribution.Manifest, error) {
+ dcontext.GetLogger(oamh.ctx).Debug("(*orasArtifactManifestHandler).Unmarshal")
+
+ dm := &orasartifact.DeserializedManifest{}
+ if err := dm.UnmarshalJSON(content); err != nil {
+ return nil, err
+ }
+
+ return dm, nil
+}
+
+func (ah *orasArtifactManifestHandler) Put(ctx context.Context, man distribution.Manifest, skipDependencyVerification bool) (digest.Digest, error) {
+ dcontext.GetLogger(ah.ctx).Debug("(*orasArtifactManifestHandler).Put")
+
+ da, ok := man.(*orasartifact.DeserializedManifest)
+ if !ok {
+ return "", fmt.Errorf("wrong type put to orasArtifactManifestHandler: %T", man)
+ }
+
+ if err := ah.verifyManifest(ah.ctx, *da, skipDependencyVerification); err != nil {
+ return "", err
+ }
+
+ mt, payload, err := da.Payload()
+ if err != nil {
+ return "", err
+ }
+
+ revision, err := ah.blobStore.Put(ctx, mt, payload)
+ if err != nil {
+ dcontext.GetLogger(ctx).Errorf("error putting payload into blobstore: %v", err)
+ return "", err
+ }
+
+ err = ah.indexReferrers(ctx, *da, revision.Digest)
+ if err != nil {
+ dcontext.GetLogger(ctx).Errorf("error indexing referrers: %v", err)
+ return "", err
+ }
+
+ return revision.Digest, nil
+}
+
+// verifyManifest ensures that the manifest content is valid from the
+// perspective of the registry. As a policy, the registry only tries to
+// store valid content, leaving trust policies of that content up to
+// consumers.
+func (oamh *orasArtifactManifestHandler) verifyManifest(ctx context.Context, dm orasartifact.DeserializedManifest, skipDependencyVerification bool) error {
+ var errs distribution.ErrManifestVerification
+
+ if dm.ArtifactType() == "" {
+ errs = append(errs, distribution.ErrManifestVerification{errors.New("artifactType invalid")})
+ }
+
+ if !skipDependencyVerification {
+ bs := oamh.repository.Blobs(ctx)
+
+ // All references must exist.
+ for _, blobDesc := range dm.References() {
+ desc, err := bs.Stat(ctx, blobDesc.Digest)
+ if err != nil && err != distribution.ErrBlobUnknown {
+ errs = append(errs, err)
+ }
+ if err != nil || desc.Digest == "" {
+ // On error here, we always append unknown blob errors.
+ errs = append(errs, distribution.ErrManifestBlobUnknown{Digest: blobDesc.Digest})
+ }
+ }
+
+ ms, err := oamh.repository.Manifests(ctx)
+ if err != nil {
+ return err
+ }
+
+ // Validate subject manifest.
+ subject := dm.Subject()
+ exists, err := ms.Exists(ctx, subject.Digest)
+ if !exists || err == distribution.ErrBlobUnknown {
+ errs = append(errs, distribution.ErrManifestBlobUnknown{Digest: subject.Digest})
+ } else if err != nil {
+ errs = append(errs, err)
+ }
+ }
+
+ if len(errs) != 0 {
+ return errs
+ }
+
+ return nil
+}
+
+// indexReferrers indexes the subject of the given revision in its referrers index store.
+func (oamh *orasArtifactManifestHandler) indexReferrers(ctx context.Context, dm orasartifact.DeserializedManifest, revision digest.Digest) error {
+ artifactType := dm.ArtifactType()
+ subject := dm.Subject()
+
+ if err := oamh.referrersStore(ctx, subject.Digest, artifactType).linkBlob(ctx, distribution.Descriptor{Digest: revision}); err != nil {
+ return err
+ }
+
+ return nil
+}
diff --git a/registry/storage/paths.go b/registry/storage/paths.go
index 4e9ad0856e8..f14afaa4f4f 100644
--- a/registry/storage/paths.go
+++ b/registry/storage/paths.go
@@ -74,6 +74,8 @@ const (
// manifestRevisionsPathSpec: /v2/repositories//_manifests/revisions/
// manifestRevisionPathSpec: /v2/repositories//_manifests/revisions///
// manifestRevisionLinkPathSpec: /v2/repositories//_manifests/revisions///link
+// referrersPathSpec: /v2/repositories//_manifests/revisions///ref/
+// referrerRevisionLinkPathSpec: /v2/repositories//_manifests/revisions///ref////link
//
// Tags:
//
@@ -193,6 +195,25 @@ func pathFor(spec pathSpec) (string, error) {
}
return path.Join(root, path.Join(components...)), nil
+ case referrersPathSpec:
+ revisionPrefix, err := pathFor(manifestRevisionPathSpec{name: v.name, revision: v.revision})
+ if err != nil {
+ return "", err
+ }
+
+ return path.Join(revisionPrefix, "ref", v.artifactType), nil
+ case referrerRevisionLinkPathSpec:
+ metadataPrefix, err := pathFor(referrersPathSpec{name: v.name, revision: v.revision, artifactType: v.artifactType})
+ if err != nil {
+ return "", err
+ }
+
+ metadataSuffix, err := digestPathComponents(v.referrerRevision, false)
+ if err != nil {
+ return "", err
+ }
+
+ return path.Join(append(append([]string{metadataPrefix}, metadataSuffix...), "link")...), nil
case layerLinkPathSpec:
components, err := digestPathComponents(v.digest, false)
if err != nil {
@@ -319,6 +340,26 @@ type manifestTagIndexPathSpec struct {
func (manifestTagIndexPathSpec) pathSpec() {}
+// referrersPathSpec describes the directory path for manifest referrers of type artifactType.
+type referrersPathSpec struct {
+ name string
+ revision digest.Digest
+ artifactType string
+}
+
+func (referrersPathSpec) pathSpec() {}
+
+// referrerRevisionLinkPathSpec describes the path components required to look
+// up the referrer revision link of a manifest revision.
+type referrerRevisionLinkPathSpec struct {
+ name string
+ revision digest.Digest
+ artifactType string
+ referrerRevision digest.Digest
+}
+
+func (referrerRevisionLinkPathSpec) pathSpec() {}
+
// manifestTagIndexEntryPathSpec contains the entries of the index by revision.
type manifestTagIndexEntryPathSpec struct {
name string
diff --git a/registry/storage/registry.go b/registry/storage/registry.go
index 45939083f7a..988dfd37c85 100644
--- a/registry/storage/registry.go
+++ b/registry/storage/registry.go
@@ -3,12 +3,14 @@ package storage
import (
"context"
"regexp"
+ "strings"
"github.com/distribution/distribution/v3"
"github.com/distribution/distribution/v3/reference"
"github.com/distribution/distribution/v3/registry/storage/cache"
storagedriver "github.com/distribution/distribution/v3/registry/storage/driver"
"github.com/docker/libtrust"
+ "github.com/opencontainers/go-digest"
)
// registry is the top-level implementation of Registry for use in the storage
@@ -266,10 +268,48 @@ func (repo *repository) Manifests(ctx context.Context, options ...distribution.M
}
}
+ referrersStore := func(ctx context.Context, revision digest.Digest, artifactType string) *linkedBlobStore {
+ artifactTypeDigest := ""
+
+ if artifactType != "" {
+ // NOTE (aviral26)
+ // This might cause a problem on Windows file system because of long file paths.
+ // A workaround is to run this server using wsl2.
+ artifactTypeDigest = strings.Split(digest.FromString(artifactType).String(), ":")[1]
+ }
+
+ return &linkedBlobStore{
+ blobStore: repo.blobStore,
+ repository: repo,
+ ctx: ctx,
+ linkPathFns: []linkPathFunc{
+ func(name string, referrerRevision digest.Digest) (string, error) {
+ return pathFor(referrerRevisionLinkPathSpec{
+ name: name,
+ revision: revision,
+ artifactType: artifactTypeDigest,
+ referrerRevision: referrerRevision,
+ })
+ },
+ },
+ linkDirectoryPathSpec: referrersPathSpec{
+ name: repo.Named().Name(),
+ revision: revision,
+ artifactType: artifactTypeDigest,
+ },
+ blobAccessController: &linkedBlobStatter{
+ blobStore: repo.blobStore,
+ repository: repo,
+ linkPathFns: []linkPathFunc{manifestRevisionLinkPath},
+ },
+ }
+ }
+
ms := &manifestStore{
ctx: ctx,
repository: repo,
blobStore: blobStore,
+ referrersStore: referrersStore,
schema1Handler: v1Handler,
schema2Handler: &schema2ManifestHandler{
ctx: ctx,
@@ -282,6 +322,12 @@ func (repo *repository) Manifests(ctx context.Context, options ...distribution.M
repository: repo,
blobStore: blobStore,
},
+ orasArtifactHandler: &orasArtifactManifestHandler{
+ ctx: ctx,
+ repository: repo,
+ blobStore: blobStore,
+ referrersStore: referrersStore,
+ },
ocischemaHandler: &ocischemaManifestHandler{
ctx: ctx,
repository: repo,
diff --git a/releases/v0.0.1-alpha.toml b/releases/v0.0.1-alpha.toml
new file mode 100644
index 00000000000..6f48fdf4471
--- /dev/null
+++ b/releases/v0.0.1-alpha.toml
@@ -0,0 +1,31 @@
+# commit to be tagged for new release
+commit = "HEAD"
+
+project_name = "registry"
+github_repo = "oras-project/distribution"
+
+# previous release
+previous = "main"
+
+pre_release = true
+
+preface = """
+The v0.0.1-alpha registry experimental release provides basic support for phase 1 of
+[reference types](phase-1-reference-types). Clients can push and pull manifests
+of media type `application/vnd.cncf.oras.artifact.manifest.v1+json` (ORAS
+Artifact Manifests) and discover existing reference types using the `/referrers`
+[api](referrers-api).
+
+This release includes the following changes pertaining to reference types:
+
+* Adds CRUD operations for ORAS Artifact Manifests
+* Adds support for `GET /referrers` API
+ * In this release candidate, pagination is not implemented
+ * The implementation uses a [reverse index](reverse-index) as an optimization
+
+The release **does not** implement garbage collection of reference types.
+
+[phase-1-reference-types]: https://github.com/oras-project/artifacts-spec/blob/main/scenarios.md#phase-1---reference-types
+[referrers-api]: https://github.com/oras-project/artifacts-spec/blob/main/manifest-referrers-api.md
+[reverse-index]: docs/referrers.md
+"""
diff --git a/vendor/github.com/oras-project/artifacts-spec/LICENSE b/vendor/github.com/oras-project/artifacts-spec/LICENSE
new file mode 100644
index 00000000000..a67d1693894
--- /dev/null
+++ b/vendor/github.com/oras-project/artifacts-spec/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright 2021 ORAS Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/vendor/github.com/oras-project/artifacts-spec/specs-go/v1/descriptor.go b/vendor/github.com/oras-project/artifacts-spec/specs-go/v1/descriptor.go
new file mode 100644
index 00000000000..7e4edfd0df9
--- /dev/null
+++ b/vendor/github.com/oras-project/artifacts-spec/specs-go/v1/descriptor.go
@@ -0,0 +1,40 @@
+// Copyright 2021 ORAS Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package v1
+
+import digest "github.com/opencontainers/go-digest"
+
+// Descriptor describes the disposition of targeted content.
+type Descriptor struct {
+ // MediaType is the media type of the object this schema refers to.
+ MediaType string `json:"mediaType,omitempty"`
+
+ // ArtifactType is the artifact type of the object this schema refers to.
+ //
+ // When the descriptor is used for blobs, this property must be empty.
+ ArtifactType string `json:"artifactType,omitempty"`
+
+ // Digest is the digest of the targeted content.
+ Digest digest.Digest `json:"digest"`
+
+ // Size specifies the size in bytes of the blob.
+ Size int64 `json:"size"`
+
+ // URLs specifies a list of URLs from which this object MAY be downloaded
+ URLs []string `json:"urls,omitempty"`
+
+ // Annotations contains arbitrary metadata relating to the targeted content.
+ Annotations map[string]string `json:"annotations,omitempty"`
+}
diff --git a/vendor/github.com/oras-project/artifacts-spec/specs-go/v1/manifest.go b/vendor/github.com/oras-project/artifacts-spec/specs-go/v1/manifest.go
new file mode 100644
index 00000000000..67ded906fa1
--- /dev/null
+++ b/vendor/github.com/oras-project/artifacts-spec/specs-go/v1/manifest.go
@@ -0,0 +1,32 @@
+// Copyright 2021 ORAS Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package v1
+
+// Manifest describes an ORAS artifact.
+// This structure provides `application/vnd.oras.artifact.manifest.v1+json` mediatype when marshalled to JSON.
+type Manifest struct {
+ // ArtifactType is the artifact type of the object this schema refers to.
+ ArtifactType string `json:"artifactType"`
+
+ // Blobs is a collection of blobs referenced by this manifest.
+ Blobs []Descriptor `json:"blobs"`
+
+ // Subject is an optional reference to any existing manifest within the repository.
+ // When specified, the artifact is said to be dependent upon the referenced subject.
+ Subject Descriptor `json:"subject"`
+
+ // Annotations contains arbitrary metadata for the artifact manifest.
+ Annotations map[string]string `json:"annotations,omitempty"`
+}
diff --git a/vendor/github.com/oras-project/artifacts-spec/specs-go/v1/mediatype.go b/vendor/github.com/oras-project/artifacts-spec/specs-go/v1/mediatype.go
new file mode 100644
index 00000000000..fd5767f7582
--- /dev/null
+++ b/vendor/github.com/oras-project/artifacts-spec/specs-go/v1/mediatype.go
@@ -0,0 +1,20 @@
+// Copyright 2021 ORAS Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package v1
+
+const (
+ // MediaTypeArtifactManifest specifies the media type for an ORAS artifact reference type manifest.
+ MediaTypeArtifactManifest = "application/vnd.cncf.oras.artifact.manifest.v1+json"
+)
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 25194388b48..60706f26832 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -139,6 +139,9 @@ github.com/opencontainers/go-digest
## explicit
github.com/opencontainers/image-spec/specs-go
github.com/opencontainers/image-spec/specs-go/v1
+# github.com/oras-project/artifacts-spec v0.0.0-20210910233110-813953a626ae
+## explicit
+github.com/oras-project/artifacts-spec/specs-go/v1
# github.com/prometheus/client_golang v1.1.0
github.com/prometheus/client_golang/prometheus
github.com/prometheus/client_golang/prometheus/internal