From 72fb230c81e5b0b620f89eecc6f3d114461d6667 Mon Sep 17 00:00:00 2001 From: Mangirdas Judeikis Date: Sun, 2 Mar 2025 11:03:31 +0200 Subject: [PATCH 1/2] failed approach :/ --- .../syncagent.kcp.io_publishedresources.yaml | 50 ++++++++++++++++++- internal/projection/naming.go | 36 ++++++++++--- internal/sync/syncer_related.go | 45 ++++++++++++++--- .../syncagent/v1alpha1/published_resource.go | 8 ++- .../v1alpha1/zz_generated.deepcopy.go | 11 +++- .../syncagent/v1alpha1/relatedresourcespec.go | 23 +++++++-- 6 files changed, 150 insertions(+), 23 deletions(-) diff --git a/deploy/crd/kcp.io/syncagent.kcp.io_publishedresources.yaml b/deploy/crd/kcp.io/syncagent.kcp.io_publishedresources.yaml index 9fd7551..f155cf8 100644 --- a/deploy/crd/kcp.io/syncagent.kcp.io_publishedresources.yaml +++ b/deploy/crd/kcp.io/syncagent.kcp.io_publishedresources.yaml @@ -318,6 +318,7 @@ spec: type: object related: items: + description: RelatedResourceSpec describes a related resource that should be synchronized properties: identifier: description: |- @@ -329,6 +330,50 @@ spec: kind: description: ConfigMap or Secret type: string + labelSelector: + description: LabelSelector is used to filter the related resource in the service cluster. + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic mutation: description: |- Mutation configures optional transformation rules for the related resource. @@ -411,6 +456,7 @@ spec: description: '"service" or "kcp"' type: string reference: + description: Reference to the related resource in the service cluster. properties: name: properties: @@ -453,8 +499,10 @@ spec: - identifier - kind - origin - - reference type: object + x-kubernetes-validations: + - message: must specify exactly one of reference or labelSelector + rule: (has(self.reference) && !has(self.labelSelector)) || (!has(self.reference) && has(self.labelSelector)) type: array resource: description: |- diff --git a/internal/projection/naming.go b/internal/projection/naming.go index 39df5a6..79c3d5a 100644 --- a/internal/projection/naming.go +++ b/internal/projection/naming.go @@ -41,14 +41,7 @@ func GenerateLocalObjectName(pr *syncagentv1alpha1.PublishedResource, object met naming = &syncagentv1alpha1.ResourceNaming{} } - replacer := strings.NewReplacer( - // order of elements is important here, "$fooHash" needs to be defined before "$foo" - syncagentv1alpha1.PlaceholderRemoteClusterName, clusterName.String(), - syncagentv1alpha1.PlaceholderRemoteNamespaceHash, shortSha1Hash(object.GetNamespace()), - syncagentv1alpha1.PlaceholderRemoteNamespace, object.GetNamespace(), - syncagentv1alpha1.PlaceholderRemoteNameHash, shortSha1Hash(object.GetName()), - syncagentv1alpha1.PlaceholderRemoteName, object.GetName(), - ) + replacer := getReplacer(object, clusterName) result := types.NamespacedName{} @@ -69,6 +62,33 @@ func GenerateLocalObjectName(pr *syncagentv1alpha1.PublishedResource, object met return result } +func GenerateLocalLabelSelector(pr *syncagentv1alpha1.RelatedResourceSpec, object metav1.Object, clusterName logicalcluster.Name) *metav1.LabelSelector { + replacer := getReplacer(object, clusterName) + + result := metav1.LabelSelector{} + + result.MatchLabels = map[string]string{} + + for key, value := range pr.LabelSelector.MatchLabels { + result.MatchLabels[replacer.Replace(key)] = replacer.Replace(value) + } + + // TODO: MatchExpressions are not yet supported with the current naming scheme. + result.MatchExpressions = pr.LabelSelector.MatchExpressions + return &result +} + +func getReplacer(object metav1.Object, clusterName logicalcluster.Name) *strings.Replacer { + return strings.NewReplacer( + // order of elements is important here, "$fooHash" needs to be defined before "$foo" + syncagentv1alpha1.PlaceholderRemoteClusterName, clusterName.String(), + syncagentv1alpha1.PlaceholderRemoteNamespaceHash, shortSha1Hash(object.GetNamespace()), + syncagentv1alpha1.PlaceholderRemoteNamespace, object.GetNamespace(), + syncagentv1alpha1.PlaceholderRemoteNameHash, shortSha1Hash(object.GetName()), + syncagentv1alpha1.PlaceholderRemoteName, object.GetName(), + ) +} + func shortSha1Hash(value string) string { hash := sha1.New() if _, err := hash.Write([]byte(value)); err != nil { diff --git a/internal/sync/syncer_related.go b/internal/sync/syncer_related.go index fd4155c..636d4c2 100644 --- a/internal/sync/syncer_related.go +++ b/internal/sync/syncer_related.go @@ -70,10 +70,32 @@ func (s *ResourceSyncer) processRelatedResource(log *zap.SugaredLogger, stateSto dest = local } - // to find the source related object, we first need to determine its name/namespace - sourceKey, err := resolveResourceReference(source.object, relRes.Reference) - if err != nil { - return false, fmt.Errorf("failed to determine related object's source key: %w", err) + var sourceKey *ctrlruntimeclient.ObjectKey + switch { + case relRes.Reference != nil: + // to find the source related object, we first need to determine its name/namespace + sourceKey, err = resolveResourceReference(source.object, *relRes.Reference) + if err != nil { + return false, fmt.Errorf("failed to determine related object's source key: %w", err) + } + case relRes.LabelSelector != nil: + // if no reference is given, we can use a label selector to find the source object + sourceObjs := &unstructured.UnstructuredList{} + sourceObjs.SetAPIVersion("v1") + sourceObjs.SetKind(relRes.Kind) + + // TODO: would need to handle replacer here as well to select right object. + + if err := source.client.List(source.ctx, sourceObjs, ctrlruntimeclient.MatchingLabels(relRes.LabelSelector.MatchLabels)); err != nil { + return false, fmt.Errorf("failed to list related objects: %w", err) + } + if len(sourceObjs.Items) == 0 || len(sourceObjs.Items) > 1 { + return false, fmt.Errorf("expected exactly one related object, got %d", len(sourceObjs.Items)) + } + sourceKey = &ctrlruntimeclient.ObjectKey{ + Namespace: sourceObjs.Items[0].GetNamespace(), + Name: sourceObjs.Items[0].GetName(), + } } // find the source related object @@ -92,9 +114,18 @@ func (s *ResourceSyncer) processRelatedResource(log *zap.SugaredLogger, stateSto } // do the same to find the destination object - destKey, err := resolveResourceReference(dest.object, relRes.Reference) - if err != nil { - return false, fmt.Errorf("failed to determine related object's destination key: %w", err) + var destKey *ctrlruntimeclient.ObjectKey + switch { + case relRes.Reference != nil: + destKey, err = resolveResourceReference(dest.object, *relRes.Reference) + if err != nil { + return false, fmt.Errorf("failed to determine related object's destination key: %w", err) + } + case relRes.LabelSelector != nil: + destKey = &ctrlruntimeclient.ObjectKey{ + Namespace: sourceObj.GetNamespace(), + Name: sourceObj.GetName(), + } } destObj := &unstructured.Unstructured{} diff --git a/sdk/apis/syncagent/v1alpha1/published_resource.go b/sdk/apis/syncagent/v1alpha1/published_resource.go index 1abe61a..8121020 100644 --- a/sdk/apis/syncagent/v1alpha1/published_resource.go +++ b/sdk/apis/syncagent/v1alpha1/published_resource.go @@ -158,6 +158,8 @@ type ResourceTemplateMutation struct { Template string `json:"template"` } +// +kubebuilder:validation:XValidation:rule="(has(self.reference) && !has(self.labelSelector)) || (!has(self.reference) && has(self.labelSelector))",message="must specify exactly one of reference or labelSelector" +// RelatedResourceSpec describes a related resource that should be synchronized type RelatedResourceSpec struct { // Identifier is a unique name for this related resource. The name must be unique within one // PublishedResource and is the key by which consumers (end users) can identify and consume the @@ -171,7 +173,11 @@ type RelatedResourceSpec struct { // ConfigMap or Secret Kind string `json:"kind"` - Reference RelatedResourceReference `json:"reference"` + // Reference to the related resource in the service cluster. + Reference *RelatedResourceReference `json:"reference,omitempty"` + + // LabelSelector is used to filter the related resource in the service cluster. + LabelSelector *metav1.LabelSelector `json:"labelSelector,omitempty"` // Mutation configures optional transformation rules for the related resource. // Status mutations are only performed when the related resource originates in kcp. diff --git a/sdk/apis/syncagent/v1alpha1/zz_generated.deepcopy.go b/sdk/apis/syncagent/v1alpha1/zz_generated.deepcopy.go index 6ee4a74..136d747 100644 --- a/sdk/apis/syncagent/v1alpha1/zz_generated.deepcopy.go +++ b/sdk/apis/syncagent/v1alpha1/zz_generated.deepcopy.go @@ -181,7 +181,16 @@ func (in *RelatedResourceReference) DeepCopy() *RelatedResourceReference { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RelatedResourceSpec) DeepCopyInto(out *RelatedResourceSpec) { *out = *in - in.Reference.DeepCopyInto(&out.Reference) + if in.Reference != nil { + in, out := &in.Reference, &out.Reference + *out = new(RelatedResourceReference) + (*in).DeepCopyInto(*out) + } + if in.LabelSelector != nil { + in, out := &in.LabelSelector, &out.LabelSelector + *out = new(v1.LabelSelector) + (*in).DeepCopyInto(*out) + } if in.Mutation != nil { in, out := &in.Mutation, &out.Mutation *out = new(ResourceMutationSpec) diff --git a/sdk/applyconfiguration/syncagent/v1alpha1/relatedresourcespec.go b/sdk/applyconfiguration/syncagent/v1alpha1/relatedresourcespec.go index 3ac0143..91a92ab 100644 --- a/sdk/applyconfiguration/syncagent/v1alpha1/relatedresourcespec.go +++ b/sdk/applyconfiguration/syncagent/v1alpha1/relatedresourcespec.go @@ -18,14 +18,19 @@ limitations under the License. package v1alpha1 +import ( + v1 "k8s.io/client-go/applyconfigurations/meta/v1" +) + // RelatedResourceSpecApplyConfiguration represents a declarative configuration of the RelatedResourceSpec type for use // with apply. type RelatedResourceSpecApplyConfiguration struct { - Identifier *string `json:"identifier,omitempty"` - Origin *string `json:"origin,omitempty"` - Kind *string `json:"kind,omitempty"` - Reference *RelatedResourceReferenceApplyConfiguration `json:"reference,omitempty"` - Mutation *ResourceMutationSpecApplyConfiguration `json:"mutation,omitempty"` + Identifier *string `json:"identifier,omitempty"` + Origin *string `json:"origin,omitempty"` + Kind *string `json:"kind,omitempty"` + Reference *RelatedResourceReferenceApplyConfiguration `json:"reference,omitempty"` + LabelSelector *v1.LabelSelectorApplyConfiguration `json:"labelSelector,omitempty"` + Mutation *ResourceMutationSpecApplyConfiguration `json:"mutation,omitempty"` } // RelatedResourceSpecApplyConfiguration constructs a declarative configuration of the RelatedResourceSpec type for use with @@ -66,6 +71,14 @@ func (b *RelatedResourceSpecApplyConfiguration) WithReference(value *RelatedReso return b } +// WithLabelSelector sets the LabelSelector field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the LabelSelector field is set to the value of the last call. +func (b *RelatedResourceSpecApplyConfiguration) WithLabelSelector(value *v1.LabelSelectorApplyConfiguration) *RelatedResourceSpecApplyConfiguration { + b.LabelSelector = value + return b +} + // WithMutation sets the Mutation field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the Mutation field is set to the value of the last call. From e16d1ea05d09812370e79594c2b595dc67ed8660 Mon Sep 17 00:00:00 2001 From: Mangirdas Judeikis Date: Sun, 2 Mar 2025 11:26:50 +0200 Subject: [PATCH 2/2] try to get metadata out --- Makefile | 2 +- internal/sync/syncer_related.go | 18 +++++++++++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 504a8f3..97becda 100644 --- a/Makefile +++ b/Makefile @@ -36,7 +36,7 @@ build: $(CMD) .PHONY: $(CMD) $(CMD): %: $(BUILD_DEST)/% -$(BUILD_DEST)/%: cmd/% +$(BUILD_DEST)/%: clean cmd/% go build $(GOTOOLFLAGS) -o $@ ./cmd/$* GOLANGCI_LINT = _tools/golangci-lint diff --git a/internal/sync/syncer_related.go b/internal/sync/syncer_related.go index 636d4c2..58fd161 100644 --- a/internal/sync/syncer_related.go +++ b/internal/sync/syncer_related.go @@ -25,7 +25,9 @@ import ( "go.uber.org/zap" "github.com/kcp-dev/api-syncagent/internal/mutation" + "github.com/kcp-dev/api-syncagent/internal/projection" syncagentv1alpha1 "github.com/kcp-dev/api-syncagent/sdk/apis/syncagent/v1alpha1" + "github.com/kcp-dev/logicalcluster/v3" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -85,12 +87,22 @@ func (s *ResourceSyncer) processRelatedResource(log *zap.SugaredLogger, stateSto sourceObjs.SetKind(relRes.Kind) // TODO: would need to handle replacer here as well to select right object. + cn, ok := source.object.GetLabels()[remoteObjectClusterLabel] + if !ok { + return false, fmt.Errorf("missing cluster label on source object") + } + clusterName := logicalcluster.Name(cn) + labels := projection.GenerateLocalLabelSelector(&relRes, source.object, clusterName) - if err := source.client.List(source.ctx, sourceObjs, ctrlruntimeclient.MatchingLabels(relRes.LabelSelector.MatchLabels)); err != nil { + if err := source.client.List(source.ctx, sourceObjs, ctrlruntimeclient.MatchingLabels(labels.MatchLabels)); err != nil { return false, fmt.Errorf("failed to list related objects: %w", err) } - if len(sourceObjs.Items) == 0 || len(sourceObjs.Items) > 1 { - return false, fmt.Errorf("expected exactly one related object, got %d", len(sourceObjs.Items)) + if len(sourceObjs.Items) == 0 { + return false, nil + } + if len(sourceObjs.Items) > 1 { + // HACK: we take first one, as we can't select multiple objects or by name only! + log.Warnw("found multiple related objects, taking first one", "count", len(sourceObjs.Items)) } sourceKey = &ctrlruntimeclient.ObjectKey{ Namespace: sourceObjs.Items[0].GetNamespace(),