Repository navigation
fix(repo): fail the sync when template values cannot be loaded - #9
Merged
Merged
Conversation
`SyncRepos` logged a warning and carried on when `loadValuesFromConfigMap`
failed. That function returns `(nil, err)` on every error path, so `values` was
nil, the `if values != nil` guard installed no templating engine, and the copy,
commit and push all still ran — publishing files with their `{{ }}` placeholders
intact and then setting Available/Synced.
The resource is then terminal. `TargetCommitId` matches the commit that was just
pushed, so Observe sees an up-to-date resource: it never retries and never
corrects itself. An operator sees READY=True SYNCED=True over a target repo full
of unrendered templates.
The trigger does not need to be exotic. provider-runtime's GetConfigMapValue
returns `string(cm.Data[ref.Key])`, which for an ABSENT key is "" with a NIL
error — so a typo in `configMapKeyRef.key` does not fail as a missing key, it
reaches `json.Unmarshal([]byte(""))` and fails as "unexpected end of JSON input".
Writing YAML into a key the controller parses as JSON does the same, as does a
ConfigMap the provider cannot read.
A CR that sets `configMapKeyRef` has asked for substitution. If the values cannot
be read, the honest outcome is a failed sync — not a green one over unrendered
output. The warning log and the CannotLoadConfigMap event are kept; only the
silent continue is replaced with failSync.
Tests (internal/controllers/repo/configmap_values_test.go), plain unit tests with
a fake client — the existing repo_test.go is `//go:build integration` and needs
Docker, these do not:
- a typo'd key is an error, not an empty value set
- YAML in a JSON key is an error
- a valid key still returns its values
- a load failure yields nil values — the precondition that made the silent
skip possible, asserted so a future refactor dropping the early return has
something to fail against
go build ./..., go vet and go test ./internal/... clean.
Found by an adversarial review of the write-path templating; it survived five
independent refutation attempts.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JumEqTGGJ2hf3vjE3xFQsv
braghettos
added a commit
that referenced
this pull request
Sep 10, 2026
…o/templating-engine
Follow-up on review feedback: the first commit inferred intent from
`placeholdersToOverride` being empty, and that inference is not always right.
git-provider templates whatever file it is given, not only Helm manifests, and a
file can legitimately template with no inputs at all — `{{ now | date
"2006-01-02" }}`, `{{ uuidv4 }}`, `{{ env "USER" }}`, `{{ randAlphaNum 6 }}`.
Under the first commit alone those stopped rendering, which is a regression for
anyone relying on them.
So the CR can now say what it means, and the annotation the repo controller has
carried all along is extended to localresource:
(absent) render only if placeholdersToOverride is set <- default
gotemplate always render, including with no placeholders
none never render, even with placeholders declared
Anything else is rejected with an error naming the offending value, rather than
silently treated as one of the above — guessing is how the original mismatch went
unnoticed.
The default is unchanged from the first commit and is the safe reading: a
resource with no values to inject can only be damaged by a render pass. It fixes
the Helm case that prompted this (`function "toYaml" not defined` on
LocalResource/publish-sock-shop-003) and matches the repo controller, which
likewise installs a pass only when it has values. `gotemplate` restores the
value-free behaviour for anyone who wants it; `none` states verbatim intent
explicitly, and keeps holding if placeholders are added later.
The annotation string now has ONE definition, in the new
internal/controllers/common/templating package, referenced by both controllers.
repo.AnnotationTemplatingEngine is kept as a deprecated alias so existing
references keep compiling. This is deliberate: the two controllers have now
shipped three production defects by disagreeing about a shared rule — which auth
methods consume a username (#5/#6), when to install a templating pass (this),
and whether a failed ConfigMap load should abort a sync (#9). A shared constant
is a small thing, but it is the seam where two of those started.
The decision itself is extracted into `templatingOptions(annotations, values)`
so it is readable and testable in one place instead of reconstructed from a
switch inside the sync path.
Tests (internal/controllers/localresource/templating_options_test.go) exercise
the real copier, so they assert what lands in the target repo rather than which
options were selected:
- default + no values -> verbatim (the sock-shop case)
- default + values -> substituted (unchanged)
- gotemplate + no values -> renders `{{ "x" | upper }}` to `X`
- none + values declared -> still verbatim
- unknown engine -> error naming the value
- gotemplate on a Helm builtin -> still fails, and should: the annotation is a
choice, not a promise that Helm syntax survives a Go render
docs/local-resource.md documents all three modes, with the Helm failure modes
spelled out (toYaml/include fail; `{{ .Values.x }}` silently becomes `<no
value>`; `{{/* */}}` headers are deleted).
go build ./..., go vet and go test ./internal/... clean — 8 packages ok.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JumEqTGGJ2hf3vjE3xFQsv
braghettos
added a commit
that referenced
this pull request
Sep 10, 2026
…that fail
Two changes, both consequences of the same root cause: git-provider rendered with
Go's `{{ }}`, a delimiter Helm already owns, and reported failures without saying
which file they came from.
## 1. Default delimiters are now `{% %}`
The files this provider publishes are overwhelmingly Kubernetes manifests and
Helm charts. Sharing `{{ }}` made it impossible to publish a chart WITH
substitution at all:
{{ toYaml x }} / {{ include "y" . }} fail — Helm builtins, not sprig ones
{{ .Values.replicas }} parses, renders to the literal `<no value>`
{{/* ... */}} deleted (helm create writes one into
every _helpers.tpl)
With `{% %}` both systems coexist: this provider substitutes its own placeholders
at publish time and leaves Helm's untouched for install time. That case was
previously impossible — `none` preserved Helm but never substituted, and
`gotemplate` substituted but destroyed the chart.
`{% %}` was chosen by testing candidates against real YAML rather than by taste:
<< >> breaks on the merge key `<<: *defaults` (parse error: expected :=)
[[ ]] breaks on flow sequences `[[1,2],[3,4]]` (parse error: unexpected ",")
{% %} safe against both, and not Helm syntax
`krateo.io/templating-delims: "left,right"` overrides it — `"{{,}}"` restores Go's
native syntax where the content is known not to be a chart. A malformed value is
rejected, not silently ignored: quietly using different delimiters than the author
asked for leaves their placeholders unsubstituted in a committed file.
This is a behaviour change, and the blast radius was measured before making it:
on the live cluster 0 of 13 LocalResources declare placeholdersToOverride and no
Repo CRs exist at all. The only `{{ }}` users in existence were this repo's own
fixtures and docs, both updated here. A CR still written with `{{ .name }}` now
gets it left LITERAL in the output — visible in the committed file, not silently
blanked.
## 2. status.templatingErrors lists every file that failed
Go reports template positions against an anonymous template
("template: template:61: ..."), which is useless when a sync copies more than one
file. The copier now attributes each failure to its source path, and — the part
that matters — collects them ALL rather than stopping at the first, so one sync
reports every offending file:
status:
templatingErrors:
- path: templates/a.yaml
message: 'template: template:1: function "toYaml" not defined'
- path: templates/b.yaml
message: 'template: template:1: function "include" not defined'
Copy still returns an error, so callers abort before committing; a failed sync
publishes nothing, and the list is cleared on the next successful one. Both
controllers populate it through one shared converter.
## Shared definitions
apis/common/v1alpha1 (TemplatingError) and internal/controllers/common/templating
(annotations, engines, delimiter parsing, status conversion) exist so repo and
localresource cannot describe the same concept differently. These two controllers
have now produced four defects by disagreeing about a shared rule — usernames
(#5/#6), when to template (#8), ConfigMap load failure (#9), and this.
## Verified
go build ./... · go vet · go test ./internal/... ./apis/... 8 packages ok
controller-gen object + crd (v0.19.0, the pinned version) reproducible
kubectl apply --dry-run=server -f crds/ on a live cluster both CRDs configured
New tests: delimiter coexistence (Helm syntax preserved while `{% %}` substitutes),
`{{,}}` opt-in still works and still collides with Helm (honest outcome of asking
for it), malformed delimiters rejected, multi-file error collection with paths,
single-failure message names the file, errors reset between copies.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JumEqTGGJ2hf3vjE3xFQsv
braghettos
added a commit
that referenced
this pull request
Sep 10, 2026
…titute (#8) * fix(localresource): do not render files when there is nothing to substitute `copier.WithGoTemplate(values)` was installed unconditionally, so every file a LocalResource commits went through text/template + sprig — including when `placeholdersToOverride` was absent and `values` was nil. Rendering is not a no-op on content that merely looks like a template, so a CR asking to copy bytes did not get its bytes copied. Found on krateo-057. LocalResource/publish-sock-shop-003, publishing a Helm chart through the Blueprint Builder, is stuck at: create failed: unable to copy files: template: template:61: function "toYaml" not defined `toYaml` is a Helm builtin, not a sprig one — as are `include`, `required` and `tpl` — so a Helm chart template can never be committed verbatim through this path. Which is precisely what the Blueprint Builder exists to do. The CR sets no placeholders at all; it was asking for a byte-for-byte copy and got a render. The loud failure is the lucky half. A template that merely PARSES is executed against an empty value set: the added test shows `replicas: {{ .Values.replicas }}` committed as `replicas: <no value>`. A corrupted chart that still looks like a chart, with no error anywhere and nothing downstream able to tell. The fix mirrors the sibling repo controller, which already installs a templating pass only when it has values (`internal/controllers/repo/repo.go`, `if values != nil`). The two controllers simply disagreed about this. Behaviour with placeholders present is unchanged. Tests (internal/tools/copier/helm_verbatim_test.go), all four passing: - a Helm chart copies verbatim with no templating pass installed - with the pass installed it fails with the exact krateo-057 error string - the silent case, asserted rather than described: `{{ .Values.replicas }}` renders to `<no value>` and would be committed - placeholders still substitute when the CR actually asks for them No API, CRD or chart change: a LocalResource that wants substitution keeps declaring placeholdersToOverride, and one that does not now gets what it asked for. Consumers needing a choice of engine on LocalResource (repo has the `krateo.io/templating-engine` annotation, localresource does not) is a separate follow-up and is not needed to unblock this. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JumEqTGGJ2hf3vjE3xFQsv * feat(localresource): let the CR say whether to template, via krateo.io/templating-engine Follow-up on review feedback: the first commit inferred intent from `placeholdersToOverride` being empty, and that inference is not always right. git-provider templates whatever file it is given, not only Helm manifests, and a file can legitimately template with no inputs at all — `{{ now | date "2006-01-02" }}`, `{{ uuidv4 }}`, `{{ env "USER" }}`, `{{ randAlphaNum 6 }}`. Under the first commit alone those stopped rendering, which is a regression for anyone relying on them. So the CR can now say what it means, and the annotation the repo controller has carried all along is extended to localresource: (absent) render only if placeholdersToOverride is set <- default gotemplate always render, including with no placeholders none never render, even with placeholders declared Anything else is rejected with an error naming the offending value, rather than silently treated as one of the above — guessing is how the original mismatch went unnoticed. The default is unchanged from the first commit and is the safe reading: a resource with no values to inject can only be damaged by a render pass. It fixes the Helm case that prompted this (`function "toYaml" not defined` on LocalResource/publish-sock-shop-003) and matches the repo controller, which likewise installs a pass only when it has values. `gotemplate` restores the value-free behaviour for anyone who wants it; `none` states verbatim intent explicitly, and keeps holding if placeholders are added later. The annotation string now has ONE definition, in the new internal/controllers/common/templating package, referenced by both controllers. repo.AnnotationTemplatingEngine is kept as a deprecated alias so existing references keep compiling. This is deliberate: the two controllers have now shipped three production defects by disagreeing about a shared rule — which auth methods consume a username (#5/#6), when to install a templating pass (this), and whether a failed ConfigMap load should abort a sync (#9). A shared constant is a small thing, but it is the seam where two of those started. The decision itself is extracted into `templatingOptions(annotations, values)` so it is readable and testable in one place instead of reconstructed from a switch inside the sync path. Tests (internal/controllers/localresource/templating_options_test.go) exercise the real copier, so they assert what lands in the target repo rather than which options were selected: - default + no values -> verbatim (the sock-shop case) - default + values -> substituted (unchanged) - gotemplate + no values -> renders `{{ "x" | upper }}` to `X` - none + values declared -> still verbatim - unknown engine -> error naming the value - gotemplate on a Helm builtin -> still fails, and should: the annotation is a choice, not a promise that Helm syntax survives a Go render docs/local-resource.md documents all three modes, with the Helm failure modes spelled out (toYaml/include fail; `{{ .Values.x }}` silently becomes `<no value>`; `{{/* */}}` headers are deleted). go build ./..., go vet and go test ./internal/... clean — 8 packages ok. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JumEqTGGJ2hf3vjE3xFQsv * feat(templating): stop claiming Helm's delimiter, and name the files that fail Two changes, both consequences of the same root cause: git-provider rendered with Go's `{{ }}`, a delimiter Helm already owns, and reported failures without saying which file they came from. ## 1. Default delimiters are now `{% %}` The files this provider publishes are overwhelmingly Kubernetes manifests and Helm charts. Sharing `{{ }}` made it impossible to publish a chart WITH substitution at all: {{ toYaml x }} / {{ include "y" . }} fail — Helm builtins, not sprig ones {{ .Values.replicas }} parses, renders to the literal `<no value>` {{/* ... */}} deleted (helm create writes one into every _helpers.tpl) With `{% %}` both systems coexist: this provider substitutes its own placeholders at publish time and leaves Helm's untouched for install time. That case was previously impossible — `none` preserved Helm but never substituted, and `gotemplate` substituted but destroyed the chart. `{% %}` was chosen by testing candidates against real YAML rather than by taste: << >> breaks on the merge key `<<: *defaults` (parse error: expected :=) [[ ]] breaks on flow sequences `[[1,2],[3,4]]` (parse error: unexpected ",") {% %} safe against both, and not Helm syntax `krateo.io/templating-delims: "left,right"` overrides it — `"{{,}}"` restores Go's native syntax where the content is known not to be a chart. A malformed value is rejected, not silently ignored: quietly using different delimiters than the author asked for leaves their placeholders unsubstituted in a committed file. This is a behaviour change, and the blast radius was measured before making it: on the live cluster 0 of 13 LocalResources declare placeholdersToOverride and no Repo CRs exist at all. The only `{{ }}` users in existence were this repo's own fixtures and docs, both updated here. A CR still written with `{{ .name }}` now gets it left LITERAL in the output — visible in the committed file, not silently blanked. ## 2. status.templatingErrors lists every file that failed Go reports template positions against an anonymous template ("template: template:61: ..."), which is useless when a sync copies more than one file. The copier now attributes each failure to its source path, and — the part that matters — collects them ALL rather than stopping at the first, so one sync reports every offending file: status: templatingErrors: - path: templates/a.yaml message: 'template: template:1: function "toYaml" not defined' - path: templates/b.yaml message: 'template: template:1: function "include" not defined' Copy still returns an error, so callers abort before committing; a failed sync publishes nothing, and the list is cleared on the next successful one. Both controllers populate it through one shared converter. ## Shared definitions apis/common/v1alpha1 (TemplatingError) and internal/controllers/common/templating (annotations, engines, delimiter parsing, status conversion) exist so repo and localresource cannot describe the same concept differently. These two controllers have now produced four defects by disagreeing about a shared rule — usernames (#5/#6), when to template (#8), ConfigMap load failure (#9), and this. ## Verified go build ./... · go vet · go test ./internal/... ./apis/... 8 packages ok controller-gen object + crd (v0.19.0, the pinned version) reproducible kubectl apply --dry-run=server -f crds/ on a live cluster both CRDs configured New tests: delimiter coexistence (Helm syntax preserved while `{% %}` substitutes), `{{,}}` opt-in still works and still collides with Helm (honest outcome of asking for it), malformed delimiters rejected, multi-file error collection with paths, single-failure message names the file, errors reset between copies. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JumEqTGGJ2hf3vjE3xFQsv * feat(templating): report every problem in a file, not the first one status.templatingErrors named each failing FILE, but carried only one message per file, because that is all text/template gives: it stops at the first error. A file with three undefined functions reported one, and the operator fixed it, re-synced, and met the next. Worse, the most damaging class was invisible. A reference to a value that was never declared is not an error to the renderer — `{% .environment %}` with no `environment` placeholder renders the literal `<no value>` and is committed with a green status. Nothing in the previous version could report that, which made templatingErrors blind to exactly the silent corruption this series has been chasing. tools/template.Validate finds both classes in one pass. It parses with parse.SkipFuncCheck — which yields a tree even when the functions do not exist — then walks it collecting: - every function identifier absent from sprig AND from text/template's builtins - every top-level field reference absent from the declared values Before, for one file: template: template:1: function "toYaml" not defined After, same file: template:2:8: no value declared for ".environment" template:3:13: no value declared for ".a" template:3:6: function "toYaml" not defined template:4:6: function "include" not defined with line:column, which the renderer's message does not carry. Validate REPORTS, it does not gate: Render remains the source of truth for whether a file renders, and a file that renders cleanly is never inspected. The scanner runs only on the failure path, so the success path costs nothing. Two details that matter for message quality: - When Validate's own parse cannot continue (a genuine syntax error, or a `$var` it cannot resolve) it returns a single UNLOCATED problem. The copier detects that via Problem.Located() and keeps the RENDERER's error instead — otherwise a `$svc` the scanner cannot resolve would mask the undefined function the renderer would have named, which is a worse message, not a better one. - RenderFailed.Error() now counts problems and distinct files separately. "4 problems in 1 file" is a very different situation from "4 problems in 4 files", and the condition message is often all anyone reads. Helm syntax is invisible to the scanner under the default delimiters, as it should be: it is not ours to validate. Tests: every undefined function reported (Render reports exactly one, asserted); undeclared values caught where Render returns nil and emits `<no value>` (asserted); both classes mixed; clean templates silent; Helm syntax ignored under `{% %}`; syntax errors yield exactly one report; no false positives on builtins, pipelines or command arguments; multi-problem single-file counted correctly in the aggregate message. go build ./... · go vet · go test ./internal/... ./apis/... 8 packages ok Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JumEqTGGJ2hf3vjE3xFQsv * feat(templating): validate every file, and give Repo the same vocabulary Two gaps found by testing the design rather than trusting it. ## 1. The scanner only ran on files that had already failed A reference to a value that was never declared is NOT an error to text/template — it renders the literal `<no value>` and returns nil. So a file whose ONLY fault was an undeclared value never entered the failure path, the scanner never saw it, and it was committed silently. status.templatingErrors was blind to exactly the corruption it was added to surface. Demonstrated with a four-file tree: the syntax-error file and the undefined-function file were both reported, and the undeclared-value file was not. Validation now runs on every rendered file. A file with an undeclared value fails the sync instead of publishing `<no value>`. That IS a behaviour change and it is the intended one: a placeholder is declared on the same CR that references it, so referencing an undeclared one is a typo essentially every time, and the alternative is a corrupted file with a green status. Render remains the source of truth for HOW a file renders; the scanner decides whether it should have been published at all. Also confirmed while testing, and worth recording because it was asked: a syntax error in one file does NOT stop the others being checked. copyFile records and returns, so the walk continues — a tree with a syntax error, an undefined function and an undeclared value reports all three files in one sync. ## 2. Repo disagreed with LocalResource, again Repo picked its engine with `if engine == "gotemplate" { ... } else { mustache }`, so EVERY other value fell through to mustache: - "none" silently got mustache — the opposite of what it asks for - "gotmeplate" silently got mustache — and mustache renders an unresolved key as EMPTY (AllowMissingVariables defaults to true), so a typo in an annotation quietly gutted the copied files and reported success Repo now uses the shared vocabulary: `none` is honoured, `gotemplate` gets the delimiters and the scanner, an unrecognised value is rejected by name, and an absent annotation still means mustache so existing Repos are unaffected. The decision is extracted into templatingOptions(), mirroring localresource, so both controllers are readable side by side. mustache stays available and is now documented as LEGACY where someone will read it — it uses `{{ }}` so it collides with Helm exactly as Go templates did, it fails silently rather than loudly, and it cannot be scanned, so templatingErrors carries no per-problem detail for it. ## Tests New internal/controllers/repo/templating_options_test.go drives the real copier: no values copies verbatim; gotemplate coexists with Helm syntax; the scanner reaches the Repo path and names undeclared values; "none" is honoured rather than becoming mustache; an unknown engine is rejected by name; mustache still works AND the test pins WHY it is legacy (it eats `{{ .Values.replicas }}` to an empty string); the default remains mustache; malformed delimiters are rejected. go build ./... · go vet · go test ./internal/... ./apis/... 8 packages ok Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JumEqTGGJ2hf3vjE3xFQsv * test(repo): fix TC09 for the new delimiters, and pin the opt-in end to end CI caught a real break, not the flake this branch hit earlier: TC09 is the integration test for the gotemplate annotation, and its fixture used `{{ .name }}` with an expectation of substitution. With `{% %}` as the default that line is no longer ours to render, so the file arrived verbatim and the assertion failed. The test was right and the fixture was stale. TC09 now uses the default delimiters AND carries a Helm expression beside them: "Hello {% .name %}!\nreplicas: {{ .Values.replicas }}\n" -> "Hello GoTemplate!\nreplicas: {{ .Values.replicas }}\n" which makes it a stronger test than it was. The coexistence — our placeholder substituted, Helm's untouched — is the whole point of the delimiter choice, and it is only observable end to end through a real clone, render, commit and push. The unit tests can show the copier's behaviour; only this shows what lands in the remote. TC11 pins the other direction: `krateo.io/templating-delims: "{{,}}"` restores Go's native syntax for content known not to be a chart, and still substitutes. Without it the opt-in path had no integration coverage at all. go build ./... · go vet -tags integration · go test ./internal/... ./apis/... 8 packages ok. The integration suite needs Docker and Gitea, so TC09/TC11 are verified by CI rather than locally — which is how the stale fixture surfaced. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JumEqTGGJ2hf3vjE3xFQsv --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A green status over unrendered templates
SyncReposlogged a warning and carried on whenloadValuesFromConfigMapfailed:That function returns
(nil, err)on every error path. Sovaluesis nil, theif values != nilguard below installs no templating engine, and the copy, commit and push all still run — publishing files with their{{ }}placeholders intact and then settingAvailable/Synced.And it is terminal.
TargetCommitIdnow matches the commit that was just pushed, soObservesees an up-to-date resource. It never retries and never corrects itself. The operator seesREADY=True SYNCED=Trueover a target repo full of unrendered templates, with nothing but a warning in a log they have no reason to read.The trigger is a typo
This is not an exotic path. provider-runtime's
GetConfigMapValueends:For an absent key that is
""with a nil error. So a typo inconfigMapKeyRef.keynever surfaces as "missing key" — it reachesjson.Unmarshal([]byte(""))and fails asunexpected end of JSON input. Three realistic triggers, all the same outcome:configMapKeyRef.keyThe change
A CR that sets
configMapKeyRefhas asked for substitution. If the values cannot be read, the honest outcome is a failed sync — not a green one over unrendered output.The warning log and the
CannotLoadConfigMapevent are kept; only the silent continue becomesfailSync, which setsUnavailable+ReconcileErrorand returns the error so the resource is retried.Tests
internal/controllers/repo/configmap_values_test.go— plain unit tests with a fake client. (The existingrepo_test.gois//go:build integrationand needs Docker; these do not.)TestLoadValuesFromConfigMapErrorsOnMissingKeyTestLoadValuesFromConfigMapErrorsOnNonJSONTestLoadValuesFromConfigMapReturnsValuesTestLoadFailureYieldsNilValuesWhichWouldSkipTemplatinggo build ./...,go vetandgo test ./internal/...clean — 7 packages ok.Provenance
Found by an adversarial review of the write-path templating. It was handed to a skeptic told to refute it, who attacked it on five axes — reachability, a guard elsewhere, does the claimed input produce the claimed output, error shadowing, inflated severity — and it survived all five. Notably the stale non-nil
erris not an accidental escape hatch: it is reassigned atco, err := copier.NewCopier(...)before it could ever be returned.Related: #8 (unconditional templating in the localresource controller). Same family — the two controllers disagreeing about when to template — but independent changes.
🤖 Generated with Claude Code
https://claude.ai/code/session_01JumEqTGGJ2hf3vjE3xFQsv