diff --git a/README.md b/README.md index b8be22b..5cafc6c 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,23 @@ A user can initiate rolling the latest firewall set by annotating a monitor in t kubectl annotate fwmon firewall.metal-stack.io/roll-set=true ``` +## Restarting a systemd-service on the Firewall through Annotation + +A user can initiate the restart of a systemd service through annotating the `FirewallMonitor`: + +```bash +kubectl annotate fwmon firewall.metal-stack.io/restart-systemd-services=tailscale +``` + +The firewall-controller imeplements a whitelist of allowed services to restart. + +In addition, operators can overwrite this whitelist and also initiate a service restart by annotating the `Firewall` resource instead of the `FirewallMonitor`: + +```bash +kubectl annotate fw firewall.metal-stack.io/restart-systemd-services-whitelist=tailscale,frr +kubectl annotate fw firewall.metal-stack.io/restart-systemd-services=frr +``` + ## Development Most of the functionality is developed with the help of the [integration](integration) test suite. diff --git a/api/v2/types_annotations.go b/api/v2/types_annotations.go index 9d10fea..fd99a77 100644 --- a/api/v2/types_annotations.go +++ b/api/v2/types_annotations.go @@ -36,6 +36,13 @@ const ( // Defaults to 0 if no annotation is present. Negative values are allowed. FirewallWeightAnnotation = "firewall.metal-stack.io/weight" + // FirewallRestartSystemdServicesAnnotation can be used to restart a whitelisted set of systemd services running on the firewall. + // Services must be passed comma-separated. + FirewallRestartSystemdServicesAnnotation = "firewall.metal-stack.io/restart-systemd-services" + // FirewallSystemdServicesWhitelistAnnotation can be used to overwrite the default systemd service whitelisted. This allows operators + // to temporarily allow restarts of services like FRR, which might not be desired to be allowed permanently for platform users. + FirewallRestartSystemdServicesWhitelistAnnotation = "firewall.metal-stack.io/restart-systemd-services-whitelist" + // FirewallControllerSetAnnotation is a tag added to the firewall entity indicating to which set a firewall belongs to. FirewallControllerSetAnnotation = "firewall.metal.stack.io/set" ) diff --git a/controllers/firewall/reconcile.go b/controllers/firewall/reconcile.go index 13f25e6..8886944 100644 --- a/controllers/firewall/reconcile.go +++ b/controllers/firewall/reconcile.go @@ -12,14 +12,42 @@ import ( "github.com/metal-stack/metal-go/api/client/machine" "github.com/metal-stack/metal-go/api/models" "github.com/metal-stack/metal-lib/pkg/pointer" + "sigs.k8s.io/controller-runtime/pkg/client" corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/sets" ) // Reconciler must always return either an error or requeue to ensure that it detects if a firewall get lost etc. func (c *controller) Reconcile(r *controllers.Ctx[*v2.Firewall]) error { + if services, ok := r.Target.GetAnnotations()[v2.FirewallRestartSystemdServicesAnnotation]; ok { + mon := &v2.FirewallMonitor{ + ObjectMeta: metav1.ObjectMeta{ + Name: r.Target.Name, + Namespace: c.c.GetShootNamespace(), + }, + } + + err := c.c.GetShootClient().Get(r.Ctx, client.ObjectKeyFromObject(mon), mon) + if err != nil && !apierrors.IsNotFound(err) { + return fmt.Errorf("unable to get firewall monitor: %w", err) + } + + if err == nil { + if err := v2.AddAnnotation(r.Ctx, c.c.GetShootClient(), mon, v2.FirewallRestartSystemdServicesAnnotation, services); err != nil { + return fmt.Errorf("unable to pass systemd service restart annotation to the firewall monitor: %w", err) + } + } + + if err := v2.RemoveAnnotation(r.Ctx, c.c.GetSeedClient(), r.Target, v2.FirewallRestartSystemdServicesAnnotation); err != nil { + return fmt.Errorf("unable to remove systemd service restart annotation from firewall: %w", err) + } + + return controllers.RequeueAfter(0*time.Second, "removed systemd service restart annotation, requeue for regular reconcile") + } + var f *models.V1FirewallResponse defer func() { if err := c.setStatus(r, f); err != nil { diff --git a/integration/integration_test.go b/integration/integration_test.go index 5df6a7b..e586b04 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -441,6 +441,48 @@ var _ = Context("integration test", Ordered, func() { }) }) + When("the firewall gets annotated with a systemd service restart annotation", Ordered, func() { + var ( + fw *v2.Firewall + mon *v2.FirewallMonitor + ) + + BeforeEach(func() { + fw = testcommon.WaitForResourceAmount(k8sClient, ctx, namespaceName, 1, &v2.FirewallList{}, func(l *v2.FirewallList) []*v2.Firewall { + return l.GetItems() + }, 2*time.Second) + mon = testcommon.WaitForResourceAmount(k8sClient, ctx, namespaceName, 1, &v2.FirewallMonitorList{}, func(l *v2.FirewallMonitorList) []*v2.FirewallMonitor { + return l.GetItems() + }, 2*time.Second) + }) + + It("setting the annotation works", func() { + fw.Annotations = map[string]string{ + v2.FirewallRestartSystemdServicesAnnotation: "droptailer", + } + Expect(k8sClient.Update(ctx, fw)).To(Succeed()) + }) + + It("the annotation gets removed from the firewall", func() { + Eventually(func() map[string]string { + Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(fw), fw)).To(Succeed()) + return fw.Annotations + }, 5*time.Second, interval).Should(Not(HaveKey(v2.FirewallRestartSystemdServicesAnnotation)), "systemd service restart annotation was not removed") + }) + + It("the annotation was added to the firewall monitor", func() { + Eventually(func() map[string]string { + Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(mon), mon)).To(Succeed()) + return mon.Annotations + }, 5*time.Second, interval).Should(HaveKey(v2.FirewallRestartSystemdServicesAnnotation), "systemd service restart annotation was not added to the firewall monitor") + }) + + It("removing the annotation from the monitor works", func() { + mon.Annotations = nil + Expect(k8sClient.Update(ctx, mon)).To(Succeed()) + }) + }) + When("a significant change occurs", Ordered, func() { var ( installingFirewall = firewall2("Installing", "is installing")