From ab0cbbc2dabdd0e3378a8eff4dcfbb37e5271d79 Mon Sep 17 00:00:00 2001 From: Adam Rummer Date: Wed, 7 May 2025 23:18:29 +0100 Subject: [PATCH 01/10] initial commit --- .../controller/clustertunnel_controller.go | 20 ++++- internal/controller/tunnel_controller.go | 20 ++++- internal/k8s/secret.go | 83 +++++++++++++++++++ 3 files changed, 117 insertions(+), 6 deletions(-) create mode 100644 internal/k8s/secret.go diff --git a/internal/controller/clustertunnel_controller.go b/internal/controller/clustertunnel_controller.go index 053a1cd0..16391195 100644 --- a/internal/controller/clustertunnel_controller.go +++ b/internal/controller/clustertunnel_controller.go @@ -18,6 +18,7 @@ package controller import ( "context" + "github.com/adyanth/cloudflare-operator/internal/k8s" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" @@ -143,9 +144,14 @@ func (r *ClusterTunnelReconciler) Reconcile(ctx context.Context, req ctrl.Reques if err := r.Get(ctx, req.NamespacedName, tunnel); err != nil { if apierrors.IsNotFound(err) { // Tunnel object not found, could have been deleted after reconcile request. - // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers. - // Return and don't requeue - r.log.Info("Tunnel deleted, nothing to do") + // Owned objects are automatically garbage collected. + secretClient, err := k8s.NewSecretClient(r.Client, &r.log) + if err != nil { + return ctrl.Result{}, err + } + if err := secretClient.RemoveFinalizer(ctx, r.GetTunnel().GetSpec().Cloudflare.Secret, r.GetTunnel().GetNamespace(), tunnelFinalizer); err != nil { + return ctrl.Result{}, err + } return ctrl.Result{}, nil } r.log.Error(err, "unable to fetch Tunnel") @@ -156,6 +162,14 @@ func (r *ClusterTunnelReconciler) Reconcile(ctx context.Context, req ctrl.Reques return ctrl.Result{}, err } + secretClient, err := k8s.NewSecretClient(r.Client, &r.log) + if err != nil { + return ctrl.Result{}, err + } + if err := secretClient.EnsureFinalizer(ctx, r.GetTunnel().GetSpec().Cloudflare.Secret, r.GetTunnel().GetNamespace(), tunnelFinalizer); err != nil { + return ctrl.Result{}, err + } + if res, ok, err := setupTunnel(r); !ok { return res, err } diff --git a/internal/controller/tunnel_controller.go b/internal/controller/tunnel_controller.go index 5d98b475..324f5463 100644 --- a/internal/controller/tunnel_controller.go +++ b/internal/controller/tunnel_controller.go @@ -18,6 +18,7 @@ package controller import ( "context" + "github.com/adyanth/cloudflare-operator/internal/k8s" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -138,9 +139,14 @@ func (r *TunnelReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr if err := r.Get(ctx, req.NamespacedName, tunnel); err != nil { if apierrors.IsNotFound(err) { // Tunnel object not found, could have been deleted after reconcile request. - // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers. - // Return and don't requeue - r.log.Info("Tunnel deleted, nothing to do") + // Owned objects are automatically garbage collected. + secretClient, err := k8s.NewSecretClient(r.Client, &r.log) + if err != nil { + return ctrl.Result{}, err + } + if err := secretClient.RemoveFinalizer(ctx, r.GetTunnel().GetSpec().Cloudflare.Secret, r.GetTunnel().GetNamespace(), tunnelFinalizer); err != nil { + return ctrl.Result{}, err + } return ctrl.Result{}, nil } r.log.Error(err, "unable to fetch Tunnel") @@ -151,6 +157,14 @@ func (r *TunnelReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr return ctrl.Result{}, err } + secretClient, err := k8s.NewSecretClient(r.Client, &r.log) + if err != nil { + return ctrl.Result{}, err + } + if err := secretClient.EnsureFinalizer(ctx, r.GetTunnel().GetSpec().Cloudflare.Secret, r.GetTunnel().GetNamespace(), tunnelFinalizer); err != nil { + return ctrl.Result{}, err + } + if res, ok, err := setupTunnel(r); !ok { return res, err } diff --git a/internal/k8s/secret.go b/internal/k8s/secret.go new file mode 100644 index 00000000..fc5ec711 --- /dev/null +++ b/internal/k8s/secret.go @@ -0,0 +1,83 @@ +package k8s + +import ( + "context" + "fmt" + "github.com/go-logr/logr" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type SecretClient struct { + client client.Client + log *logr.Logger +} + +func NewSecretClient(client client.Client, log *logr.Logger) (*SecretClient, error) { + if client == nil { + return nil, fmt.Errorf("client cannot be nil") + } + if log == nil { + return nil, fmt.Errorf("logger cannot be nil") + } + return &SecretClient{ + client: client, + log: log, + }, nil +} + +// EnsureFinalizer idempotently adds the specified finalizer to the specified secret +func (s *SecretClient) EnsureFinalizer(ctx context.Context, secretName, secretNamespace, finalizer string) error { + var secret corev1.Secret + secretKey := client.ObjectKey{ + Name: secretName, + Namespace: secretNamespace, + } + if err := s.client.Get(ctx, secretKey, &secret); err != nil { + return err + } + + // if finalizer already exists, we are happy + for _, existingFinalizer := range secret.ObjectMeta.Finalizers { + if finalizer == existingFinalizer { + return nil + } + } + + s.log.WithValues("finalizer", finalizer).Info("creating finalizer") + secret.Finalizers = append(secret.Finalizers, finalizer) + return s.client.Update(ctx, &secret) +} + +// RemoveFinalizer removes the first instance of the finalizer from the given secret +func (s *SecretClient) RemoveFinalizer(ctx context.Context, secretName, secretNamespace, finalizer string) error { + var secret corev1.Secret + secretKey := client.ObjectKey{ + Name: secretName, + Namespace: secretNamespace, + } + if err := s.client.Get(ctx, secretKey, &secret); err != nil { + if errors.IsNotFound(err) { + return nil + } + return err + } + + s.log.WithValues("finalizer", finalizer).Info("deleting finalizer") + secret.Finalizers = removeString(secret.Finalizers, finalizer) + return s.client.Update(ctx, &secret) +} + +// removeString returns a copy of list with the first (only) occurrence +// of target removed. If target is not present, the original slice is +// returned unchanged. +func removeString(list []string, target string) []string { + for i, v := range list { + if v == target { + // splice out the element at index i + return append(list[:i], list[i+1:]...) + } + } + return list +} From d883dfaf94b9e50744cf349d3f5f21e0a5a58520 Mon Sep 17 00:00:00 2001 From: Adam Rummer Date: Wed, 7 May 2025 23:26:34 +0100 Subject: [PATCH 02/10] remove docs/deletion.md --- docs/deletion.md | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 docs/deletion.md diff --git a/docs/deletion.md b/docs/deletion.md deleted file mode 100644 index 8df0b8ce..00000000 --- a/docs/deletion.md +++ /dev/null @@ -1,6 +0,0 @@ -## Deleting a tunnel - -Remember the delete order while deleting the tunnel. -If you delete the secrets before deleting the tunnel, the operator won't be able to clean up the tunnel from Cloudflare, since it no longer has the credentials for the same. - -The correct order is to delete the tunnel, wait for it to actually get deleted (after the finalizer is removed) and then delete the secret. From 5e48ba2ea4ada3284c94f6a09c1d6cec11cd2e82 Mon Sep 17 00:00:00 2001 From: Adam Rummer Date: Wed, 7 May 2025 23:48:18 +0100 Subject: [PATCH 03/10] appease the linter --- .../controller/clustertunnel_controller.go | 7 ++-- internal/controller/tunnel_controller.go | 7 ++-- internal/k8s/secret.go | 33 ++++++++++--------- 3 files changed, 27 insertions(+), 20 deletions(-) diff --git a/internal/controller/clustertunnel_controller.go b/internal/controller/clustertunnel_controller.go index 16391195..46a08955 100644 --- a/internal/controller/clustertunnel_controller.go +++ b/internal/controller/clustertunnel_controller.go @@ -18,6 +18,7 @@ package controller import ( "context" + "github.com/adyanth/cloudflare-operator/internal/k8s" appsv1 "k8s.io/api/apps/v1" @@ -149,7 +150,8 @@ func (r *ClusterTunnelReconciler) Reconcile(ctx context.Context, req ctrl.Reques if err != nil { return ctrl.Result{}, err } - if err := secretClient.RemoveFinalizer(ctx, r.GetTunnel().GetSpec().Cloudflare.Secret, r.GetTunnel().GetNamespace(), tunnelFinalizer); err != nil { + err = secretClient.RemoveFinalizer(ctx, r.GetTunnel().GetSpec().Cloudflare.Secret, r.GetTunnel().GetNamespace(), tunnelFinalizer) + if err != nil { return ctrl.Result{}, err } return ctrl.Result{}, nil @@ -166,7 +168,8 @@ func (r *ClusterTunnelReconciler) Reconcile(ctx context.Context, req ctrl.Reques if err != nil { return ctrl.Result{}, err } - if err := secretClient.EnsureFinalizer(ctx, r.GetTunnel().GetSpec().Cloudflare.Secret, r.GetTunnel().GetNamespace(), tunnelFinalizer); err != nil { + err = secretClient.EnsureFinalizer(ctx, r.GetTunnel().GetSpec().Cloudflare.Secret, r.GetTunnel().GetNamespace(), tunnelFinalizer) + if err != nil { return ctrl.Result{}, err } diff --git a/internal/controller/tunnel_controller.go b/internal/controller/tunnel_controller.go index 324f5463..b7bdd77c 100644 --- a/internal/controller/tunnel_controller.go +++ b/internal/controller/tunnel_controller.go @@ -18,6 +18,7 @@ package controller import ( "context" + "github.com/adyanth/cloudflare-operator/internal/k8s" corev1 "k8s.io/api/core/v1" @@ -144,7 +145,8 @@ func (r *TunnelReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr if err != nil { return ctrl.Result{}, err } - if err := secretClient.RemoveFinalizer(ctx, r.GetTunnel().GetSpec().Cloudflare.Secret, r.GetTunnel().GetNamespace(), tunnelFinalizer); err != nil { + err = secretClient.RemoveFinalizer(ctx, r.GetTunnel().GetSpec().Cloudflare.Secret, r.GetTunnel().GetNamespace(), tunnelFinalizer) + if err != nil { return ctrl.Result{}, err } return ctrl.Result{}, nil @@ -161,7 +163,8 @@ func (r *TunnelReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr if err != nil { return ctrl.Result{}, err } - if err := secretClient.EnsureFinalizer(ctx, r.GetTunnel().GetSpec().Cloudflare.Secret, r.GetTunnel().GetNamespace(), tunnelFinalizer); err != nil { + err = secretClient.EnsureFinalizer(ctx, r.GetTunnel().GetSpec().Cloudflare.Secret, r.GetTunnel().GetNamespace(), tunnelFinalizer) + if err != nil { return ctrl.Result{}, err } diff --git a/internal/k8s/secret.go b/internal/k8s/secret.go index fc5ec711..e3ddd76f 100644 --- a/internal/k8s/secret.go +++ b/internal/k8s/secret.go @@ -2,28 +2,29 @@ package k8s import ( "context" - "fmt" + "errors" + "github.com/go-logr/logr" corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/errors" + k8serrors "k8s.io/apimachinery/pkg/api/errors" "sigs.k8s.io/controller-runtime/pkg/client" ) type SecretClient struct { - client client.Client - log *logr.Logger + k8sClient client.Client + log *logr.Logger } -func NewSecretClient(client client.Client, log *logr.Logger) (*SecretClient, error) { - if client == nil { - return nil, fmt.Errorf("client cannot be nil") +func NewSecretClient(k8sClient client.Client, log *logr.Logger) (*SecretClient, error) { + if k8sClient == nil { + return nil, errors.New("k8sClient cannot be nil") } if log == nil { - return nil, fmt.Errorf("logger cannot be nil") + return nil, errors.New("logger cannot be nil") } return &SecretClient{ - client: client, - log: log, + k8sClient: k8sClient, + log: log, }, nil } @@ -34,12 +35,12 @@ func (s *SecretClient) EnsureFinalizer(ctx context.Context, secretName, secretNa Name: secretName, Namespace: secretNamespace, } - if err := s.client.Get(ctx, secretKey, &secret); err != nil { + if err := s.k8sClient.Get(ctx, secretKey, &secret); err != nil { return err } // if finalizer already exists, we are happy - for _, existingFinalizer := range secret.ObjectMeta.Finalizers { + for _, existingFinalizer := range secret.Finalizers { if finalizer == existingFinalizer { return nil } @@ -47,7 +48,7 @@ func (s *SecretClient) EnsureFinalizer(ctx context.Context, secretName, secretNa s.log.WithValues("finalizer", finalizer).Info("creating finalizer") secret.Finalizers = append(secret.Finalizers, finalizer) - return s.client.Update(ctx, &secret) + return s.k8sClient.Update(ctx, &secret) } // RemoveFinalizer removes the first instance of the finalizer from the given secret @@ -57,8 +58,8 @@ func (s *SecretClient) RemoveFinalizer(ctx context.Context, secretName, secretNa Name: secretName, Namespace: secretNamespace, } - if err := s.client.Get(ctx, secretKey, &secret); err != nil { - if errors.IsNotFound(err) { + if err := s.k8sClient.Get(ctx, secretKey, &secret); err != nil { + if k8serrors.IsNotFound(err) { return nil } return err @@ -66,7 +67,7 @@ func (s *SecretClient) RemoveFinalizer(ctx context.Context, secretName, secretNa s.log.WithValues("finalizer", finalizer).Info("deleting finalizer") secret.Finalizers = removeString(secret.Finalizers, finalizer) - return s.client.Update(ctx, &secret) + return s.k8sClient.Update(ctx, &secret) } // removeString returns a copy of list with the first (only) occurrence From d0894946bf53caa439a2141c213d9eb60fc7b4d4 Mon Sep 17 00:00:00 2001 From: Adam Rummer Date: Thu, 8 May 2025 15:16:00 +0100 Subject: [PATCH 04/10] convert secretClient to generic objectClient --- .../controller/clustertunnel_controller.go | 27 ++++-- internal/controller/tunnel_controller.go | 24 ++++-- internal/k8s/object.go | 82 ++++++++++++++++++ internal/k8s/secret.go | 84 ------------------- 4 files changed, 121 insertions(+), 96 deletions(-) create mode 100644 internal/k8s/object.go delete mode 100644 internal/k8s/secret.go diff --git a/internal/controller/clustertunnel_controller.go b/internal/controller/clustertunnel_controller.go index 46a08955..757a155c 100644 --- a/internal/controller/clustertunnel_controller.go +++ b/internal/controller/clustertunnel_controller.go @@ -146,14 +146,19 @@ func (r *ClusterTunnelReconciler) Reconcile(ctx context.Context, req ctrl.Reques if apierrors.IsNotFound(err) { // Tunnel object not found, could have been deleted after reconcile request. // Owned objects are automatically garbage collected. - secretClient, err := k8s.NewSecretClient(r.Client, &r.log) - if err != nil { - return ctrl.Result{}, err - } - err = secretClient.RemoveFinalizer(ctx, r.GetTunnel().GetSpec().Cloudflare.Secret, r.GetTunnel().GetNamespace(), tunnelFinalizer) + objectClient, err := k8s.NewObjectClient(r.Client, &r.log) if err != nil { return ctrl.Result{}, err } + // ensure the secret associated with the tunnel has the finalizer removed + err = objectClient.RemoveFinalizer( + ctx, + client.ObjectKey{ + Namespace: r.GetTunnel().GetNamespace(), + Name: r.GetTunnel().GetSpec().Cloudflare.Secret, + }, + tunnelFinalizer, + ) return ctrl.Result{}, nil } r.log.Error(err, "unable to fetch Tunnel") @@ -164,11 +169,19 @@ func (r *ClusterTunnelReconciler) Reconcile(ctx context.Context, req ctrl.Reques return ctrl.Result{}, err } - secretClient, err := k8s.NewSecretClient(r.Client, &r.log) + objectClient, err := k8s.NewObjectClient(r.Client, &r.log) if err != nil { return ctrl.Result{}, err } - err = secretClient.EnsureFinalizer(ctx, r.GetTunnel().GetSpec().Cloudflare.Secret, r.GetTunnel().GetNamespace(), tunnelFinalizer) + // ensure the secret associated with the tunnel has a finalizer + err = objectClient.EnsureFinalizer( + ctx, + client.ObjectKey{ + Namespace: r.GetTunnel().GetNamespace(), + Name: r.GetTunnel().GetSpec().Cloudflare.Secret, + }, + tunnelFinalizer, + ) if err != nil { return ctrl.Result{}, err } diff --git a/internal/controller/tunnel_controller.go b/internal/controller/tunnel_controller.go index b7bdd77c..6e0e5628 100644 --- a/internal/controller/tunnel_controller.go +++ b/internal/controller/tunnel_controller.go @@ -18,7 +18,6 @@ package controller import ( "context" - "github.com/adyanth/cloudflare-operator/internal/k8s" corev1 "k8s.io/api/core/v1" @@ -141,11 +140,18 @@ func (r *TunnelReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr if apierrors.IsNotFound(err) { // Tunnel object not found, could have been deleted after reconcile request. // Owned objects are automatically garbage collected. - secretClient, err := k8s.NewSecretClient(r.Client, &r.log) + objectClient, err := k8s.NewObjectClient(r.Client, &r.log) if err != nil { return ctrl.Result{}, err } - err = secretClient.RemoveFinalizer(ctx, r.GetTunnel().GetSpec().Cloudflare.Secret, r.GetTunnel().GetNamespace(), tunnelFinalizer) + err = objectClient.RemoveFinalizer( + ctx, + client.ObjectKey{ + Namespace: r.GetTunnel().GetNamespace(), + Name: r.GetTunnel().GetSpec().Cloudflare.Secret, + }, + tunnelFinalizer, + ) if err != nil { return ctrl.Result{}, err } @@ -159,11 +165,19 @@ func (r *TunnelReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr return ctrl.Result{}, err } - secretClient, err := k8s.NewSecretClient(r.Client, &r.log) + objectClient, err := k8s.NewObjectClient(r.Client, &r.log) if err != nil { return ctrl.Result{}, err } - err = secretClient.EnsureFinalizer(ctx, r.GetTunnel().GetSpec().Cloudflare.Secret, r.GetTunnel().GetNamespace(), tunnelFinalizer) + // ensure the secret associated with the tunnel has a finalizer + err = objectClient.EnsureFinalizer( + ctx, + client.ObjectKey{ + Namespace: r.GetTunnel().GetNamespace(), + Name: r.GetTunnel().GetSpec().Cloudflare.Secret, + }, + tunnelFinalizer, + ) if err != nil { return ctrl.Result{}, err } diff --git a/internal/k8s/object.go b/internal/k8s/object.go new file mode 100644 index 00000000..5fd287e2 --- /dev/null +++ b/internal/k8s/object.go @@ -0,0 +1,82 @@ +package k8s + +import ( + "context" + "errors" + "fmt" + "github.com/go-logr/logr" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +type ObjectClient struct { + k8sClient client.Client + log *logr.Logger +} + +func NewObjectClient(k8sClient client.Client, log *logr.Logger) (*ObjectClient, error) { + if k8sClient == nil { + return nil, errors.New("k8sClient cannot be nil") + } + if log == nil { + return nil, errors.New("log cannot be nil") + } + return &ObjectClient{ + k8sClient: k8sClient, + log: log, + }, nil +} + +// EnsureFinalizer adds `finalizer` to obj exactly once. +// - It NO-OPs if the finalizer is already there. +// - It uses a strategic-merge Patch so you don’t overwrite concurrent changes. +// - obj must be a pointer that already contains the latest copy from the API server. +func (s *ObjectClient) EnsureFinalizer(ctx context.Context, key client.ObjectKey, finalizer string) error { + var obj client.Object + err := s.k8sClient.Get(ctx, key, obj) + if err != nil { + return err + } + // if it already contains the finalizer, there's nothing to do + if controllerutil.ContainsFinalizer(obj, finalizer) { + return nil + } + + base := obj.DeepCopyObject().(client.Object) + controllerutil.AddFinalizer(obj, finalizer) + s.log. + WithValues("finalizer", finalizer). + WithValues("object", fmt.Sprintf("%s/%s", obj.GetNamespace(), obj.GetName())). + Info("creating finalizer") + if err := s.k8sClient.Patch(ctx, obj, client.MergeFrom(base)); err != nil { + return fmt.Errorf("could not add finalizer %q: %w", finalizer, err) + } + return nil +} + +// RemoveFinalizer deletes `finalizer` from obj exactly once. +// - NO-OPs if the finalizer is already gone. +// - Uses a strategic-merge Patch so you never clobber concurrent changes. +// - obj must be a *live* copy fetched from the API server. +func (s *ObjectClient) RemoveFinalizer(ctx context.Context, key client.ObjectKey, finalizer string) error { + var obj client.Object + err := s.k8sClient.Get(ctx, key, obj) + if err != nil { + return err + } + // if there is no finalizer, there's nothing to do + if !controllerutil.ContainsFinalizer(obj, finalizer) { + return nil + } + + base := obj.DeepCopyObject().(client.Object) + controllerutil.RemoveFinalizer(obj, finalizer) + s.log. + WithValues("finalizer", finalizer). + WithValues("object", fmt.Sprintf("%s/%s", obj.GetNamespace(), obj.GetName())). + Info("removing finalizer") + if err := s.k8sClient.Patch(ctx, obj, client.MergeFrom(base)); err != nil { + return fmt.Errorf("could not remove finalizer %q: %w", finalizer, err) + } + return nil +} diff --git a/internal/k8s/secret.go b/internal/k8s/secret.go deleted file mode 100644 index e3ddd76f..00000000 --- a/internal/k8s/secret.go +++ /dev/null @@ -1,84 +0,0 @@ -package k8s - -import ( - "context" - "errors" - - "github.com/go-logr/logr" - corev1 "k8s.io/api/core/v1" - k8serrors "k8s.io/apimachinery/pkg/api/errors" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -type SecretClient struct { - k8sClient client.Client - log *logr.Logger -} - -func NewSecretClient(k8sClient client.Client, log *logr.Logger) (*SecretClient, error) { - if k8sClient == nil { - return nil, errors.New("k8sClient cannot be nil") - } - if log == nil { - return nil, errors.New("logger cannot be nil") - } - return &SecretClient{ - k8sClient: k8sClient, - log: log, - }, nil -} - -// EnsureFinalizer idempotently adds the specified finalizer to the specified secret -func (s *SecretClient) EnsureFinalizer(ctx context.Context, secretName, secretNamespace, finalizer string) error { - var secret corev1.Secret - secretKey := client.ObjectKey{ - Name: secretName, - Namespace: secretNamespace, - } - if err := s.k8sClient.Get(ctx, secretKey, &secret); err != nil { - return err - } - - // if finalizer already exists, we are happy - for _, existingFinalizer := range secret.Finalizers { - if finalizer == existingFinalizer { - return nil - } - } - - s.log.WithValues("finalizer", finalizer).Info("creating finalizer") - secret.Finalizers = append(secret.Finalizers, finalizer) - return s.k8sClient.Update(ctx, &secret) -} - -// RemoveFinalizer removes the first instance of the finalizer from the given secret -func (s *SecretClient) RemoveFinalizer(ctx context.Context, secretName, secretNamespace, finalizer string) error { - var secret corev1.Secret - secretKey := client.ObjectKey{ - Name: secretName, - Namespace: secretNamespace, - } - if err := s.k8sClient.Get(ctx, secretKey, &secret); err != nil { - if k8serrors.IsNotFound(err) { - return nil - } - return err - } - - s.log.WithValues("finalizer", finalizer).Info("deleting finalizer") - secret.Finalizers = removeString(secret.Finalizers, finalizer) - return s.k8sClient.Update(ctx, &secret) -} - -// removeString returns a copy of list with the first (only) occurrence -// of target removed. If target is not present, the original slice is -// returned unchanged. -func removeString(list []string, target string) []string { - for i, v := range list { - if v == target { - // splice out the element at index i - return append(list[:i], list[i+1:]...) - } - } - return list -} From 0a478e6f77112659e2f36bca389344a71f0a8a93 Mon Sep 17 00:00:00 2001 From: Adam Rummer Date: Thu, 8 May 2025 15:34:24 +0100 Subject: [PATCH 05/10] appease the linter --- .../controller/clustertunnel_controller.go | 3 +++ internal/controller/tunnel_controller.go | 1 + internal/k8s/object.go | 27 +++++++++++++++++-- 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/internal/controller/clustertunnel_controller.go b/internal/controller/clustertunnel_controller.go index 757a155c..f3b09615 100644 --- a/internal/controller/clustertunnel_controller.go +++ b/internal/controller/clustertunnel_controller.go @@ -159,6 +159,9 @@ func (r *ClusterTunnelReconciler) Reconcile(ctx context.Context, req ctrl.Reques }, tunnelFinalizer, ) + if err != nil { + return ctrl.Result{}, err + } return ctrl.Result{}, nil } r.log.Error(err, "unable to fetch Tunnel") diff --git a/internal/controller/tunnel_controller.go b/internal/controller/tunnel_controller.go index 6e0e5628..a99dc8ba 100644 --- a/internal/controller/tunnel_controller.go +++ b/internal/controller/tunnel_controller.go @@ -18,6 +18,7 @@ package controller import ( "context" + "github.com/adyanth/cloudflare-operator/internal/k8s" corev1 "k8s.io/api/core/v1" diff --git a/internal/k8s/object.go b/internal/k8s/object.go index 5fd287e2..a1507e2f 100644 --- a/internal/k8s/object.go +++ b/internal/k8s/object.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "github.com/go-logr/logr" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" @@ -42,8 +43,13 @@ func (s *ObjectClient) EnsureFinalizer(ctx context.Context, key client.ObjectKey return nil } - base := obj.DeepCopyObject().(client.Object) controllerutil.AddFinalizer(obj, finalizer) + + base, err := s.deepCopyObject(obj) + if err != nil { + return err + } + s.log. WithValues("finalizer", finalizer). WithValues("object", fmt.Sprintf("%s/%s", obj.GetNamespace(), obj.GetName())). @@ -69,8 +75,13 @@ func (s *ObjectClient) RemoveFinalizer(ctx context.Context, key client.ObjectKey return nil } - base := obj.DeepCopyObject().(client.Object) controllerutil.RemoveFinalizer(obj, finalizer) + + base, err := s.deepCopyObject(obj) + if err != nil { + return err + } + s.log. WithValues("finalizer", finalizer). WithValues("object", fmt.Sprintf("%s/%s", obj.GetNamespace(), obj.GetName())). @@ -80,3 +91,15 @@ func (s *ObjectClient) RemoveFinalizer(ctx context.Context, key client.ObjectKey } return nil } + +func (*ObjectClient) deepCopyObject(obj client.Object) (client.Object, error) { + objDeepCopy := obj.DeepCopyObject() + if objDeepCopy == nil { + return nil, errors.New("received nil object from DeepCopyObject") + } + base, ok := objDeepCopy.(client.Object) + if !ok { + return nil, errors.New("failed to convert object to client.Object") + } + return base, nil +} From e450d010bc37b17aae272cdcf61375af1f37893a Mon Sep 17 00:00:00 2001 From: Adam Rummer Date: Thu, 8 May 2025 20:58:34 +0100 Subject: [PATCH 06/10] make objectClient accept concrete object type --- internal/clients/k8s/object.go | 6 ++---- internal/controller/clustertunnel_controller.go | 4 ++++ internal/controller/tunnel_controller.go | 4 ++++ 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/internal/clients/k8s/object.go b/internal/clients/k8s/object.go index a1507e2f..69548a51 100644 --- a/internal/clients/k8s/object.go +++ b/internal/clients/k8s/object.go @@ -32,8 +32,7 @@ func NewObjectClient(k8sClient client.Client, log *logr.Logger) (*ObjectClient, // - It NO-OPs if the finalizer is already there. // - It uses a strategic-merge Patch so you don’t overwrite concurrent changes. // - obj must be a pointer that already contains the latest copy from the API server. -func (s *ObjectClient) EnsureFinalizer(ctx context.Context, key client.ObjectKey, finalizer string) error { - var obj client.Object +func (s *ObjectClient) EnsureFinalizer(ctx context.Context, key client.ObjectKey, obj client.Object, finalizer string) error { err := s.k8sClient.Get(ctx, key, obj) if err != nil { return err @@ -64,8 +63,7 @@ func (s *ObjectClient) EnsureFinalizer(ctx context.Context, key client.ObjectKey // - NO-OPs if the finalizer is already gone. // - Uses a strategic-merge Patch so you never clobber concurrent changes. // - obj must be a *live* copy fetched from the API server. -func (s *ObjectClient) RemoveFinalizer(ctx context.Context, key client.ObjectKey, finalizer string) error { - var obj client.Object +func (s *ObjectClient) RemoveFinalizer(ctx context.Context, key client.ObjectKey, obj client.Object, finalizer string) error { err := s.k8sClient.Get(ctx, key, obj) if err != nil { return err diff --git a/internal/controller/clustertunnel_controller.go b/internal/controller/clustertunnel_controller.go index be80f0b2..998c9035 100644 --- a/internal/controller/clustertunnel_controller.go +++ b/internal/controller/clustertunnel_controller.go @@ -152,12 +152,14 @@ func (r *ClusterTunnelReconciler) Reconcile(ctx context.Context, req ctrl.Reques return ctrl.Result{}, err } // ensure the secret associated with the tunnel has the finalizer removed + var secret *corev1.Secret err = objectClient.RemoveFinalizer( ctx, client.ObjectKey{ Namespace: r.GetTunnel().GetNamespace(), Name: r.GetTunnel().GetSpec().Cloudflare.Secret, }, + secret, tunnelFinalizer, ) if err != nil { @@ -178,12 +180,14 @@ func (r *ClusterTunnelReconciler) Reconcile(ctx context.Context, req ctrl.Reques return ctrl.Result{}, err } // ensure the secret associated with the tunnel has a finalizer + var secret *corev1.Secret err = objectClient.EnsureFinalizer( ctx, client.ObjectKey{ Namespace: r.GetTunnel().GetNamespace(), Name: r.GetTunnel().GetSpec().Cloudflare.Secret, }, + secret, tunnelFinalizer, ) if err != nil { diff --git a/internal/controller/tunnel_controller.go b/internal/controller/tunnel_controller.go index 34e26963..0f471567 100644 --- a/internal/controller/tunnel_controller.go +++ b/internal/controller/tunnel_controller.go @@ -146,12 +146,14 @@ func (r *TunnelReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr if err != nil { return ctrl.Result{}, err } + var secret *corev1.Secret err = objectClient.RemoveFinalizer( ctx, client.ObjectKey{ Namespace: r.GetTunnel().GetNamespace(), Name: r.GetTunnel().GetSpec().Cloudflare.Secret, }, + secret, tunnelFinalizer, ) if err != nil { @@ -172,12 +174,14 @@ func (r *TunnelReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr return ctrl.Result{}, err } // ensure the secret associated with the tunnel has a finalizer + var secret *corev1.Secret err = objectClient.EnsureFinalizer( ctx, client.ObjectKey{ Namespace: r.GetTunnel().GetNamespace(), Name: r.GetTunnel().GetSpec().Cloudflare.Secret, }, + secret, tunnelFinalizer, ) if err != nil { From 80f22e338bc86562f4840b2ac9fddcca605ce3b4 Mon Sep 17 00:00:00 2001 From: Adam Rummer Date: Thu, 8 May 2025 21:11:06 +0100 Subject: [PATCH 07/10] make objectClient accept concrete object type; appease the linter --- api/v1alpha1/zz_generated.deepcopy.go | 2 +- internal/clients/k8s/object.go | 27 ++++--------------- .../controller/accesstunnel/controller.go | 3 ++- internal/controller/adapter.go | 3 ++- .../controller/clustertunnel_controller.go | 3 ++- internal/controller/tunnel.go | 3 ++- internal/controller/tunnel_controller.go | 3 ++- .../controller/tunnelbinding_controller.go | 5 ++-- 8 files changed, 19 insertions(+), 30 deletions(-) diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 289a235f..0f48597f 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -21,7 +21,7 @@ limitations under the License. package v1alpha1 import ( - v1 "k8s.io/api/core/v1" + "k8s.io/api/core/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) diff --git a/internal/clients/k8s/object.go b/internal/clients/k8s/object.go index 69548a51..c3ff7d74 100644 --- a/internal/clients/k8s/object.go +++ b/internal/clients/k8s/object.go @@ -1,3 +1,4 @@ +// Package k8s provides client abstractions to the kubernetes API package k8s import ( @@ -43,11 +44,8 @@ func (s *ObjectClient) EnsureFinalizer(ctx context.Context, key client.ObjectKey } controllerutil.AddFinalizer(obj, finalizer) - - base, err := s.deepCopyObject(obj) - if err != nil { - return err - } + //nolint:revive // we know this will serialise, even if the compiler doesn't + base := obj.DeepCopyObject().(client.Object) s.log. WithValues("finalizer", finalizer). @@ -74,11 +72,8 @@ func (s *ObjectClient) RemoveFinalizer(ctx context.Context, key client.ObjectKey } controllerutil.RemoveFinalizer(obj, finalizer) - - base, err := s.deepCopyObject(obj) - if err != nil { - return err - } + //nolint:revive // we know this will serialise, even if the compiler doesn't + base := obj.DeepCopyObject().(client.Object) s.log. WithValues("finalizer", finalizer). @@ -89,15 +84,3 @@ func (s *ObjectClient) RemoveFinalizer(ctx context.Context, key client.ObjectKey } return nil } - -func (*ObjectClient) deepCopyObject(obj client.Object) (client.Object, error) { - objDeepCopy := obj.DeepCopyObject() - if objDeepCopy == nil { - return nil, errors.New("received nil object from DeepCopyObject") - } - base, ok := objDeepCopy.(client.Object) - if !ok { - return nil, errors.New("failed to convert object to client.Object") - } - return base, nil -} diff --git a/internal/controller/accesstunnel/controller.go b/internal/controller/accesstunnel/controller.go index c8ff2332..1175d158 100644 --- a/internal/controller/accesstunnel/controller.go +++ b/internal/controller/accesstunnel/controller.go @@ -37,8 +37,9 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ctrllog "sigs.k8s.io/controller-runtime/pkg/log" - networkingv1alpha1 "github.com/adyanth/cloudflare-operator/api/v1alpha1" "github.com/go-logr/logr" + + networkingv1alpha1 "github.com/adyanth/cloudflare-operator/api/v1alpha1" ) const CONTAINER_PORT int32 = 8000 diff --git a/internal/controller/adapter.go b/internal/controller/adapter.go index 97e79b45..ea578bd0 100644 --- a/internal/controller/adapter.go +++ b/internal/controller/adapter.go @@ -1,9 +1,10 @@ package controller import ( - networkingv1alpha2 "github.com/adyanth/cloudflare-operator/api/v1alpha2" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" + + networkingv1alpha2 "github.com/adyanth/cloudflare-operator/api/v1alpha2" ) // TunnelAdapter implementation diff --git a/internal/controller/clustertunnel_controller.go b/internal/controller/clustertunnel_controller.go index 998c9035..d74b45d1 100644 --- a/internal/controller/clustertunnel_controller.go +++ b/internal/controller/clustertunnel_controller.go @@ -31,8 +31,9 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ctrllog "sigs.k8s.io/controller-runtime/pkg/log" - networkingv1alpha2 "github.com/adyanth/cloudflare-operator/api/v1alpha2" "github.com/go-logr/logr" + + networkingv1alpha2 "github.com/adyanth/cloudflare-operator/api/v1alpha2" ) // ClusterTunnelReconciler reconciles a ClusterTunnel object diff --git a/internal/controller/tunnel.go b/internal/controller/tunnel.go index 4ee573e6..29ba6756 100644 --- a/internal/controller/tunnel.go +++ b/internal/controller/tunnel.go @@ -1,8 +1,9 @@ package controller import ( - networkingv1alpha2 "github.com/adyanth/cloudflare-operator/api/v1alpha2" "sigs.k8s.io/controller-runtime/pkg/client" + + networkingv1alpha2 "github.com/adyanth/cloudflare-operator/api/v1alpha2" ) type Tunnel interface { diff --git a/internal/controller/tunnel_controller.go b/internal/controller/tunnel_controller.go index 0f471567..5199e9d1 100644 --- a/internal/controller/tunnel_controller.go +++ b/internal/controller/tunnel_controller.go @@ -29,10 +29,11 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ctrllog "sigs.k8s.io/controller-runtime/pkg/log" - networkingv1alpha2 "github.com/adyanth/cloudflare-operator/api/v1alpha2" "github.com/go-logr/logr" appsv1 "k8s.io/api/apps/v1" "k8s.io/client-go/tools/record" + + networkingv1alpha2 "github.com/adyanth/cloudflare-operator/api/v1alpha2" ) // TunnelReconciler reconciles a Tunnel object diff --git a/internal/controller/tunnelbinding_controller.go b/internal/controller/tunnelbinding_controller.go index 82574478..be661366 100644 --- a/internal/controller/tunnelbinding_controller.go +++ b/internal/controller/tunnelbinding_controller.go @@ -26,8 +26,6 @@ import ( "github.com/adyanth/cloudflare-operator/internal/clients/cf" - networkingv1alpha1 "github.com/adyanth/cloudflare-operator/api/v1alpha1" - networkingv1alpha2 "github.com/adyanth/cloudflare-operator/api/v1alpha2" "github.com/go-logr/logr" yaml "gopkg.in/yaml.v3" corev1 "k8s.io/api/core/v1" @@ -39,6 +37,9 @@ import ( "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" ctrllog "sigs.k8s.io/controller-runtime/pkg/log" + networkingv1alpha1 "github.com/adyanth/cloudflare-operator/api/v1alpha1" + networkingv1alpha2 "github.com/adyanth/cloudflare-operator/api/v1alpha2" + appsv1 "k8s.io/api/apps/v1" "k8s.io/client-go/tools/record" ) From c0334689c760bf8be30ddac51ecbf31d548747f3 Mon Sep 17 00:00:00 2001 From: Adam Rummer Date: Thu, 8 May 2025 23:51:55 +0100 Subject: [PATCH 08/10] tested working --- internal/clients/k8s/object.go | 5 +++-- internal/controller/clustertunnel_controller.go | 4 ++-- internal/controller/tunnel_controller.go | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/internal/clients/k8s/object.go b/internal/clients/k8s/object.go index c3ff7d74..8d6f4f7e 100644 --- a/internal/clients/k8s/object.go +++ b/internal/clients/k8s/object.go @@ -43,13 +43,14 @@ func (s *ObjectClient) EnsureFinalizer(ctx context.Context, key client.ObjectKey return nil } - controllerutil.AddFinalizer(obj, finalizer) //nolint:revive // we know this will serialise, even if the compiler doesn't base := obj.DeepCopyObject().(client.Object) + controllerutil.AddFinalizer(obj, finalizer) s.log. WithValues("finalizer", finalizer). WithValues("object", fmt.Sprintf("%s/%s", obj.GetNamespace(), obj.GetName())). + WithValues("kind", obj.GetObjectKind().GroupVersionKind().Kind). Info("creating finalizer") if err := s.k8sClient.Patch(ctx, obj, client.MergeFrom(base)); err != nil { return fmt.Errorf("could not add finalizer %q: %w", finalizer, err) @@ -71,9 +72,9 @@ func (s *ObjectClient) RemoveFinalizer(ctx context.Context, key client.ObjectKey return nil } - controllerutil.RemoveFinalizer(obj, finalizer) //nolint:revive // we know this will serialise, even if the compiler doesn't base := obj.DeepCopyObject().(client.Object) + controllerutil.RemoveFinalizer(obj, finalizer) s.log. WithValues("finalizer", finalizer). diff --git a/internal/controller/clustertunnel_controller.go b/internal/controller/clustertunnel_controller.go index d74b45d1..52b5b692 100644 --- a/internal/controller/clustertunnel_controller.go +++ b/internal/controller/clustertunnel_controller.go @@ -153,7 +153,7 @@ func (r *ClusterTunnelReconciler) Reconcile(ctx context.Context, req ctrl.Reques return ctrl.Result{}, err } // ensure the secret associated with the tunnel has the finalizer removed - var secret *corev1.Secret + secret := &corev1.Secret{} err = objectClient.RemoveFinalizer( ctx, client.ObjectKey{ @@ -181,7 +181,7 @@ func (r *ClusterTunnelReconciler) Reconcile(ctx context.Context, req ctrl.Reques return ctrl.Result{}, err } // ensure the secret associated with the tunnel has a finalizer - var secret *corev1.Secret + secret := &corev1.Secret{} err = objectClient.EnsureFinalizer( ctx, client.ObjectKey{ diff --git a/internal/controller/tunnel_controller.go b/internal/controller/tunnel_controller.go index 5199e9d1..6f73bb69 100644 --- a/internal/controller/tunnel_controller.go +++ b/internal/controller/tunnel_controller.go @@ -147,7 +147,7 @@ func (r *TunnelReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr if err != nil { return ctrl.Result{}, err } - var secret *corev1.Secret + secret := &corev1.Secret{} err = objectClient.RemoveFinalizer( ctx, client.ObjectKey{ @@ -175,7 +175,7 @@ func (r *TunnelReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr return ctrl.Result{}, err } // ensure the secret associated with the tunnel has a finalizer - var secret *corev1.Secret + secret := &corev1.Secret{} err = objectClient.EnsureFinalizer( ctx, client.ObjectKey{ From 92077b43f67841c305a1f7f47fa847560dae54eb Mon Sep 17 00:00:00 2001 From: Adam Rummer Date: Fri, 9 May 2025 00:07:46 +0100 Subject: [PATCH 09/10] add kind annotation to logs --- internal/clients/k8s/object.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/clients/k8s/object.go b/internal/clients/k8s/object.go index 8d6f4f7e..07220b73 100644 --- a/internal/clients/k8s/object.go +++ b/internal/clients/k8s/object.go @@ -79,6 +79,7 @@ func (s *ObjectClient) RemoveFinalizer(ctx context.Context, key client.ObjectKey s.log. WithValues("finalizer", finalizer). WithValues("object", fmt.Sprintf("%s/%s", obj.GetNamespace(), obj.GetName())). + WithValues("kind", obj.GetObjectKind().GroupVersionKind().Kind). Info("removing finalizer") if err := s.k8sClient.Patch(ctx, obj, client.MergeFrom(base)); err != nil { return fmt.Errorf("could not remove finalizer %q: %w", finalizer, err) From 46901dcde5e09a22adb0c1bb546dce8461add7bb Mon Sep 17 00:00:00 2001 From: Adam Rummer Date: Sat, 14 Jun 2025 16:34:34 +0700 Subject: [PATCH 10/10] move secret finalizer removal to generic tunnel reconciler --- .../controller/clustertunnel_controller.go | 18 --------------- .../controller/generic_tunnel_reconciler.go | 22 +++++++++++++++++++ internal/controller/tunnel_controller.go | 17 -------------- 3 files changed, 22 insertions(+), 35 deletions(-) diff --git a/internal/controller/clustertunnel_controller.go b/internal/controller/clustertunnel_controller.go index 52b5b692..56eb11ba 100644 --- a/internal/controller/clustertunnel_controller.go +++ b/internal/controller/clustertunnel_controller.go @@ -148,24 +148,6 @@ func (r *ClusterTunnelReconciler) Reconcile(ctx context.Context, req ctrl.Reques if apierrors.IsNotFound(err) { // Tunnel object not found, could have been deleted after reconcile request. // Owned objects are automatically garbage collected. - objectClient, err := k8s.NewObjectClient(r.Client, &r.log) - if err != nil { - return ctrl.Result{}, err - } - // ensure the secret associated with the tunnel has the finalizer removed - secret := &corev1.Secret{} - err = objectClient.RemoveFinalizer( - ctx, - client.ObjectKey{ - Namespace: r.GetTunnel().GetNamespace(), - Name: r.GetTunnel().GetSpec().Cloudflare.Secret, - }, - secret, - tunnelFinalizer, - ) - if err != nil { - return ctrl.Result{}, err - } return ctrl.Result{}, nil } r.log.Error(err, "unable to fetch Tunnel") diff --git a/internal/controller/generic_tunnel_reconciler.go b/internal/controller/generic_tunnel_reconciler.go index 67f2beab..6c4b1e2f 100644 --- a/internal/controller/generic_tunnel_reconciler.go +++ b/internal/controller/generic_tunnel_reconciler.go @@ -3,6 +3,7 @@ package controller import ( "errors" "fmt" + "sigs.k8s.io/controller-runtime/pkg/client" "time" "github.com/adyanth/cloudflare-operator/internal/clients/cf" @@ -199,6 +200,27 @@ func cleanupTunnel(r GenericTunnelReconciler) (ctrl.Result, bool, error) { return ctrl.Result{}, false, err } r.GetRecorder().Event(r.GetTunnel().GetObject(), corev1.EventTypeNormal, "FinalizerUnset", "Tunnel Finalizer removed") + + // ensure the secret associated with the tunnel has the finalizer removed, only once the tunnel finalizer can be removed + log := r.GetLog() + objectClient, err := k8s.NewObjectClient(r.GetClient(), &log) + if err != nil { + return ctrl.Result{}, false, err + } + secret := &corev1.Secret{} + err = objectClient.RemoveFinalizer( + r.GetContext(), + client.ObjectKey{ + Namespace: r.GetTunnel().GetNamespace(), + Name: r.GetTunnel().GetSpec().Cloudflare.Secret, + }, + secret, + tunnelFinalizer, + ) + if err != nil { + return ctrl.Result{}, false, err + } + return ctrl.Result{}, true, nil } } diff --git a/internal/controller/tunnel_controller.go b/internal/controller/tunnel_controller.go index 6f73bb69..8cd27f3e 100644 --- a/internal/controller/tunnel_controller.go +++ b/internal/controller/tunnel_controller.go @@ -143,23 +143,6 @@ func (r *TunnelReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr if apierrors.IsNotFound(err) { // Tunnel object not found, could have been deleted after reconcile request. // Owned objects are automatically garbage collected. - objectClient, err := k8s.NewObjectClient(r.Client, &r.log) - if err != nil { - return ctrl.Result{}, err - } - secret := &corev1.Secret{} - err = objectClient.RemoveFinalizer( - ctx, - client.ObjectKey{ - Namespace: r.GetTunnel().GetNamespace(), - Name: r.GetTunnel().GetSpec().Cloudflare.Secret, - }, - secret, - tunnelFinalizer, - ) - if err != nil { - return ctrl.Result{}, err - } return ctrl.Result{}, nil } r.log.Error(err, "unable to fetch Tunnel")