Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 29 additions & 13 deletions specs/cli/referrer.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ OCI Referrers API.

| Subcommand | Method | Endpoint |
|---|---|---|
| `list` (tag resolution only) | `HEAD` | `/v2/<name>/manifests/<tag>` |
| `list` | `GET` | `/v2/<name>/referrers/<digest>[?artifactType=<type>]` |

> **Note:** The referrers endpoint is read-only — referrers are created
Expand All @@ -27,13 +28,15 @@ regshape referrer list [OPTIONS]
## Image Reference Format

The `referrer list` subcommand accepts `--image-ref` / `-i` using the standard
container image reference syntax with the registry always embedded. The
reference **must** include a digest — tag-only references are rejected because
the referrers API requires a digest.
container image reference syntax with the registry always embedded. Tags are
resolved to a digest with a manifest `HEAD` request before querying the referrers
API. Digest references are used directly, with no manifest lookup.

| Subcommand | Accepted formats | Notes |
|---|---|---|
| `list` | `registry/repo@sha256:...` | Digest is required |
| `list` | `registry/repo:tag` | Resolve the tag using `Docker-Content-Digest` |
| `list` | `registry/repo@sha256:...`, `registry/repo@sha512:...` | Use the digest directly |
| `list` | `registry/repo` | Resolve the default tag, `latest` |

> **Authentication:** Run `regshape auth login` before using referrer commands
> against authenticated registries. Credentials are resolved automatically
Expand All @@ -46,13 +49,13 @@ the referrers API requires a digest.

### `referrer list`

List all referrers for the manifest identified by digest in IMAGE_REF.
List referrers for the manifest identified by tag or digest in IMAGE_REF.

#### Options

| Option | Short | Type | Default | Description |
|---|---|---|---|---|
| `--image-ref` | `-i` | string | required | Image reference with embedded registry and digest (e.g., `registry/repo@sha256:abc...`) |
| `--image-ref` | `-i` | string | required | Image reference with embedded registry and tag or digest (e.g., `registry/repo:v1.0` or `registry/repo@sha256:abc...`) |
| `--artifact-type` | `-t` | string | none | Filter referrers to this artifact type (e.g., `application/vnd.example.sbom.v1`) |
| `--all` | | flag | false | Follow pagination and return all referrers (default: single page only) |
| `--json` | | flag | false | Output the full `ReferrerList` JSON object instead of one referrer per line |
Expand All @@ -63,11 +66,15 @@ List all referrers for the manifest identified by digest in IMAGE_REF.

#### Behavior

1. Parse `--image-ref` to extract registry, repository, and digest. Reject
tag-only references with exit code 2 and an error message directing the
user to use a digest reference.
1. Parse `--image-ref` to extract registry, repository, and reference. Explicit
digest references must use `sha256` or `sha512`; unsupported digest algorithms
are rejected with exit code 2.
2. Resolve credentials from the Docker credential store for the extracted
registry.
registry. For a tag reference, call `head_manifest()` using the same registry
client and accepting all supported manifest and index media types. Use the
returned `Docker-Content-Digest` as the subject digest. A missing or blank
digest header, authentication failure, missing tag, or transport failure
produces exit code 1 without querying referrers or writing the output file.
3. If `--all` is set, call `list_referrers_all()` which follows `Link`
pagination headers to retrieve every page and returns a merged
`ReferrerList`. Otherwise, call `list_referrers()` which returns a
Expand All @@ -89,15 +96,22 @@ List all referrers for the manifest identified by digest in IMAGE_REF.
| Code | Meaning |
|---|---|
| 0 | Success (including when there are zero referrers) |
| 1 | HTTP error (manifest not found, auth failure, connection error) |
| 2 | Invalid reference (tag-only reference instead of digest) |
| 1 | Reference parsing error, tag resolution failure (including a missing digest header), or registry/transport error |
| 2 | Command usage error or unsupported explicit digest algorithm |

#### Examples

