Skip to content

feat: add renderPatches support for --patches/patches.yaml - #696

Merged
dmmordvi merged 6 commits into
2from
feat/render-patches
Aug 24, 2026
Merged

feat: add renderPatches support for --patches/patches.yaml#696
dmmordvi merged 6 commits into
2from
feat/render-patches

Conversation

@dmmordvi

@dmmordvi dmmordvi commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a second kind of rule to patches.yaml / --patches: renderPatches, applied to the rendered
resources right after the chart is rendered. Unlike diffPatches, which only normalize the live and
dry-apply objects before they are compared, render patches change what goes into the release and to
the cluster — a way to fix up a resource you do not own (a subchart's manifest, a field a controller
fights over, metadata a policy engine demands) without forking templates.

Key changes

  • New renderPatches top-level key, next to diffPatches, in both chart-shipped patches.yaml and
    --patches files: same rule shape (match, type: jq, patch), same chart-subtree scoping for
    chart-shipped rules, same "jq program must return exactly one object" contract.
  • New pipeline stage spec.BuildRenderPatchedResourceSpecs, placed after list expansion and before
    the existing patchers, so rules match real resources and nelm's own metadata lands on top of user
    patches. Semantics specific to render patches:
    • a patch may not change the resource identity (apiVersion/kind/name/namespace), and output
      that is not a resource at all is rejected outright;
    • metadata.annotations and metadata.labels in the output must be string maps, the same check the
      manifest decoder does: a non-string value (.metadata.annotations["werf.io/weight"] = 10) is
      rejected instead of being silently dropped by the apimachinery accessors, which would leave
      ResourceMeta without the annotations while the object keeps the bad value;
    • StoreAs is re-derived from the patched object, so adding or removing helm.sh/hook in a render
      patch really does reclassify the resource. StoreAsNone, carried by standalone CRDs, is exempt
      and survives patching — it is not releasable at all, and without the exemption a single render
      rule anywhere would turn every standalone CRD into a released, release-owned resource;
    • rules are applied in order and each matches against the result of the preceding ones, so a rule
      can key off a label or annotation an earlier rule added. ApplyPatches is deliberately not
      reused for this: its matchers must keep seeing the unpatched resource, so that diff patches take
      the same rule path for the live and the dry-apply object.
  • Patch machinery renamed to be kind-neutral (DiffPatchPatch, CompiledDiffPatchCompiledPatch,
    ApplyDiffPatchesApplyPatches, diff_patch.gopatches.go): matching, compiling and
    transforming were never diff-specific, only the point of application is.
  • User jq programs are cancellable: CompiledPatch.transform runs gojq via RunWithContext, and
    ApplyPatches takes a context.Context, threaded from the operation context in both the render
    stage and the live/dry-apply diff paths — a looping patch (def f: f; f) now aborts on Ctrl-C or
    command timeout with context.Canceled instead of hanging nelm. Matters more with this PR, since
    chart render/chart lint now execute user jq locally.
  • One resolve-and-compile point: resolvePatches collects chart-shipped and file rules and compiles
    both kinds immediately, so a bad regexp or jq program fails before anything is written into a
    release or sent to the cluster. plan.BuildResourceInfosOptions.DiffPatches now takes compiled
    patches and the compile step inside BuildResourceInfos is gone.
  • Wiring: render patches apply in release install, release plan install, chart render and
    chart lint. release rollback, release uninstall and the rollback-on-failure path render
    nothing, so they use diff patches only and ignore render rules silently instead of erroring — the
    rules were already baked into the stored release, and the same patches file has to work for every
    command.
  • --patches and --no-default-patches added to chart render and chart lint, which had no patch
    flags at all; chart lint now also honours diff patches, which it silently dropped before.
  • Docs and tests: docs/reference.md regenerated; existing patch tests moved to the new names and
    extended to cover both kinds; new patches_ai_test.go covers matching, chart scope, namespace
    handling, identity rejection, StoreAs re-derivation and the StoreAsNone exemption, chaining by
    patched metadata, non-string annotations/labels, and contract violations, plus cancellation of an
    infinite jq program through Transform, ApplyPatches and BuildRenderPatchedResourceSpecs.

Why

