diff --git a/hips/hip-9999.md b/hips/hip-9999.md new file mode 100644 index 00000000..fe06c83a --- /dev/null +++ b/hips/hip-9999.md @@ -0,0 +1,278 @@ +--- +hip: "9999" +title: "OCI Artifact Selection and Referrers Support" +authors: ["Aleksei Sviridkin "] +created: "2025-11-29" +type: "feature" +status: "draft" +--- + +## Abstract + +This proposal gives Helm a deterministic way to select a chart from an OCI Image Index that holds more than one manifest, and adds optional support for the OCI Referrers API. Since v3.20.2 / v3.21 / v4.2 Helm no longer errors on an Image Index (regression fix [helm/helm#31776](https://github.com/helm/helm/pull/31776)), but it still picks the chart by scanning for hardcoded Helm media types and keeping the last match — with no awareness of `artifactType`, chart name, or version. For an index that carries a chart alongside its container image, or more than one chart, that is order-dependent and can silently return (and mislabel) the wrong chart. This HIP defines selection by the descriptor `artifactType` (with a `config.mediaType` fallback), disambiguation by chart name and version via descriptor annotations, sets `artifactType` on `helm push`, and optionally associates a chart with an image through the `subject` field and Referrers API. + +## Motivation + +The OCI Image and Distribution specifications v1.1 (February 2024) introduced first-class artifacts, per-descriptor `artifactType`, and the Referrers API — primitives that let a single OCI reference hold multiple related manifests and express relationships between them. These primitives did not exist when Helm's OCI support was designed, so Helm still treats one OCI reference as exactly one chart. + +Today a publisher who wants both a chart and its container image under one version must fall back to workarounds: + +- Tag suffixes (`:v1.0.0` for the image, `:v1.0.0-helm` for the chart). +- Separate repository paths (`registry/app` vs `registry/app-chart`). +- Entirely separate registries. + +These break atomic versioning, complicate CI/CD, and need extra tooling to keep the two artifacts in lockstep. The idea of using an Image Index to carry these together is not new — it was raised as far back as [helm/helm#8332](https://github.com/helm/helm/issues/8332) (2020) — but the OCI 1.1 primitives needed to do it cleanly only landed in 2024. + +The relevant gap is no longer that Helm crashes on an index. That regression was fixed in [helm/helm#31776](https://github.com/helm/helm/pull/31776). The gap is that Helm's selection logic is not artifact-aware. In `pkg/registry/client.go` the pull collects every descriptor whose media type is in a fixed allow-list (across the whole index graph) and then assigns the chart by a `switch` over media types that keeps the last match: + +```go +// pkg/registry/client.go (v4.2.2), lines 462-475 +for _, descriptor := range genericResult.Descriptors { + d := descriptor + switch d.MediaType { + case ConfigMediaType: + configDescriptor = &d // overwritten on each match -> last wins + case ChartLayerMediaType: + chartDescriptor = &d // last wins + case LegacyChartLayerMediaType: + chartDescriptor = &d + } +} +``` + +There is no use of `artifactType`, no `platform` filtering, and no matching against the requested chart name or version. Two consequences follow for any index with more than one chart-shaped manifest: + +- **Order-dependent selection.** Reordering the manifests in the index changes which chart you get. +- **Silent mislabeling.** Helm names the downloaded chart from the last path segment of the reference, not from the selected chart's metadata, so a pull of `oci://registry/app` can write `app-1.0.0.tgz` that actually contains a different chart. + +Selecting on the descriptor `artifactType` reads what is already in the index response, so it fetches fewer manifests than the current path, not more; single-manifest charts (the vast majority) are unaffected. Pulling and installing stay pure Helm — only assembling a multi-artifact index needs external tooling (`oras`, `crane`), which is an opt-in publisher choice this HIP does not add to Helm. + +Tools such as ArtifactHub, Argo CD, and GitOps pipelines would benefit from a single, digest-pinned source of truth per version. The OCI 1.1 specifications already provide the mechanism (`artifactType` on index descriptors, annotations for naming, the Referrers API for relationships); Helm should use them. + +## Real-world motivation + +This is not hypothetical. As a maintainer of [Cozystack](https://github.com/cozystack/cozystack) (a CNCF Sandbox Kubernetes platform), I hit the chart/image split directly in air-gapped delivery. + +Cozystack publishes its ≈158 first-party charts (223 including vendored sub-charts) as a single OCI artifact (`cozystack-packages`) and its 30+ first-party container images as separate `ghcr.io/cozystack/cozystack/*` references. The charts are fetched by Flux's source-controller over its own OCI/HTTP client; the images are pulled by containerd. There is no per-component unit that co-versions a chart with the image it deploys, so an operator mirroring the platform into an air-gapped registry runs two independent flows with two redirection mechanisms, and nothing ties a chart digest to its image digest. + +Being able to ship a chart and its image as one digest-pinned OCI reference — and to pull the chart out of a multi-artifact bundle deterministically by name — would remove a whole class of "which chart goes with which image, and did both get mirrored" bookkeeping. The same need shows up for anyone assembling an offline bundle of many charts with external tooling (`oras`, `crane`): the bundle is assemblable today, but Helm cannot select a specific chart from it by name. + +## Rationale + +### Why `artifactType` first, then `config.mediaType` fallback + +The OCI Image Specification states that "artifacts have historically been created without an `artifactType` field, and tooling to work with artifacts should fallback to the `config.mediaType` value." Following that guidance keeps compatibility with charts pushed by current Helm (which do not set `artifactType`), by third-party tools, and by registries that do not preserve it. + +### Selection by `artifactType` is a Helm policy over a spec-provided field + +The OCI Image Index spec defines `artifactType`, `platform`, and `annotations` as per-descriptor fields and says "if multiple manifests match a client or runtime's requirements, the first matching entry SHOULD be used," but it only spells out `platform` matching in detail. Selecting an entry by `artifactType` is therefore a Helm-defined policy layered on a spec-provided field, not an OCI mandate. It is sound because `artifactType` equals the config `mediaType` for an image manifest, so it identifies a Helm chart at the index level without fetching every manifest. + +### Why disambiguate by chart name and version + +`artifactType` alone identifies "this is a Helm chart," not "this is the chart you asked for." An index may carry several charts. To select deterministically, Helm matches the requested chart name against the `org.opencontainers.image.title` annotation and the requested version against `org.opencontainers.image.version` (falling back to the chart's `Chart.yaml` after fetch when annotations are absent). `org.opencontainers.image.ref.name` is deliberately not used: the spec scopes it to the OCI image-layout (`index.json` on disk), not to registry indexes. + +### Why skip descriptors with a `platform` field + +Descriptors with a `platform` field are architecture-specific container images. A Helm chart is not platform-specific, so skipping platform descriptors avoids needless manifest fetches and never confuses an image for a chart. + +### Why add Referrers API support + +The Referrers API lets a client discover all artifacts (charts, SBOMs, signatures) associated with a specific image via the `subject` field. For Helm this enables finding the chart that deploys a given image, keeping chart and image together, and security workflows that link a deployment to its image. + +This association is optional and explicit: `--subject` takes a user-supplied digest, Helm does not infer which images a chart templates (a chart often references several, and the attached image need not be one of them), and the selection logic in §1 works with no `subject` at all. The two halves of this HIP are independent — selection is the load-bearing change and can be evaluated on its own. + +## Specification + +This HIP introduces three related changes. + +### 1. Artifact selection from an Image Index + +When `helm pull`, `helm install`, or `helm dependency update` resolves an OCI reference to an Image Index (`application/vnd.oci.image.index.v1+json`), Helm MUST select a chart manifest as follows: + +1. **Candidate set.** Consider only descriptors WITHOUT a `platform` field. A descriptor is a chart candidate if its `artifactType` equals `application/vnd.cncf.helm.config.v1+json`. If no descriptor carries `artifactType` (legacy indexes), fall back to fetching the non-platform manifests and treating those whose `config.mediaType` equals the Helm config media type as candidates. +2. **Disambiguation.** If more than one candidate remains, match the requested chart name against the descriptor's `org.opencontainers.image.title` annotation, and the requested version against `org.opencontainers.image.version` (or the chart version after fetch). The requested chart name is the last path segment of the reference; the version is the resolved `--version`. +3. **Resolution.** Select the unique candidate that matches. If exactly one candidate exists overall, select it. If multiple candidates remain after disambiguation (ambiguous), return an error listing the candidates rather than guessing — this replaces today's silent last-match-wins behavior. +4. **Not found.** If no candidate matches, return an error indicating no matching Helm chart was found in the index. + +Example index carrying a container image and a chart: + +```json +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.index.v1+json", + "manifests": [ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:abc123...", + "size": 401, + "platform": { "architecture": "amd64", "os": "linux" } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:def456...", + "size": 587, + "artifactType": "application/vnd.cncf.helm.config.v1+json", + "annotations": { + "org.opencontainers.image.title": "demoapp", + "org.opencontainers.image.version": "0.1.0" + } + } + ] +} +``` + +Helm skips the first descriptor (it has a `platform`) and selects the second by `artifactType`; the `title`/`version` annotations let it pick `demoapp` specifically when the index holds more than one chart. + +### 2. Set `artifactType` on push + +`helm push` MUST set the manifest `artifactType` to `application/vnd.cncf.helm.config.v1+json` (equal to the config `mediaType`) and SHOULD set the `org.opencontainers.image.title` and `org.opencontainers.image.version` annotations to the chart name and version. This exposes the chart's identity at the index level so selection needs no manifest fetch. + +```json +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "artifactType": "application/vnd.cncf.helm.config.v1+json", + "config": { + "mediaType": "application/vnd.cncf.helm.config.v1+json", + "digest": "sha256:...", + "size": 137 + }, + "layers": [ "..." ] +} +``` + +### 3. Referrers API support + +A new optional `--subject` flag on `helm push` associates a chart with another artifact (typically the container image it deploys) that already lives in the same repository: + +```bash +helm push mychart-1.0.0.tgz oci://registry.example.com/myapp --subject sha256:abc123... +``` + +`--subject` takes a digest. Because the Referrers API is per-repository, the subject must already exist in the chart's target repository; Helm resolves the digest against that repository to a complete descriptor (`mediaType` + `size`) so the manifest carries a spec-valid `subject` — a digest-only descriptor is not valid and a Referrers-aware registry may reject it. Helm then sets the manifest `subject` field and handles the registry response per OCI Distribution Spec 1.1: if the registry returns the `OCI-Subject` header the referrer is tracked automatically; otherwise Helm MUST fall back to the Referrers Tag Schema. A cross-repository subject reference is out of scope here (see Open Issues). + +### 4. Push into an existing tag + +`helm push` keeps its current behavior of overwriting the tag, and this HIP does NOT add index-merge or auto-index-creation logic to Helm. Assembling a multi-artifact Image Index (a chart with its image, or several charts) is done with external tooling (`oras`, `crane`) and pushed as a unit; Helm only needs to *select* from such an index on pull. Letting `helm push` publish a manifest by digest / without moving a tag — so a chart can be folded into an externally-assembled index without clobbering an existing tag — is a reasonable enhancement, tracked in Open Issues rather than specified here, to avoid baking implicit index logic into `helm push`. + +## Backwards Compatibility + +This proposal is backwards compatible: + +1. **Single manifests** (the overwhelming majority of charts today) are unaffected — there is no index to select from. +2. **Existing charts without `artifactType`** remain selectable through the `config.mediaType` fallback. +3. **`artifactType` is optional per OCI spec**; registries MUST NOT error on it. +4. **`--subject` is opt-in**; existing `helm push` workflows are unchanged. +5. **Selection becomes safer, not different, for the common case.** A reference that resolves to a single chart behaves exactly as before; only ambiguous multi-candidate indexes change — from silent last-match-wins to deterministic selection or an explicit error. + +## Security Implications + +1. **No new attack vectors.** Selection uses the same validation applied after a manifest fetch; the chart bytes are still digest-verified. +2. **Referrers association is weak by design.** `subject` expresses an association, not a cryptographic binding; integrity still rests on digests and any signatures attached as referrers. +3. **Registry trust model unchanged.** Users still trust the registry to serve correct manifests. +4. **Removes a silent-mislabeling foot-gun.** Erroring on ambiguity instead of returning the last match prevents a chart being delivered under another chart's name. + +## How to Teach This + +### Documentation updates + +1. Document Image Index support and selection rules in the helm.sh OCI guide. +2. Show how to assemble a multi-artifact Image Index with `oras` or `crane`. +3. Document the `--subject` flag and Referrers integration. + +### Example: assemble a chart + image index + +```bash +# Push the chart (helm sets artifactType + title/version annotations under this HIP) +helm push myapp-1.0.0.tgz oci://registry.example.com/myapp + +# Build a scratch image (or use a real one) in the same repository +crane append --oci-empty-base --new_layer layer.tar --new_tag registry.example.com/myapp:appimg + +# Assemble an index referencing both manifests, then push it to the chart's tag +oras manifest push registry.example.com/myapp:1.0.0 \ + --media-type application/vnd.oci.image.index.v1+json index.json + +# Helm selects the chart from the combined reference +helm pull oci://registry.example.com/myapp --version 1.0.0 +``` + +### Example: associate a chart with its image + +```bash +IMAGE_DIGEST=$(crane digest registry.example.com/myapp:appimg) +helm push myapp-1.0.0.tgz oci://registry.example.com/myapp --subject "$IMAGE_DIGEST" +oras discover registry.example.com/myapp@"$IMAGE_DIGEST" +``` + +## Reproduction + +The current behavior and the gap this HIP closes are reproducible offline against the reference OCI registry (`registry:2`) with `helm` ≥ v3.20.2, `oras`, and `crane`. + +```bash +docker run -d -p 5001:5000 registry:2 +helm create demoapp && (cd demoapp && sed -i 's/^version:.*/version: 0.1.0/' Chart.yaml) +helm package demoapp +helm push demoapp-0.1.0.tgz oci://localhost:5001/poc --plain-http # -> poc/demoapp:0.1.0 + +# Assemble a chart+image index at poc/demoapp:0.1.0 (image manifest + chart manifest +# with artifactType); see index.json in this PoC. Then: +helm pull oci://localhost:5001/poc/demoapp --version 0.1.0 --plain-http +# Pulled: the chart is extracted from the mixed index (works on >= v3.20.2 / v4.2; +# fails on v3.18.0-v3.20.1 / v4.0-v4.1 with "image.index...: not found"). +``` + +The gap — order-dependent, name-blind selection — reproduces with two charts in one index: + +```bash +# Index order [demoapp, otherapp], pull .../poc/demoapp -> file demoapp-*.tgz, content demoapp +# Index order [otherapp, demoapp], pull .../poc/demoapp -> file demoapp-*.tgz, content OTHERAPP +``` + +Reversing the manifest order flips the chart returned, while the output file keeps the requested name — demonstrating both the non-determinism and the mislabeling that selection by `artifactType` + `title`/`version` removes. + +## Reference Implementation + +A reference implementation is available at [helm/helm#31583](https://github.com/helm/helm/pull/31583). + +## Rejected Ideas + +### Requiring an explicit `--artifact-type` flag + +Rejected as needless verbosity; the artifact type is always `application/vnd.cncf.helm.config.v1+json` for Helm charts. + +### `artifactType` without the `config.mediaType` fallback + +Rejected; it would break existing charts and third-party tooling that do not set `artifactType`. + +### Using `org.opencontainers.image.ref.name` for name selection + +Rejected; the OCI spec scopes `ref.name` to the image-layout (`index.json` on disk), not registry indexes. `org.opencontainers.image.title` is the registry-valid carrier for an artifact name. + +### Keeping last-match-wins for ambiguous indexes + +Rejected; returning the last media-type match silently is exactly the mislabeling foot-gun this HIP removes. Ambiguity that cannot be resolved by name/version MUST be an error. + +### Making `--subject` mandatory or auto-detecting the subject + +Rejected; most users do not need Referrers integration, and auto-detecting the "main" image in an index is ambiguous and may not match intent. Explicit is better than implicit. + +## Open Issues + +1. **Fetch a chart via the Referrers API.** Should `helm pull` support fetching the chart that refers to an image, e.g. `helm pull --referrer-of oci://registry.example.com/myapp@sha256:abc123...`? +2. **Multiple charts referring to one image.** When several charts reference the same image via the Referrers API, return all and let the user select. +3. **Push without a tag / by digest.** Allow `helm push` to publish a manifest without moving a tag (pull already works by digest), so a chart can be folded into an externally-assembled index without overwriting an existing tag. +4. **Cross-repository subject.** Accept an `--subject` that points at a different repository (resolving the descriptor there), rather than requiring the subject to live in the chart's own repository. + +## References + +- [OCI Image Specification v1.1](https://github.com/opencontainers/image-spec/releases/tag/v1.1.0) +- [OCI Distribution Specification v1.1](https://github.com/opencontainers/distribution-spec/releases/tag/v1.1.0) +- [OCI Image Index Specification](https://github.com/opencontainers/image-spec/blob/main/image-index.md) +- [OCI Descriptor Specification](https://github.com/opencontainers/image-spec/blob/main/descriptor.md) +- [OCI Annotations](https://github.com/opencontainers/image-spec/blob/main/annotations.md) +- [HIP-0006: OCI Support](https://github.com/helm/community/blob/main/hips/hip-0006.md) +- [helm/helm#8332: SemVer and signed releases with OCI registries (2020, the original ask)](https://github.com/helm/helm/issues/8332) +- [helm/helm#31776: regression fix — pull from OCI indices](https://github.com/helm/helm/pull/31776) +- [helm/helm#31582: Support for OCI Image Index](https://github.com/helm/helm/issues/31582) +- [helm/helm#31583: Reference Implementation](https://github.com/helm/helm/pull/31583)