```bash
# Log in first
regshape auth login -r acr.io -u alice

# List referrers by tag (automatically resolved to a digest)
regshape referrer list -i acr.io/myrepo/myimage:v1.0

# Resolve a tag, fetch all pages, filter, and save JSON
regshape referrer list -i acr.io/myrepo/myimage:v1.0 --all \
--artifact-type application/vnd.example.sbom.v1 --json -o referrers.json

# List all referrers for a manifest by digest (single page)
regshape referrer list -i acr.io/myrepo/myimage@sha256:abc123...

Expand Down Expand Up @@ -162,8 +176,10 @@ Full OCI Image Index response:

| Scenario | Message | Exit Code |
|----------|---------|-----------|
| Tag-only reference | `Error [<ref>]: referrer list requires a digest reference (registry/repo@sha256:...); use 'manifest get' to resolve a tag to a digest` | 2 |
| Unsupported digest algorithm | `Error [<ref>]: digest references must use sha256 or sha512` | 2 |
| Tag HEAD response lacks a digest | `Error [<ref>]: Cannot resolve tag '<tag>'` with detail `HEAD response is missing Docker-Content-Digest` | 1 |
| Auth failure | `Error [<ref>]: Authentication failed for <registry>: <detail>` | 1 |
| Tag not found | `Error [<ref>]: Manifest not found: <registry>/<repo>:<tag>` | 1 |
| Manifest not found | `Error [<ref>]: Manifest not found: <registry>/<repo>@<digest>` | 1 |
| Connection error | `Error [<ref>]: <exception message>` | 1 |

Expand Down
61 changes: 41 additions & 20 deletions src/regshape/cli/referrer.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
.. module:: regshape.cli.referrer
:platform: Unix, Windows
:synopsis: Click command group providing the ``list`` subcommand for
OCI referrer operations.
OCI referrer operations with automatic tag-to-digest resolution.

.. moduleauthor:: ToddySM <toddysm@gmail.com>
"""
Expand All @@ -18,12 +18,13 @@
from regshape.cli.formatting import emit_error, emit_json, emit_table, emit_text
from regshape.libs.decorators import telemetry_options
from regshape.libs.decorators.scenario import track_scenario
from regshape.libs.errors import AuthError, ReferrerError
from regshape.libs.refs import parse_image_ref
from regshape.libs.errors import AuthError, ManifestError, ReferrerError
from regshape.libs.manifests import head_manifest
from regshape.libs.models.mediatype import ALL_MANIFEST_MEDIA_TYPES
from regshape.libs.referrers import list_referrers, list_referrers_all
from regshape.libs.refs import parse_image_ref
from regshape.libs.transport import RegistryClient, TransportConfig


# ===========================================================================
# Public Click group
# ===========================================================================
Expand All @@ -46,9 +47,9 @@ def referrer():
required=True,
metavar="IMAGE_REF",
help=(
"Image reference with embedded registry and digest "
"(e.g. registry/repo@sha256:abc...). "
"Tag-only references are rejected — a digest is required."
"Image reference with embedded registry and tag or digest "
"(e.g. registry/repo:tag or registry/repo@sha256:abc...). "
"Tags are resolved to a digest automatically."
),
)
@click.option(
Expand Down Expand Up @@ -84,12 +85,21 @@ def referrer():
)
@click.pass_context
@track_scenario("referrer list")
def referrer_list(ctx, image_ref, artifact_type, fetch_all, as_json, output):
"""List referrers for the manifest identified by digest in IMAGE_REF.

IMAGE_REF must embed the registry and use a digest reference
(``registry/repo@sha256:...``). Tag-only references are rejected —
use ``manifest get`` to resolve a tag to a digest first.
def referrer_list(
ctx: click.Context,
image_ref: str,
artifact_type: str | None,
fetch_all: bool,
as_json: bool,
output: str | None,
) -> None:
"""List referrers for the manifest identified by IMAGE_REF.

IMAGE_REF must embed the registry and may use a tag or digest reference.
Tags are resolved with a manifest HEAD request before querying referrers;
digest references are used directly without a manifest lookup. References
without a tag or digest resolve the ``latest`` tag. Resolution failures,
including a missing digest response header, exit with code 1.

Use ``--artifact-type`` to filter by artifact type. Use ``--all``
to follow pagination and retrieve every referrer. Use ``--json``
Expand All @@ -102,19 +112,31 @@ def referrer_list(ctx, image_ref, artifact_type, fetch_all, as_json, output):
except ValueError as exc:
emit_error(image_ref, str(exc))

# The referrers API requires a digest reference.
if not reference.startswith("sha256:") and not reference.startswith("sha512:"):
is_digest = "@" in image_ref
if is_digest and not reference.startswith(("sha256:", "sha512:")):
emit_error(
image_ref,
"referrer list requires a digest reference "
"(registry/repo@sha256:...); "
"use 'manifest get' to resolve a tag to a digest",
"digest references must use sha256 or sha512",
exit_code=2,
)

client = RegistryClient(TransportConfig(registry=registry, insecure=insecure))

try:
if not is_digest:
digest, _, _ = head_manifest(
client=client,
repo=repo,
reference=reference,
accept=",".join(sorted(ALL_MANIFEST_MEDIA_TYPES)),
)
if not digest.strip():
raise ManifestError(
f"Cannot resolve tag {reference!r}",
"HEAD response is missing Docker-Content-Digest",
)
reference = digest

if fetch_all:
result = list_referrers_all(
client=client,
Expand All @@ -129,7 +151,7 @@ def referrer_list(ctx, image_ref, artifact_type, fetch_all, as_json, output):
digest=reference,
artifact_type=artifact_type,
)
except (AuthError, ReferrerError, requests.exceptions.RequestException) as exc:
except (AuthError, ManifestError, ReferrerError, requests.exceptions.RequestException) as exc:
emit_error(image_ref, str(exc))

if as_json:
Expand All @@ -156,4 +178,3 @@ def referrer_list(ctx, image_ref, artifact_type, fetch_all, as_json, output):
# Internal helpers — output and error
# ===========================================================================


Loading
Loading