@@ -18,6 +18,7 @@ package sync
1818
1919import (
2020 "encoding/json"
21+ "errors"
2122 "fmt"
2223 "regexp"
2324
@@ -27,7 +28,9 @@ import (
2728 "github.com/kcp-dev/api-syncagent/internal/mutation"
2829 syncagentv1alpha1 "github.com/kcp-dev/api-syncagent/sdk/apis/syncagent/v1alpha1"
2930
31+ corev1 "k8s.io/api/core/v1"
3032 apierrors "k8s.io/apimachinery/pkg/api/errors"
33+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3134 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
3235 "k8s.io/apimachinery/pkg/types"
3336 ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client"
@@ -70,29 +73,19 @@ func (s *ResourceSyncer) processRelatedResource(log *zap.SugaredLogger, stateSto
7073 dest = local
7174 }
7275
73- // to find the source related object, we first need to determine its name/namespace
74- sourceKey , err := resolveResourceReference (source . object , relRes . Reference )
76+ // find the source object by applying the ResourceSourceSpec
77+ sourceObj , err := resolveResourceSource (source , relRes )
7578 if err != nil {
76- return false , fmt .Errorf ("failed to determine related object's source key : %w" , err )
79+ return false , fmt .Errorf ("failed to get source object : %w" , err )
7780 }
7881
79- // find the source related object
80- sourceObj := & unstructured.Unstructured {}
81- sourceObj .SetAPIVersion ("v1" ) // we only support ConfigMaps and Secrets, both are in core/v1
82- sourceObj .SetKind (relRes .Kind )
83-
84- err = source .client .Get (source .ctx , * sourceKey , sourceObj )
85- if err != nil {
86- // the source object doesn't exist yet, so we can just stop
87- if apierrors .IsNotFound (err ) {
88- return false , nil
89- }
90-
91- return false , fmt .Errorf ("failed to get source object: %w" , err )
82+ // the source object doesn't exist yet, so we can just stop
83+ if sourceObj == nil {
84+ return false , nil
9285 }
9386
9487 // do the same to find the destination object
95- destKey , err := resolveResourceReference (dest . object , relRes . Reference )
88+ destKey , err := resolveResourceDestination (dest , relRes )
9689 if err != nil {
9790 return false , fmt .Errorf ("failed to determine related object's destination key: %w" , err )
9891 }
@@ -201,23 +194,171 @@ func (s *ResourceSyncer) processRelatedResource(log *zap.SugaredLogger, stateSto
201194 return false , nil
202195}
203196
204- func resolveResourceReference ( obj * unstructured. Unstructured , ref syncagentv1alpha1.RelatedResourceReference ) (* ctrlruntimeclient. ObjectKey , error ) {
205- jsonData , err := obj .MarshalJSON ()
197+ func resolveResourceSource ( side syncSide , relRes syncagentv1alpha1.RelatedResourceSpec ) (* unstructured. Unstructured , error ) {
198+ jsonData , err := side . object .MarshalJSON ()
206199 if err != nil {
207200 return nil , err
208201 }
209202
210- name , err := resolveResourceLocator (string (jsonData ), ref .Name )
203+ // resolving the namespace first allows us to scope down any .List() calls
204+ // for the name of the object
205+ namespace := side .object .GetNamespace ()
206+ if relRes .Source .Namespace != nil {
207+ namespace , err = resolveResourceSourceNamespace (side , string (jsonData ), * relRes .Source .Namespace )
208+ if err != nil {
209+ return nil , fmt .Errorf ("failed to resolve namespace: %w" , err )
210+ }
211+
212+ if namespace == "" {
213+ return nil , nil
214+ }
215+ } else if namespace == "" {
216+ return nil , errors .New ("primary object is cluster-scoped and no source namespace configuration was provided" )
217+ }
218+
219+ obj , err := resolveResourceSourceName (side , string (jsonData ), relRes , relRes .Source .RelatedResourceSourceSpec , namespace )
211220 if err != nil {
212- return nil , fmt .Errorf ("cannot determine name : %w" , err )
221+ return nil , fmt .Errorf ("failed to resolve : %w" , err )
213222 }
214223
215- namespace := obj .GetNamespace ()
216- if ref .Namespace != nil {
217- namespace , err = resolveResourceLocator (string (jsonData ), * ref .Namespace )
224+ return obj , nil
225+ }
226+
227+ func resolveResourceSourceNamespace (side syncSide , jsonData string , spec syncagentv1alpha1.RelatedResourceSourceSpec ) (string , error ) {
228+ switch {
229+ case spec .Reference != nil :
230+ return resolveResourceReference (jsonData , * spec .Reference )
231+
232+ case spec .Selector != nil :
233+ namespaces := & corev1.NamespaceList {}
234+
235+ selector , err := metav1 .LabelSelectorAsSelector (& spec .Selector .LabelSelector )
218236 if err != nil {
219- return nil , fmt .Errorf ("cannot determine namespace: %w" , err )
237+ return "" , fmt .Errorf ("invalid selector configured: %w" , err )
238+ }
239+
240+ opts := & ctrlruntimeclient.ListOptions {
241+ LabelSelector : selector ,
242+ Limit : 2 ,
220243 }
244+
245+ if err := side .client .List (side .ctx , namespaces , opts ); err != nil {
246+ return "" , fmt .Errorf ("failed to evaluate label selector: %w" , err )
247+ }
248+
249+ switch len (namespaces .Items ) {
250+ case 0 :
251+ // it's okay if the source namespace, and therefore the source related object, doesn't exist (yet)
252+ return "" , nil
253+ case 1 :
254+ return namespaces .Items [0 ].Name , nil
255+ default :
256+ return "" , fmt .Errorf ("expected one namespace, but found %d matching the label selector" , len (namespaces .Items ))
257+ }
258+
259+ case spec .Expression != "" :
260+ return "" , errors .New ("not yet implemented" )
261+
262+ default :
263+ return "" , errors .New ("invalid sourceSpec: no mechanism configured" )
264+ }
265+ }
266+
267+ func resolveResourceSourceName (side syncSide , jsonData string , relRes syncagentv1alpha1.RelatedResourceSpec , spec syncagentv1alpha1.RelatedResourceSourceSpec , namespace string ) (* unstructured.Unstructured , error ) {
268+ switch {
269+ case spec .Reference != nil :
270+ name , err := resolveResourceReference (jsonData , * spec .Reference )
271+ if err != nil {
272+ return nil , err
273+ }
274+
275+ // we assume an operator on the service side will fill-in this value later
276+ if name == "" {
277+ return nil , nil
278+ }
279+
280+ // find the source related object
281+ sourceObj := & unstructured.Unstructured {}
282+ sourceObj .SetAPIVersion ("v1" ) // we only support ConfigMaps and Secrets, both are in core/v1
283+ sourceObj .SetKind (relRes .Kind )
284+
285+ err = side .client .Get (side .ctx , types.NamespacedName {Name : name , Namespace : namespace }, sourceObj )
286+ if err != nil {
287+ // the source object doesn't exist yet, so we can just stop
288+ if apierrors .IsNotFound (err ) {
289+ return nil , nil
290+ }
291+
292+ return nil , fmt .Errorf ("failed to get source object: %w" , err )
293+ }
294+
295+ return sourceObj , nil
296+
297+ case spec .Selector != nil :
298+ sourceObjs := & unstructured.UnstructuredList {}
299+ sourceObjs .SetAPIVersion ("v1" ) // we only support ConfigMaps and Secrets, both are in core/v1
300+ sourceObjs .SetKind (relRes .Kind )
301+
302+ selector , err := metav1 .LabelSelectorAsSelector (& spec .Selector .LabelSelector )
303+ if err != nil {
304+ return nil , fmt .Errorf ("invalid selector configured: %w" , err )
305+ }
306+
307+ opts := & ctrlruntimeclient.ListOptions {
308+ LabelSelector : selector ,
309+ Limit : 2 ,
310+ Namespace : namespace ,
311+ }
312+
313+ if err := side .client .List (side .ctx , sourceObjs , opts ); err != nil {
314+ return nil , fmt .Errorf ("failed to evaluate label selector: %w" , err )
315+ }
316+
317+ switch len (sourceObjs .Items ) {
318+ case 0 :
319+ // it's okay if the source object doesn't exist (yet)
320+ return nil , nil
321+ case 1 :
322+ return & sourceObjs .Items [0 ], nil
323+ default :
324+ return nil , fmt .Errorf ("expected one %s object, but found %d matching the label selector" , relRes .Kind , len (sourceObjs .Items ))
325+ }
326+
327+ case spec .Expression != "" :
328+ return nil , errors .New ("not yet implemented" )
329+
330+ default :
331+ return nil , errors .New ("invalid sourceSpec: no mechanism configured" )
332+ }
333+ }
334+
335+ func resolveResourceDestination (side syncSide , relRes syncagentv1alpha1.RelatedResourceSpec ) (* types.NamespacedName , error ) {
336+ jsonData , err := side .object .MarshalJSON ()
337+ if err != nil {
338+ return nil , err
339+ }
340+
341+ namespace := side .object .GetNamespace ()
342+ if relRes .Source .Namespace != nil {
343+ namespace , err = resolveResourceDestinationSpec (string (jsonData ), * relRes .Destination .Namespace )
344+ if err != nil {
345+ return nil , fmt .Errorf ("failed to resolve namespace: %w" , err )
346+ }
347+
348+ if namespace == "" {
349+ return nil , nil
350+ }
351+ } else if namespace == "" {
352+ return nil , errors .New ("primary object is cluster-scoped and no source namespace configuration was provided" )
353+ }
354+
355+ name , err := resolveResourceDestinationSpec (string (jsonData ), relRes .Destination .RelatedResourceDestinationSpec )
356+ if err != nil {
357+ return nil , fmt .Errorf ("failed to resolve name: %w" , err )
358+ }
359+
360+ if name == "" {
361+ return nil , nil
221362 }
222363
223364 return & types.NamespacedName {
@@ -226,13 +367,26 @@ func resolveResourceReference(obj *unstructured.Unstructured, ref syncagentv1alp
226367 }, nil
227368}
228369
229- func resolveResourceLocator (jsonData string , loc syncagentv1alpha1.ResourceLocator ) (string , error ) {
230- gval := gjson .Get (jsonData , loc .Path )
370+ func resolveResourceDestinationSpec (jsonData string , spec syncagentv1alpha1.RelatedResourceDestinationSpec ) (string , error ) {
371+ switch {
372+ case spec .Reference != nil :
373+ return resolveResourceReference (jsonData , * spec .Reference )
374+
375+ case spec .Expression != "" :
376+ return "" , errors .New ("not yet implemented" )
377+
378+ default :
379+ return "" , errors .New ("invalid sourceSpec: no mechanism configured" )
380+ }
381+ }
382+
383+ func resolveResourceReference (jsonData string , ref syncagentv1alpha1.RelatedResourceReference ) (string , error ) {
384+ gval := gjson .Get (jsonData , ref .Path )
231385 if ! gval .Exists () {
232- return "" , fmt .Errorf ("cannot find %s in document" , loc .Path )
386+ return "" , fmt .Errorf ("cannot find %s in document" , ref .Path )
233387 }
234388
235- if re := loc .Regex ; re != nil {
389+ if re := ref .Regex ; re != nil {
236390 if re .Pattern == "" {
237391 return re .Replacement , nil
238392 }
0 commit comments