Skip to content

Commit 9d243a4

Browse files
committed
implement resolving, update tests
On-behalf-of: @SAP [email protected]
1 parent d69980c commit 9d243a4

File tree

3 files changed

+284
-54
lines changed

3 files changed

+284
-54
lines changed

internal/sync/syncer_related.go

Lines changed: 183 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package sync
1818

1919
import (
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

Comments
 (0)