crossplane-contrib / crossplane-contrib/function-go-templating

observed.composite.resource.spec missing fields on first reconcile causes silent resource drops

Open
#579 2 comments 0 reactions 0 assignees View on GitHub
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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.