Skip to content

Block a resource whose live object is controlled by another owner #199

Description

@sourcehawk

Problem

Two owners of one kind whose components render the same object no longer share a Server-Side Apply field manager (#197, #198), so for resources that carry a controller reference the second owner's apply is rejected by the API server and its component reports an error. That leaves two gaps. For a resource registered with Unowned(), or one whose owner reference cannot be set because of a scope mismatch, nothing carries the second owner's identity into the object, so its forced apply still takes the fields it declares and the fields move between the two owners' managers on every reconcile while both report converged. And where the apply is rejected, the outcome is an apply error with the API server's "Only one reference can have Controller set to true" text rather than a condition that says which owner holds the object. konsole-is/camunda-operator#262 works around this with a hand-written guard that reads the live object and blocks when another owner controls it; that logic belongs in the framework.

Approach

Add a resource option, component.BlockOnForeignController(), evaluated in reconcileResources after the resource's own guard and before the apply. It reads the live object through rec.Client, and when the object exists and has a controller reference whose UID is not rec.Owner.GetUID(), it records concepts.GuardStatusBlocked with a reason naming the controlling owner (controlled by <Kind> <name>) and stops the resources after it. It runs in the reconcile loop rather than as a WithGuard callback, because a custom guard receives only a copy of the desired object and no client. An object with no controller reference at all is not blocked; contention between two Unowned() owners is then still undetected, and the docs say so. Unlike other guards the check also covers every path that would delete the object: during suspension a resource another owner controls is neither scaled down nor deleted (it counts as suspended, so the component reports the usual Suspended condition), and a deletion asked for by Delete(), DeleteWhen(), GatedBy() or a disabled component feature gate is skipped; each skip is logged with the controlling owner. Combining the option with ReadOnly() is a build error. docs/component.md (guards, resource options) and the docs/primitives.md Server-Side Apply section, which previously said a shared name is the operator's responsibility and pointed at a hand-written guard, are updated.

Verification

  • envtest spec: owner A reconciles a ConfigMap, owner B registers the same ConfigMap with the option; B's component reports Blocked with a reason naming A, B performs no apply, and the ConfigMap's data, owner references and managedFields are unchanged.
  • envtest spec: the same with both registrations Unowned() and A having set a controller reference by other means; B is blocked. With no controller reference on the object, B is not blocked.
  • envtest spec: once A's owner reference is removed (A orphans or deletes the object), B's next reconcile is unblocked and applies.
  • Unit test that combining the option with ReadOnly() is a build error, and that a suspended component, a disabled feature gate and a Delete() registration neither apply nor delete an object another owner controls.
  • make all passes; docs updated and make sync-plugin run.

Out of scope

Detecting contention between owners that both apply without a controller reference. Without any owner identity on the object there is nothing to compare; the docs state that a shared name between such owners is the operator's responsibility.

Context

Follow-up to #197 and #198. Consumer-side workaround and tests: konsole-is/camunda-operator#262.

Metadata

Metadata

Assignees

Labels

featureUser-observable capability or surface

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions