Conversation
Implements a new `oci-push` builtin promotion step that copies or retags OCI artifacts (container images and Helm charts) between registries as part of a promotion pipeline. Uses go-containerregistry for server-side copy with support for multi-arch images and manifest annotations. Also extracts shared OCI registry helpers (reference parsing, credential resolution, HTTP transport) from oci-download into oci_common.go for reuse by both steps. Refs: #3762 Signed-off-by: Eron Wright <eron.wright@akuity.io>
✅ Deploy Preview for docs-kargo-io ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
E2E Test ResultsTested the Setup
Promotionsteps:
- uses: oci-push
as: retag-chart
config:
imageRef: "oci://ghcr.io/eronwright/charts/mychart:${{ chartFrom('oci://ghcr.io/eronwright/charts/mychart').Version }}"
destRef: "oci://ghcr.io/eronwright/charts/mychart:promoted"
annotations:
io.kargo.promoted-by: "kargo"
io.kargo.stage: "${{ ctx.stage }}"Result: ✅ SucceededStep outputs: Manifest annotations verified on GHCR: {
"io.kargo.promoted-by": "kargo",
"io.kargo.stage": "retag",
"org.opencontainers.image.created": "2026-02-23T16:06:15-08:00",
"org.opencontainers.image.description": "A minimal chart for testing oci-push",
"org.opencontainers.image.title": "mychart",
"org.opencontainers.image.version": "0.2.0"
} |
Signed-off-by: Eron Wright <eron.wright@akuity.io>
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #5782 +/- ##
==========================================
+ Coverage 56.16% 56.33% +0.17%
==========================================
Files 450 455 +5
Lines 37742 38155 +413
==========================================
+ Hits 21198 21496 +298
- Misses 15284 15370 +86
- Partials 1260 1289 +29 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
mutate.Annotations wraps images in mutate.image, whose Layers() enumerates layers via ConfigFile().RootFS.DiffIDs. For non-Docker OCI artifacts (e.g. Helm charts), the config blob has no RootFS field, so DiffIDs is empty and Layers() returns nothing. This causes blobs to be omitted from cross-repository pushes, resulting in MANIFEST_BLOB_UNKNOWN errors on registries like GHCR. Replace mutate.Annotations with thin annotatedImage and annotatedIndex wrappers that override only manifest-related methods (Manifest, RawManifest, Digest, Size) while delegating Layers() and all other methods to the base image. This preserves MountableLayer wrapping for cross-repo blob mounting and avoids the broken DiffIDs enumeration path. Signed-off-by: Eron Wright <eron.wright@akuity.io>
E2E Test Report: Cross-repo Helm chart push with annotations on GHCRTested cross-repository OCI push of a Helm chart with annotations to a brand-new GHCR repository (no pre-existing blobs). Test setup (namespace
Result: Manifest on GHCR ( {
"config": { "mediaType": "application/vnd.cncf.helm.config.v1+json" },
"layers": [{ "mediaType": "application/vnd.cncf.helm.chart.content.v1.tar+gzip" }],
"annotations": {
"io.kargo.promoted-by": "kargo",
"io.kargo.source-repo": "ghcr.io/eronwright/charts/mychart",
"io.kargo.test": "custom-wrapper",
"org.opencontainers.image.created": "2026-02-23T16:06:15-08:00",
"org.opencontainers.image.description": "A minimal chart for testing oci-push",
"org.opencontainers.image.title": "mychart",
"org.opencontainers.image.version": "0.2.0"
}
}
Previously tested in earlier iterations: retag-in-place (same repo), cross-repo without annotations, image index push. All passed. |
Allow annotation keys to be prefixed with "index:" or "manifest:" to control whether they target the index manifest or child image manifests. Unprefixed keys default to the image manifest. For single images, "index:"-prefixed keys are silently ignored. When manifest annotations are applied to an image index, child descriptors are rewritten with updated digests and sizes to stay consistent with the annotated content. Signed-off-by: Eron Wright <eron.wright@akuity.io> Entire-Checkpoint: e87ef149e998
Move annotatedImage/annotatedIndex types from oci_pusher.go into a dedicated mutate.go with a unified Annotations() entrypoint that type-checks for v1.Image vs v1.ImageIndex. Adopt the compute() pattern with sync.Mutex for thread-safe lazy evaluation, matching the go-containerregistry mutate package style. Consolidate pushImage/pushIndex into a single push() method that delegates annotation handling to Annotations(). Add mutate_test.go covering Docker images, OCI/Helm images (empty DiffIDs regression), and multi-arch indexes. Signed-off-by: Eron Wright <eron.wright@akuity.io>
E2E Test: Container Image (multi-arch nginx)Tested Pure copy (no annotations)Digest is preserved across registries:
The pushed image was verified with With scoped annotationsAnnotations used: annotations:
io.kargo.promoted-by: kargo
io.kargo.source-repo: public.ecr.aws/nginx/nginx
"index:io.kargo.index-test": index-annotation-value
"manifest:io.kargo.manifest-test": manifest-annotation-valueAll digests changed as expected (annotations mutate manifest content):
Annotation scoping worked correctly:
|
Enforce a 1 GiB maximum compressed artifact size when oci-push copies artifacts across repositories, preventing accidental promotion of very large images through the pipeline. The limit is skipped for same-repo retags since no blob transfer occurs. Size is computed from manifest metadata without downloading blobs. Signed-off-by: Eron Wright <eron.wright@akuity.io>
|
Added a 1 GiB size limit for cross-repository copies in Same-repository retags skip the check since no blob transfer occurs. Tested against the live cluster:
|
|
Verified that same-repo retag + annotate correctly bypasses the size limit:
So the three cases work as expected:
|
|
Closing this PR for now, in favor of a more limited solution focusing on tagging, not replication. To recap, this PR offers an |
|
Re-opening after further offline discussions. |
Signed-off-by: Eron Wright <eron.wright@akuity.io>
…_SIZE Allow hosted environments to tune, disable, or block cross-repo OCI pushes by reading an integer env var at runner creation time. Unset defaults to 1 GiB (preserving current behavior), 0 blocks all cross-repo pushes, and -1 disables the limit entirely. Signed-off-by: Eron Wright <eron.wright@akuity.io>
Use human-friendly byte formatting (GiB/MiB/KiB/bytes) via new pkg/fmt.FormatBytes helper. Add a clear "cross-repository push is disabled" message when the limit is set to zero. Signed-off-by: Eron Wright <eron.wright@akuity.io>
There was a problem hiding this comment.
Note that these functions were extracted from oci_downloader.go to oci_common.go for sharing purposes with oci_pusher.go.
Expose MAX_OCI_PUSH_ARTIFACT_SIZE as a first-class Helm value instead of requiring raw env var overrides via controller.env. Signed-off-by: Eron Wright <eron.wright@akuity.io>
6c78442 to
8a81a2b
Compare
Summary
Adds a new
oci-pushpromotion step that copies/retags OCI artifacts (container images and Helm charts) between registries.parseOCIReference,buildOCIRemoteOptions, etc.) fromoci-downloadintooci_common.goso both steps reuse the same credential resolution and transport logicoci-pushwithStepCapabilityAccessCredentialsoci://prefix)annotationsfield to set OCI manifest annotations on the pushed artifact, with scoped prefixes (index:,manifest:) for controlling placement on image indexes vs child manifestsmutate.go) to work around a limitation ofgo-containerregistry.image(destination ref),digest(sha256),tagCloses #3762
Test plan
go test -race ./pkg/promotion/runner/builtin/...)make lint-go— no issues in changed files)compressed artifact size 6.1 GiB exceeds maximum allowed size of 1.0 GiB