@@ -39,9 +39,14 @@ import (
3939 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
4040 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
4141 "k8s.io/apimachinery/pkg/labels"
42+ "k8s.io/apimachinery/pkg/runtime/schema"
4243 "k8s.io/apimachinery/pkg/types"
44+ "k8s.io/apimachinery/pkg/util/sets"
45+ "k8s.io/client-go/util/workqueue"
4346 "k8s.io/utils/ptr"
4447 ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client"
48+ "sigs.k8s.io/controller-runtime/pkg/cluster"
49+ "sigs.k8s.io/controller-runtime/pkg/event"
4550 "sigs.k8s.io/controller-runtime/pkg/handler"
4651 "sigs.k8s.io/controller-runtime/pkg/manager"
4752 "sigs.k8s.io/controller-runtime/pkg/predicate"
@@ -67,6 +72,41 @@ type Reconciler struct {
6772 localCRD * apiextensionsv1.CustomResourceDefinition
6873 stateNamespace string
6974 agentName string
75+ relatedIndex * sync.RelatedObjectIndex
76+ }
77+
78+ // relatedResourceEventHandler enqueues the primary object whenever a kcp-origin
79+ // related resource changes. It uses the RelatedObjectIndex to reverse-map the
80+ // changed related object back to its owning primary object.
81+ type relatedResourceEventHandler struct {
82+ clusterName string
83+ relatedIndex * sync.RelatedObjectIndex
84+ group string
85+ resource string
86+ }
87+
88+ func (h * relatedResourceEventHandler ) Create (_ context.Context , evt event.TypedCreateEvent [* unstructured.Unstructured ], q workqueue.TypedRateLimitingInterface [mcreconcile.Request ]) {
89+ h .enqueue (evt .Object , q )
90+ }
91+
92+ func (h * relatedResourceEventHandler ) Update (_ context.Context , evt event.TypedUpdateEvent [* unstructured.Unstructured ], q workqueue.TypedRateLimitingInterface [mcreconcile.Request ]) {
93+ h .enqueue (evt .ObjectNew , q )
94+ }
95+
96+ func (h * relatedResourceEventHandler ) Delete (_ context.Context , evt event.TypedDeleteEvent [* unstructured.Unstructured ], q workqueue.TypedRateLimitingInterface [mcreconcile.Request ]) {
97+ h .enqueue (evt .Object , q )
98+ }
99+
100+ func (h * relatedResourceEventHandler ) Generic (_ context.Context , evt event.TypedGenericEvent [* unstructured.Unstructured ], q workqueue.TypedRateLimitingInterface [mcreconcile.Request ]) {
101+ h .enqueue (evt .Object , q )
102+ }
103+
104+ func (h * relatedResourceEventHandler ) enqueue (obj * unstructured.Unstructured , q workqueue.TypedRateLimitingInterface [mcreconcile.Request ]) {
105+ req , ok := h .relatedIndex .Get (h .clusterName , h .group , h .resource , obj .GetNamespace (), obj .GetName ())
106+ if ! ok {
107+ return
108+ }
109+ q .Add (req )
70110}
71111
72112// Create creates a new controller and importantly does *not* add it to the manager,
@@ -120,6 +160,7 @@ func Create(
120160 stateNamespace : stateNamespace ,
121161 agentName : agentName ,
122162 localCRD : localCRD ,
163+ relatedIndex : sync .NewRelatedObjectIndex (),
123164 }
124165
125166 ctrlOptions := mccontroller.Options {
@@ -161,6 +202,59 @@ func Create(
161202 return nil , fmt .Errorf ("failed to setup local-side watch: %w" , err )
162203 }
163204
205+ // Watch origin:kcp related resources in the virtual workspace so that changes
206+ // to them trigger reconciliation of their owning primary object.
207+ watchedGVKs := sets .New [schema.GroupVersionKind ]()
208+ for _ , relRes := range pubRes .Spec .Related {
209+ if relRes .Origin != syncagentv1alpha1 .RelatedResourceOriginKcp {
210+ continue
211+ }
212+
213+ gvr := schema.GroupVersionResource {
214+ Group : relRes .Group ,
215+ Version : relRes .Version ,
216+ Resource : relRes .Resource ,
217+ }
218+
219+ // Use the local REST mapper to determine the Kind. Core resources (ConfigMap,
220+ // Secret, …) and CRDs installed on the service cluster are covered by this.
221+ gvk , err := localManager .GetRESTMapper ().KindFor (gvr )
222+ if err != nil {
223+ log .Warnw ("failed to determine Kind for origin:kcp related resource, skipping watch" , "gvr" , gvr , "error" , err )
224+ continue
225+ }
226+
227+ // Deduplicate: only set up one watch per GVK.
228+ if watchedGVKs .Has (gvk ) {
229+ continue
230+ }
231+
232+ watchedGVKs .Insert (gvk )
233+
234+ relatedDummy := & unstructured.Unstructured {}
235+ relatedDummy .SetGroupVersionKind (gvk )
236+
237+ group := relRes .Group
238+ resource := relRes .Resource
239+
240+ enqueueForRelated := mchandler.TypedEventHandlerFunc [* unstructured.Unstructured , mcreconcile.Request ](
241+ func (clusterName string , _ cluster.Cluster ) handler.TypedEventHandler [* unstructured.Unstructured , mcreconcile.Request ] {
242+ return & relatedResourceEventHandler {
243+ clusterName : clusterName ,
244+ relatedIndex : reconciler .relatedIndex ,
245+ group : group ,
246+ resource : resource ,
247+ }
248+ },
249+ )
250+
251+ if err := c .MultiClusterWatch (mcsource .TypedKind (relatedDummy , enqueueForRelated )); err != nil {
252+ return nil , fmt .Errorf ("failed to setup watch for origin:kcp related resource %v: %w" , gvk , err )
253+ }
254+
255+ log .Infow ("Set up watch for origin:kcp related resource" , "gvk" , gvk )
256+ }
257+
164258 log .Info ("Done setting up unmanaged controller." )
165259
166260 return c , nil
@@ -226,7 +320,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, request mcreconcile.Request)
226320 }
227321
228322 // sync main object
229- syncer , err := sync .NewResourceSyncer (log , r .localClient , vwClient , r .pubRes , r .localCRD , mutation .NewMutator , r .stateNamespace , r .agentName )
323+ syncer , err := sync .NewResourceSyncer (log , r .localClient , vwClient , r .pubRes , r .localCRD , mutation .NewMutator , r .stateNamespace , r .agentName , r . relatedIndex )
230324 if err != nil {
231325 recorder .Event (remoteObj , corev1 .EventTypeWarning , "ReconcilingError" , "Failed to process object: a provider-side issue has occurred." )
232326 return reconcile.Result {}, fmt .Errorf ("failed to create syncer: %w" , err )
0 commit comments