Synchronization Controllers
The managed-CRD controller watches kbind-managed CustomResourceDefinitions and their Connections in the consumer cluster. It starts a dynamic instance syncer for each selected group/version/resource, or GVR. Both are implemented in engine/sync.
They are responsible for:
- Starting, stopping, and rebuilding syncers as CRDs and provider connections change.
- Selecting instances covered by a Ready ClusterBinding or namespaced Binding.
- Applying consumer spec to owned provider objects and copying provider status back.
- Reporting ownership conflicts without overwriting foreign objects.
- Deleting owned provider copies before releasing consumer instance finalizers.
Overview
The managed-CRD controller resolves the provider from the CRD's core.kbind.io/connection annotation. A missing annotation or an API error is returned for retry.
flowchart TD
start(["Managed CRD or Connection event"])
crd{"CRD exists<br/>and is not deleting?"}
connection{"Connection Ready<br/>and localClusterUID set?"}
engaged{"Provider client and cache<br/>engaged?"}
current{"Existing syncer has current generation<br/>and the same provider instance?"}
stop_old(["Stop the previous syncer"])
version(["Choose storage or served version<br/>Build consumer informer and instance controller"])
watches(["Watch consumer and provider instances<br/>Watch bindings covering this API"])
run(["Start controller and informer<br/>Wait for cache sync and record syncer"])
stop_syncer(["Stop existing syncer"])
wait_provider(["Stop existing syncer<br/>Requeue after 2 seconds"])
stop(["Stop"])
start --> crd
crd -->|no| stop_syncer
crd -->|yes| connection
connection -->|no| stop_syncer
connection -->|yes| engaged
engaged -->|no| wait_provider
engaged -->|yes| current
current -->|yes| stop
current -->|no| stop_old
stop_old --> version
version --> watches
watches --> run
run --> stop
stop_syncer --> stop
wait_provider --> stop
The generation check detects schema changes. Comparing the provider instance also detects re-engagement after an outage, so a syncer cannot keep the old client/cache simply because the CRD generation is unchanged.
Instance reconciliation
Consumer events, binding events, and provider cache events enqueue instances. Provider events are filtered by consumer-cluster UID and mapped back to the consumer object key. Reads of provider objects use the engaged cluster's API reader, while writes use its client.
flowchart TD
start(["Consumer, provider, or binding event"])
object{"Consumer object<br/>exists?"}
resolve(["Resolve covering binding<br/>ClusterBinding before namespaced Binding"])
deleting{"Consumer object<br/>deleting?"}
cleanup(["Run instance deletion"])
bound{"Covering binding<br/>Ready?"}
map_key(["Map consumer key to provider key"])
finalizer{"Syncer finalizer<br/>already present?"}
add_finalizer(["Add finalizer and requeue"])
target(["Read provider object"])
ownership{"Provider target ownership?"}
adopt{"conflictPolicy<br/>is Adopt?"}
conflict(["Record conflict annotation<br/>and Warning Event"])
namespace(["Ensure provider namespace<br/>for a new namespaced object"])
apply(["Clear old conflict marker<br/>Apply spec and ownership markers"])
status(["Read provider object again<br/>Copy status if present"])
stop(["Stop"])
start --> object
object -->|no| stop
object -->|yes| resolve
resolve --> deleting
deleting -->|yes| cleanup
cleanup --> stop
deleting -->|no| bound
bound -->|no| stop
bound -->|yes| map_key
map_key --> finalizer
finalizer -->|no| add_finalizer
add_finalizer --> stop
finalizer -->|yes| target
target --> ownership
ownership -->|absent| namespace
namespace --> apply
ownership -->|ours| apply
ownership -->|no ownership markers| adopt
ownership -->|owned by another| conflict
adopt -->|yes| apply
adopt -->|no| conflict
conflict --> stop
apply --> status
status --> stop
Ownership requires both the consumer-cluster UID and consumer-object UID. Adopt accepts only markerless objects, never objects owned by another consumer/object. Spec apply uses server-side apply with forced field ownership. An absent provider status does not clear an existing consumer status.
Forbidden namespace creation or spec apply emits a Warning Event and retries after 30 seconds. Other API errors are returned for retry. Successful instance reconciliation schedules a ten-minute backstop, with watches and the consumer informer's resync providing additional triggers.
Instance deletion
flowchart TD
start(["Deleting consumer object"])
finalizer{"Syncer finalizer<br/>present?"}
orphan{"deletion-policy<br/>is Orphan?"}
read_provider(["Read mapped provider object"])
owned{"Provider copy exists<br/>and is ours?"}
delete_copy(["Request provider deletion<br/>Requeue after 2 seconds"])
release(["Remove consumer syncer finalizer"])
stop(["Stop"])
start --> finalizer
finalizer -->|no| stop
finalizer -->|yes| orphan
orphan -->|yes| release
orphan -->|no| read_provider
read_provider --> owned
owned -->|yes| delete_copy
delete_copy --> stop
owned -->|no| release
release --> stop
This path runs even if the binding is no longer Ready. An owned provider object must disappear before its consumer finalizer is released. Provider read/delete errors are retried, not treated as absence. Foreign objects are left untouched.
The stock Mapper preserves scope, namespace, and name. It is a compile-time extension point for instance keys, not a CRD isolation flag. Related-resource sync remains in binding reconciliation and does not use that mapper.
See Resource Synchronization for conflict policy, finalizers, related-resource selection, and supported behavior.
Implementation: managed-CRD controller, binding resolution, and instance syncer.