Skip to content

Add helm_oci_manifest so rules_img's multi_deploy can push a chart - #305

Open
rdark wants to merge 2 commits into
periareon:mainfrom
rdark:richard/pla-2603-helm-deploy-multi-deploy
Open

rdark wants to merge 2 commits into
periareon:mainfrom
rdark:richard/pla-2603-helm-deploy-multi-deploy

Conversation

@rdark

@rdark rdark commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Closes #276.

multi_deploy pushes many images from one bazel run (concurrently in later versions), but each chart still needs its own bazel run of helm_push. helm_push cannot provide DeployInfo, because DeployInfo describes an image push or load, not a command to run. This PR describes the chart as the OCI artifact helm push creates, and lets rules_img push that.

Digest
The pushed manifest is byte-identical to the one helm push creates, so its digest is the one helm_oci_digest gives. For tests/helm_oci_manifest:simple, helm_oci_manifest, helm_oci_digest and helm push (Helm 3.16.3 and 4.3.1) all give sha256:b6137ff1…. rules_img writes a top-level mediaType that helm push does not, so helm_oci_manifest sets omit_media_type (rules_img#805, released in 0.3.24) to leave it out.

Usage

load("@rules_helm//helm:defs.bzl", "helm_chart")
load("@rules_helm//helm:rules_img.bzl", "helm_oci_manifest")
load("@rules_img//img:multi_deploy.bzl", "multi_deploy")

helm_chart(name = "chart")

# Describes the push; it is not runnable on its own.
helm_oci_manifest(name = "chart_oci_manifest", chart = ":chart", registry_url = "oci://registry.example.com/charts")

# `bazel run :deploy` pushes the chart. Image push targets can go in the same list.
multi_deploy(name = "deploy", operations = [":chart_oci_manifest"])

The target is an image_manifest with the Helm config media type, the chart .tgz as its only layer, the annotations helm push writes, and an image_push_spec for <registry_url>/<chart name>:<chart version> (+ in the version becomes _, as Helm does). It is built from rules_img's public rules only. helm_chart does not create it; it is opt-in through the new helm/rules_img.bzl.

Not supported, and documented in the macro: push_cmd and plugins such as cm-push, HELM_REGISTRY_USERNAME/HELM_REGISTRY_PASSWORD login (credentials come from rules_img's Docker config and credential helpers), include_images, and charts from helm_import (their metadata has no created value).

Two issues a user might otherwise hit: rules_img expands Go templates in annotation values, so {{ in chart metadata is escaped; and the manifest sets soci = "disabled", since a SOCI index would add an annotation helm push does not write.

Dependency change

rules_img is now a non-dev dependency at 0.3.24, the first release with omit_media_type. 0.3.14 is the first version that reads annotations_file as a JSON object with values verbatim; earlier versions split KEY=VALUE lines and trim them. 0.3.18 adds multi_deploy --sink, which the offline test uses. omit_platform (0.3.21) is not needed: it changes only the manifest descriptor, and the pushed bytes on 0.3.24 are identical with and without it.

This has a cost for consumers who never load helm/rules_img.bzl. In a throwaway consumer module with one helm_chart, built from an empty repository cache, the change added rules_img+ (about 1.3 MB of source), hermetic_launcher+ (164 KB) and the img_toolchain hub repo (20 KB) to the fetched repositories, because both modules register toolchains. No img tool binaries were fetched. Module resolution also raises rules_img to at least 0.3.24, bazel_features to 1.47.0 and package_metadata to 0.0.10, and adds hermetic_launcher 0.0.17.

Moving to 0.3.17 or later broke tests/with_image_deps_img on Linux, because img_pull started making :image an alias. #304 fixed that before this PR.

Implementation

The chart-reading and annotation code moves out of oci_digest.go into helm/private/oci_digest/ociartifact, unchanged. oci_digest keeps its output (the simple and created digest goldens pass unchanged), and a new oci_parts tool writes the config blob, annotations, tag and destination for rules_img.

Testing

  • tests/helm_oci_manifest runs each multi_deploy with --sink oci:<dir>, offline, and checks the Helm config and layer media types, that the layer is the chart .tgz, that the config blob and manifest equal goldens from a real helm push, that each manifest's digest equals helm_oci_digest's, the reference for a trailing-slash registry_url and for registry_url_file, a {{ description and a + version. It also passes with SOCI forced on, and fails on every chart's digest with omit_media_type = False.
  • Go unit tests for ociartifact and oci_parts.
  • bazel test //... on macOS with Bazel 9.3.1 (with and without --config=strict) and with Bazel 7.7.2 and --config=strict, as ci-min-bazel runs it: 68 pass, 3 skipped (the Linux-only with_image_deps_img tests).
  • By hand: pushed the three test charts to a local registry with bazel run //tests/helm_oci_manifest:deploy, and pushed the same .tgz files with helm push from Helm 3.16.3 and 4.3.1. Each chart got the same digest from all three pushes, equal to its helm_oci_digest output. helm pull, helm show chart and helm template accept the pushed charts with both Helm versions, and the pulled .tgz is byte-identical to the built one.
  • Not run locally: Linux and Windows (CI runs them), Helm 3.17.3, Flux pulling a chart, and registries that need credentials.

@rdark
rdark marked this pull request as ready for review October 19, 2026 15:32
@rdark
rdark force-pushed the richard/pla-2603-helm-deploy-multi-deploy branch 2 times, most recently from c286b80 to ab3e11c Compare October 20, 2026 07:46
@rdark rdark changed the title Add helm_deploy so rules_img's multi_deploy can push a chart Add helm_oci_manifest so rules_img's multi_deploy can push a chart Oct 20, 2026
@rdark

rdark commented Oct 20, 2026

Copy link
Copy Markdown
Contributor Author

CI failures are 429's from ECR

@rdark
rdark marked this pull request as draft October 24, 2026 16:59
@rdark
rdark force-pushed the richard/pla-2603-helm-deploy-multi-deploy branch from ab3e11c to 197c5e9 Compare October 24, 2026 16:59
@rdark

rdark commented Oct 24, 2026

Copy link
Copy Markdown
Contributor Author

@abrisco I moved back into draft status - I think it would be better to wait on a rules_img release with rules_img#805 in, then we have byte-identical manifests from day 1. Will move back out of draft when that happens

@rdark
rdark force-pushed the richard/pla-2603-helm-deploy-multi-deploy branch from 197c5e9 to 3d6e739 Compare November 5, 2026 11:48
`helm_push` cannot provide `DeployInfo`, so a chart could not be one of a
`multi_deploy`'s operations. `helm_oci_manifest` in the new
helm/rules_img.bzl describes the chart as the OCI artifact `helm push`
creates: an `image_manifest` with the Helm config media type, the chart
.tgz as its only layer, the annotations `helm push` writes, and a push
spec for `<registry_url>/<chart name>:<chart version>`.

The manifest sets `omit_media_type`, because `helm push` writes no
top-level `mediaType`. The pushed manifest is then byte-identical to the
`helm push` one, and its digest is the one `helm_oci_digest` gives.

Move the chart-reading and annotation code from oci_digest into a shared
ociartifact package, add an oci_parts tool for the rules_img inputs, and
make rules_img a non-dev dependency at 0.3.24, the first release with
`omit_media_type`.
@rdark
rdark force-pushed the richard/pla-2603-helm-deploy-multi-deploy branch from 3d6e739 to d289818 Compare November 5, 2026 13:01
@rdark

rdark commented Nov 5, 2026

Copy link
Copy Markdown
Contributor Author

@abrisco rules_img made a release with our needed changes in to make the manifest byte-identical so moved back out of draft

@rdark
rdark marked this pull request as ready for review November 5, 2026 13:04

@abrisco abrisco left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the delay here, I'll take a look this weekend!

Comment thread MODULE.bazel
bazel_dep(name = "rules_go", version = "0.63.0")
bazel_dep(name = "gazelle", version = "0.53.0")
bazel_dep(name = "bazel_lib", version = "3.0.0")
bazel_dep(name = "rules_img", version = "0.3.24")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way to avoid this being a mandatory dependency?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not with the existing macro in this module. Bazel doesn't have any concept of optional deps.

Few options that I can think of:

  1. Separate rules_helm_img module in this repo (e.g in module/rules_helm_img). Needs a second BCR module and release workflow change. Looks like rules_img published img_tool like this.
  2. Have the caller pass the rules in. Should work but a bit clunky and a weird API
  3. Ship just helm_oci_parts and then document 30 or so lines or rules_img wiring to copy. Also clunky.

If we really want to avoid this dep in the root module, I think 1. is probably the least worst option.

LMK what you think!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@abrisco I've opened a speculative PR demonstrating 1 here: rdark#2

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Include rules_img's DeployInfo provider on helm_push targets

2 participants