@@ -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,31 @@ 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+ if err := setupRelatedResourceWatches (c , localManager , remoteManager , pubRes , localDummy , remoteDummy , log ); err != nil {
169+ return nil , err
170+ }
171+
172+ log .Info ("Done setting up unmanaged controller." )
173+
174+ return c , nil
175+ }
176+
177+ // setupRelatedResourceWatches sets up watches for all related resources that have a Watch
178+ // config, on their respective origin side, so that changes trigger primary reconciliation.
179+ func setupRelatedResourceWatches (
180+ c mccontroller.Controller ,
181+ localManager manager.Manager ,
182+ remoteManager mcmanager.Manager ,
183+ pubRes * syncagentv1alpha1.PublishedResource ,
184+ localDummy , remoteDummy * unstructured.Unstructured ,
185+ log * zap.SugaredLogger ,
186+ ) error {
187+ // Deduplication is per-origin to allow the same GVK on both sides.
188+ watchedKcpGVKs := sets .New [schema.GroupVersionKind ]()
189+ watchedServiceGVKs := sets .New [schema.GroupVersionKind ]()
190+
170191 for _ , relRes := range pubRes .Spec .Related {
171- if relRes .Origin != syncagentv1alpha1 . RelatedResourceOriginKcp || relRes . Watch == nil {
192+ if relRes .Watch == nil {
172193 continue
173194 }
174195
@@ -178,62 +199,155 @@ func Create(
178199 Resource : relRes .Resource ,
179200 }
180201
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
202+ // Use the REST mapper of the origin side: related resources may have projected GVKs
203+ // that differ between kcp and the service cluster, so we must resolve using the
204+ // mapper that actually knows about the GVR on that side.
205+ var originRESTMapper meta.RESTMapper
206+ if relRes .Origin == syncagentv1alpha1 .RelatedResourceOriginKcp {
207+ originRESTMapper = remoteManager .GetLocalManager ().GetRESTMapper ()
208+ } else {
209+ originRESTMapper = localManager .GetRESTMapper ()
186210 }
187211
188- // Deduplicate: only set up one watch per GVK.
189- if watchedGVKs . Has ( gvk ) {
190- continue
212+ gvk , err := originRESTMapper . KindFor ( gvr )
213+ if err != nil {
214+ return fmt . Errorf ( "failed to determine Kind for related resource %v (origin: %s): %w" , gvr , relRes . Origin , err )
191215 }
192- watchedGVKs .Insert (gvk )
193216
194217 relatedDummy := & unstructured.Unstructured {}
195218 relatedDummy .SetGroupVersionKind (gvk )
196219
197- var enqueueForRelated mchandler.TypedEventHandlerFunc [* unstructured.Unstructured , mcreconcile.Request ]
220+ if relRes .Origin == syncagentv1alpha1 .RelatedResourceOriginKcp {
221+ if watchedKcpGVKs .Has (gvk ) {
222+ continue
223+ }
224+ watchedKcpGVKs .Insert (gvk )
198225
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 ,
206- }
226+ enqueueForRelated , err := buildKcpRelatedHandler (relRes .Watch , gvk , remoteDummy , log )
227+ if err != nil {
228+ return err
207229 }
208230
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 ,
219- }
231+ if err := c .MultiClusterWatch (mcsource .TypedKind (relatedDummy , enqueueForRelated )); err != nil {
232+ return fmt .Errorf ("failed to setup watch for kcp-origin related resource %v: %w" , gvk , err )
233+ }
234+ } else {
235+ if watchedServiceGVKs .Has (gvk ) {
236+ continue
220237 }
238+ watchedServiceGVKs .Insert (gvk )
221239
222- default :
223- log . Warnw ( "origin:kcp related resource has Watch set but neither byOwner nor byLabel configured, skipping" , "gvk" , gvk )
224- continue
225- }
240+ enqueueForRelated , err := buildServiceRelatedHandler ( relRes . Watch , gvk , localDummy , localManager , log )
241+ if err != nil {
242+ return err
243+ }
226244
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 )
245+ if err := c .Watch (source .TypedKind (localManager .GetCache (), relatedDummy , enqueueForRelated )); err != nil {
246+ return fmt .Errorf ("failed to setup watch for service-origin related resource %v: %w" , gvk , err )
247+ }
229248 }
230249
231- log .Infow ("Set up watch for origin:kcp related resource" , "gvk" , gvk )
250+ log .Infow ("Set up watch for related resource" , "gvk" , gvk , "origin" , relRes . Origin )
232251 }
233252
234- log .Info ("Done setting up unmanaged controller." )
253+ return nil
254+ }
235255
236- return c , nil
256+ // buildKcpRelatedHandler constructs the per-cluster event handler for a kcp-origin related resource.
257+ func buildKcpRelatedHandler (
258+ watch * syncagentv1alpha1.RelatedResourceWatch ,
259+ gvk schema.GroupVersionKind ,
260+ remoteDummy * unstructured.Unstructured ,
261+ log * zap.SugaredLogger ,
262+ ) (mchandler.TypedEventHandlerFunc [* unstructured.Unstructured , mcreconcile.Request ], error ) {
263+ switch {
264+ case watch .ByOwner != nil :
265+ ownerGVK := remoteDummy .GroupVersionKind ()
266+ return func (clusterName string , _ cluster.Cluster ) handler.TypedEventHandler [* unstructured.Unstructured , mcreconcile.Request ] {
267+ return & byOwnerEventHandler {
268+ clusterName : clusterName ,
269+ ownerGVK : ownerGVK ,
270+ }
271+ }, nil
272+
273+ case watch .BySelector != nil :
274+ labelSelector := watch .BySelector
275+ primaryDummy := remoteDummy .DeepCopy ()
276+ return func (clusterName string , cl cluster.Cluster ) handler.TypedEventHandler [* unstructured.Unstructured , mcreconcile.Request ] {
277+ return & bySelectorEventHandler {
278+ clusterName : clusterName ,
279+ client : cl .GetClient (),
280+ primaryDummy : primaryDummy ,
281+ labelSelector : labelSelector ,
282+ log : log ,
283+ }
284+ }, nil
285+
286+ default :
287+ return nil , fmt .Errorf ("related resource %v (origin: kcp) has Watch set but neither byOwner nor bySelector configured" , gvk )
288+ }
289+ }
290+
291+ // buildServiceRelatedHandler constructs the event handler for a service-cluster-origin related resource.
292+ // It maps the changed related resource back to the remote (kcp) primary via sync metadata on the local primary.
293+ func buildServiceRelatedHandler (
294+ watch * syncagentv1alpha1.RelatedResourceWatch ,
295+ gvk schema.GroupVersionKind ,
296+ localDummy * unstructured.Unstructured ,
297+ localManager manager.Manager ,
298+ log * zap.SugaredLogger ,
299+ ) (handler.TypedEventHandler [* unstructured.Unstructured , mcreconcile.Request ], error ) {
300+ localClient := localManager .GetClient ()
301+
302+ switch {
303+ case watch .ByOwner != nil :
304+ ownerGVK := localDummy .GroupVersionKind ()
305+ primaryDummy := localDummy .DeepCopy ()
306+ return handler .TypedEnqueueRequestsFromMapFunc (func (ctx context.Context , obj * unstructured.Unstructured ) []mcreconcile.Request {
307+ for _ , ref := range obj .GetOwnerReferences () {
308+ refGV , err := schema .ParseGroupVersion (ref .APIVersion )
309+ if err != nil || refGV .Group != ownerGVK .Group || refGV .Version != ownerGVK .Version || ref .Kind != ownerGVK .Kind {
310+ continue
311+ }
312+ localPrimary := primaryDummy .DeepCopy ()
313+ if err := localClient .Get (ctx , types.NamespacedName {Namespace : obj .GetNamespace (), Name : ref .Name }, localPrimary ); err != nil {
314+ log .Warnw ("Failed to fetch local primary for byOwner watch" , "owner" , ref .Name , "error" , err )
315+ return nil
316+ }
317+ if req := sync .RemoteNameForLocalObject (localPrimary ); req != nil {
318+ return []mcreconcile.Request {* req }
319+ }
320+ return nil
321+ }
322+ return nil
323+ }), nil
324+
325+ case watch .BySelector != nil :
326+ selector , err := metav1 .LabelSelectorAsSelector (watch .BySelector )
327+ if err != nil {
328+ return nil , fmt .Errorf ("failed to convert bySelector for service-origin related resource %v: %w" , gvk , err )
329+ }
330+ primaryDummy := localDummy .DeepCopy ()
331+ return handler .TypedEnqueueRequestsFromMapFunc (func (ctx context.Context , _ * unstructured.Unstructured ) []mcreconcile.Request {
332+ primaryList := & unstructured.UnstructuredList {}
333+ primaryList .SetAPIVersion (primaryDummy .GetAPIVersion ())
334+ primaryList .SetKind (primaryDummy .GetKind () + "List" )
335+ if err := localClient .List (ctx , primaryList , & ctrlruntimeclient.ListOptions {LabelSelector : selector }); err != nil {
336+ log .Warnw ("Failed to list local primary objects for bySelector watch" , "selector" , selector .String (), "error" , err )
337+ return nil
338+ }
339+ var reqs []mcreconcile.Request
340+ for i := range primaryList .Items {
341+ if req := sync .RemoteNameForLocalObject (& primaryList .Items [i ]); req != nil {
342+ reqs = append (reqs , * req )
343+ }
344+ }
345+ return reqs
346+ }), nil
347+
348+ default :
349+ return nil , fmt .Errorf ("related resource %v (origin: service) has Watch set but neither byOwner nor bySelector configured" , gvk )
350+ }
237351}
238352
239353func (r * Reconciler ) Reconcile (ctx context.Context , request mcreconcile.Request ) (reconcile.Result , error ) {
0 commit comments