Documentation
¶
Overview ¶
Package driftdetection implements provider-agnostic, per-field drift ownership for Crossplane managed resources.
It is a decorator over crossplane-runtime's TypedExternalConnector and contains NO per-resource logic: managed resources are inspected through fieldpath's unstructured projection, so one implementation serves every managed resource type in both API scopes.
Semantics ¶
The configuration is a peer of forProvider:
spec:
driftDetection:
mode: enabled # enabled (default) | warn | disabled
ignore:
- paths:
- forProvider.description
The modes are:
- enabled: detect and correct drift. Paths under ignore are owned by someone else: seeded from spec on create, then neither compared nor written from spec.
- warn: detect drift, report it on the DriftDetected condition, do not correct it. The adoption mode: observe what actually drifts before deciding what to hand over.
- disabled: neither detect nor correct.
A resource with no driftDetection block behaves exactly as it did before the field existed: the decorator reads the (absent) configuration, delegates, and touches nothing.
Why an ignored field is substituted rather than skipped ¶
Several controllers in this provider build their update request from spec.forProvider as a whole. A field merely skipped in the comparison would still be written from spec the next time any sibling field changed, which reverts the field's real owner. So an ignored path is substituted: the value observed on the external resource is written over the spec value before the resource is compared and before the update request is built. The field then compares equal by construction, and the update carries the value its real owner set.
This relies on a resource's observation type carrying a counterpart for the ignored field under the same name: forProvider.x has a counterpart at status.atProvider.x, and the controller fills it in on every observation.
Ignore-path eligibility ¶
Ignore paths are default-deny. A path is accepted only when every one of the following holds, and is otherwise rejected outright by ReadConfig:
- it is not identity-participating (identityDenylist). GitHub addresses every resource of this provider under its organization, so the org field and its reference and selector companions are denied: substituting one would re-address the object rather than retarget a value. No other forProvider field takes part in an external identifier; each resource is addressed by its external-name, which is not a spec field.
- it names a real field under spec.forProvider, checked by reflecting over the resource's own generated type. A name that happens to resolve under status.atProvider is not itself a forProvider field, and accepting it would capture an observed value that apply's lenient unstructured-to-typed conversion then drops: no error, and the field is never substituted.
- it has a counterpart in status.atProvider on the resource's own type. A write-only field has no observed value to carry forward.
- it is listed in eligibleIgnorePaths: the controller actually populates that counterpart on Observe. This provider's status.atProvider is deliberately near-empty and most observation fields are declared but never written, so a field that merely has a counterpart in the type would be observed as absent and keep its seed value, and the update would still send the spec value. eligibleIgnorePaths is empty: no controller mirrors any forProvider field into status.atProvider today, so every ignore path is rejected. A path becomes eligible only together with the controller change that populates its counterpart.
A path naming any ineligible class is rejected outright by ReadConfig — never silently skipped, and never partially applied alongside the paths that were fine. mode: warn and mode: disabled need no ignore paths and are fully available.
Freshness and race safety ¶
crossplane-runtime constructs one external client per reconcile (Connect at the top of Reconcile, Disconnect deferred to its end) and calls Observe strictly before Update on the same in-memory object. This decorator therefore captures the observed values of ignored paths into per-reconcile state during Observe, and Update consumes that snapshot instead of re-reading status.atProvider.
That ordering is what makes the substitution safe:
- The values written on update are from this reconcile's observation, not a previous one. Re-reading status.atProvider in Update would read whatever the object happens to carry, which for any code path that does not repopulate it is last poll's value.
- Update fails closed if no observation was captured. Falling back to the spec value would revert the field's real owner, which is the exact outcome the ignore list exists to prevent.
- Substitution always happens on a deep copy, so the reconciler's managed resource — and any cache it was decoded from — is never mutated, and there is no interaction with ResourceLateInitialized persistence.
A lost-update window remains between the observation and the mutation: if the external owner changes the field in that interval, the update writes the value observed a moment earlier and the owner reasserts on its next pass. It cannot be closed by a read-modify-write client. The window only opens during updates that were going to happen anyway — drift confined to ignored paths never triggers an update.
Index ¶
Constants ¶
const ( ReasonInSync xpv2.ConditionReason = "InSync" ReasonDrifted xpv2.ConditionReason = "Drifted" ReasonIgnored xpv2.ConditionReason = "DriftIgnored" ReasonUnobserved xpv2.ConditionReason = "IgnoredPathUnobserved" )
Condition reasons for TypeDriftDetected.
const TypeDriftDetected xpv2.ConditionType = "DriftDetected"
TypeDriftDetected is set on resources whose drift is observed but not corrected — either because mode is warn, or because every drifting field is under ignore.
Variables ¶
This section is empty.
Functions ¶
func WrapClient ¶
func WrapClient[T resource.Managed](inner managed.TypedExternalClient[T]) managed.TypedExternalClient[T]
WrapClient decorates a single TypedExternalClient. The returned client holds per-reconcile observation state and must not outlive one reconcile — which is what WrapConnector guarantees, since crossplane-runtime connects once per reconcile and disconnects at its end.
func WrapConnector ¶
func WrapConnector[T resource.Managed](c managed.TypedExternalConnector[T]) managed.TypedExternalConnector[T]
WrapConnector decorates a TypedExternalConnector so every client it produces honours spec.driftDetection. This is the single wiring point per controller; there is no per-resource logic anywhere in this package.
Types ¶
type Config ¶
Config is the resolved drift detection configuration for one resource.
func ReadConfig ¶
ReadConfig extracts the drift detection configuration from any managed resource without knowing its type. A resource whose schema has no driftDetection block yields the inert default.
Every ignore path is validated against the eligibility gate before it is accepted (see checkEligible). A single ineligible path fails the whole configuration: there is no partial application and no "ignore the rest of the list" fallback, because a config half-applied from a rejected request is as unreviewable as one silently accepted in full.
type Mode ¶
type Mode string
Mode selects the drift detection behaviour for a managed resource.
const ( // ModeEnabled detects and corrects drift. Default when unset, so adding // the field to an API is not a behaviour change for existing resources. ModeEnabled Mode = "enabled" // ModeWarn detects drift and reports it, but does not correct it. ModeWarn Mode = "warn" // ModeDisabled neither detects nor corrects drift. ModeDisabled Mode = "disabled" )
Directories
¶
| Path | Synopsis |
|---|---|
|
Package ddtest holds the test helper every controller package uses to prove that wrapping its external client with the drift detection decorator changes nothing for a resource that carries no spec.driftDetection block.
|
Package ddtest holds the test helper every controller package uses to prove that wrapping its external client with the drift detection decorator changes nothing for a resource that carries no spec.driftDetection block. |