Skip to content

Commit 73b9b10

Browse files
committed
implement templating in label selectors
On-behalf-of: @SAP [email protected]
1 parent e611836 commit 73b9b10

File tree

3 files changed

+103
-20
lines changed

3 files changed

+103
-20
lines changed

internal/sync/syncer_related.go

Lines changed: 69 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"slices"
2525
"strings"
2626

27+
"github.com/kcp-dev/logicalcluster/v3"
2728
"github.com/tidwall/gjson"
2829
"go.uber.org/zap"
2930

@@ -68,7 +69,7 @@ func (s *ResourceSyncer) processRelatedResource(log *zap.SugaredLogger, stateSto
6869
dest syncSide
6970
)
7071

71-
if relRes.Origin == "service" {
72+
if relRes.Origin == syncagentv1alpha1.RelatedResourceOriginService {
7273
origin = local
7374
dest = remote
7475
} else {
@@ -159,7 +160,7 @@ func (s *ResourceSyncer) processRelatedResource(log *zap.SugaredLogger, stateSto
159160

160161
// now that the related object was successfully synced, we can remember its details on the
161162
// main object
162-
if relRes.Origin == "service" {
163+
if relRes.Origin == syncagentv1alpha1.RelatedResourceOriginService {
163164
// TODO: Improve this logic, the added index is just a hack until we find a better solution
164165
// to let the user know about the related object (this annotation is not relevant for the
165166
// syncing logic, it's purely for the end-user).
@@ -211,14 +212,15 @@ func resolveRelatedResourceObjects(relatedOrigin, relatedDest syncSide, relRes s
211212
// resolving the originNamespace first allows us to scope down any .List() calls later
212213
originNamespace := relatedOrigin.object.GetNamespace()
213214
destNamespace := relatedDest.object.GetNamespace()
215+
origin := relRes.Origin
214216

215217
namespaceMap := map[string]string{
216218
originNamespace: destNamespace,
217219
}
218220

219221
if nsSpec := relRes.Object.Namespace; nsSpec != nil {
220222
var err error
221-
namespaceMap, err = resolveRelatedResourceOriginNamespaces(relatedOrigin, relatedDest, *nsSpec)
223+
namespaceMap, err = resolveRelatedResourceOriginNamespaces(relatedOrigin, relatedDest, origin, *nsSpec)
222224
if err != nil {
223225
return nil, fmt.Errorf("failed to resolve namespace: %w", err)
224226
}
@@ -248,7 +250,7 @@ func resolveRelatedResourceObjects(relatedOrigin, relatedDest syncSide, relRes s
248250
return objects, nil
249251
}
250252

251-
func resolveRelatedResourceOriginNamespaces(relatedOrigin, relatedDest syncSide, spec syncagentv1alpha1.RelatedResourceObjectSpec) (map[string]string, error) {
253+
func resolveRelatedResourceOriginNamespaces(relatedOrigin, relatedDest syncSide, origin syncagentv1alpha1.RelatedResourceOrigin, spec syncagentv1alpha1.RelatedResourceObjectSpec) (map[string]string, error) {
252254
switch {
253255
//nolint:staticcheck
254256
case spec.Reference != nil:
@@ -294,7 +296,7 @@ func resolveRelatedResourceOriginNamespaces(relatedOrigin, relatedDest syncSide,
294296
for _, namespace := range namespaces.Items {
295297
name := namespace.Name
296298

297-
destinationName, err := applyRewrites(relatedOrigin, relatedDest, name, spec.Selector.Rewrite)
299+
destinationName, err := applySelectorRewrites(relatedOrigin, relatedDest, name, spec.Selector.Rewrite)
298300
if err != nil {
299301
return nil, fmt.Errorf("failed to rewrite origin namespace: %w", err)
300302
}
@@ -305,7 +307,7 @@ func resolveRelatedResourceOriginNamespaces(relatedOrigin, relatedDest syncSide,
305307
return namespaceMap, nil
306308

307309
case spec.Template != nil:
308-
originValue, destValue, err := applyTemplateBothSides(relatedOrigin, relatedDest, *spec.Template)
310+
originValue, destValue, err := applyTemplateBothSides(relatedOrigin, relatedDest, origin, *spec.Template)
309311
if err != nil {
310312
return nil, fmt.Errorf("failed to apply template: %w", err)
311313
}
@@ -391,7 +393,12 @@ func resolveRelatedResourceObjectsInNamespace(relatedOrigin, relatedDest syncSid
391393
originObjects.SetAPIVersion("v1") // we only support ConfigMaps and Secrets, both are in core/v1
392394
originObjects.SetKind(relRes.Kind)
393395

394-
selector, err := metav1.LabelSelectorAsSelector(&spec.Selector.LabelSelector)
396+
labelSelector, err := templateLabelSelector(relatedOrigin, relatedDest, relRes.Origin, &spec.Selector.LabelSelector)
397+
if err != nil {
398+
return nil, fmt.Errorf("failed to apply templates to label selector: %w", err)
399+
}
400+
401+
selector, err := metav1.LabelSelectorAsSelector(labelSelector)
395402
if err != nil {
396403
return nil, fmt.Errorf("invalid selector configured: %w", err)
397404
}
@@ -409,7 +416,7 @@ func resolveRelatedResourceObjectsInNamespace(relatedOrigin, relatedDest syncSid
409416
for _, originObject := range originObjects.Items {
410417
name := originObject.GetName()
411418

412-
destinationName, err := applyRewrites(relatedOrigin, relatedDest, name, spec.Selector.Rewrite)
419+
destinationName, err := applySelectorRewrites(relatedOrigin, relatedDest, name, spec.Selector.Rewrite)
413420
if err != nil {
414421
return nil, fmt.Errorf("failed to rewrite origin name: %w", err)
415422
}
@@ -420,7 +427,7 @@ func resolveRelatedResourceObjectsInNamespace(relatedOrigin, relatedDest syncSid
420427
return nameMap, nil
421428

422429
case spec.Template != nil:
423-
originValue, destValue, err := applyTemplateBothSides(relatedOrigin, relatedDest, *spec.Template)
430+
originValue, destValue, err := applyTemplateBothSides(relatedOrigin, relatedDest, relRes.Origin, *spec.Template)
424431
if err != nil {
425432
return nil, fmt.Errorf("failed to apply template: %w", err)
426433
}
@@ -468,7 +475,7 @@ func resolveReference(jsonData []byte, ref syncagentv1alpha1.RelatedResourceObje
468475
return strVal, nil
469476
}
470477

471-
func applyRewrites(relatedOrigin, relatedDest syncSide, value string, rewrite syncagentv1alpha1.RelatedResourceSelectorRewrite) (string, error) {
478+
func applySelectorRewrites(relatedOrigin, relatedDest syncSide, value string, rewrite syncagentv1alpha1.RelatedResourceSelectorRewrite) (string, error) {
472479
switch {
473480
case rewrite.Regex != nil:
474481
return applyRegularExpression(value, *rewrite.Regex)
@@ -496,14 +503,8 @@ func applyTemplate(relatedOrigin, relatedDest syncSide, tpl syncagentv1alpha1.Te
496503
return "", errors.New("not yet implemented")
497504
}
498505

499-
func applyTemplateBothSides(relatedOrigin, relatedDest syncSide, tpl syncagentv1alpha1.TemplateExpression) (originValue, destValue string, err error) {
500-
// clusterName and workspacePath are only set on the kcp side of the sync.
501-
clusterName := relatedDest.clusterName
502-
workspacePath := relatedDest.workspacePath
503-
if clusterName == "" {
504-
clusterName = relatedOrigin.clusterName
505-
workspacePath = relatedOrigin.workspacePath
506-
}
506+
func applyTemplateBothSides(relatedOrigin, relatedDest syncSide, origin syncagentv1alpha1.RelatedResourceOrigin, tpl syncagentv1alpha1.TemplateExpression) (originValue, destValue string, err error) {
507+
clusterName, workspacePath := clusterIdent(relatedOrigin, relatedDest, origin)
507508

508509
// evaluate the template for the origin object side
509510
ctx := templating.NewRelatedObjectContext(relatedOrigin.object, clusterName, workspacePath)
@@ -521,3 +522,53 @@ func applyTemplateBothSides(relatedOrigin, relatedDest syncSide, tpl syncagentv1
521522

522523
return originValue, destValue, nil
523524
}
525+
526+
// templateLabelSelector applies Go templating logic to all keys and values in the MatchLabels of
527+
// a label selector.
528+
func templateLabelSelector(relatedOrigin, relatedDest syncSide, origin syncagentv1alpha1.RelatedResourceOrigin, selector *metav1.LabelSelector) (*metav1.LabelSelector, error) {
529+
clusterName, workspacePath := clusterIdent(relatedOrigin, relatedDest, origin)
530+
531+
localObject := relatedOrigin.object
532+
remoteObject := relatedDest.object
533+
if origin == syncagentv1alpha1.RelatedResourceOriginKcp {
534+
localObject = relatedDest.object
535+
remoteObject = relatedOrigin.object
536+
}
537+
538+
ctx := templating.NewRelatedObjectLabelContext(localObject, remoteObject, clusterName, workspacePath)
539+
540+
newMatchLabels := map[string]string{}
541+
for key, value := range selector.MatchLabels {
542+
if strings.Contains(key, "{{") {
543+
rendered, err := templating.Render(key, ctx)
544+
if err != nil {
545+
return nil, fmt.Errorf("failed to evaluate key as template: %w", err)
546+
}
547+
548+
key = rendered
549+
}
550+
551+
if strings.Contains(value, "{{") {
552+
rendered, err := templating.Render(value, ctx)
553+
if err != nil {
554+
return nil, fmt.Errorf("failed to evaluate value as template: %w", err)
555+
}
556+
557+
value = rendered
558+
}
559+
560+
newMatchLabels[key] = value
561+
}
562+
563+
selector.MatchLabels = newMatchLabels
564+
565+
return selector, nil
566+
}
567+
568+
func clusterIdent(relatedOrigin, relatedDest syncSide, origin syncagentv1alpha1.RelatedResourceOrigin) (logicalcluster.Name, logicalcluster.Path) {
569+
if origin == syncagentv1alpha1.RelatedResourceOriginKcp {
570+
return relatedOrigin.clusterName, relatedOrigin.workspacePath
571+
}
572+
573+
return relatedDest.clusterName, relatedDest.workspacePath
574+
}

internal/sync/templating/related.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,28 @@ func NewRelatedObjectContext(object *unstructured.Unstructured, clusterName logi
4646
ClusterPath: clusterPath,
4747
}
4848
}
49+
50+
// relatedObjectContext is the data available to Go templates in the keys and values
51+
// of a label selector for a related object.
52+
type relatedObjectLabelContext struct {
53+
// LocalObject ist the primary object copy on the local side of the sync
54+
// (i.e. on the service cluster).
55+
LocalObject map[string]any
56+
// RemoteObject is the primary object original, in kcp.
57+
RemoteObject map[string]any
58+
// ClusterName is the internal cluster identifier (e.g. "34hg2j4gh24jdfgf")
59+
// of the kcp workspace that the synchronization is currently processing
60+
// (where the remote object exists).
61+
ClusterName logicalcluster.Name
62+
// ClusterPath is the workspace path (e.g. "root:customer:projectx").
63+
ClusterPath logicalcluster.Path
64+
}
65+
66+
func NewRelatedObjectLabelContext(localObject, remoteObject *unstructured.Unstructured, clusterName logicalcluster.Name, clusterPath logicalcluster.Path) relatedObjectLabelContext {
67+
return relatedObjectLabelContext{
68+
LocalObject: localObject.Object,
69+
RemoteObject: remoteObject.Object,
70+
ClusterName: clusterName,
71+
ClusterPath: clusterPath,
72+
}
73+
}

sdk/apis/syncagent/v1alpha1/published_resource.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,15 +167,22 @@ type ResourceTemplateMutation struct {
167167
Template string `json:"template"`
168168
}
169169

170+
type RelatedResourceOrigin string
171+
172+
const (
173+
RelatedResourceOriginService RelatedResourceOrigin = "service"
174+
RelatedResourceOriginKcp RelatedResourceOrigin = "kcp"
175+
)
176+
170177
type RelatedResourceSpec struct {
171178
// Identifier is a unique name for this related resource. The name must be unique within one
172179
// PublishedResource and is the key by which consumers (end users) can identify and consume the
173180
// related resource. Common names are "connection-details" or "credentials".
174181
// The identifier must be an alphanumeric string.
175182
Identifier string `json:"identifier"`
176183

177-
// "service" or "kcp"
178-
Origin string `json:"origin"`
184+
// +kubebuilder:validation:Enum=service;kcp
185+
Origin RelatedResourceOrigin `json:"origin"`
179186

180187
// ConfigMap or Secret
181188
Kind string `json:"kind"`

0 commit comments

Comments
 (0)