From 5f257ea5a3b72dbe7ebc9fbe1e94c98698df22d0 Mon Sep 17 00:00:00 2001 From: Ashley Dumaine Date: Wed, 11 Dec 2024 17:04:50 -0500 Subject: [PATCH] add controllers for processing dependent resource deletion --- PROJECT | 2 + cmd/main.go | 18 +++ config/rbac/role.yaml | 29 ++-- internal/controller/addressset_controller.go | 142 ++++++++++++++++++ .../controller/firewallrule_controller.go | 137 +++++++++++++++++ .../controller/linodefirewall_controller.go | 2 +- .../linodefirewall_controller_helpers.go | 34 +++-- 7 files changed, 329 insertions(+), 35 deletions(-) create mode 100644 internal/controller/addressset_controller.go create mode 100644 internal/controller/firewallrule_controller.go diff --git a/PROJECT b/PROJECT index f2c00d41..f82c5c67 100644 --- a/PROJECT +++ b/PROJECT @@ -116,6 +116,7 @@ resources: - api: crdVersion: v1 namespaced: true + controller: true domain: cluster.x-k8s.io group: infrastructure kind: AddressSet @@ -124,6 +125,7 @@ resources: - api: crdVersion: v1 namespaced: true + controller: true domain: cluster.x-k8s.io group: infrastructure kind: FirewallRule diff --git a/cmd/main.go b/cmd/main.go index 95723e4a..821eabc8 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -347,6 +347,24 @@ func setupControllers(mgr manager.Manager, flags flagVars, linodeClientConfig, d setupLog.Error(err, "unable to create controller", "controller", "LinodeFirewall") os.Exit(1) } + + // FirewallRule Controller + if err := (&controller.FirewallRuleReconciler{ + Client: mgr.GetClient(), + WatchFilterValue: flags.clusterWatchFilter, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "FirewallRule") + os.Exit(1) + } + + // AddressSet Controller + if err := (&controller.AddressSetReconciler{ + Client: mgr.GetClient(), + WatchFilterValue: flags.clusterWatchFilter, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "AddressSet") + os.Exit(1) + } } // setupWebhooks initializes webhooks for the specified resources in the manager. diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 701772f1..bad40942 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -31,7 +31,16 @@ rules: resources: - addresssets - firewallrules + - linodeclusters + - linodefirewalls + - linodemachines + - linodeobjectstoragebuckets + - linodeobjectstoragekeys + - linodeplacementgroups + - linodevpcs verbs: + - create + - delete - get - list - patch @@ -54,24 +63,8 @@ rules: - apiGroups: - infrastructure.cluster.x-k8s.io resources: - - linodeclusters - - linodefirewalls - - linodemachines - - linodeobjectstoragebuckets - - linodeobjectstoragekeys - - linodeplacementgroups - - linodevpcs - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - infrastructure.cluster.x-k8s.io - resources: + - addresssets/status + - firewallrules/status - linodeclusters/status - linodefirewalls/status - linodemachines/status diff --git a/internal/controller/addressset_controller.go b/internal/controller/addressset_controller.go new file mode 100644 index 00000000..31865300 --- /dev/null +++ b/internal/controller/addressset_controller.go @@ -0,0 +1,142 @@ +/* +Copyright 2023 Akamai Technologies, Inc. + +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 controller + +import ( + "context" + wrappedruntimeclient "github.com/linode/cluster-api-provider-linode/observability/wrappers/runtimeclient" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/client-go/util/retry" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "strings" + "time" + + wrappedruntimereconciler "github.com/linode/cluster-api-provider-linode/observability/wrappers/runtimereconciler" + "github.com/linode/cluster-api-provider-linode/util/reconciler" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/predicate" + + infrav1alpha2 "github.com/linode/cluster-api-provider-linode/api/v1alpha2" +) + +// AddressSetReconciler reconciles a FirewallRule object +type AddressSetReconciler struct { + client.Client + Scheme *runtime.Scheme + WatchFilterValue string + ReconcileTimeout time.Duration +} + +//+kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=addresssets,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=addresssets/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=addresssets/finalizers,verbs=update +//+kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=linodefirewalls,verbs=get;list;watch + +func (r *AddressSetReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + ctx, cancel := context.WithTimeout(ctx, reconciler.DefaultedLoopTimeout(r.ReconcileTimeout)) + defer cancel() + + log := ctrl.LoggerFrom(ctx).WithName("LinodeFirewallReconciler").WithValues("name", req.NamespacedName.String()) + addrSet := &infrav1alpha2.AddressSet{} + if err := r.TracedClient().Get(ctx, req.NamespacedName, addrSet); err != nil { + if err = client.IgnoreNotFound(err); err != nil { + log.Error(err, "failed to fetch AddressSet") + } + + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + if !addrSet.DeletionTimestamp.IsZero() { + log.Info("deleting firewallrule", "namespace", addrSet.Namespace, "name", addrSet.Name) + for _, finalizer := range addrSet.Finalizers { + parts := strings.Split(finalizer, ".") + if len(parts) == 3 && parts[0] == "lfw" { + lfw := &infrav1alpha2.LinodeFirewall{} + if err := r.TracedClient().Get(ctx, client.ObjectKey{Namespace: parts[1], Name: parts[2]}, lfw); err != nil { + if apierrors.IsNotFound(err) { + // remove the finalizer since the LinodeFirewall doesn't exist anymore + controllerutil.RemoveFinalizer(addrSet, finalizer) + return ctrl.Result{}, nil + } + log.Error(err, "failed to fetch referenced LinodeFirewall") + return ctrl.Result{}, nil + } + for _, rule := range lfw.Spec.InboundRules { + for _, addrSetRef := range rule.AddressSetRefs { + if addrSetRef.Namespace == "" { + addrSetRef.Namespace = lfw.Namespace + } + if addrSetRef.Namespace == addrSet.Namespace && addrSetRef.Name == addrSet.Name { + // bail out, can't clean up the AddressSet + log.Info("cannot clean up AddressSet, still in use by LinodeFirewall", "namespace", lfw.Namespace, "name", lfw.Name) + return ctrl.Result{}, nil + } + } + } + for _, rule := range lfw.Spec.OutboundRules { + for _, addrSetRef := range rule.AddressSetRefs { + if addrSetRef.Namespace == "" { + addrSetRef.Namespace = lfw.Namespace + } + if addrSetRef.Namespace == addrSet.Namespace && addrSetRef.Name == addrSet.Name { + // bail out, can't clean up the AddressSet + log.Info("cannot clean up AddressSet, still in use by LinodeFirewall", "namespace", lfw.Namespace, "name", lfw.Name) + return ctrl.Result{}, nil + } + } + } + // TODO: check the FWRuleRefs + controllerutil.RemoveFinalizer(addrSet, finalizer) + if retryErr := retry.RetryOnConflict(retry.DefaultRetry, func() error { + return r.Update(ctx, addrSet) + }); retryErr != nil { + log.Error(retryErr, "failed to remove finalizer") + return ctrl.Result{}, nil + } + log.Info("removed AddressSet finalizer", "namespace", addrSet.Namespace, "name", addrSet.Name, "finalizer", finalizer) + } + } + } + + return ctrl.Result{}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *AddressSetReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&infrav1alpha2.AddressSet{}). + WithEventFilter(predicate.Funcs{ + // only enqueue delete requests + DeleteFunc: func(e event.DeleteEvent) bool { + return true + }, + CreateFunc: func(e event.CreateEvent) bool { + return false + }, + UpdateFunc: func(e event.UpdateEvent) bool { + return false + }, + }). + Complete(wrappedruntimereconciler.NewRuntimeReconcilerWithTracing(r, wrappedruntimereconciler.DefaultDecorator())) +} + +func (r *AddressSetReconciler) TracedClient() client.Client { + return wrappedruntimeclient.NewRuntimeClientWithTracing(r.Client, wrappedruntimereconciler.DefaultDecorator()) +} diff --git a/internal/controller/firewallrule_controller.go b/internal/controller/firewallrule_controller.go new file mode 100644 index 00000000..741f3e71 --- /dev/null +++ b/internal/controller/firewallrule_controller.go @@ -0,0 +1,137 @@ +/* +Copyright 2023 Akamai Technologies, Inc. + +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 controller + +import ( + "context" + wrappedruntimeclient "github.com/linode/cluster-api-provider-linode/observability/wrappers/runtimeclient" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/client-go/util/retry" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "strings" + "time" + + wrappedruntimereconciler "github.com/linode/cluster-api-provider-linode/observability/wrappers/runtimereconciler" + "github.com/linode/cluster-api-provider-linode/util/reconciler" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/predicate" + + infrav1alpha2 "github.com/linode/cluster-api-provider-linode/api/v1alpha2" +) + +// FirewallRuleReconciler reconciles a FirewallRule object +type FirewallRuleReconciler struct { + client.Client + Scheme *runtime.Scheme + WatchFilterValue string + ReconcileTimeout time.Duration +} + +//+kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=firewallrules,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=firewallrules/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=firewallrules/finalizers,verbs=update +//+kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=linodefirewalls,verbs=get;list;watch + +func (r *FirewallRuleReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + ctx, cancel := context.WithTimeout(ctx, reconciler.DefaultedLoopTimeout(r.ReconcileTimeout)) + defer cancel() + + log := ctrl.LoggerFrom(ctx).WithName("LinodeFirewallReconciler").WithValues("name", req.NamespacedName.String()) + fwRule := &infrav1alpha2.FirewallRule{} + if err := r.TracedClient().Get(ctx, req.NamespacedName, fwRule); err != nil { + if err = client.IgnoreNotFound(err); err != nil { + log.Error(err, "failed to fetch FirewallRule") + } + + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + if !fwRule.DeletionTimestamp.IsZero() { + log.Info("deleting firewallrule", "namespace", fwRule.Namespace, "name", fwRule.Name) + for _, finalizer := range fwRule.Finalizers { + parts := strings.Split(finalizer, ".") + if len(parts) == 3 && parts[0] == "lfw" { + lfw := &infrav1alpha2.LinodeFirewall{} + if err := r.TracedClient().Get(ctx, client.ObjectKey{Namespace: parts[1], Name: parts[2]}, lfw); err != nil { + if apierrors.IsNotFound(err) { + // remove the finalizer since the LinodeFirewall doesn't exist anymore + controllerutil.RemoveFinalizer(fwRule, finalizer) + return ctrl.Result{}, nil + } + log.Error(err, "failed to fetch referenced LinodeFirewall") + return ctrl.Result{}, nil + } + for _, ruleRef := range lfw.Spec.InboundRuleRefs { + if ruleRef.Namespace == "" { + ruleRef.Namespace = lfw.Namespace + } + if ruleRef.Namespace == fwRule.Namespace && ruleRef.Name == fwRule.Name { + // bail out, can't clean up the FirewallRule + log.Info("cannot clean up FirewallRule, still in use by LinodeFirewall", "namespace", lfw.Namespace, "name", lfw.Name) + return ctrl.Result{}, nil + } + } + for _, ruleRef := range lfw.Spec.OutboundRuleRefs { + if ruleRef.Namespace == "" { + ruleRef.Namespace = lfw.Namespace + } + if ruleRef.Namespace == fwRule.Namespace && ruleRef.Name == fwRule.Name { + // bail out, can't clean up the FirewallRule + log.Info("cannot clean up FirewallRule, still in use by LinodeFirewall", "namespace", lfw.Namespace, "name", lfw.Name) + return ctrl.Result{}, nil + } + } + controllerutil.RemoveFinalizer(fwRule, finalizer) + if retryErr := retry.RetryOnConflict(retry.DefaultRetry, func() error { + return r.Update(ctx, fwRule) + }); retryErr != nil { + log.Error(retryErr, "failed to remove finalizer") + return ctrl.Result{}, nil + } + log.Info("removed FirewallRule finalizer", "namespace", fwRule.Namespace, "name", fwRule.Name, "finalizer", finalizer) + } + } + } + + return ctrl.Result{}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *FirewallRuleReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&infrav1alpha2.FirewallRule{}). + WithEventFilter(predicate.Funcs{ + // only enqueue delete requests + DeleteFunc: func(e event.DeleteEvent) bool { + return true + }, + CreateFunc: func(e event.CreateEvent) bool { + return false + }, + UpdateFunc: func(e event.UpdateEvent) bool { + return false + }, + }). + Complete(wrappedruntimereconciler.NewRuntimeReconcilerWithTracing(r, wrappedruntimereconciler.DefaultDecorator())) +} + +func (r *FirewallRuleReconciler) TracedClient() client.Client { + return wrappedruntimeclient.NewRuntimeClientWithTracing(r.Client, wrappedruntimereconciler.DefaultDecorator()) +} diff --git a/internal/controller/linodefirewall_controller.go b/internal/controller/linodefirewall_controller.go index 27a5a646..57d43171 100644 --- a/internal/controller/linodefirewall_controller.go +++ b/internal/controller/linodefirewall_controller.go @@ -21,6 +21,7 @@ import ( "errors" "fmt" "net/http" + crcontroller "sigs.k8s.io/controller-runtime/pkg/controller" "time" "github.com/go-logr/logr" @@ -36,7 +37,6 @@ import ( ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" - crcontroller "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/handler" diff --git a/internal/controller/linodefirewall_controller_helpers.go b/internal/controller/linodefirewall_controller_helpers.go index 9033f918..e8e905a0 100644 --- a/internal/controller/linodefirewall_controller_helpers.go +++ b/internal/controller/linodefirewall_controller_helpers.go @@ -328,16 +328,17 @@ func addAddrSetFinalizer(ctx context.Context, k8sClient clients.K8sClient, firew addrSetRef.Namespace = firewall.Namespace } finalizer := getFinalizer(firewall) - if !controllerutil.ContainsFinalizer(addrSet, finalizer) { - controllerutil.AddFinalizer(addrSet, finalizer) - if retryErr := retry.RetryOnConflict(retry.DefaultRetry, func() error { - if err := k8sClient.Get(ctx, client.ObjectKey{Namespace: addrSetRef.Namespace, Name: addrSetRef.Name}, addrSet); err != nil { - return err - } + if retryErr := retry.RetryOnConflict(retry.DefaultRetry, func() error { + if err := k8sClient.Get(ctx, client.ObjectKey{Namespace: addrSetRef.Namespace, Name: addrSetRef.Name}, addrSet); err != nil { + return err + } + if !controllerutil.ContainsFinalizer(addrSet, finalizer) { + controllerutil.AddFinalizer(addrSet, finalizer) return k8sClient.Update(ctx, addrSet) - }); retryErr != nil { - return nil, retryErr } + return nil + }); retryErr != nil { + return nil, retryErr } return addrSet, nil } @@ -348,16 +349,17 @@ func addFWRuleFinalizer(ctx context.Context, k8sClient clients.K8sClient, firewa fwRuleRef.Namespace = firewall.Namespace } finalizer := getFinalizer(firewall) - if !controllerutil.ContainsFinalizer(fwRule, finalizer) { - controllerutil.AddFinalizer(fwRule, finalizer) - if retryErr := retry.RetryOnConflict(retry.DefaultRetry, func() error { - if err := k8sClient.Get(ctx, client.ObjectKey{Namespace: fwRuleRef.Namespace, Name: fwRuleRef.Name}, fwRule); err != nil { - return err - } + if retryErr := retry.RetryOnConflict(retry.DefaultRetry, func() error { + if err := k8sClient.Get(ctx, client.ObjectKey{Namespace: fwRuleRef.Namespace, Name: fwRuleRef.Name}, fwRule); err != nil { + return err + } + if !controllerutil.ContainsFinalizer(fwRule, finalizer) { + controllerutil.AddFinalizer(fwRule, finalizer) return k8sClient.Update(ctx, fwRule) - }); retryErr != nil { - return nil, retryErr } + return nil + }); retryErr != nil { + return nil, retryErr } return fwRule, nil }