Context
pkg/component/read.go:readResource does a straight Client.Get on each read-only
resource and treats every error, including IsNotFound, as a hard failure. The error
bubbles up from Component.Reconcile and propagates to controller-runtime, which
applies exponential backoff.
The problem
When the consumer has a watch on the type of the missing read-only resource (i.e.
they'd be re-enqueued the moment it appears), the backoff is purely redundant —
it just delays the eventual reconcile and produces error-log noise in the
meantime. The watch-driven enqueue would react in milliseconds.
This is a common shape: an operator references external objects (user-provided
Secrets carrying credentials, sibling-operator-produced contract CRDs) and wants
to observe them, hash their content for rollout triggers, etc. The cleanest place
to express "I'm waiting for this object" is the Component itself.
Workarounds today (all unsatisfying)
- Accept the backoff and log noise.
- Pre-resolve refs in the controller body before building the Component, attach
a synthetic Blocked guard if any are missing, skip registering the read-only
resource that pass. Loses framework symmetry; the existence check lives in
user code, the hash flow lives in the framework.
- Special-case
IsNotFound in the controller body. Asymmetric and hacky.
Proposed solution shape
A ResourceOptions flag (or a separate constructor) that opts the read-only
resource into "Blocked on absence" behavior: on IsNotFound, treat the resource
as Guard-Blocked with a reason like "waiting for <kind> <namespace>/<name>"
rather than returning an error. Subsequent resources in the Component skip per
the existing guard-blocked semantics. The consumer is expected to have a watch
on the type; the framework neither requires nor verifies that.
This keeps fail-fast as the default (correct when no watch exists) while giving
consumers a clean opt-in for the watch-backed case.
Open question
Whether this belongs on ResourceOptions (per-resource), as a new
secret.NewReadOnlyBuilder().BlockedOnAbsence() style builder, or as a separate
Component registration method. Happy to PR whichever shape you'd prefer.
Context
pkg/component/read.go:readResourcedoes a straightClient.Geton each read-onlyresource and treats every error, including
IsNotFound, as a hard failure. The errorbubbles up from
Component.Reconcileand propagates to controller-runtime, whichapplies exponential backoff.
The problem
When the consumer has a watch on the type of the missing read-only resource (i.e.
they'd be re-enqueued the moment it appears), the backoff is purely redundant —
it just delays the eventual reconcile and produces error-log noise in the
meantime. The watch-driven enqueue would react in milliseconds.
This is a common shape: an operator references external objects (user-provided
Secrets carrying credentials, sibling-operator-produced contract CRDs) and wants
to observe them, hash their content for rollout triggers, etc. The cleanest place
to express "I'm waiting for this object" is the Component itself.
Workarounds today (all unsatisfying)
a synthetic Blocked guard if any are missing, skip registering the read-only
resource that pass. Loses framework symmetry; the existence check lives in
user code, the hash flow lives in the framework.
IsNotFoundin the controller body. Asymmetric and hacky.Proposed solution shape
A
ResourceOptionsflag (or a separate constructor) that opts the read-onlyresource into "Blocked on absence" behavior: on
IsNotFound, treat the resourceas Guard-Blocked with a reason like
"waiting for <kind> <namespace>/<name>"rather than returning an error. Subsequent resources in the Component skip per
the existing guard-blocked semantics. The consumer is expected to have a watch
on the type; the framework neither requires nor verifies that.
This keeps fail-fast as the default (correct when no watch exists) while giving
consumers a clean opt-in for the watch-backed case.
Open question
Whether this belongs on
ResourceOptions(per-resource), as a newsecret.NewReadOnlyBuilder().BlockedOnAbsence()style builder, or as a separateComponentregistration method. Happy to PR whichever shape you'd prefer.