diff --git a/deploy/crd/kcp.io/syncagent.kcp.io_publishedresources.yaml b/deploy/crd/kcp.io/syncagent.kcp.io_publishedresources.yaml index da26f48..8781258 100644 --- a/deploy/crd/kcp.io/syncagent.kcp.io_publishedresources.yaml +++ b/deploy/crd/kcp.io/syncagent.kcp.io_publishedresources.yaml @@ -778,6 +778,70 @@ 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: + 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: + 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 759a74b..109523e 100644 --- a/hack/tools.checksums +++ b/hack/tools.checksums @@ -1,14 +1,17 @@ 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 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 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..36dbd0f 100644 --- a/internal/controller/sync/controller.go +++ b/internal/controller/sync/controller.go @@ -36,12 +36,16 @@ 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" + "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,11 +165,191 @@ func Create( return nil, fmt.Errorf("failed to setup local-side watch: %w", err) } + 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.Watch == nil { + continue + } + + gvr := schema.GroupVersionResource{ + Group: relRes.Group, + Version: relRes.Version, + Resource: relRes.Resource, + } + + // 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() + } + + 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) + } + + relatedDummy := &unstructured.Unstructured{} + relatedDummy.SetGroupVersionKind(gvk) + + if relRes.Origin == syncagentv1alpha1.RelatedResourceOriginKcp { + if watchedKcpGVKs.Has(gvk) { + continue + } + watchedKcpGVKs.Insert(gvk) + + enqueueForRelated, err := buildKcpRelatedHandler(relRes.Watch, gvk, remoteDummy, log) + if err != nil { + return err + } + + 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) + + enqueueForRelated, err := buildServiceRelatedHandler(relRes.Watch, gvk, localDummy, localManager, log) + if err != nil { + return 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 related resource", "gvk", gvk, "origin", relRes.Origin) + } + + return 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) { log := r.log.With("cluster", request.ClusterName, "request", request.NamespacedName) log.Debug("Processing") diff --git a/internal/controller/sync/related_handlers.go b/internal/controller/sync/related_handlers.go new file mode 100644 index 0000000..28eebb8 --- /dev/null +++ b/internal/controller/sync/related_handlers.go @@ -0,0 +1,135 @@ +/* +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" + "fmt" + + "go.uber.org/zap" + + 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" + "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 matching the configured GVK. +type byOwnerEventHandler struct { + clusterName string + ownerGVK schema.GroupVersionKind +} + +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() { + 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{ + NamespacedName: types.NamespacedName{ + Namespace: obj.GetNamespace(), + Name: ref.Name, + }, + }, + }) + return + } + } +} + +// 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 *bySelectorEventHandler) Create(ctx context.Context, evt event.TypedCreateEvent[*unstructured.Unstructured], q workqueue.TypedRateLimitingInterface[mcreconcile.Request]) { + h.enqueue(ctx, evt.Object, q) +} + +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 *bySelectorEventHandler) Delete(ctx context.Context, evt event.TypedDeleteEvent[*unstructured.Unstructured], q workqueue.TypedRateLimitingInterface[mcreconcile.Request]) { + h.enqueue(ctx, evt.Object, q) +} + +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 *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 label selector. + primaryList := &unstructured.UnstructuredList{} + primaryList.SetAPIVersion(h.primaryDummy.GetAPIVersion()) + primaryList.SetKind(h.primaryDummy.GetKind() + "List") + + 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 + } + + 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..c8aa68b 100644 --- a/sdk/apis/syncagent/v1alpha1/published_resource.go +++ b/sdk/apis/syncagent/v1alpha1/published_resource.go @@ -256,8 +256,38 @@ 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 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 + // is enqueued as the primary object. + // +optional + ByOwner *RelatedResourceWatchByOwner `json:"byOwner,omitempty"` + + // 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 + BySelector *metav1.LabelSelector `json:"bySelector,omitempty"` +} + +// RelatedResourceWatchByOwner configures reverse lookup via OwnerReferences. +// 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 // is copied from the origin to the destination. diff --git a/sdk/apis/syncagent/v1alpha1/zz_generated.deepcopy.go b/sdk/apis/syncagent/v1alpha1/zz_generated.deepcopy.go index 61724a3..45a1cec 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,46 @@ 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.BySelector != nil { + in, out := &in.BySelector, &out.BySelector + *out = new(v1.LabelSelector) + (*in).DeepCopyInto(*out) + } +} + +// 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..1bbc75a --- /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 + +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 *syncagentv1alpha1.RelatedResourceWatchByOwner `json:"byOwner,omitempty"` + BySelector *v1.LabelSelectorApplyConfiguration `json:"bySelector,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 syncagentv1alpha1.RelatedResourceWatchByOwner) *RelatedResourceWatchApplyConfiguration { + b.ByOwner = &value + return b +} + +// 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/utils.go b/sdk/applyconfiguration/utils.go index 57601ad..ff1588c 100644 --- a/sdk/applyconfiguration/utils.go +++ b/sdk/applyconfiguration/utils.go @@ -55,6 +55,8 @@ 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("ResourceCELMutation"): return &syncagentv1alpha1.ResourceCELMutationApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("ResourceDeleteMutation"):