From f9297115d5f9ac47a7155e1e552ee3191849738d Mon Sep 17 00:00:00 2001 From: Iskren Date: Tue, 17 Mar 2026 03:29:58 +0200 Subject: [PATCH 1/2] Watch and sync changes to related resources change copyright year of related handlers On-behalf-of: SAP Signed-off-by: Iskren Petrov Watch and sync changes to related resources --- .../syncagent.kcp.io_publishedresources.yaml | 29 ++++ hack/tools.checksums | 2 + internal/controller/sync/controller.go | 70 +++++++++ internal/controller/sync/related_handlers.go | 144 ++++++++++++++++++ .../syncagent/v1alpha1/published_resource.go | 30 ++++ .../v1alpha1/zz_generated.deepcopy.go | 47 ++++++ .../syncagent/v1alpha1/relatedresourcespec.go | 9 ++ .../v1alpha1/relatedresourcewatch.go | 54 +++++++ .../v1alpha1/relatedresourcewatchbyowner.go | 39 +++++ sdk/applyconfiguration/utils.go | 4 + 10 files changed, 428 insertions(+) create mode 100644 internal/controller/sync/related_handlers.go create mode 100644 sdk/applyconfiguration/syncagent/v1alpha1/relatedresourcewatch.go create mode 100644 sdk/applyconfiguration/syncagent/v1alpha1/relatedresourcewatchbyowner.go diff --git a/deploy/crd/kcp.io/syncagent.kcp.io_publishedresources.yaml b/deploy/crd/kcp.io/syncagent.kcp.io_publishedresources.yaml index da26f48..c5b8dfd 100644 --- a/deploy/crd/kcp.io/syncagent.kcp.io_publishedresources.yaml +++ b/deploy/crd/kcp.io/syncagent.kcp.io_publishedresources.yaml @@ -778,6 +778,35 @@ spec: Version is the API version of the related resource. This can be left blank to automatically use the preferred version. type: string + watch: + description: |- + Watch configures how the agent identifies the owning primary object when a related + resource with origin: kcp changes. When set, the agent sets up a watch on the related + resource type and uses the configured rule to enqueue the correct primary object. + Without this field, changes to origin:kcp related resources do not trigger reconciliation. + properties: + byLabel: + additionalProperties: + type: string + description: |- + ByLabel configures the watch handler to list primary objects matching a label selector + derived from the changed object. Each map key is a label key on the primary object; + each value is a Go template expression evaluated with the changed object available as + .watchObject (with fields .name, .namespace, .labels). + type: object + byOwner: + description: |- + ByOwner configures the watch handler to inspect the OwnerReferences of the changed + object. When an OwnerReference with the given Kind is found, the referenced owner + is enqueued as the primary object. + properties: + kind: + description: Kind is the Kind to look for in the OwnerReferences of the changed related object. + type: string + required: + - kind + type: object + type: object required: - identifier - object diff --git a/hack/tools.checksums b/hack/tools.checksums index 759a74b..66dfa6f 100644 --- a/hack/tools.checksums +++ b/hack/tools.checksums @@ -1,4 +1,5 @@ boilerplate|GOARCH=amd64;GOOS=linux|6f05fc3be207ae2ed99e125509a08df677cb007e197e16607c654a434b91d47f +boilerplate|GOARCH=arm64;GOOS=darwin|3ac82c58f440ac8461746674e39311ba332d6d960966a060dd3be734b1111522 boilerplate|GOARCH=arm64;GOOS=linux|70253486ed7a803a35a9abb2bab4db2f1f7748d5266bf7a1c2ee298fda2b208a etcd|GOARCH=amd64;GOOS=linux|435d74510f3216bab1932fb6d7a6b5fe8245301143fcd25f7e65dfb7dcf8904a etcd|GOARCH=arm64;GOOS=linux|cc8c645e5a8df0f35f2a5c51d9b9383037eef0cf0167c52e648457b3971a7a09 @@ -11,4 +12,5 @@ kube-apiserver|GOARCH=arm64;GOOS=linux|6ade6c2646e2c01fde1095407452afc2b65e89d6d kubectl|GOARCH=amd64;GOOS=linux|9591f3d75e1581f3f7392e6ad119aab2f28ae7d6c6e083dc5d22469667f27253 kubectl|GOARCH=arm64;GOOS=linux|95df604e914941f3172a93fa8feeb1a1a50f4011dfbe0c01e01b660afc8f9b85 yq|GOARCH=amd64;GOOS=linux|0c2b24e645b57d8e7c0566d18643a6d4f5580feeea3878127354a46f2a1e4598 +yq|GOARCH=arm64;GOOS=darwin|164e10e5f7df62990e4f3823205e7ea42ba5660523a428df07c7386c0b62e3d9 yq|GOARCH=arm64;GOOS=linux|9477ac3cc447b6c083986129e35af8122eb2b938fe55c9c3e40436fb966e5813 diff --git a/internal/controller/sync/controller.go b/internal/controller/sync/controller.go index 509d8f4..d342235 100644 --- a/internal/controller/sync/controller.go +++ b/internal/controller/sync/controller.go @@ -39,9 +39,12 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/sets" "k8s.io/utils/ptr" ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/cluster" "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/manager" "sigs.k8s.io/controller-runtime/pkg/predicate" @@ -161,6 +164,73 @@ func Create( return nil, fmt.Errorf("failed to setup local-side watch: %w", err) } + // Watch origin:kcp related resources so that changes to them trigger reconciliation + // of the owning primary object. Only related resources with a Watch config are covered. + watchedGVKs := sets.New[schema.GroupVersionKind]() + for _, relRes := range pubRes.Spec.Related { + if relRes.Origin != syncagentv1alpha1.RelatedResourceOriginKcp || relRes.Watch == nil { + continue + } + + gvr := schema.GroupVersionResource{ + Group: relRes.Group, + Version: relRes.Version, + Resource: relRes.Resource, + } + + // Use the local REST mapper to determine the Kind. + gvk, err := localManager.GetRESTMapper().KindFor(gvr) + if err != nil { + log.Warnw("Failed to determine Kind for origin:kcp related resource, skipping watch", "gvr", gvr, "error", err) + continue + } + + // Deduplicate: only set up one watch per GVK. + if watchedGVKs.Has(gvk) { + continue + } + watchedGVKs.Insert(gvk) + + relatedDummy := &unstructured.Unstructured{} + relatedDummy.SetGroupVersionKind(gvk) + + var enqueueForRelated mchandler.TypedEventHandlerFunc[*unstructured.Unstructured, mcreconcile.Request] + + switch { + case relRes.Watch.ByOwner != nil: + ownerKind := relRes.Watch.ByOwner.Kind + enqueueForRelated = func(clusterName string, _ cluster.Cluster) handler.TypedEventHandler[*unstructured.Unstructured, mcreconcile.Request] { + return &byOwnerEventHandler{ + clusterName: clusterName, + ownerKind: ownerKind, + } + } + + case relRes.Watch.ByLabel != nil: + labelTemplates := relRes.Watch.ByLabel + primaryDummy := remoteDummy.DeepCopy() + enqueueForRelated = func(clusterName string, cl cluster.Cluster) handler.TypedEventHandler[*unstructured.Unstructured, mcreconcile.Request] { + return &byLabelEventHandler{ + clusterName: clusterName, + client: cl.GetClient(), + primaryDummy: primaryDummy, + labelTemplates: labelTemplates, + log: log, + } + } + + default: + log.Warnw("origin:kcp related resource has Watch set but neither byOwner nor byLabel configured, skipping", "gvk", gvk) + continue + } + + if err := c.MultiClusterWatch(mcsource.TypedKind(relatedDummy, enqueueForRelated)); err != nil { + return nil, fmt.Errorf("failed to setup watch for origin:kcp related resource %v: %w", gvk, err) + } + + log.Infow("Set up watch for origin:kcp related resource", "gvk", gvk) + } + log.Info("Done setting up unmanaged controller.") return c, nil diff --git a/internal/controller/sync/related_handlers.go b/internal/controller/sync/related_handlers.go new file mode 100644 index 0000000..843a20f --- /dev/null +++ b/internal/controller/sync/related_handlers.go @@ -0,0 +1,144 @@ +/* +Copyright 2026 The KCP Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package sync + +import ( + "context" + + "go.uber.org/zap" + + "github.com/kcp-dev/api-syncagent/internal/sync/templating" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/util/workqueue" + ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + mcreconcile "sigs.k8s.io/multicluster-runtime/pkg/reconcile" +) + +// byOwnerEventHandler enqueues the primary object by inspecting the OwnerReferences +// of the changed related object and finding one with the configured Kind. +type byOwnerEventHandler struct { + clusterName string + ownerKind string +} + +func (h *byOwnerEventHandler) Create(_ context.Context, evt event.TypedCreateEvent[*unstructured.Unstructured], q workqueue.TypedRateLimitingInterface[mcreconcile.Request]) { + h.enqueue(evt.Object, q) +} + +func (h *byOwnerEventHandler) Update(_ context.Context, evt event.TypedUpdateEvent[*unstructured.Unstructured], q workqueue.TypedRateLimitingInterface[mcreconcile.Request]) { + h.enqueue(evt.ObjectNew, q) +} + +func (h *byOwnerEventHandler) Delete(_ context.Context, evt event.TypedDeleteEvent[*unstructured.Unstructured], q workqueue.TypedRateLimitingInterface[mcreconcile.Request]) { + h.enqueue(evt.Object, q) +} + +func (h *byOwnerEventHandler) Generic(_ context.Context, evt event.TypedGenericEvent[*unstructured.Unstructured], q workqueue.TypedRateLimitingInterface[mcreconcile.Request]) { + h.enqueue(evt.Object, q) +} + +func (h *byOwnerEventHandler) enqueue(obj *unstructured.Unstructured, q workqueue.TypedRateLimitingInterface[mcreconcile.Request]) { + for _, ref := range obj.GetOwnerReferences() { + if ref.Kind == h.ownerKind { + q.Add(mcreconcile.Request{ + ClusterName: h.clusterName, + Request: reconcile.Request{ + NamespacedName: types.NamespacedName{ + Namespace: obj.GetNamespace(), + Name: ref.Name, + }, + }, + }) + return + } + } +} + +// byLabelEventHandler enqueues primary objects by evaluating label templates against +// the changed related object and listing primaries matching the resulting label selector. +type byLabelEventHandler struct { + clusterName string + client ctrlruntimeclient.Client + primaryDummy *unstructured.Unstructured + labelTemplates map[string]string + log *zap.SugaredLogger +} + +func (h *byLabelEventHandler) Create(ctx context.Context, evt event.TypedCreateEvent[*unstructured.Unstructured], q workqueue.TypedRateLimitingInterface[mcreconcile.Request]) { + h.enqueue(ctx, evt.Object, q) +} + +func (h *byLabelEventHandler) Update(ctx context.Context, evt event.TypedUpdateEvent[*unstructured.Unstructured], q workqueue.TypedRateLimitingInterface[mcreconcile.Request]) { + h.enqueue(ctx, evt.ObjectNew, q) +} + +func (h *byLabelEventHandler) Delete(ctx context.Context, evt event.TypedDeleteEvent[*unstructured.Unstructured], q workqueue.TypedRateLimitingInterface[mcreconcile.Request]) { + h.enqueue(ctx, evt.Object, q) +} + +func (h *byLabelEventHandler) Generic(ctx context.Context, evt event.TypedGenericEvent[*unstructured.Unstructured], q workqueue.TypedRateLimitingInterface[mcreconcile.Request]) { + h.enqueue(ctx, evt.Object, q) +} + +func (h *byLabelEventHandler) enqueue(ctx context.Context, obj *unstructured.Unstructured, q workqueue.TypedRateLimitingInterface[mcreconcile.Request]) { + // Build the template context using the changed related object. + data := map[string]any{ + "watchObject": map[string]any{ + "name": obj.GetName(), + "namespace": obj.GetNamespace(), + "labels": obj.GetLabels(), + }, + } + + // Evaluate each label template to build the selector. + matchingLabels := ctrlruntimeclient.MatchingLabels{} + for key, tpl := range h.labelTemplates { + value, err := templating.Render(tpl, data) + if err != nil { + h.log.Warnw("Failed to evaluate byLabel template", "key", key, "template", tpl, "error", err) + return + } + matchingLabels[key] = value + } + + // List primary objects matching the derived label selector. + primaryList := &unstructured.UnstructuredList{} + primaryList.SetAPIVersion(h.primaryDummy.GetAPIVersion()) + primaryList.SetKind(h.primaryDummy.GetKind() + "List") + + if err := h.client.List(ctx, primaryList, matchingLabels); err != nil { + h.log.Warnw("Failed to list primary objects for byLabel watch", "selector", matchingLabels, "error", err) + return + } + + for i := range primaryList.Items { + primary := &primaryList.Items[i] + q.Add(mcreconcile.Request{ + ClusterName: h.clusterName, + Request: reconcile.Request{ + NamespacedName: types.NamespacedName{ + Namespace: primary.GetNamespace(), + Name: primary.GetName(), + }, + }, + }) + } +} diff --git a/sdk/apis/syncagent/v1alpha1/published_resource.go b/sdk/apis/syncagent/v1alpha1/published_resource.go index 94c80ea..71eed55 100644 --- a/sdk/apis/syncagent/v1alpha1/published_resource.go +++ b/sdk/apis/syncagent/v1alpha1/published_resource.go @@ -256,6 +256,36 @@ type RelatedResourceSpec struct { // Mutation configures optional transformation rules for the related resource. // Status mutations are only performed when the related resource originates in kcp. Mutation *ResourceMutationSpec `json:"mutation,omitempty"` + + // Watch configures how the agent identifies the owning primary object when a related + // resource with origin: kcp changes. When set, the agent sets up a watch on the related + // resource type and uses the configured rule to enqueue the correct primary object. + // Without this field, changes to origin:kcp related resources do not trigger reconciliation. + Watch *RelatedResourceWatch `json:"watch,omitempty"` +} + +// RelatedResourceWatch configures how the watch handler maps a changed related resource +// back to its owning primary object. +// Exactly one of ByOwner or ByLabel must be set. +type RelatedResourceWatch struct { + // ByOwner configures the watch handler to inspect the OwnerReferences of the changed + // object. When an OwnerReference with the given Kind is found, the referenced owner + // is enqueued as the primary object. + // +optional + ByOwner *RelatedResourceWatchByOwner `json:"byOwner,omitempty"` + + // ByLabel configures the watch handler to list primary objects matching a label selector + // derived from the changed object. Each map key is a label key on the primary object; + // each value is a Go template expression evaluated with the changed object available as + // .watchObject (with fields .name, .namespace, .labels). + // +optional + ByLabel map[string]string `json:"byLabel,omitempty"` +} + +// RelatedResourceWatchByOwner configures reverse lookup via OwnerReferences. +type RelatedResourceWatchByOwner struct { + // Kind is the Kind to look for in the OwnerReferences of the changed related object. + Kind string `json:"kind"` } // RelatedResourceProjection describes how the source GVK of a related resource (i.e. diff --git a/sdk/apis/syncagent/v1alpha1/zz_generated.deepcopy.go b/sdk/apis/syncagent/v1alpha1/zz_generated.deepcopy.go index 61724a3..a69d656 100644 --- a/sdk/apis/syncagent/v1alpha1/zz_generated.deepcopy.go +++ b/sdk/apis/syncagent/v1alpha1/zz_generated.deepcopy.go @@ -304,6 +304,11 @@ func (in *RelatedResourceSpec) DeepCopyInto(out *RelatedResourceSpec) { *out = new(ResourceMutationSpec) (*in).DeepCopyInto(*out) } + if in.Watch != nil { + in, out := &in.Watch, &out.Watch + *out = new(RelatedResourceWatch) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RelatedResourceSpec. @@ -316,6 +321,48 @@ func (in *RelatedResourceSpec) DeepCopy() *RelatedResourceSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RelatedResourceWatch) DeepCopyInto(out *RelatedResourceWatch) { + *out = *in + if in.ByOwner != nil { + in, out := &in.ByOwner, &out.ByOwner + *out = new(RelatedResourceWatchByOwner) + **out = **in + } + if in.ByLabel != nil { + in, out := &in.ByLabel, &out.ByLabel + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RelatedResourceWatch. +func (in *RelatedResourceWatch) DeepCopy() *RelatedResourceWatch { + if in == nil { + return nil + } + out := new(RelatedResourceWatch) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RelatedResourceWatchByOwner) DeepCopyInto(out *RelatedResourceWatchByOwner) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RelatedResourceWatchByOwner. +func (in *RelatedResourceWatchByOwner) DeepCopy() *RelatedResourceWatchByOwner { + if in == nil { + return nil + } + out := new(RelatedResourceWatchByOwner) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ResourceCELMutation) DeepCopyInto(out *ResourceCELMutation) { *out = *in diff --git a/sdk/applyconfiguration/syncagent/v1alpha1/relatedresourcespec.go b/sdk/applyconfiguration/syncagent/v1alpha1/relatedresourcespec.go index 5496c40..68e1ef7 100644 --- a/sdk/applyconfiguration/syncagent/v1alpha1/relatedresourcespec.go +++ b/sdk/applyconfiguration/syncagent/v1alpha1/relatedresourcespec.go @@ -35,6 +35,7 @@ type RelatedResourceSpecApplyConfiguration struct { Projection *RelatedResourceProjectionApplyConfiguration `json:"projection,omitempty"` Object *RelatedResourceObjectApplyConfiguration `json:"object,omitempty"` Mutation *ResourceMutationSpecApplyConfiguration `json:"mutation,omitempty"` + Watch *RelatedResourceWatchApplyConfiguration `json:"watch,omitempty"` } // RelatedResourceSpecApplyConfiguration constructs a declarative configuration of the RelatedResourceSpec type for use with @@ -122,3 +123,11 @@ func (b *RelatedResourceSpecApplyConfiguration) WithMutation(value *ResourceMuta b.Mutation = value return b } + +// WithWatch sets the Watch 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 Watch field is set to the value of the last call. +func (b *RelatedResourceSpecApplyConfiguration) WithWatch(value *RelatedResourceWatchApplyConfiguration) *RelatedResourceSpecApplyConfiguration { + b.Watch = value + return b +} diff --git a/sdk/applyconfiguration/syncagent/v1alpha1/relatedresourcewatch.go b/sdk/applyconfiguration/syncagent/v1alpha1/relatedresourcewatch.go new file mode 100644 index 0000000..443a515 --- /dev/null +++ b/sdk/applyconfiguration/syncagent/v1alpha1/relatedresourcewatch.go @@ -0,0 +1,54 @@ +/* +Copyright The KCP Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by applyconfiguration-gen-v0.33. DO NOT EDIT. + +package v1alpha1 + +// RelatedResourceWatchApplyConfiguration represents a declarative configuration of the RelatedResourceWatch type for use +// with apply. +type RelatedResourceWatchApplyConfiguration struct { + ByOwner *RelatedResourceWatchByOwnerApplyConfiguration `json:"byOwner,omitempty"` + ByLabel map[string]string `json:"byLabel,omitempty"` +} + +// RelatedResourceWatchApplyConfiguration constructs a declarative configuration of the RelatedResourceWatch type for use with +// apply. +func RelatedResourceWatch() *RelatedResourceWatchApplyConfiguration { + return &RelatedResourceWatchApplyConfiguration{} +} + +// WithByOwner sets the ByOwner 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 ByOwner field is set to the value of the last call. +func (b *RelatedResourceWatchApplyConfiguration) WithByOwner(value *RelatedResourceWatchByOwnerApplyConfiguration) *RelatedResourceWatchApplyConfiguration { + b.ByOwner = value + return b +} + +// WithByLabel puts the entries into the ByLabel field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, the entries provided by each call will be put on the ByLabel field, +// overwriting an existing map entries in ByLabel field with the same key. +func (b *RelatedResourceWatchApplyConfiguration) WithByLabel(entries map[string]string) *RelatedResourceWatchApplyConfiguration { + if b.ByLabel == nil && len(entries) > 0 { + b.ByLabel = make(map[string]string, len(entries)) + } + for k, v := range entries { + b.ByLabel[k] = v + } + return b +} diff --git a/sdk/applyconfiguration/syncagent/v1alpha1/relatedresourcewatchbyowner.go b/sdk/applyconfiguration/syncagent/v1alpha1/relatedresourcewatchbyowner.go new file mode 100644 index 0000000..7700892 --- /dev/null +++ b/sdk/applyconfiguration/syncagent/v1alpha1/relatedresourcewatchbyowner.go @@ -0,0 +1,39 @@ +/* +Copyright The KCP Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by applyconfiguration-gen-v0.33. DO NOT EDIT. + +package v1alpha1 + +// RelatedResourceWatchByOwnerApplyConfiguration represents a declarative configuration of the RelatedResourceWatchByOwner type for use +// with apply. +type RelatedResourceWatchByOwnerApplyConfiguration struct { + Kind *string `json:"kind,omitempty"` +} + +// RelatedResourceWatchByOwnerApplyConfiguration constructs a declarative configuration of the RelatedResourceWatchByOwner type for use with +// apply. +func RelatedResourceWatchByOwner() *RelatedResourceWatchByOwnerApplyConfiguration { + return &RelatedResourceWatchByOwnerApplyConfiguration{} +} + +// WithKind sets the Kind 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 Kind field is set to the value of the last call. +func (b *RelatedResourceWatchByOwnerApplyConfiguration) WithKind(value string) *RelatedResourceWatchByOwnerApplyConfiguration { + b.Kind = &value + return b +} diff --git a/sdk/applyconfiguration/utils.go b/sdk/applyconfiguration/utils.go index 57601ad..55b573a 100644 --- a/sdk/applyconfiguration/utils.go +++ b/sdk/applyconfiguration/utils.go @@ -55,6 +55,10 @@ func ForKind(kind schema.GroupVersionKind) interface{} { return &syncagentv1alpha1.RelatedResourceSelectorRewriteApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("RelatedResourceSpec"): return &syncagentv1alpha1.RelatedResourceSpecApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("RelatedResourceWatch"): + return &syncagentv1alpha1.RelatedResourceWatchApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("RelatedResourceWatchByOwner"): + return &syncagentv1alpha1.RelatedResourceWatchByOwnerApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("ResourceCELMutation"): return &syncagentv1alpha1.ResourceCELMutationApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("ResourceDeleteMutation"): From 638d72825c6c404db0c42f3033e41ee5cf0922c7 Mon Sep 17 00:00:00 2001 From: Iskren Date: Wed, 1 Apr 2026 02:27:21 +0300 Subject: [PATCH 2/2] resolve Christoph's comments fix linter issue fix verify ci --- .../syncagent.kcp.io_publishedresources.yaml | 63 ++++-- hack/tools.checksums | 1 + internal/controller/sync/controller.go | 198 ++++++++++++++---- internal/controller/sync/related_handlers.go | 69 +++--- .../syncagent/v1alpha1/published_resource.go | 20 +- .../v1alpha1/zz_generated.deepcopy.go | 10 +- .../v1alpha1/relatedresourcewatch.go | 30 +-- .../v1alpha1/relatedresourcewatchbyowner.go | 39 ---- sdk/applyconfiguration/utils.go | 2 - 9 files changed, 265 insertions(+), 167 deletions(-) delete mode 100644 sdk/applyconfiguration/syncagent/v1alpha1/relatedresourcewatchbyowner.go diff --git a/deploy/crd/kcp.io/syncagent.kcp.io_publishedresources.yaml b/deploy/crd/kcp.io/syncagent.kcp.io_publishedresources.yaml index c5b8dfd..8781258 100644 --- a/deploy/crd/kcp.io/syncagent.kcp.io_publishedresources.yaml +++ b/deploy/crd/kcp.io/syncagent.kcp.io_publishedresources.yaml @@ -785,28 +785,63 @@ spec: resource type and uses the configured rule to enqueue the correct primary object. Without this field, changes to origin:kcp related resources do not trigger reconciliation. properties: - byLabel: - additionalProperties: - type: string - description: |- - ByLabel configures the watch handler to list primary objects matching a label selector - derived from the changed object. Each map key is a label key on the primary object; - each value is a Go template expression evaluated with the changed object available as - .watchObject (with fields .name, .namespace, .labels). - type: object byOwner: description: |- ByOwner configures the watch handler to inspect the OwnerReferences of the changed object. When an OwnerReference with the given Kind is found, the referenced owner is enqueued as the primary object. + type: object + bySelector: + description: |- + BySelector configures the watch handler to list primary objects matching the given label + selector. When a related object changes, all primary objects matching this selector + are enqueued for reconciliation. properties: - kind: - description: Kind is the Kind to look for in the OwnerReferences of the changed related object. - type: string - required: - - kind + 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 type: object + x-kubernetes-validations: + - message: exactly one of byOwner or bySelector must be set + rule: has(self.byOwner) != has(self.bySelector) required: - identifier - object diff --git a/hack/tools.checksums b/hack/tools.checksums index 66dfa6f..109523e 100644 --- a/hack/tools.checksums +++ b/hack/tools.checksums @@ -6,6 +6,7 @@ etcd|GOARCH=arm64;GOOS=linux|cc8c645e5a8df0f35f2a5c51d9b9383037eef0cf0167c52e648 gimps|GOARCH=amd64;GOOS=linux|b597efc7e2c72097a44c001b41a06ccca97610963e1f1aec74c3d99c0e0b6c11 gimps|GOARCH=arm64;GOOS=linux|2588daec997b4f4b3a8d8875f780fd6faf3c39c933519e7899e19a686476c8e4 golangci-lint|GOARCH=amd64;GOOS=linux|8a01a08dad47a14824d7d0f14af07c7144105fc079386c9c31fbe85f08f91643 +golangci-lint|GOARCH=arm64;GOOS=darwin|5fd0b6a09353eb0101d3ae81d5e3cf4707b77210c66fb92ae152d7280d959419 golangci-lint|GOARCH=arm64;GOOS=linux|2ed9cf2ad070dabc7947ba34cdc5142910be830306f063719898bc8fb44a7074 kube-apiserver|GOARCH=amd64;GOOS=linux|ca822082ec39e54a25836a4011ddb66e482e317a7a4f1a1f73882bbd2cf5a2a1 kube-apiserver|GOARCH=arm64;GOOS=linux|6ade6c2646e2c01fde1095407452afc2b65e89d6da16da29ee39f6223ccaf63b diff --git a/internal/controller/sync/controller.go b/internal/controller/sync/controller.go index d342235..36dbd0f 100644 --- a/internal/controller/sync/controller.go +++ b/internal/controller/sync/controller.go @@ -36,6 +36,7 @@ import ( corev1 "k8s.io/api/core/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/labels" @@ -164,11 +165,31 @@ func Create( return nil, fmt.Errorf("failed to setup local-side watch: %w", err) } - // Watch origin:kcp related resources so that changes to them trigger reconciliation - // of the owning primary object. Only related resources with a Watch config are covered. - watchedGVKs := sets.New[schema.GroupVersionKind]() + if err := setupRelatedResourceWatches(c, localManager, remoteManager, pubRes, localDummy, remoteDummy, log); err != nil { + return nil, err + } + + log.Info("Done setting up unmanaged controller.") + + return c, nil +} + +// setupRelatedResourceWatches sets up watches for all related resources that have a Watch +// config, on their respective origin side, so that changes trigger primary reconciliation. +func setupRelatedResourceWatches( + c mccontroller.Controller, + localManager manager.Manager, + remoteManager mcmanager.Manager, + pubRes *syncagentv1alpha1.PublishedResource, + localDummy, remoteDummy *unstructured.Unstructured, + log *zap.SugaredLogger, +) error { + // Deduplication is per-origin to allow the same GVK on both sides. + watchedKcpGVKs := sets.New[schema.GroupVersionKind]() + watchedServiceGVKs := sets.New[schema.GroupVersionKind]() + for _, relRes := range pubRes.Spec.Related { - if relRes.Origin != syncagentv1alpha1.RelatedResourceOriginKcp || relRes.Watch == nil { + if relRes.Watch == nil { continue } @@ -178,62 +199,155 @@ func Create( Resource: relRes.Resource, } - // Use the local REST mapper to determine the Kind. - gvk, err := localManager.GetRESTMapper().KindFor(gvr) - if err != nil { - log.Warnw("Failed to determine Kind for origin:kcp related resource, skipping watch", "gvr", gvr, "error", err) - continue + // Use the REST mapper of the origin side: related resources may have projected GVKs + // that differ between kcp and the service cluster, so we must resolve using the + // mapper that actually knows about the GVR on that side. + var originRESTMapper meta.RESTMapper + if relRes.Origin == syncagentv1alpha1.RelatedResourceOriginKcp { + originRESTMapper = remoteManager.GetLocalManager().GetRESTMapper() + } else { + originRESTMapper = localManager.GetRESTMapper() } - // Deduplicate: only set up one watch per GVK. - if watchedGVKs.Has(gvk) { - continue + gvk, err := originRESTMapper.KindFor(gvr) + if err != nil { + return fmt.Errorf("failed to determine Kind for related resource %v (origin: %s): %w", gvr, relRes.Origin, err) } - watchedGVKs.Insert(gvk) relatedDummy := &unstructured.Unstructured{} relatedDummy.SetGroupVersionKind(gvk) - var enqueueForRelated mchandler.TypedEventHandlerFunc[*unstructured.Unstructured, mcreconcile.Request] + if relRes.Origin == syncagentv1alpha1.RelatedResourceOriginKcp { + if watchedKcpGVKs.Has(gvk) { + continue + } + watchedKcpGVKs.Insert(gvk) - switch { - case relRes.Watch.ByOwner != nil: - ownerKind := relRes.Watch.ByOwner.Kind - enqueueForRelated = func(clusterName string, _ cluster.Cluster) handler.TypedEventHandler[*unstructured.Unstructured, mcreconcile.Request] { - return &byOwnerEventHandler{ - clusterName: clusterName, - ownerKind: ownerKind, - } + enqueueForRelated, err := buildKcpRelatedHandler(relRes.Watch, gvk, remoteDummy, log) + if err != nil { + return err } - case relRes.Watch.ByLabel != nil: - labelTemplates := relRes.Watch.ByLabel - primaryDummy := remoteDummy.DeepCopy() - enqueueForRelated = func(clusterName string, cl cluster.Cluster) handler.TypedEventHandler[*unstructured.Unstructured, mcreconcile.Request] { - return &byLabelEventHandler{ - clusterName: clusterName, - client: cl.GetClient(), - primaryDummy: primaryDummy, - labelTemplates: labelTemplates, - log: log, - } + if err := c.MultiClusterWatch(mcsource.TypedKind(relatedDummy, enqueueForRelated)); err != nil { + return fmt.Errorf("failed to setup watch for kcp-origin related resource %v: %w", gvk, err) + } + } else { + if watchedServiceGVKs.Has(gvk) { + continue } + watchedServiceGVKs.Insert(gvk) - default: - log.Warnw("origin:kcp related resource has Watch set but neither byOwner nor byLabel configured, skipping", "gvk", gvk) - continue - } + enqueueForRelated, err := buildServiceRelatedHandler(relRes.Watch, gvk, localDummy, localManager, log) + if err != nil { + return err + } - if err := c.MultiClusterWatch(mcsource.TypedKind(relatedDummy, enqueueForRelated)); err != nil { - return nil, fmt.Errorf("failed to setup watch for origin:kcp related resource %v: %w", gvk, err) + if err := c.Watch(source.TypedKind(localManager.GetCache(), relatedDummy, enqueueForRelated)); err != nil { + return fmt.Errorf("failed to setup watch for service-origin related resource %v: %w", gvk, err) + } } - log.Infow("Set up watch for origin:kcp related resource", "gvk", gvk) + log.Infow("Set up watch for related resource", "gvk", gvk, "origin", relRes.Origin) } - log.Info("Done setting up unmanaged controller.") + return nil +} - return c, nil +// buildKcpRelatedHandler constructs the per-cluster event handler for a kcp-origin related resource. +func buildKcpRelatedHandler( + watch *syncagentv1alpha1.RelatedResourceWatch, + gvk schema.GroupVersionKind, + remoteDummy *unstructured.Unstructured, + log *zap.SugaredLogger, +) (mchandler.TypedEventHandlerFunc[*unstructured.Unstructured, mcreconcile.Request], error) { + switch { + case watch.ByOwner != nil: + ownerGVK := remoteDummy.GroupVersionKind() + return func(clusterName string, _ cluster.Cluster) handler.TypedEventHandler[*unstructured.Unstructured, mcreconcile.Request] { + return &byOwnerEventHandler{ + clusterName: clusterName, + ownerGVK: ownerGVK, + } + }, nil + + case watch.BySelector != nil: + labelSelector := watch.BySelector + primaryDummy := remoteDummy.DeepCopy() + return func(clusterName string, cl cluster.Cluster) handler.TypedEventHandler[*unstructured.Unstructured, mcreconcile.Request] { + return &bySelectorEventHandler{ + clusterName: clusterName, + client: cl.GetClient(), + primaryDummy: primaryDummy, + labelSelector: labelSelector, + log: log, + } + }, nil + + default: + return nil, fmt.Errorf("related resource %v (origin: kcp) has Watch set but neither byOwner nor bySelector configured", gvk) + } +} + +// buildServiceRelatedHandler constructs the event handler for a service-cluster-origin related resource. +// It maps the changed related resource back to the remote (kcp) primary via sync metadata on the local primary. +func buildServiceRelatedHandler( + watch *syncagentv1alpha1.RelatedResourceWatch, + gvk schema.GroupVersionKind, + localDummy *unstructured.Unstructured, + localManager manager.Manager, + log *zap.SugaredLogger, +) (handler.TypedEventHandler[*unstructured.Unstructured, mcreconcile.Request], error) { + localClient := localManager.GetClient() + + switch { + case watch.ByOwner != nil: + ownerGVK := localDummy.GroupVersionKind() + primaryDummy := localDummy.DeepCopy() + return handler.TypedEnqueueRequestsFromMapFunc(func(ctx context.Context, obj *unstructured.Unstructured) []mcreconcile.Request { + for _, ref := range obj.GetOwnerReferences() { + refGV, err := schema.ParseGroupVersion(ref.APIVersion) + if err != nil || refGV.Group != ownerGVK.Group || refGV.Version != ownerGVK.Version || ref.Kind != ownerGVK.Kind { + continue + } + localPrimary := primaryDummy.DeepCopy() + if err := localClient.Get(ctx, types.NamespacedName{Namespace: obj.GetNamespace(), Name: ref.Name}, localPrimary); err != nil { + log.Warnw("Failed to fetch local primary for byOwner watch", "owner", ref.Name, "error", err) + return nil + } + if req := sync.RemoteNameForLocalObject(localPrimary); req != nil { + return []mcreconcile.Request{*req} + } + return nil + } + return nil + }), nil + + case watch.BySelector != nil: + selector, err := metav1.LabelSelectorAsSelector(watch.BySelector) + if err != nil { + return nil, fmt.Errorf("failed to convert bySelector for service-origin related resource %v: %w", gvk, err) + } + primaryDummy := localDummy.DeepCopy() + return handler.TypedEnqueueRequestsFromMapFunc(func(ctx context.Context, _ *unstructured.Unstructured) []mcreconcile.Request { + primaryList := &unstructured.UnstructuredList{} + primaryList.SetAPIVersion(primaryDummy.GetAPIVersion()) + primaryList.SetKind(primaryDummy.GetKind() + "List") + if err := localClient.List(ctx, primaryList, &ctrlruntimeclient.ListOptions{LabelSelector: selector}); err != nil { + log.Warnw("Failed to list local primary objects for bySelector watch", "selector", selector.String(), "error", err) + return nil + } + var reqs []mcreconcile.Request + for i := range primaryList.Items { + if req := sync.RemoteNameForLocalObject(&primaryList.Items[i]); req != nil { + reqs = append(reqs, *req) + } + } + return reqs + }), nil + + default: + return nil, fmt.Errorf("related resource %v (origin: service) has Watch set but neither byOwner nor bySelector configured", gvk) + } } func (r *Reconciler) Reconcile(ctx context.Context, request mcreconcile.Request) (reconcile.Result, error) { diff --git a/internal/controller/sync/related_handlers.go b/internal/controller/sync/related_handlers.go index 843a20f..28eebb8 100644 --- a/internal/controller/sync/related_handlers.go +++ b/internal/controller/sync/related_handlers.go @@ -18,12 +18,13 @@ package sync import ( "context" + "fmt" "go.uber.org/zap" - "github.com/kcp-dev/api-syncagent/internal/sync/templating" - + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/util/workqueue" ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client" @@ -33,10 +34,10 @@ import ( ) // byOwnerEventHandler enqueues the primary object by inspecting the OwnerReferences -// of the changed related object and finding one with the configured Kind. +// of the changed related object and finding one matching the configured GVK. type byOwnerEventHandler struct { clusterName string - ownerKind string + ownerGVK schema.GroupVersionKind } func (h *byOwnerEventHandler) Create(_ context.Context, evt event.TypedCreateEvent[*unstructured.Unstructured], q workqueue.TypedRateLimitingInterface[mcreconcile.Request]) { @@ -57,7 +58,11 @@ func (h *byOwnerEventHandler) Generic(_ context.Context, evt event.TypedGenericE func (h *byOwnerEventHandler) enqueue(obj *unstructured.Unstructured, q workqueue.TypedRateLimitingInterface[mcreconcile.Request]) { for _, ref := range obj.GetOwnerReferences() { - if ref.Kind == h.ownerKind { + refGV, err := schema.ParseGroupVersion(ref.APIVersion) + if err != nil { + continue + } + if refGV.Group == h.ownerGVK.Group && refGV.Version == h.ownerGVK.Version && ref.Kind == h.ownerGVK.Kind { q.Add(mcreconcile.Request{ ClusterName: h.clusterName, Request: reconcile.Request{ @@ -72,60 +77,46 @@ func (h *byOwnerEventHandler) enqueue(obj *unstructured.Unstructured, q workqueu } } -// byLabelEventHandler enqueues primary objects by evaluating label templates against -// the changed related object and listing primaries matching the resulting label selector. -type byLabelEventHandler struct { - clusterName string - client ctrlruntimeclient.Client - primaryDummy *unstructured.Unstructured - labelTemplates map[string]string - log *zap.SugaredLogger +// bySelectorEventHandler enqueues primary objects by listing primaries matching the configured +// label selector whenever a related object changes. +type bySelectorEventHandler struct { + clusterName string + client ctrlruntimeclient.Client + primaryDummy *unstructured.Unstructured + labelSelector *metav1.LabelSelector + log *zap.SugaredLogger } -func (h *byLabelEventHandler) Create(ctx context.Context, evt event.TypedCreateEvent[*unstructured.Unstructured], q workqueue.TypedRateLimitingInterface[mcreconcile.Request]) { +func (h *bySelectorEventHandler) Create(ctx context.Context, evt event.TypedCreateEvent[*unstructured.Unstructured], q workqueue.TypedRateLimitingInterface[mcreconcile.Request]) { h.enqueue(ctx, evt.Object, q) } -func (h *byLabelEventHandler) Update(ctx context.Context, evt event.TypedUpdateEvent[*unstructured.Unstructured], q workqueue.TypedRateLimitingInterface[mcreconcile.Request]) { +func (h *bySelectorEventHandler) Update(ctx context.Context, evt event.TypedUpdateEvent[*unstructured.Unstructured], q workqueue.TypedRateLimitingInterface[mcreconcile.Request]) { h.enqueue(ctx, evt.ObjectNew, q) } -func (h *byLabelEventHandler) Delete(ctx context.Context, evt event.TypedDeleteEvent[*unstructured.Unstructured], q workqueue.TypedRateLimitingInterface[mcreconcile.Request]) { +func (h *bySelectorEventHandler) Delete(ctx context.Context, evt event.TypedDeleteEvent[*unstructured.Unstructured], q workqueue.TypedRateLimitingInterface[mcreconcile.Request]) { h.enqueue(ctx, evt.Object, q) } -func (h *byLabelEventHandler) Generic(ctx context.Context, evt event.TypedGenericEvent[*unstructured.Unstructured], q workqueue.TypedRateLimitingInterface[mcreconcile.Request]) { +func (h *bySelectorEventHandler) Generic(ctx context.Context, evt event.TypedGenericEvent[*unstructured.Unstructured], q workqueue.TypedRateLimitingInterface[mcreconcile.Request]) { h.enqueue(ctx, evt.Object, q) } -func (h *byLabelEventHandler) enqueue(ctx context.Context, obj *unstructured.Unstructured, q workqueue.TypedRateLimitingInterface[mcreconcile.Request]) { - // Build the template context using the changed related object. - data := map[string]any{ - "watchObject": map[string]any{ - "name": obj.GetName(), - "namespace": obj.GetNamespace(), - "labels": obj.GetLabels(), - }, - } - - // Evaluate each label template to build the selector. - matchingLabels := ctrlruntimeclient.MatchingLabels{} - for key, tpl := range h.labelTemplates { - value, err := templating.Render(tpl, data) - if err != nil { - h.log.Warnw("Failed to evaluate byLabel template", "key", key, "template", tpl, "error", err) - return - } - matchingLabels[key] = value +func (h *bySelectorEventHandler) enqueue(ctx context.Context, _ *unstructured.Unstructured, q workqueue.TypedRateLimitingInterface[mcreconcile.Request]) { + selector, err := metav1.LabelSelectorAsSelector(h.labelSelector) + if err != nil { + h.log.Warnw("Failed to convert bySelector selector", "error", err) + return } - // List primary objects matching the derived label selector. + // List primary objects matching the label selector. primaryList := &unstructured.UnstructuredList{} primaryList.SetAPIVersion(h.primaryDummy.GetAPIVersion()) primaryList.SetKind(h.primaryDummy.GetKind() + "List") - if err := h.client.List(ctx, primaryList, matchingLabels); err != nil { - h.log.Warnw("Failed to list primary objects for byLabel watch", "selector", matchingLabels, "error", err) + if err := h.client.List(ctx, primaryList, &ctrlruntimeclient.ListOptions{LabelSelector: selector}); err != nil { + h.log.Warnw("Failed to list primary objects for bySelector watch", "selector", fmt.Sprintf("%v", selector), "error", err) return } diff --git a/sdk/apis/syncagent/v1alpha1/published_resource.go b/sdk/apis/syncagent/v1alpha1/published_resource.go index 71eed55..c8aa68b 100644 --- a/sdk/apis/syncagent/v1alpha1/published_resource.go +++ b/sdk/apis/syncagent/v1alpha1/published_resource.go @@ -266,7 +266,8 @@ type RelatedResourceSpec struct { // RelatedResourceWatch configures how the watch handler maps a changed related resource // back to its owning primary object. -// Exactly one of ByOwner or ByLabel must be set. +// Exactly one of ByOwner or BySelector must be set. +// +kubebuilder:validation:XValidation:rule="has(self.byOwner) != has(self.bySelector)",message="exactly one of byOwner or bySelector must be set" type RelatedResourceWatch struct { // ByOwner configures the watch handler to inspect the OwnerReferences of the changed // object. When an OwnerReference with the given Kind is found, the referenced owner @@ -274,19 +275,18 @@ type RelatedResourceWatch struct { // +optional ByOwner *RelatedResourceWatchByOwner `json:"byOwner,omitempty"` - // ByLabel configures the watch handler to list primary objects matching a label selector - // derived from the changed object. Each map key is a label key on the primary object; - // each value is a Go template expression evaluated with the changed object available as - // .watchObject (with fields .name, .namespace, .labels). + // BySelector configures the watch handler to list primary objects matching the given label + // selector. When a related object changes, all primary objects matching this selector + // are enqueued for reconciliation. // +optional - ByLabel map[string]string `json:"byLabel,omitempty"` + BySelector *metav1.LabelSelector `json:"bySelector,omitempty"` } // RelatedResourceWatchByOwner configures reverse lookup via OwnerReferences. -type RelatedResourceWatchByOwner struct { - // Kind is the Kind to look for in the OwnerReferences of the changed related object. - Kind string `json:"kind"` -} +// The agent already knows the GVK of the primary object, so no further configuration +// is needed: when a related object changes, its OwnerReferences are inspected for a +// reference whose Kind matches the primary object's Kind. +type RelatedResourceWatchByOwner struct{} // RelatedResourceProjection describes how the source GVK of a related resource (i.e. // the GVK on the related resource's origin side) should be modified when an object diff --git a/sdk/apis/syncagent/v1alpha1/zz_generated.deepcopy.go b/sdk/apis/syncagent/v1alpha1/zz_generated.deepcopy.go index a69d656..45a1cec 100644 --- a/sdk/apis/syncagent/v1alpha1/zz_generated.deepcopy.go +++ b/sdk/apis/syncagent/v1alpha1/zz_generated.deepcopy.go @@ -329,12 +329,10 @@ func (in *RelatedResourceWatch) DeepCopyInto(out *RelatedResourceWatch) { *out = new(RelatedResourceWatchByOwner) **out = **in } - if in.ByLabel != nil { - in, out := &in.ByLabel, &out.ByLabel - *out = make(map[string]string, len(*in)) - for key, val := range *in { - (*out)[key] = val - } + if in.BySelector != nil { + in, out := &in.BySelector, &out.BySelector + *out = new(v1.LabelSelector) + (*in).DeepCopyInto(*out) } } diff --git a/sdk/applyconfiguration/syncagent/v1alpha1/relatedresourcewatch.go b/sdk/applyconfiguration/syncagent/v1alpha1/relatedresourcewatch.go index 443a515..1bbc75a 100644 --- a/sdk/applyconfiguration/syncagent/v1alpha1/relatedresourcewatch.go +++ b/sdk/applyconfiguration/syncagent/v1alpha1/relatedresourcewatch.go @@ -18,11 +18,17 @@ limitations under the License. package v1alpha1 +import ( + v1 "k8s.io/client-go/applyconfigurations/meta/v1" + + syncagentv1alpha1 "github.com/kcp-dev/api-syncagent/sdk/apis/syncagent/v1alpha1" +) + // RelatedResourceWatchApplyConfiguration represents a declarative configuration of the RelatedResourceWatch type for use // with apply. type RelatedResourceWatchApplyConfiguration struct { - ByOwner *RelatedResourceWatchByOwnerApplyConfiguration `json:"byOwner,omitempty"` - ByLabel map[string]string `json:"byLabel,omitempty"` + ByOwner *syncagentv1alpha1.RelatedResourceWatchByOwner `json:"byOwner,omitempty"` + BySelector *v1.LabelSelectorApplyConfiguration `json:"bySelector,omitempty"` } // RelatedResourceWatchApplyConfiguration constructs a declarative configuration of the RelatedResourceWatch type for use with @@ -34,21 +40,15 @@ func RelatedResourceWatch() *RelatedResourceWatchApplyConfiguration { // WithByOwner sets the ByOwner 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 ByOwner field is set to the value of the last call. -func (b *RelatedResourceWatchApplyConfiguration) WithByOwner(value *RelatedResourceWatchByOwnerApplyConfiguration) *RelatedResourceWatchApplyConfiguration { - b.ByOwner = value +func (b *RelatedResourceWatchApplyConfiguration) WithByOwner(value syncagentv1alpha1.RelatedResourceWatchByOwner) *RelatedResourceWatchApplyConfiguration { + b.ByOwner = &value return b } -// WithByLabel puts the entries into the ByLabel field in the declarative configuration -// and returns the receiver, so that objects can be build by chaining "With" function invocations. -// If called multiple times, the entries provided by each call will be put on the ByLabel field, -// overwriting an existing map entries in ByLabel field with the same key. -func (b *RelatedResourceWatchApplyConfiguration) WithByLabel(entries map[string]string) *RelatedResourceWatchApplyConfiguration { - if b.ByLabel == nil && len(entries) > 0 { - b.ByLabel = make(map[string]string, len(entries)) - } - for k, v := range entries { - b.ByLabel[k] = v - } +// WithBySelector sets the BySelector 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 BySelector field is set to the value of the last call. +func (b *RelatedResourceWatchApplyConfiguration) WithBySelector(value *v1.LabelSelectorApplyConfiguration) *RelatedResourceWatchApplyConfiguration { + b.BySelector = value return b } diff --git a/sdk/applyconfiguration/syncagent/v1alpha1/relatedresourcewatchbyowner.go b/sdk/applyconfiguration/syncagent/v1alpha1/relatedresourcewatchbyowner.go deleted file mode 100644 index 7700892..0000000 --- a/sdk/applyconfiguration/syncagent/v1alpha1/relatedresourcewatchbyowner.go +++ /dev/null @@ -1,39 +0,0 @@ -/* -Copyright The KCP Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -// Code generated by applyconfiguration-gen-v0.33. DO NOT EDIT. - -package v1alpha1 - -// RelatedResourceWatchByOwnerApplyConfiguration represents a declarative configuration of the RelatedResourceWatchByOwner type for use -// with apply. -type RelatedResourceWatchByOwnerApplyConfiguration struct { - Kind *string `json:"kind,omitempty"` -} - -// RelatedResourceWatchByOwnerApplyConfiguration constructs a declarative configuration of the RelatedResourceWatchByOwner type for use with -// apply. -func RelatedResourceWatchByOwner() *RelatedResourceWatchByOwnerApplyConfiguration { - return &RelatedResourceWatchByOwnerApplyConfiguration{} -} - -// WithKind sets the Kind 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 Kind field is set to the value of the last call. -func (b *RelatedResourceWatchByOwnerApplyConfiguration) WithKind(value string) *RelatedResourceWatchByOwnerApplyConfiguration { - b.Kind = &value - return b -} diff --git a/sdk/applyconfiguration/utils.go b/sdk/applyconfiguration/utils.go index 55b573a..ff1588c 100644 --- a/sdk/applyconfiguration/utils.go +++ b/sdk/applyconfiguration/utils.go @@ -57,8 +57,6 @@ func ForKind(kind schema.GroupVersionKind) interface{} { return &syncagentv1alpha1.RelatedResourceSpecApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("RelatedResourceWatch"): return &syncagentv1alpha1.RelatedResourceWatchApplyConfiguration{} - case v1alpha1.SchemeGroupVersion.WithKind("RelatedResourceWatchByOwner"): - return &syncagentv1alpha1.RelatedResourceWatchByOwnerApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("ResourceCELMutation"): return &syncagentv1alpha1.ResourceCELMutationApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("ResourceDeleteMutation"):