diff --git a/controllers/firewall_monitor_annotation_controller.go b/controllers/firewall_monitor_annotation_controller.go new file mode 100644 index 00000000..905c7004 --- /dev/null +++ b/controllers/firewall_monitor_annotation_controller.go @@ -0,0 +1,176 @@ +package controllers + +import ( + "context" + "fmt" + "slices" + "strings" + + "github.com/go-logr/logr" + firewallv2 "github.com/metal-stack/firewall-controller-manager/api/v2" + "github.com/metal-stack/firewall-controller/v2/pkg/updater" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/tools/record" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +const firewallControllerService = "firewall-controller.service" + +var ( + systemdServiceRestartWhitelist = []string{ + "droptailer.service", + "firewall-controller.service", + "nftables-exporter.service", + "node-exporter.service", + "tailscaled.service", + } +) + +type FirewallMonitorAnnotationController struct { + ShootClient client.Client + SeedClient client.Client + FirewallName string + SeedNamespace string + Log logr.Logger + Recorder record.EventRecorder +} + +func (r *FirewallMonitorAnnotationController) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&firewallv2.FirewallMonitor{}, + builder.WithPredicates( + predicate.AnnotationChangedPredicate{}, + ), + ). + WithEventFilter(predicate.Funcs{ + DeleteFunc: func(de event.DeleteEvent) bool { + return false + }, + }). + WithEventFilter(predicate.NewPredicateFuncs(func(object client.Object) bool { + return object.GetNamespace() == firewallv2.FirewallShootNamespace && object.GetName() == r.FirewallName + })). + Named("FirewallMonitorAnnotationController"). + Complete(r) +} + +func (r *FirewallMonitorAnnotationController) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + var ( + fw = &firewallv2.Firewall{ + ObjectMeta: metav1.ObjectMeta{ + Name: r.FirewallName, + Namespace: r.SeedNamespace, + }, + } + fwmon = &firewallv2.FirewallMonitor{} + ) + + if err := r.ShootClient.Get(ctx, req.NamespacedName, fwmon); err != nil { + if apierrors.IsNotFound(err) { + r.Log.V(1).Info("object is gone, stop reconciling") + return reconcile.Result{}, nil + } + + return reconcile.Result{}, fmt.Errorf("error retrieving object: %w", err) + } + + if err := r.SeedClient.Get(ctx, client.ObjectKeyFromObject(fw), fw); err != nil { + if apierrors.IsNotFound(err) { + r.Log.V(1).Info("object is gone, stop reconciling") + return reconcile.Result{}, nil + } + + return reconcile.Result{}, fmt.Errorf("error retrieving object: %w", err) + } + + services, ok := fwmon.Annotations[firewallv2.FirewallRestartSystemdServicesAnnotation] + if !ok { + return reconcile.Result{}, nil + } + + var ( + restartFirewallController bool + whitelist = systemdServiceRestartWhitelist + ) + + if overwrite, ok := fw.GetAnnotations()[firewallv2.FirewallRestartSystemdServicesWhitelistAnnotation]; ok { + whitelist = strings.Split(overwrite, ",") + } + + for serviceName := range strings.SplitSeq(services, ",") { + if !strings.HasSuffix(serviceName, ".service") { + serviceName = serviceName + ".service" + } + + if !slices.Contains(whitelist, serviceName) { + r.Log.Info("skipping service restart because not in whitelist", "service-name", serviceName) + continue + } + + // If the firewall-controller itself should be restarted, we have to first remove the annotation from the node. + // Otherwise, the annotation is never removed and it restarts itself indefinitely. + if serviceName == firewallControllerService { + restartFirewallController = true + continue + } + + r.Log.Info("restart service", "service-name", serviceName) + + if err := updater.Restart(ctx, serviceName); err != nil { + r.Recorder.Event( + fwmon, + corev1.EventTypeWarning, + "ServiceRestarted", + fmt.Sprintf("systemd service restart of service %q failed: %s", serviceName, err), + ) + + r.Log.Error(err, "error restarting service", "service-name", serviceName) + } else { + r.Recorder.Event( + fwmon, + corev1.EventTypeNormal, + "ServiceRestarted", + fmt.Sprintf("systemd service %q was restarted through monitor annotation", serviceName), + ) + } + } + + r.Log.Info("Removing annotation from firewall monitor", "annotation", firewallv2.FirewallRestartSystemdServicesAnnotation) + + patch := client.MergeFrom(fwmon.DeepCopy()) + delete(fwmon.Annotations, firewallv2.FirewallRestartSystemdServicesAnnotation) + if err := r.ShootClient.Patch(ctx, fwmon, patch); err != nil { + return reconcile.Result{}, err + } + + if restartFirewallController { + r.Log.Info("restart firewall-controller") + + if err := updater.Restart(ctx, firewallControllerService); err != nil { + r.Recorder.Event( + fwmon, + corev1.EventTypeWarning, + "ServiceRestarted", + fmt.Sprintf("systemd service restart of service %q failed: %s", firewallControllerService, err), + ) + + r.Log.Error(err, "error restarting firewall-controller") + } else { + r.Recorder.Event( + fwmon, + corev1.EventTypeNormal, + "ServiceRestarted", + fmt.Sprintf("systemd service %q was restarted through monitor annotation", firewallControllerService), + ) + } + } + + return ctrl.Result{}, nil +} diff --git a/go.mod b/go.mod index ba31d4a7..a22b08d2 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/google/go-cmp v0.7.0 github.com/google/nftables v0.3.0 github.com/ks2211/go-suricata v0.0.0-20200823200910-986ce1470707 - github.com/metal-stack/firewall-controller-manager v0.6.0 + github.com/metal-stack/firewall-controller-manager v0.6.1-0.20260529122307-ec72cac16dfe github.com/metal-stack/metal-go v0.43.0 github.com/metal-stack/metal-lib v0.24.0 github.com/metal-stack/metal-networker v0.46.3 diff --git a/go.sum b/go.sum index f2623ca2..e0fb4589 100644 --- a/go.sum +++ b/go.sum @@ -126,8 +126,8 @@ github.com/mdlayher/netlink v1.9.0 h1:G8+GLq2x3v4D4MVIqDdNUhTUC7TKiCy/6MDkmItfKc github.com/mdlayher/netlink v1.9.0/go.mod h1:YBnl5BXsCoRuwBjKKlZ+aYmEoq0r12FDA/3JC+94KDg= github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos= github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ= -github.com/metal-stack/firewall-controller-manager v0.6.0 h1:+/VV/VXJa4NRFBRHBw5NxkT2Ap1vXjkFdfBRO5t4MfA= -github.com/metal-stack/firewall-controller-manager v0.6.0/go.mod h1:bQjb3pVL3R6XPUqWA/WX8ktlzcgVYWDbsFANKcrW3FA= +github.com/metal-stack/firewall-controller-manager v0.6.1-0.20260529122307-ec72cac16dfe h1:WdRxxR1iDtnI7WjQBjxr1C5ik2KLLWzPX61CqKoidSg= +github.com/metal-stack/firewall-controller-manager v0.6.1-0.20260529122307-ec72cac16dfe/go.mod h1:bQjb3pVL3R6XPUqWA/WX8ktlzcgVYWDbsFANKcrW3FA= github.com/metal-stack/metal-go v0.43.0 h1:uODD0YCwnAYzyvFxWNakZrymBoMz1FAvP5hkhsR83VQ= github.com/metal-stack/metal-go v0.43.0/go.mod h1:GSfXrAj55LGsUSMHWGDsmq5n056NG0yb1JM8bgfvKOw= github.com/metal-stack/metal-hammer v0.13.17 h1:W2IrWmnz6IXpL7Y35RfVgSVO66EVdqeF+/WeopgycMI= diff --git a/main.go b/main.go index 6f359da8..5af3e4be 100644 --- a/main.go +++ b/main.go @@ -292,6 +292,19 @@ func main() { panic(err) } + // FirewallMonitorAnnotationReconciler + if err = (&controllers.FirewallMonitorAnnotationController{ + ShootClient: shootMgr.GetClient(), + SeedClient: seedMgr.GetClient(), + Log: ctrl.Log.WithName("controllers").WithName("FirewallMonitorAnnotation"), + FirewallName: firewallName, + SeedNamespace: seedNamespace, + Recorder: shootMgr.GetEventRecorderFor("FirewallMonitorAnnotation"), + }).SetupWithManager(shootMgr); err != nil { + l.Error("unable to create firewall monitor annotation controller", "error", err) + panic(err) + } + // +kubebuilder:scaffold:builder setupLog.Info("starting firewall-controller", "version", v.V.String()) diff --git a/pkg/updater/common.go b/pkg/updater/common.go index bf8763cd..90c732c8 100644 --- a/pkg/updater/common.go +++ b/pkg/updater/common.go @@ -98,7 +98,7 @@ func slurpFile(url string) (string, error) { const done = "done" -func restart(ctx context.Context, unitName string) error { +func Restart(ctx context.Context, unitName string) error { dbc, err := dbus.NewWithContext(ctx) if err != nil { return fmt.Errorf("unable to connect to dbus: %w", err) diff --git a/pkg/updater/nftables-exporter.go b/pkg/updater/nftables-exporter.go index 75076233..e71c4ff2 100644 --- a/pkg/updater/nftables-exporter.go +++ b/pkg/updater/nftables-exporter.go @@ -64,7 +64,7 @@ func (u *Updater) updateNFTablesExporter(ctx context.Context, f *firewallv2.Fire return err } - err = restart(ctx, "nftables-exporter.service") + err = Restart(ctx, "nftables-exporter.service") if err != nil { u.log.Error(err, "error restarting nftables-exporter") return err