diff --git a/internal/controller/gateway_controller.go b/internal/controller/gateway_controller.go index 00dcbcd..dd92e7c 100644 --- a/internal/controller/gateway_controller.go +++ b/internal/controller/gateway_controller.go @@ -10,6 +10,7 @@ import ( "github.com/cloudflare/cloudflare-go/v2" "github.com/cloudflare/cloudflare-go/v2/shared" "github.com/cloudflare/cloudflare-go/v2/zero_trust" + appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -19,7 +20,6 @@ import ( "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/client-go/tools/record" - "k8s.io/utils/ptr" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" @@ -29,11 +29,9 @@ import ( gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" ) -const ( - gatewayClassFinalizer = "cfargotunnel.com/finalizer" - gatewayFinalizer = "cfargotunnel.com/finalizer" - controllerName = "github.com/pl4nty/cloudflare-kubernetes-gateway" -) +const gatewayClassFinalizer = "cfargotunnel.com/finalizer" +const gatewayFinalizer = "cfargotunnel.com/finalizer" +const controllerName = "github.com/pl4nty/cloudflare-kubernetes-gateway" // Definitions to manage status conditions const ( @@ -46,8 +44,8 @@ const ( // GatewayReconciler reconciles a Gateway object type GatewayReconciler struct { client.Client - Scheme *runtime.Scheme - Recorder record.EventRecorder + Scheme *runtime.Scheme + Recorder record.EventRecorder } // The following markers are used to generate the rules permissions (RBAC) on config/rbac using controller-gen @@ -56,11 +54,11 @@ type GatewayReconciler struct { // +kubebuilder:rbac:groups=gateway.networking.k8s.io,resources=gatewayclasses,verbs=get;update // +kubebuilder:rbac:groups=gateway.networking.k8s.io,resources=gatewayclasses/finalizers,verbs=update -// +kubebuilder:rbac:groups=gateway.networking.k8s.io,resources=gateways,verbs=get;list;watch;update -// +kubebuilder:rbac:groups=gateway.networking.k8s.io,resources=gateways/status,verbs=update +// +kubebuilder:rbac:groups=gateway.networking.k8s.io,resources=gateways,verbs=get;list;update;watch // +kubebuilder:rbac:groups=gateway.networking.k8s.io,resources=gateways/finalizers,verbs=update +// +kubebuilder:rbac:groups=gateway.networking.k8s.io,resources=gateways/status,verbs=update +// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=create;get;list;update;watch;delete // +kubebuilder:rbac:groups=core,resources=events,verbs=create -// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;delete // +kubebuilder:rbac:groups=core,resources=secrets,verbs=get // +kubebuilder:rbac:groups=core,resources=configmaps,verbs=get;list;watch @@ -74,8 +72,9 @@ type GatewayReconciler struct { // For further info: // - About Operator Pattern: https://kubernetes.io/docs/concepts/extend-kubernetes/operator/ // - About Controllers: https://kubernetes.io/docs/concepts/architecture/controller/ -// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.20.0/pkg/reconcile -// nolint:gocyclo +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.18.2/pkg/reconcile +// +//nolint:gocyclo func (r *GatewayReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { log := log.FromContext(ctx) @@ -83,8 +82,7 @@ func (r *GatewayReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct // The purpose is check if the Custom Resource for the Kind Gateway // is applied on the cluster if not we return nil to stop the reconciliation gateway := &gatewayv1.Gateway{} - err := r.Get(ctx, req.NamespacedName, gateway) - if err != nil { + if err := r.Get(ctx, req.NamespacedName, gateway); err != nil { if apierrors.IsNotFound(err) { // If the custom resource is not found then it usually means that it was deleted or not created // In this way, we will stop the reconciliation @@ -96,7 +94,7 @@ func (r *GatewayReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct return ctrl.Result{}, err } - // check if parent GatewayClass is ours + // check if parent GatewayClass is ours and update finalizer gatewayClass := &gatewayv1.GatewayClass{} if err := r.Get(ctx, types.NamespacedName{Name: string(gateway.Spec.GatewayClassName)}, gatewayClass); err != nil { if apierrors.IsNotFound(err) { @@ -126,24 +124,7 @@ func (r *GatewayReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct } } - // Let's just set the status as Unknown when no status is available - if len(gateway.Status.Conditions) == 0 { - meta.SetStatusCondition(&gateway.Status.Conditions, metav1.Condition{Type: typeAvailableGateway, Status: metav1.ConditionUnknown, Reason: "Reconciling", Message: "Starting reconciliation"}) - if err = r.Status().Update(ctx, gateway); err != nil { - log.Error(err, "Failed to update Gateway status") - return ctrl.Result{}, err - } - - // Let's re-fetch the gateway Custom Resource after updating the status - // so that we have the latest state of the resource on the cluster and we will avoid - // raising the error "the object has been modified, please apply - // your changes to the latest version and try again" which would re-trigger the reconciliation - // if we try to update it again in the following operations - if err := r.Get(ctx, req.NamespacedName, gateway); err != nil { - log.Error(err, "Failed to re-fetch gateway") - return ctrl.Result{}, err - } - } + account, api, err := InitCloudflareApi(ctx, r.Client, gatewayClass.Name) // Let's add a finalizer. Then, we can define some operations which should // occur before the custom resource is deleted. @@ -151,23 +132,16 @@ func (r *GatewayReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct if !controllerutil.ContainsFinalizer(gateway, gatewayFinalizer) { log.Info("Adding Finalizer for Gateway") if ok := controllerutil.AddFinalizer(gateway, gatewayFinalizer); !ok { - err = fmt.Errorf("finalizer for Gateway was not added") - log.Error(err, "Failed to add finalizer for Gateway") - return ctrl.Result{}, err + log.Error(nil, "Failed to add finalizer into the Gateway") + return ctrl.Result{Requeue: true}, nil } - if err = r.Update(ctx, gateway); err != nil { - log.Error(err, "Failed to update custom resource to add finalizer") + if err := r.Update(ctx, gateway); err != nil { + log.Error(err, "Failed to update Gateway to add finalizer") return ctrl.Result{}, err } } - account, api, err := InitCloudflareApi(ctx, r.Client, gatewayClass.Name) - if err != nil { - log.Error(err, "Failed to load Cloudflare API") - return ctrl.Result{}, err - } - // Check if the Gateway instance is marked to be deleted, which is // indicated by the deletion timestamp being set. isGatewayMarkedToBeDeleted := gateway.GetDeletionTimestamp() != nil @@ -176,18 +150,18 @@ func (r *GatewayReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct log.Info("Performing Finalizer Operations for Gateway before delete CR") // Let's add here a status "Downgrade" to reflect that this resource began its process to be terminated. - meta.SetStatusCondition(&gateway.Status.Conditions, metav1.Condition{Type: typeDegradedGateway, + meta.SetStatusCondition(&gateway.Status.Conditions, metav1.Condition{Type: string(gatewayv1.GatewayConditionAccepted), Status: metav1.ConditionUnknown, Reason: string(gatewayv1.GatewayReasonPending), ObservedGeneration: gateway.Generation, - Message: fmt.Sprintf("Performing finalizer operations for the custom resource: %s ", gateway.Name)}) + Message: fmt.Sprintf("Performing finalizer operations for the Gateway: %s ", gateway.Name)}) if err := r.Status().Update(ctx, gateway); err != nil { - log.Error(err, "Failed to update Gateway status") + log.Error(err, "Failed to update Gateway finalizer status") return ctrl.Result{}, err } // Perform all operations required before removing the finalizer and allow // the Kubernetes API to remove the custom resource. - if err := r.doFinalizerOperationsForGateway(gateway, ctx, gatewayClass, account, api); err != nil { + if err := r.doFinalizerOperationsForGateway(ctx, gatewayClass, gateway, account, api); err != nil { log.Error(err, "Failed to complete finalizer operations for Gateway") return ctrl.Result{Requeue: true}, nil } @@ -201,20 +175,19 @@ func (r *GatewayReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct return ctrl.Result{}, err } - meta.SetStatusCondition(&gateway.Status.Conditions, metav1.Condition{Type: typeDegradedGateway, + meta.SetStatusCondition(&gateway.Status.Conditions, metav1.Condition{Type: string(gatewayv1.GatewayConditionAccepted), Status: metav1.ConditionTrue, Reason: "Finalizing", ObservedGeneration: gateway.Generation, Message: fmt.Sprintf("Finalizer operations for custom resource %s name were successfully accomplished", gateway.Name)}) if err := r.Status().Update(ctx, gateway); err != nil { - log.Error(err, "Failed to update Gateway status") + log.Error(err, "Failed to update Gateway finalizer status") return ctrl.Result{}, err } log.Info("Removing Finalizer for Gateway after successfully perform the operations") if ok := controllerutil.RemoveFinalizer(gateway, gatewayFinalizer); !ok { - err = fmt.Errorf("finalizer for Gateway was not removed") log.Error(err, "Failed to remove finalizer for Gateway") - return ctrl.Result{}, err + return ctrl.Result{Requeue: true}, nil } if err := r.Update(ctx, gateway); err != nil { @@ -333,14 +306,13 @@ func (r *GatewayReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct meta.SetStatusCondition(&gateway.Status.Conditions, metav1.Condition{Type: string(gatewayv1.GatewayConditionAccepted), Status: metav1.ConditionFalse, Reason: string(gatewayv1.GatewayReasonListenersNotValid), ObservedGeneration: gateway.Generation, Message: fmt.Sprintf("No valid listeners for gateway (%s)", gateway.Name)}) - log.Info("No valid listeners for gateway", "gateway", gateway.Name) if err := r.Status().Update(ctx, gateway); err != nil { if strings.Contains(err.Error(), "apply your changes to the latest version and try again") { - log.Info("Conflict when updating Gateway status, retrying", "error", err.Error()) + log.Info("Conflict when updating Gateway listener status, retrying") return ctrl.Result{Requeue: true}, nil } else { - log.Error(err, "Failed to update Gateway status") + log.Error(err, "Failed to update Gateway listener status") return ctrl.Result{}, err } } @@ -353,10 +325,10 @@ func (r *GatewayReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct if err := r.Status().Update(ctx, gateway); err != nil { if strings.Contains(err.Error(), "apply your changes to the latest version and try again") { - log.Info("Conflict when updating Gateway status, retrying", "error", err.Error()) + log.Info("Conflict when updating Gateway listener status, retrying") return ctrl.Result{Requeue: true}, nil } else { - log.Error(err, "Failed to update Gateway status") + log.Error(err, "Failed to update Gateway listener status") return ctrl.Result{}, err } } @@ -368,7 +340,7 @@ func (r *GatewayReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct }) if err != nil { if strings.Contains(err.Error(), "429 Too Many Requests") { - log.Info("Rate limited by Cloudflare, requeueing after 10 minutes") + log.Error(err, "Rate limited, requeueing after 10 minutes") return ctrl.Result{ RequeueAfter: time.Minute * 10, // https://developers.cloudflare.com/fundamentals/api/reference/limits/ }, nil @@ -439,11 +411,6 @@ func (r *GatewayReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct return ctrl.Result{}, err } - if err := r.Get(ctx, req.NamespacedName, gateway); err != nil { - log.Error(err, "Failed to re-fetch gateway") - return ctrl.Result{}, err - } - // The following implementation will update the status meta.SetStatusCondition(&gateway.Status.Conditions, metav1.Condition{Type: string(gatewayv1.GatewayConditionProgrammed), Status: metav1.ConditionTrue, Reason: string(gatewayv1.GatewayReasonProgrammed), ObservedGeneration: gateway.Generation, @@ -451,7 +418,7 @@ func (r *GatewayReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct if err := r.Status().Update(ctx, gateway); err != nil { if strings.Contains(err.Error(), "apply your changes to the latest version and try again") { - log.Info("Conflict when updating Gateway status, retrying", "error", err.Error()) + log.Info("Conflict when updating Gateway listener status, retrying") return ctrl.Result{Requeue: true}, nil } else { log.Error(err, "Failed to update Gateway status") @@ -479,26 +446,27 @@ func (r *GatewayReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct // Check if the deployment already exists, if not create a new one found := &appsv1.Deployment{} - // Define a new deployment - dep, err := r.deploymentForGateway(gateway, token) - if err != nil { - log.Error(err, "Failed to define new Deployment resource for Gateway") + err = r.Get(ctx, types.NamespacedName{Name: gateway.Name, Namespace: gateway.Namespace}, found) + // TODO update existing deployment eg image changes + if err != nil && apierrors.IsNotFound(err) { + // Define a new deployment + dep, err := r.deploymentForGateway(gateway, token) + if err != nil { + log.Error(err, "Failed to define new Deployment resource for Gateway") - // The following implementation will update the status - meta.SetStatusCondition(&gateway.Status.Conditions, metav1.Condition{Type: typeAvailableGateway, - Status: metav1.ConditionFalse, Reason: "Reconciling", ObservedGeneration: gateway.Generation, - Message: fmt.Sprintf("Failed to create Deployment for the custom resource (%s): (%s)", gateway.Name, err)}) + // The following implementation will update the status + meta.SetStatusCondition(&gateway.Status.Conditions, metav1.Condition{Type: string(gatewayv1.GatewayConditionAccepted), + Status: metav1.ConditionFalse, Reason: "Reconciling", ObservedGeneration: gateway.Generation, + Message: fmt.Sprintf("Failed to create Deployment for the custom resource (%s): (%s)", gateway.Name, err)}) + + if err := r.Status().Update(ctx, gateway); err != nil { + log.Error(err, "Failed to update Gateway status") + return ctrl.Result{}, err + } - if err := r.Status().Update(ctx, gateway); err != nil { - log.Error(err, "Failed to update Gateway status") return ctrl.Result{}, err } - return ctrl.Result{}, err - } - err = r.Get(ctx, types.NamespacedName{Name: gateway.Name, Namespace: gateway.Namespace}, found) - if err != nil && apierrors.IsNotFound(err) { - log.Info("Creating a new Deployment", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name) if err = r.Create(ctx, dep); err != nil { @@ -513,26 +481,18 @@ func (r *GatewayReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct return ctrl.Result{RequeueAfter: time.Minute}, nil } else if err != nil { log.Error(err, "Failed to get Deployment") - // Let's return the error for the reconciliation be re-triggered again + // Let's return the error for the reconciliation be re-trigged again return ctrl.Result{}, err } else { - if err = r.Update(ctx, dep); err != nil { - log.Error(err, "Failed to update Deployment", - "Deployment.Namespace", found.Namespace, "Deployment.Name", found.Name) - - // Re-fetch the gateway Custom Resource before updating the status - // so that we have the latest state of the resource on the cluster and we will avoid - // raising the error "the object has been modified, please apply - // your changes to the latest version and try again" which would re-trigger the reconciliation - if err := r.Get(ctx, req.NamespacedName, gateway); err != nil { - log.Error(err, "Failed to re-fetch gateway") - return ctrl.Result{}, err - } + // Define a new deployment + dep, err := r.deploymentForGateway(gateway, token) + if err != nil { + log.Error(err, "Failed to define new Deployment resource for Gateway") // The following implementation will update the status - meta.SetStatusCondition(&gateway.Status.Conditions, metav1.Condition{Type: typeAvailableGateway, + meta.SetStatusCondition(&gateway.Status.Conditions, metav1.Condition{Type: string(gatewayv1.GatewayConditionAccepted), Status: metav1.ConditionFalse, Reason: "Reconciling", ObservedGeneration: gateway.Generation, - Message: fmt.Sprintf("Failed to update the deployment for the custom resource (%s): (%s)", gateway.Name, err)}) + Message: fmt.Sprintf("Failed to update Deployment for the custom resource (%s): (%s)", gateway.Name, err)}) if err := r.Status().Update(ctx, gateway); err != nil { log.Error(err, "Failed to update Gateway status") @@ -541,6 +501,16 @@ func (r *GatewayReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct return ctrl.Result{}, err } + + if err := r.Update(ctx, dep); err != nil { + if strings.Contains(err.Error(), "apply your changes to the latest version and try again") { + log.Info("Conflict when updating Deployment, retrying") + return ctrl.Result{Requeue: true}, nil + } else { + log.Error(err, "Failed to update Deployment") + return ctrl.Result{}, err + } + } } if err := r.Get(ctx, req.NamespacedName, gateway); err != nil { @@ -549,13 +519,13 @@ func (r *GatewayReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct } // The following implementation will update the status - meta.SetStatusCondition(&gateway.Status.Conditions, metav1.Condition{Type: typeAvailableGateway, + meta.SetStatusCondition(&gateway.Status.Conditions, metav1.Condition{Type: string(gatewayv1.GatewayConditionProgrammed), Status: metav1.ConditionTrue, Reason: string(gatewayv1.GatewayReasonProgrammed), ObservedGeneration: gateway.Generation, - Message: fmt.Sprintf("Deployment for custom resource (%s) reconciled successfully", gateway.Name)}) + Message: fmt.Sprintf("Tunnel and deployment for gateway (%s) reconciled successfully", gateway.Name)}) if err := r.Status().Update(ctx, gateway); err != nil { if strings.Contains(err.Error(), "apply your changes to the latest version and try again") { - log.Info("Conflict when updating Gateway status, retrying", "error", err.Error()) + log.Info("Conflict when updating Gateway listener status, retrying") return ctrl.Result{Requeue: true}, nil } else { log.Error(err, "Failed to update Gateway status") @@ -567,7 +537,7 @@ func (r *GatewayReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct } // finalizeGateway will perform the required operations before delete the CR. -func (r *GatewayReconciler) doFinalizerOperationsForGateway(cr *gatewayv1.Gateway, ctx context.Context, gatewayClass *gatewayv1.GatewayClass, account string, api *cloudflare.Client) error { +func (r *GatewayReconciler) doFinalizerOperationsForGateway(ctx context.Context, gatewayClass *gatewayv1.GatewayClass, gateway *gatewayv1.Gateway, account string, api *cloudflare.Client) error { // Note: It is not recommended to use finalizers with the purpose of deleting resources which are // created and managed in the reconciliation. These ones, such as the Deployment created on this reconcile, // are defined as dependent of the custom resource. See that we use the method ctrl.SetControllerReference. @@ -579,7 +549,7 @@ func (r *GatewayReconciler) doFinalizerOperationsForGateway(cr *gatewayv1.Gatewa tunnel, err := api.ZeroTrust.Tunnels.List(ctx, zero_trust.TunnelListParams{ AccountID: cloudflare.String(account), IsDeleted: cloudflare.Bool(false), - Name: cloudflare.String(cr.Name), + Name: cloudflare.String(gateway.Name), }) if err != nil { log.Error(err, "Failed to get tunnel from Cloudflare API") @@ -615,7 +585,7 @@ func (r *GatewayReconciler) doFinalizerOperationsForGateway(cr *gatewayv1.Gatewa } // if GatewayClass has no other Gateways, remove its finalizer - gateways := &gatewayv1.GatewayList{Items: []gatewayv1.Gateway{{Spec: gatewayv1.GatewaySpec{GatewayClassName: cr.Spec.GatewayClassName}}}} + gateways := &gatewayv1.GatewayList{Items: []gatewayv1.Gateway{{Spec: gatewayv1.GatewaySpec{GatewayClassName: gateway.Spec.GatewayClassName}}}} if err := r.List(ctx, gateways); err != nil { log.Error(err, "Failed to list Gateways") return err @@ -628,10 +598,10 @@ func (r *GatewayReconciler) doFinalizerOperationsForGateway(cr *gatewayv1.Gatewa } // The following implementation will raise an event - r.Recorder.Event(cr, "Warning", "Deleting", - fmt.Sprintf("Custom Resource %s is being deleted from the namespace %s", - cr.Name, - cr.Namespace)) + r.Recorder.Event(gateway, "Warning", "Deleting", + fmt.Sprintf("Gateway %s is being deleted from the namespace %s", + gateway.Name, + gateway.Namespace)) return nil } @@ -686,7 +656,7 @@ func (r *GatewayReconciler) deploymentForGateway( }, }, SecurityContext: &corev1.PodSecurityContext{ - RunAsNonRoot: ptr.To(true), + RunAsNonRoot: &[]bool{true}[0], // IMPORTANT: seccomProfile was introduced with Kubernetes 1.19 // If you are looking for to produce solutions to be supported // on lower versions you must remove this option. @@ -707,9 +677,9 @@ func (r *GatewayReconciler) deploymentForGateway( // Ensure restrictive context for the container // More info: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted SecurityContext: &corev1.SecurityContext{ - RunAsNonRoot: ptr.To(true), - RunAsUser: ptr.To(int64(1001)), - AllowPrivilegeEscalation: ptr.To(false), + RunAsNonRoot: &[]bool{true}[0], + RunAsUser: &[]int64{1001}[0], + AllowPrivilegeEscalation: &[]bool{false}[0], Capabilities: &corev1.Capabilities{ Drop: []corev1.Capability{ "ALL", @@ -739,10 +709,17 @@ func (r *GatewayReconciler) deploymentForGateway( // labelsForGateway returns the labels for selecting the resources // More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/ func labelsForGateway(name string) map[string]string { + // skip imageTag, to allow version updates to existing deployments + // var imageTag string + // image, err := imageForGateway() + // if err == nil { + // imageTag = strings.Split(image, ":")[1] + // } return map[string]string{ - "app.kubernetes.io/name": "cloudflare-kubernetes-gateway", - "cfargotunnel.com/name": name, + "app.kubernetes.io/name": "cloudflare-kubernetes-gateway", + // "app.kubernetes.io/version": imageTag, "app.kubernetes.io/managed-by": "GatewayController", + "cfargotunnel.com/name": name, } } @@ -752,30 +729,19 @@ func imageForGateway() (string, error) { var imageEnvVar = "GATEWAY_IMAGE" image, found := os.LookupEnv(imageEnvVar) if !found { - return "", fmt.Errorf("Unable to find %s environment variable with the image", imageEnvVar) + return "", fmt.Errorf("unable to find %s environment variable with the image", imageEnvVar) } return image, nil } // SetupWithManager sets up the controller with the Manager. -// The whole idea is to be watching the resources that matter for the controller. -// When a resource that the controller is interested in changes, the Watch triggers -// the controller’s reconciliation loop, ensuring that the actual state of the resource -// matches the desired state as defined in the controller’s logic. -// -// Notice how we configured the Manager to monitor events such as the creation, update, -// or deletion of a Custom Resource (CR) of the Gateway kind, as well as any changes -// to the Deployment that the controller manages and owns. +// Note that the Deployment will be also watched in order to ensure its +// desirable state on the cluster func (r *GatewayReconciler) SetupWithManager(mgr ctrl.Manager) error { + pred := predicate.GenerationChangedPredicate{} return ctrl.NewControllerManagedBy(mgr). - // Watch the Gateway CR(s) and trigger reconciliation whenever it - // is created, updated, or deleted For(&gatewayv1.Gateway{}). - Named("cloudflare-gateway"). - // Watch the Deployment managed by the GatewayReconciler. If any changes occur to the Deployment - // owned and managed by this controller, it will trigger reconciliation, ensuring that the cluster - // state aligns with the desired state. See that the ownerRef was set when the Deployment was created. Owns(&appsv1.Deployment{}). - WithEventFilter(predicate.GenerationChangedPredicate{}). + WithEventFilter(pred). Complete(r) }