diffPatches can only hide drift — there was no supported way to adjust a rendered manifest, so the
options were forking the subchart or post-processing YAML outside nelm. Render patches keep that fix
in the chart (or in a CI-supplied patches file), reuse the matcher and jq machinery diffPatches
already ships, and stay visible in nelm chart render output.

Verification

  • Hand-run against a scratch chart with the built binary, chart render in local mode (no cluster):
    chart-shipped renderPatches dropped spec.replicas and added a label in the rendered output,
    --no-default-patches ignored them, a rule from --patches applied, and helm.sh/hook added by a
    render patch showed up in the manifest.
  • Error paths, same setup: identity change (.metadata.name = "renamed"), output that is not a
    resource (.spec, .spec.template.spec.containers[]), empty, ., ., .metadata.name, and an
    unparsable jq program — the last fails at resolve time, before any rendered result is used.
  • task test:unit (full suite) plus task test:unit tags=ai_tests paths="./pkg/resource/spec" for
    the new tagged tests.
  • Linted with explicit package paths (golangci-lint run <pkg dirs except helm> ./cmd/...), 0 issues.
    Worth knowing: on macOS task lint silently narrows to ./cmd/..., because the Taskfile's default
    path expression relies on GNU find -printf.
  • Review fixes (StoreAsNone exemption, metadata string-map check, per-rule matcher re-derivation)
    have no hand-run of their own beyond unit tests. The live-cluster run below is on the PR head
    commit, so it exercises the render path with those fixes in place, but does not target them.
  • Live cluster (minikube, Kubernetes 1.34), scratch chart shipping a renderPatches rule over a
    ConfigMap: chart render applied the rule, --no-default-patches ignored it, and a rule from
    --patches layered on top of the chart-shipped one. release install put the patched data in
    the cluster; editing the rule made release plan install show the expected -/+ diff and the
    next release install performed an Update; release uninstall removed the resource.
  • Context-cancellation fix: covered by the three new tagged unit tests only; no separate hand-run.
  • Not run: release rollback and the rollback-on-failure path were never exercised against a
    cluster, so that wiring is covered by unit tests only.

Review focus / risks

  • The rename touches every diff-patch call site. diffPatches is unreleased (it exists only on 2,
    in no tag), so there is no user-facing break, but plan.BuildResourceInfosOptions.DiffPatches
    changing to []*spec.CompiledPatch is an API change for embedders.
  • namespaces: matches differently at render time on purpose: there is no live object, so an empty
    namespace is treated as the release namespace and cluster-scoped resources are indistinguishable
    from namespaced ones without an explicit namespace. Combine namespaces: with kinds: when that
    matters. pkg/resource/spec/resource_spec.go carries the reasoning.
  • StoreAs is re-derived only in the render-patch stage, deliberately: the shared
    BuildPatchedResourceSpecs loop is untouched, so --add-annotation helm.sh/hook=... still does not
    reclassify a resource while a render patch does.
  • Render patches reach the cluster, so the cost of a bad rule is higher than for diff patches. Guarded
    by the identity check, the one-object contract and the metadata string-map check; not guarded
    against value-level mistakes elsewhere in the object, e.g. jq division yields a float and
    .spec.replicas /= 2 renders 1.5 for the API to reject.
  • chart lint starts applying diff patches, which changes its dry-run diff results for charts that
    ship patches.yaml.

Review in cubic

Signed-off-by: Dmitry Mordvinov <dmitry.mordvinov@flant.com>
Signed-off-by: Dmitry Mordvinov <dmitry.mordvinov@flant.com>
… re-derived ResMeta after patching

Signed-off-by: Dmitry Mordvinov <dmitry.mordvinov@flant.com>
Signed-off-by: Dmitry Mordvinov <dmitry.mordvinov@flant.com>
Signed-off-by: Dmitry Mordvinov <dmitry.mordvinov@flant.com>
Signed-off-by: Dmitry Mordvinov <dmitry.mordvinov@flant.com>
@dmmordvi
dmmordvi merged commit 191171c into 2 Aug 24, 2026
8 checks passed
@dmmordvi
dmmordvi deleted the feat/render-patches branch August 24, 2026 11:40
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.

1 participant