Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,23 @@ A user can initiate rolling the latest firewall set by annotating a monitor in t
kubectl annotate fwmon <firewall-name> 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-name> 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-name> firewall.metal-stack.io/restart-systemd-services-whitelist=tailscale,frr
kubectl annotate fw <firewall-name> firewall.metal-stack.io/restart-systemd-services=frr
```

## Development

Most of the functionality is developed with the help of the [integration](integration) test suite.
Expand Down
7 changes: 7 additions & 0 deletions api/v2/types_annotations.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down
28 changes: 28 additions & 0 deletions controllers/firewall/reconcile.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
42 changes: 42 additions & 0 deletions integration/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Loading