crossplane-contrib / crossplane-contrib/function-go-templating
observed.composite.resource.spec missing fields on first reconcile causes silent resource drops
- Dominant language
- Go
- Stars
- 100
- Forks
- 65
- Avg merge
- 2d 6h
- Merged PRs (30d)
- 9
Description
## Bug Description
When using `observed.composite.resource.spec` in a Go template, fields that are present in the XR may not be available on the first reconcile cycle. This causes `{{- with .fieldName }}` blocks to silently skip, resulting in composed resources never being created.
## Root Cause
On the first reconcile after XR creation, `observed.composite.resource.spec` may not contain all user-specified fields. The `desired.composite.resource.spec` always has the full spec. When combined with resources that fail quickly (triggering the circuit breaker), the template never gets a second chance to execute with the now-populated observed state.
## Environment
- function-go-templating: v0.11.0
- Crossplane: v1.19.x (latest stable)
- Cluster: Kind (local development)
## Steps to Reproduce
1. Create a Composition with an inline template that uses `{{- with .observed.composite.resource.spec -}}` and accesses multiple sub-objects via `{{- with .service }}`, `{{- with .autoscaling }}`, etc.
2. Include a resource that will fail immediately (e.g., AppAutoscaling Policy that depends on a Target that doesn't exist yet)
3. Create an XR with all fields populated
## Expected Behavior
All `with` blocks should execute on every reconcile since the XR spec is fully populated.
## Actual Behavior
- First reconcile: some `with` blocks don't execute because `observed.composite.resource.spec` is missing those fields
- The failing resource triggers the circuit breaker
- Subsequent reconciles are throttled — the template never re-executes with the complete observed state
- Result: some composed resources are never created
## Evidence
- Decoder test: `yaml.NewYAMLOrJSONDecoder` correctly decodes 6+ documents from well-formed YAML
- `convertToMap` uses `protojson.Marshal(req)` with default options — proto3 omits zero/empty fields
- When the same template uses only 3 resources (no failing ones), all fields are available on the second reconcile (circuit breaker doesn't activate)
- Switching from `observed` to `desired` composite spec resolves the issue completely
## Workaround
Use `desired` with fallback to `observed`:
```go
{{- $spec := (.desired.composite.resource.spec | default .observed.composite.resource.spec) -}}
{{- with $spec -}}
...
{{- end -}}
```
This ensures the full user-specified spec is always available regardless of reconcile timing.
## Recommendation
Consider documenting that `desired.composite.resource.spec` should be preferred over `observed.composite.resource.spec` for resource creation templates, since `desired` always reflects the user's intent while `observed` may lag behind. Alternatively, the function could pre-merge desired and observed composite state before template execution.
Contributor guide
No contributing guide indexed for this repository
Research direction
Start with the convertToMap path and the yaml.NewYAMLOrJSONDecoder decoder test mentioned in the report, then reproduce a first reconcile with a quickly failing resource and the circuit breaker. Compare observed and desired composite specs during template execution; done means the populated XR fields no longer cause resource-producing with blocks to be skipped, with regression coverage for the first reconcile.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- backend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 52/100