fix(source): evaluate FQDN templates on typed objects like unstructured - #6611
fix(source): evaluate FQDN templates on typed objects like unstructured#6611zanarellidev wants to merge 7 commits into
Conversation
Retry failed FQDN template execution with an unstructured-style data shape so shared templates using JSON keys under Spec (e.g. .Spec.hostnames) work for gateway-httproute as well as unstructured. Signed-off-by: zanarelli <zanarelli.dev@gmail.com>
|
Hi @zanarellidev. Thanks for your PR. I'm waiting for a kubernetes-sigs member to verify that this patch is reasonable to test. If it is, they should reply with Regular contributors should join the org to skip this step. Once the patch is verified, the new status will be reflected by the I understand the commands that are listed here. DetailsInstructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. |
cb4336b to
c75e92d
Compare
|
Not too sure. The approach |
|
Good catch. For a shared JSON-style template against typed HTTPRoute, step 1 does fail every sync and step 2 always runs. That cost is real for that path. Why try/fail was chosen anyway: always normalizing typed objects to the unstructured/JSON map shape before Execute fixes JSON keys in one pass, but it silently breaks Go-field templates that already work today (e.g. Options considered:
I lean toward (2) as the clean product fix, or (4) if we want to keep a single |
|
I'm not too sure what the right solution should be. There are missing tests related to fqdn in source/gateway_httproute_test.go and in source/unstructured_fqdn_test.go, that syntax works aka for httproute something like |
Adds an integration-level test through the real gateway route resolver (not just source/template/engine_test.go's isolated unit tests) proving a JSON-style Spec.hostnames template now evaluates successfully against a typed HTTPRoute via the unstructured fallback, matching the exact scenario and error from kubernetes-sigs#6593.
|
Good catch, that gap was real. Added an integration test through the actual gateway resolver path (not just the isolated |
Shortened the fallback comment in engine.go and the corresponding test doc comments; removed the kubernetes-sigs#6593 references from code comments per ivankatliarchuk's review.
|
Ivan, adjusted as requested. Thanks for the help. |
ivankatliarchuk
left a comment
There was a problem hiding this comment.
So there is currently an asymmetry in how templating behaves for different casings.
Typed HTTPRoute (--source=gateway-httproute), with this PR applied:
- .Spec.Hostnames (capital) - succeeds on the first tmpl.Execute attempt directly against the real struct, since Hostnames is the actual Go field name. No retry ever
triggers; it just works, same as before this PR existed. - .Spec.hostnames (lowercase) - first attempt hard-errors (can't evaluate field hostnames in type v1.HTTPRouteSpec), which is an error, so the retry fires, converts to
the map shape, and the second attempt succeeds via the JSON key.
Unstructured source (any object, dynamic-client-backed):
- .Spec.hostnames (lowercase) - .Spec is already map[string]any with the raw JSON key hostnames; direct map lookup on the first attempt succeeds immediately.
- .Spec.Hostnames (capital) - map lookup for key "Hostnames" misses. A map miss is not an error in Go's text/template (confirmed by the throwaway test earlier: err= output="DONE", zero range iterations). So tmpl.Execute returns successfully with empty output — the retry gate (if err != nil) never fires, because there's no error to catch, and there's nowhere to fall back to anyway since the map is already the "final" shape.
- Only lowercase works; capital silently renders nothing, no error, no log line.
Most likely, we should widen the fix; make both casings resolves correctly on unstructured as well. Something like
// source/unstructured.go
// addTitleCaseAliases makes each key in m reachable by both its native JSON
// and a naive Title-cased variant. Recurses so nested access works both ways too.
func addTitleCaseAliases(m map[string]any) map[string]any {}
...usage...
if spec, ok := u.Object["spec"].(map[string]any); ok {
w.Spec = addTitleCaseAliases(copyMap(spec)) // copy first — see caveat below
}
|
/ok-to-test |
Coverage Report for CI Build 31402468539Warning Build has drifted: This PR's base is out of sync with its target branch, so coverage data may include unrelated changes. Coverage increased (+0.1%) to 81.891%Details
Uncovered ChangesNo uncovered changes found. Coverage Regressions22 previously-covered lines in 3 files lost coverage.
Coverage Stats
💛 - Coveralls |
A map key miss in text/template renders empty instead of erroring, so templates on the unstructured source silently produced nothing when using the Go field name casing (.Spec.Hostnames) instead of the raw JSON key (.Spec.hostnames). Alias each map key to its title-cased form so both spellings resolve.
…late Extract wrapTemplateErr to remove the duplicated kind/namespace/name formatting between the two retry error paths, and propagate the retry's own error instead of re-wrapping the original one.
|
Pushed both remaining items:
One tradeoff to flag: anything that ranges over the whole |
|
/lgtm |
mloiseleur found three real regressions in the FQDN-template typed/ unstructured retry added earlier in this branch, each with a reproducing test: - The unstructured retry in execTemplate used the default text/template missingkey mode, so a template referencing a field that doesn't exist on either the typed object or its JSON shape rendered "<no value>" instead of failing. Now cloning the template and setting missingkey=error for that retry only, so a real typo in --fqdn-template fails loudly instead of producing a wrong-but-valid hostname/target. - titleCaseKey only capitalized the first letter of a JSON key, so CRD fields using Go initialisms (cert-manager's spec.url / spec.dnsNames) never matched their real generated Go names (URL, DNSNames). It now upper-cases a leading word in full when it's a known initialism. - withTitleCaseAliases recursed into nested map values to alias their keys too, silently doubling every key one level down. Any template ranging or taking len() over nested data (not just the top-level Spec/ Status fields the alias exists for) saw twice as many entries as were declared. Aliasing is now shallow; nested JSON keys stay reachable through their literal path. go test -race ./source/... ./source/template/... green. Signed-off-by: zanarelli <zanarelli.dev@gmail.com>
|
New changes are detected. LGTM label has been removed. |
|
[APPROVALNOTIFIER] This PR is NOT APPROVED This pull-request has been approved by: The full list of commands accepted by this bot can be found here. DetailsNeeds approval from an approver in each of these files:Approvers can indicate their approval by writing |
|
@mloiseleur thanks for the concrete repros, all three are real and now fixed in
|
What does it do?
On FQDN template execution failure, retry with an unstructured-style data shape (
Spec/Statusas JSON maps, plus Name/Namespace/...). Shared--fqdn-templatevalues that use JSON keys under Spec (e.g.{{ range .Spec.hostnames }}) then work for typed gateway routes the same way they already work for the unstructured source.Motivation
#6593: the same
--fqdn-templateis used for unstructured and typed sources. Unstructured wraps objects soSpecis a map with JSON keys; typedv1.HTTPRouteSpecexposes Go field names, so.Spec.hostnamesfails withcan't evaluate field hostnames in type v1.HTTPRouteSpec(including when hostnames are empty/absent).More
Validation:
go test ./source/template/(typed hostnames, empty hostnames, unstructured, Name+Spec mix)TestExecFQDNExecutionErrorstill fails closed on real template errorsFixes #6593