Skip to content

Commit bc2d16a

Browse files
authored
Merge pull request #83 from kcp-dev/multi-refs
Allow references to select multiple values
2 parents c083316 + 4e8761d commit bc2d16a

File tree

6 files changed

+474
-77
lines changed

6 files changed

+474
-77
lines changed

docs/content/publish-resources/index.md

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -303,20 +303,23 @@ existing" and not create an error.
303303

304304
#### References
305305

306-
A reference is a JSONPath-like expression that are evaluated on both sides of the synchronization.
307-
You configure a single path expression (like `spec.secretName`) and the sync agent will evaluate it
308-
in the original primary object (in kcp) and again in the copied primary object (on the service
309-
cluster). Since the primary object has already been mutated, the `spec.secretName` is already
310-
rewritten/adjusted to work on the service cluster (for example it was changed from `my-secret` to
311-
`jk23h4wz47329rz2r72r92-secret` on the service cluster side). By doing it this way, admins only have
312-
to think about mutations and rewrites once (when configuring the primary object in the
313-
PublishedResource) and the path will yield 2 ready to use values (`my-secret` and the computed value).
314-
315-
The value selected by the path expression must be a string (or number, but it will be coalesced into
316-
a string) and can then be further adjusted by applying a regular expression to it.
317-
318-
References can only ever select one related object. Their upside is that they are simple to understand
319-
and easy to use, but require a "link" in the primary object that would point to the related object.
306+
A reference is a JSONPath-like expression (more precisely, it follows the [gjson syntax](https://github.com/tidwall/gjson?tab=readme-ov-file#path-syntax))
307+
that are evaluated on both sides of the synchronization. You configure a single path expression
308+
(like `spec.secretName`) and the sync agent will evaluate it in the original primary object (in kcp)
309+
and again in the copied primary object (on the service cluster). Since the primary object has already
310+
been mutated, the `spec.secretName` is already rewritten/adjusted to work on the service cluster
311+
(for example it was changed from `my-secret` to `jk23h4wz47329rz2r72r92-secret` on the service
312+
cluster side). By doing it this way, admins only have to think about mutations and rewrites once
313+
(when configuring the primary object in the PublishedResource) and the path will yield 2 ready to
314+
use values (`my-secret` and the computed value).
315+
316+
References can either return a single scalar (strings or integers that will be auto-converted to a
317+
string) (like in `spec.secretName`) or a list of strings/numbers (like `spec.users.#.name`). A
318+
reference must return the same number of items on both the local and remote object, otherwise the
319+
agent will not be able to map local related names to remote related names correctly.
320+
321+
A regular expression can be configured to be applied to each found value (i.e. if the reference returns
322+
a list of values, the regular expression is applied to each individual value).
320323

321324
Here's an example on how to use references to locate the related object.
322325

docs/generators/crd-ref/crd.template.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ description: |
44
{{- if .Description }}
55
{{ .Description | indent 2 }}
66
{{- else }}
7-
Custom resource definition (CRD) schema reference page for the {{ .Title }}
8-
resource ({{ .NamePlural }}.{{ .Group }}), as part of the Giant Swarm
7+
Custom resource definition (CRD) schema reference page for the {{ .Title }}
8+
resource ({{ .NamePlural }}.{{ .Group }}), as part of the Giant Swarm
99
Management API documentation.
1010
{{- end }}
1111
weight: {{ .Weight }}
@@ -78,7 +78,7 @@ This CRD is being replaced by <a href="../{{ .FullName }}/">{{ .ShortName }}</a>
7878
</div>
7979
{{with .Description}}
8080
<div class="property-description">
81-
{{.|markdown}}
81+
{% raw %}{{.|markdown}}{% endraw %}
8282
</div>
8383
{{end}}
8484
</div>

internal/sync/syncer_related.go

Lines changed: 50 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -251,27 +251,25 @@ func resolveRelatedResourceObjects(relatedOrigin, relatedDest syncSide, relRes s
251251
func resolveRelatedResourceOriginNamespaces(relatedOrigin, relatedDest syncSide, origin syncagentv1alpha1.RelatedResourceOrigin, spec syncagentv1alpha1.RelatedResourceObjectSpec) (map[string]string, error) {
252252
switch {
253253
case spec.Reference != nil:
254-
originNamespace, err := resolveObjectReference(relatedOrigin.object, *spec.Reference)
254+
originNamespaces, err := resolveObjectReference(relatedOrigin.object, *spec.Reference)
255255
if err != nil {
256256
return nil, err
257257
}
258258

259-
if originNamespace == "" {
259+
if len(originNamespaces) == 0 {
260260
return nil, nil
261261
}
262262

263-
destNamespace, err := resolveObjectReference(relatedDest.object, *spec.Reference)
263+
destNamespaces, err := resolveObjectReference(relatedDest.object, *spec.Reference)
264264
if err != nil {
265265
return nil, err
266266
}
267267

268-
if destNamespace == "" {
269-
return nil, nil
268+
if len(destNamespaces) != len(originNamespaces) {
269+
return nil, fmt.Errorf("cannot sync related resources: found %d namespaces on the origin, but %d on the destination side", len(originNamespaces), len(destNamespaces))
270270
}
271271

272-
return map[string]string{
273-
originNamespace: destNamespace,
274-
}, nil
272+
return mapSlices(originNamespaces, destNamespaces), nil
275273

276274
case spec.Selector != nil:
277275
namespaces := &corev1.NamespaceList{}
@@ -327,6 +325,22 @@ func resolveRelatedResourceOriginNamespaces(relatedOrigin, relatedDest syncSide,
327325
}
328326
}
329327

328+
func mapSlices(a, b []string) map[string]string {
329+
mapping := map[string]string{}
330+
for i, aItem := range a {
331+
bItem := b[i]
332+
333+
// ignore any origin<->dest pair where either of the sides is empty
334+
if bItem == "" || aItem == "" {
335+
continue
336+
}
337+
338+
mapping[aItem] = bItem
339+
}
340+
341+
return mapping
342+
}
343+
330344
func resolveRelatedResourceObjectsInNamespaces(relatedOrigin, relatedDest syncSide, relRes syncagentv1alpha1.RelatedResourceSpec, spec syncagentv1alpha1.RelatedResourceObjectSpec, namespaceMap map[string]string) ([]resolvedObject, error) {
331345
result := []resolvedObject{}
332346

@@ -368,27 +382,25 @@ func resolveRelatedResourceObjectsInNamespaces(relatedOrigin, relatedDest syncSi
368382
func resolveRelatedResourceObjectsInNamespace(relatedOrigin, relatedDest syncSide, relRes syncagentv1alpha1.RelatedResourceSpec, spec syncagentv1alpha1.RelatedResourceObjectSpec, namespace string) (map[string]string, error) {
369383
switch {
370384
case spec.Reference != nil:
371-
originName, err := resolveObjectReference(relatedOrigin.object, *spec.Reference)
385+
originNames, err := resolveObjectReference(relatedOrigin.object, *spec.Reference)
372386
if err != nil {
373387
return nil, err
374388
}
375389

376-
if originName == "" {
390+
if len(originNames) == 0 {
377391
return nil, nil
378392
}
379393

380-
destName, err := resolveObjectReference(relatedDest.object, *spec.Reference)
394+
destNames, err := resolveObjectReference(relatedDest.object, *spec.Reference)
381395
if err != nil {
382396
return nil, err
383397
}
384398

385-
if destName == "" {
386-
return nil, nil
399+
if len(destNames) != len(originNames) {
400+
return nil, fmt.Errorf("cannot sync related resources: found %d names on the origin, but %d on the destination side", len(originNames), len(destNames))
387401
}
388402

389-
return map[string]string{
390-
originName: destName,
391-
}, nil
403+
return mapSlices(originNames, destNames), nil
392404

393405
case spec.Selector != nil:
394406
originObjects := &unstructured.UnstructuredList{}
@@ -447,34 +459,44 @@ func resolveRelatedResourceObjectsInNamespace(relatedOrigin, relatedDest syncSid
447459
}
448460
}
449461

450-
func resolveObjectReference(object *unstructured.Unstructured, ref syncagentv1alpha1.RelatedResourceObjectReference) (string, error) {
462+
func resolveObjectReference(object *unstructured.Unstructured, ref syncagentv1alpha1.RelatedResourceObjectReference) ([]string, error) {
451463
data, err := object.MarshalJSON()
452464
if err != nil {
453-
return "", err
465+
return nil, err
454466
}
455467

456468
return resolveReference(data, ref)
457469
}
458470

459-
func resolveReference(jsonData []byte, ref syncagentv1alpha1.RelatedResourceObjectReference) (string, error) {
460-
gval := gjson.Get(string(jsonData), ref.Path)
461-
if !gval.Exists() {
462-
return "", nil
471+
func resolveReference(jsonData []byte, ref syncagentv1alpha1.RelatedResourceObjectReference) ([]string, error) {
472+
result := gjson.Get(string(jsonData), ref.Path)
473+
if !result.Exists() {
474+
return nil, nil
463475
}
464476

465-
// this does apply some coalescing, like turning numbers into strings
466-
strVal := gval.String()
477+
var values []string
478+
if result.IsArray() {
479+
for _, elem := range result.Array() {
480+
values = append(values, strings.TrimSpace(elem.String()))
481+
}
482+
} else {
483+
values = append(values, strings.TrimSpace(result.String()))
484+
}
467485

468486
if re := ref.Regex; re != nil {
469487
var err error
470488

471-
strVal, err = applyRegularExpression(strVal, *re)
472-
if err != nil {
473-
return "", err
489+
for i, value := range values {
490+
value, err = applyRegularExpression(value, *re)
491+
if err != nil {
492+
return nil, err
493+
}
494+
495+
values[i] = value
474496
}
475497
}
476498

477-
return strVal, nil
499+
return values, nil
478500
}
479501

480502
// applyTemplate is used after a label selector has been applied and a list of namespaces or objects

test/crds/backup.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ type Backup struct {
2828
}
2929

3030
type BackupSpec struct {
31-
Source string `json:"source"`
32-
Destination string `json:"destination"`
31+
Source string `json:"source"`
32+
Destination string `json:"destination"`
33+
Items []BackupItem `json:"items,omitempty"`
34+
}
35+
36+
type BackupItem struct {
37+
Name string `json:"name"`
3338
}

test/crds/backup.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,10 @@ spec:
2525
type: string
2626
destination:
2727
type: string
28+
items:
29+
type: array
30+
items:
31+
type: object
32+
properties:
33+
name:
34+
type: string

0 commit comments

Comments
 (0)