diff --git a/docs/plugins.md b/docs/plugins.md index 3abeeeef..97dcc297 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -175,6 +175,14 @@ config: remove: boolean value, if true the annotation is removed, if false the annotation is added or changed, optional ``` +### alterFinalizer +Adds or removes a finalizer. +```yaml +config: + key: the finalizer key to add or remove, required + remove: boolean value, if true the finalizer is removed, if false the finalizer is added, optional +``` + ### alterLabel Adds, changes or removes a label. ```yaml diff --git a/plugin/impl/finalizer.go b/plugin/impl/finalizer.go new file mode 100644 index 00000000..125d044c --- /dev/null +++ b/plugin/impl/finalizer.go @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company +// SPDX-License-Identifier: Apache-2.0 + +package impl + +import ( + "github.com/sapcc/ucfgwrap" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + "github.com/sapcc/maintenance-controller/plugin" +) + +// AlterFinalizer is a trigger plugin, which can alter properties of the Finalizer CRO of the node. +type AlterFinalizer struct { + Key string + Remove bool +} + +// New creates a new AlterFinalizer instance with the given config. +func (a *AlterFinalizer) New(config *ucfgwrap.Config) (plugin.Trigger, error) { + conf := struct { + Key string `config:"key" validate:"required"` + Remove bool `config:"remove"` + }{} + if err := config.Unpack(&conf); err != nil { + return nil, err + } + return &AlterFinalizer{Key: conf.Key, Remove: conf.Remove}, nil +} + +func (a *AlterFinalizer) ID() string { + return "alterFinalizer" +} + +// Trigger ensures the Finalizer with the provided key is removed if removes is set to true. +// Otherwise, it sets the Finalizer with the provided key if required. +func (a *AlterFinalizer) Trigger(params plugin.Parameters) error { + if !a.Remove { + controllerutil.AddFinalizer(params.Node, a.Key) + return nil + } + + controllerutil.RemoveFinalizer(params.Node, a.Key) + return nil +} diff --git a/plugin/impl/finalizer_test.go b/plugin/impl/finalizer_test.go new file mode 100644 index 00000000..83c3af2f --- /dev/null +++ b/plugin/impl/finalizer_test.go @@ -0,0 +1,76 @@ +// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company +// SPDX-License-Identifier: Apache-2.0 + +package impl + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/sapcc/ucfgwrap" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/sapcc/maintenance-controller/plugin" +) + +var _ = Describe("The Finalizer plugin", func() { + Describe("AlterFinalizer", func() { + It("fails parsing incorrect config", func() { + _, err := ucfgwrap.FromYAML([]byte("invalid_yaml")) + errMsg := "type 'string' is not supported on top level of config, only dictionary or list" + Expect(err).To(MatchError(errMsg)) + + config, err := ucfgwrap.FromYAML([]byte("value: test")) + Expect(err).To(Not(HaveOccurred())) + + var base AlterFinalizer + _, err = base.New(&config) + Expect(err).To(MatchError("string value is not set accessing 'key'")) + }) + + It("has valid configuration", func() { + config, err := ucfgwrap.FromYAML([]byte("key: test.com/finalizer\nremove: true")) + Expect(err).To(Not(HaveOccurred())) + + var base AlterFinalizer + plugin, err := base.New(&config) + Expect(err).To(Succeed()) + Expect(plugin).To(Equal(&AlterFinalizer{ + Key: "test.com/finalizer", + Remove: true, + })) + }) + + It("adds finalizer when not present", func() { + config, err := ucfgwrap.FromYAML([]byte("key: test.com/finalizer\nremove: false")) + Expect(err).To(Not(HaveOccurred())) + + var base AlterFinalizer + addFinalizer, err := base.New(&config) + Expect(err).To(Not(HaveOccurred())) + + testNode := &v1.Node{ObjectMeta: metav1.ObjectMeta{Name: "test-node"}} + err = addFinalizer.Trigger(plugin.Parameters{Node: testNode}) + Expect(err).To(Not(HaveOccurred())) + Expect(testNode.Finalizers).To(ContainElement("test.com/finalizer")) + }) + + It("removes finalizer when present", func() { + testNode := &v1.Node{ObjectMeta: metav1.ObjectMeta{ + Name: "test-node", + Finalizers: []string{"test.com/finalizer"}, + }} + + config, err := ucfgwrap.FromYAML([]byte("key: test.com/finalizer\nremove: true")) + Expect(err).To(Succeed()) + + var base AlterFinalizer + removeFinalizer, err := base.New(&config) + Expect(err).To(Not(HaveOccurred())) + + err = removeFinalizer.Trigger(plugin.Parameters{Node: testNode}) + Expect(err).To(Not(HaveOccurred())) + Expect(testNode.Finalizers).ToNot(ContainElement("test.com/finalizer")) + }) + }) +})