@@ -36,6 +36,7 @@ import (
3636
3737 corev1 "k8s.io/api/core/v1"
3838 apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
39+ "k8s.io/apimachinery/pkg/api/meta"
3940 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
4041 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
4142 "k8s.io/apimachinery/pkg/labels"
@@ -164,11 +165,13 @@ func Create(
164165 return nil , fmt .Errorf ("failed to setup local-side watch: %w" , err )
165166 }
166167
167- // Watch origin:kcp related resources so that changes to them trigger reconciliation
168- // of the owning primary object. Only related resources with a Watch config are covered.
169- watchedGVKs := sets .New [schema.GroupVersionKind ]()
168+ // Watch related resources on their origin side so that changes to them trigger
169+ // reconciliation of the owning primary object. Only related resources with a Watch
170+ // config are covered. Deduplication is per-origin to allow the same GVK on both sides.
171+ watchedKcpGVKs := sets .New [schema.GroupVersionKind ]()
172+ watchedServiceGVKs := sets .New [schema.GroupVersionKind ]()
170173 for _ , relRes := range pubRes .Spec .Related {
171- if relRes .Origin != syncagentv1alpha1 . RelatedResourceOriginKcp || relRes . Watch == nil {
174+ if relRes .Watch == nil {
172175 continue
173176 }
174177
@@ -178,57 +181,134 @@ func Create(
178181 Resource : relRes .Resource ,
179182 }
180183
181- // Use the local REST mapper to determine the Kind.
182- gvk , err := localManager .GetRESTMapper ().KindFor (gvr )
183- if err != nil {
184- log .Warnw ("Failed to determine Kind for origin:kcp related resource, skipping watch" , "gvr" , gvr , "error" , err )
185- continue
184+ // Use the REST mapper of the origin side: related resources may have projected GVKs
185+ // that differ between kcp and the service cluster, so we must resolve using the
186+ // mapper that actually knows about the GVR on that side.
187+ var originRESTMapper meta.RESTMapper
188+ if relRes .Origin == syncagentv1alpha1 .RelatedResourceOriginKcp {
189+ originRESTMapper = remoteManager .GetLocalManager ().GetRESTMapper ()
190+ } else {
191+ originRESTMapper = localManager .GetRESTMapper ()
186192 }
187193
188- // Deduplicate: only set up one watch per GVK.
189- if watchedGVKs . Has ( gvk ) {
190- continue
194+ gvk , err := originRESTMapper . KindFor ( gvr )
195+ if err != nil {
196+ return nil , fmt . Errorf ( "failed to determine Kind for related resource %v (origin: %s): %w" , gvr , relRes . Origin , err )
191197 }
192- watchedGVKs .Insert (gvk )
193198
194199 relatedDummy := & unstructured.Unstructured {}
195200 relatedDummy .SetGroupVersionKind (gvk )
196201
197- var enqueueForRelated mchandler.TypedEventHandlerFunc [* unstructured.Unstructured , mcreconcile.Request ]
202+ if relRes .Origin == syncagentv1alpha1 .RelatedResourceOriginKcp {
203+ // Deduplicate: only set up one watch per GVK per origin side.
204+ if watchedKcpGVKs .Has (gvk ) {
205+ continue
206+ }
207+ watchedKcpGVKs .Insert (gvk )
208+
209+ // The related resource lives in the kcp workspace; watch it via MultiClusterWatch.
210+ var enqueueForRelated mchandler.TypedEventHandlerFunc [* unstructured.Unstructured , mcreconcile.Request ]
211+
212+ switch {
213+ case relRes .Watch .ByOwner != nil :
214+ ownerGVK := remoteDummy .GroupVersionKind ()
215+ enqueueForRelated = func (clusterName string , _ cluster.Cluster ) handler.TypedEventHandler [* unstructured.Unstructured , mcreconcile.Request ] {
216+ return & byOwnerEventHandler {
217+ clusterName : clusterName ,
218+ ownerGVK : ownerGVK ,
219+ }
220+ }
198221
199- switch {
200- case relRes .Watch .ByOwner != nil :
201- ownerKind := relRes .Watch .ByOwner .Kind
202- enqueueForRelated = func (clusterName string , _ cluster.Cluster ) handler.TypedEventHandler [* unstructured.Unstructured , mcreconcile.Request ] {
203- return & byOwnerEventHandler {
204- clusterName : clusterName ,
205- ownerKind : ownerKind ,
222+ case relRes .Watch .BySelector != nil :
223+ labelSelector := relRes .Watch .BySelector
224+ primaryDummy := remoteDummy .DeepCopy ()
225+ enqueueForRelated = func (clusterName string , cl cluster.Cluster ) handler.TypedEventHandler [* unstructured.Unstructured , mcreconcile.Request ] {
226+ return & bySelectorEventHandler {
227+ clusterName : clusterName ,
228+ client : cl .GetClient (),
229+ primaryDummy : primaryDummy ,
230+ labelSelector : labelSelector ,
231+ log : log ,
232+ }
206233 }
234+
235+ default :
236+ return nil , fmt .Errorf ("related resource %v (origin: %s) has Watch set but neither byOwner nor bySelector configured" , gvk , relRes .Origin )
207237 }
208238
209- case relRes .Watch .ByLabel != nil :
210- labelTemplates := relRes .Watch .ByLabel
211- primaryDummy := remoteDummy .DeepCopy ()
212- enqueueForRelated = func (clusterName string , cl cluster.Cluster ) handler.TypedEventHandler [* unstructured.Unstructured , mcreconcile.Request ] {
213- return & byLabelEventHandler {
214- clusterName : clusterName ,
215- client : cl .GetClient (),
216- primaryDummy : primaryDummy ,
217- labelTemplates : labelTemplates ,
218- log : log ,
239+ if err := c .MultiClusterWatch (mcsource .TypedKind (relatedDummy , enqueueForRelated )); err != nil {
240+ return nil , fmt .Errorf ("failed to setup watch for kcp-origin related resource %v: %w" , gvk , err )
241+ }
242+ } else {
243+ // Deduplicate: only set up one watch per GVK per origin side.
244+ if watchedServiceGVKs .Has (gvk ) {
245+ continue
246+ }
247+ watchedServiceGVKs .Insert (gvk )
248+
249+ // The related resource lives on the service cluster; watch it via the local manager.
250+ // Map the changed related resource back to the remote (kcp) primary object by going
251+ // through the local primary, which carries the remote cluster/name in its metadata.
252+ localClient := localManager .GetClient ()
253+
254+ var enqueueForRelated handler.TypedEventHandler [* unstructured.Unstructured , mcreconcile.Request ]
255+
256+ switch {
257+ case relRes .Watch .ByOwner != nil :
258+ ownerGVK := localDummy .GroupVersionKind ()
259+ primaryDummy := localDummy .DeepCopy ()
260+ enqueueForRelated = handler .TypedEnqueueRequestsFromMapFunc (func (ctx context.Context , obj * unstructured.Unstructured ) []mcreconcile.Request {
261+ for _ , ref := range obj .GetOwnerReferences () {
262+ refGV , err := schema .ParseGroupVersion (ref .APIVersion )
263+ if err != nil || refGV .Group != ownerGVK .Group || refGV .Version != ownerGVK .Version || ref .Kind != ownerGVK .Kind {
264+ continue
265+ }
266+ localPrimary := primaryDummy .DeepCopy ()
267+ if err := localClient .Get (ctx , types.NamespacedName {Namespace : obj .GetNamespace (), Name : ref .Name }, localPrimary ); err != nil {
268+ log .Warnw ("Failed to fetch local primary for byOwner watch" , "owner" , ref .Name , "error" , err )
269+ return nil
270+ }
271+ if req := sync .RemoteNameForLocalObject (localPrimary ); req != nil {
272+ return []mcreconcile.Request {* req }
273+ }
274+ return nil
275+ }
276+ return nil
277+ })
278+
279+ case relRes .Watch .BySelector != nil :
280+ selector , err := metav1 .LabelSelectorAsSelector (relRes .Watch .BySelector )
281+ if err != nil {
282+ return nil , fmt .Errorf ("failed to convert bySelector for service-origin related resource %v: %w" , gvk , err )
219283 }
284+ primaryDummy := localDummy .DeepCopy ()
285+ enqueueForRelated = handler .TypedEnqueueRequestsFromMapFunc (func (ctx context.Context , _ * unstructured.Unstructured ) []mcreconcile.Request {
286+ primaryList := & unstructured.UnstructuredList {}
287+ primaryList .SetAPIVersion (primaryDummy .GetAPIVersion ())
288+ primaryList .SetKind (primaryDummy .GetKind () + "List" )
289+ if err := localClient .List (ctx , primaryList , & ctrlruntimeclient.ListOptions {LabelSelector : selector }); err != nil {
290+ log .Warnw ("Failed to list local primary objects for bySelector watch" , "selector" , selector .String (), "error" , err )
291+ return nil
292+ }
293+ var reqs []mcreconcile.Request
294+ for i := range primaryList .Items {
295+ if req := sync .RemoteNameForLocalObject (& primaryList .Items [i ]); req != nil {
296+ reqs = append (reqs , * req )
297+ }
298+ }
299+ return reqs
300+ })
301+
302+ default :
303+ return nil , fmt .Errorf ("related resource %v (origin: %s) has Watch set but neither byOwner nor bySelector configured" , gvk , relRes .Origin )
220304 }
221305
222- default :
223- log .Warnw ("origin:kcp related resource has Watch set but neither byOwner nor byLabel configured, skipping" , "gvk" , gvk )
224- continue
225- }
226-
227- if err := c .MultiClusterWatch (mcsource .TypedKind (relatedDummy , enqueueForRelated )); err != nil {
228- return nil , fmt .Errorf ("failed to setup watch for origin:kcp related resource %v: %w" , gvk , err )
306+ if err := c .Watch (source .TypedKind (localManager .GetCache (), relatedDummy , enqueueForRelated )); err != nil {
307+ return nil , fmt .Errorf ("failed to setup watch for service-origin related resource %v: %w" , gvk , err )
308+ }
229309 }
230310
231- log .Infow ("Set up watch for origin:kcp related resource" , "gvk" , gvk )
311+ log .Infow ("Set up watch for related resource" , "gvk" , gvk , "origin" , relRes . Origin )
232312 }
233313
234314 log .Info ("Done setting up unmanaged controller." )
0 commit comments