Skip to content

Commit 750cf33

Browse files
authored
Merge pull request kubernetes-sigs#1481 from rashmigottipati/finalizer-library
✨Finalizer helper library
2 parents a905949 + 5eb033d commit 750cf33

File tree

3 files changed

+337
-0
lines changed

3 files changed

+337
-0
lines changed

pkg/finalizer/finalizer.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
Copyright 2021 The Kubernetes Authors.
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
http://www.apache.org/licenses/LICENSE-2.0
7+
Unless required by applicable law or agreed to in writing, software
8+
distributed under the License is distributed on an "AS IS" BASIS,
9+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
See the License for the specific language governing permissions and
11+
limitations under the License.
12+
*/
13+
14+
package finalizer
15+
16+
import (
17+
"context"
18+
"fmt"
19+
20+
utilerrors "k8s.io/apimachinery/pkg/util/errors"
21+
"sigs.k8s.io/controller-runtime/pkg/client"
22+
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
23+
)
24+
25+
type finalizers map[string]Finalizer
26+
27+
// Result struct holds information about what parts of an object were updated by finalizer(s).
28+
type Result struct {
29+
// Updated will be true if at least one of the object's non-status field
30+
// was updated by some registered finalizer.
31+
Updated bool
32+
// StatusUpdated will be true if at least one of the object's status' fields
33+
// was updated by some registered finalizer.
34+
StatusUpdated bool
35+
}
36+
37+
// NewFinalizers returns the Finalizers interface
38+
func NewFinalizers() Finalizers {
39+
return finalizers{}
40+
}
41+
42+
func (f finalizers) Register(key string, finalizer Finalizer) error {
43+
if _, ok := f[key]; ok {
44+
return fmt.Errorf("finalizer for key %q already registered", key)
45+
}
46+
f[key] = finalizer
47+
return nil
48+
}
49+
50+
func (f finalizers) Finalize(ctx context.Context, obj client.Object) (Result, error) {
51+
var (
52+
res Result
53+
errList []error
54+
)
55+
res.Updated = false
56+
for key, finalizer := range f {
57+
if dt := obj.GetDeletionTimestamp(); dt.IsZero() && !controllerutil.ContainsFinalizer(obj, key) {
58+
controllerutil.AddFinalizer(obj, key)
59+
res.Updated = true
60+
} else if !dt.IsZero() && controllerutil.ContainsFinalizer(obj, key) {
61+
finalizerRes, err := finalizer.Finalize(ctx, obj)
62+
if err != nil {
63+
// Even when the finalizer fails, it may need to signal to update the primary
64+
// object (e.g. it may set a condition and need a status update).
65+
res.Updated = res.Updated || finalizerRes.Updated
66+
res.StatusUpdated = res.StatusUpdated || finalizerRes.StatusUpdated
67+
errList = append(errList, fmt.Errorf("finalizer %q failed: %v", key, err))
68+
} else {
69+
// If the finalizer succeeds, we remove the finalizer from the primary
70+
// object's metadata, so we know it will need an update.
71+
res.Updated = true
72+
controllerutil.RemoveFinalizer(obj, key)
73+
// The finalizer may have updated the status too.
74+
res.StatusUpdated = res.StatusUpdated || finalizerRes.StatusUpdated
75+
}
76+
}
77+
}
78+
return res, utilerrors.NewAggregate(errList)
79+
}

pkg/finalizer/finalizer_test.go

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
package finalizer
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"testing"
7+
8+
. "github.com/onsi/ginkgo"
9+
. "github.com/onsi/gomega"
10+
corev1 "k8s.io/api/core/v1"
11+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
12+
"sigs.k8s.io/controller-runtime/pkg/client"
13+
"sigs.k8s.io/controller-runtime/pkg/envtest/printer"
14+
)
15+
16+
type mockFinalizer struct {
17+
result Result
18+
err error
19+
}
20+
21+
func (f mockFinalizer) Finalize(context.Context, client.Object) (Result, error) {
22+
return f.result, f.err
23+
}
24+
func TestFinalizer(t *testing.T) {
25+
RegisterFailHandler(Fail)
26+
suiteName := "Finalizer Suite"
27+
RunSpecsWithDefaultAndCustomReporters(t, suiteName, []Reporter{printer.NewlineReporter{}, printer.NewProwReporter(suiteName)})
28+
}
29+
30+
var _ = Describe("TestFinalizer", func() {
31+
var err error
32+
var pod *corev1.Pod
33+
var finalizers Finalizers
34+
var f mockFinalizer
35+
BeforeEach(func() {
36+
pod = &corev1.Pod{
37+
ObjectMeta: metav1.ObjectMeta{},
38+
}
39+
finalizers = NewFinalizers()
40+
f = mockFinalizer{}
41+
})
42+
Describe("Register", func() {
43+
It("successfully registers a finalizer", func() {
44+
err = finalizers.Register("finalizers.sigs.k8s.io/testfinalizer", f)
45+
Expect(err).To(BeNil())
46+
})
47+
48+
It("should fail when trying to register a finalizer that was already registered", func() {
49+
err = finalizers.Register("finalizers.sigs.k8s.io/testfinalizer", f)
50+
Expect(err).To(BeNil())
51+
52+
// calling Register again with the same key should return an error
53+
err = finalizers.Register("finalizers.sigs.k8s.io/testfinalizer", f)
54+
Expect(err).NotTo(BeNil())
55+
Expect(err.Error()).To(ContainSubstring("already registered"))
56+
57+
})
58+
})
59+
60+
Describe("Finalize", func() {
61+
It("successfully finalizes and returns true for Updated when deletion timestamp is nil and finalizer does not exist", func() {
62+
err = finalizers.Register("finalizers.sigs.k8s.io/testfinalizer", f)
63+
Expect(err).To(BeNil())
64+
65+
pod.DeletionTimestamp = nil
66+
pod.Finalizers = []string{}
67+
68+
result, err := finalizers.Finalize(context.TODO(), pod)
69+
Expect(err).To(BeNil())
70+
Expect(result.Updated).To(BeTrue())
71+
// when deletion timestamp is nil and finalizer is not present, the registered finalizer would be added to the obj
72+
Expect(len(pod.Finalizers)).To(Equal(1))
73+
Expect(pod.Finalizers[0]).To(Equal("finalizers.sigs.k8s.io/testfinalizer"))
74+
75+
})
76+
77+
It("successfully finalizes and returns true for Updated when deletion timestamp is not nil and the finalizer exists", func() {
78+
now := metav1.Now()
79+
pod.DeletionTimestamp = &now
80+
81+
err = finalizers.Register("finalizers.sigs.k8s.io/testfinalizer", f)
82+
Expect(err).To(BeNil())
83+
84+
pod.Finalizers = []string{"finalizers.sigs.k8s.io/testfinalizer"}
85+
86+
result, err := finalizers.Finalize(context.TODO(), pod)
87+
Expect(err).To(BeNil())
88+
Expect(result.Updated).To(BeTrue())
89+
// finalizer will be removed from the obj upon successful finalization
90+
Expect(len(pod.Finalizers)).To(Equal(0))
91+
})
92+
93+
It("should return no error and return false for Updated when deletion timestamp is nil and finalizer doesn't exist", func() {
94+
pod.DeletionTimestamp = nil
95+
pod.Finalizers = []string{}
96+
97+
result, err := finalizers.Finalize(context.TODO(), pod)
98+
Expect(err).To(BeNil())
99+
Expect(result.Updated).To(BeFalse())
100+
Expect(len(pod.Finalizers)).To(Equal(0))
101+
102+
})
103+
104+
It("should return no error and return false for Updated when deletion timestamp is not nil and the finalizer doesn't exist", func() {
105+
now := metav1.Now()
106+
pod.DeletionTimestamp = &now
107+
pod.Finalizers = []string{}
108+
109+
result, err := finalizers.Finalize(context.TODO(), pod)
110+
Expect(err).To(BeNil())
111+
Expect(result.Updated).To(BeFalse())
112+
Expect(len(pod.Finalizers)).To(Equal(0))
113+
114+
})
115+
116+
It("successfully finalizes multiple finalizers and returns true for Updated when deletion timestamp is not nil and the finalizer exists", func() {
117+
now := metav1.Now()
118+
pod.DeletionTimestamp = &now
119+
120+
err = finalizers.Register("finalizers.sigs.k8s.io/testfinalizer", f)
121+
Expect(err).To(BeNil())
122+
123+
err = finalizers.Register("finalizers.sigs.k8s.io/newtestfinalizer", f)
124+
Expect(err).To(BeNil())
125+
126+
pod.Finalizers = []string{"finalizers.sigs.k8s.io/testfinalizer", "finalizers.sigs.k8s.io/newtestfinalizer"}
127+
128+
result, err := finalizers.Finalize(context.TODO(), pod)
129+
Expect(err).To(BeNil())
130+
Expect(result.Updated).To(BeTrue())
131+
Expect(result.StatusUpdated).To(BeFalse())
132+
Expect(len(pod.Finalizers)).To(Equal(0))
133+
})
134+
135+
It("should return result as false and a non-nil error", func() {
136+
now := metav1.Now()
137+
pod.DeletionTimestamp = &now
138+
pod.Finalizers = []string{"finalizers.sigs.k8s.io/testfinalizer"}
139+
140+
f.result.Updated = false
141+
f.result.StatusUpdated = false
142+
f.err = fmt.Errorf("finalizer failed for %q", pod.Finalizers[0])
143+
144+
err = finalizers.Register("finalizers.sigs.k8s.io/testfinalizer", f)
145+
Expect(err).To(BeNil())
146+
147+
result, err := finalizers.Finalize(context.TODO(), pod)
148+
Expect(err).ToNot(BeNil())
149+
Expect(err.Error()).To(ContainSubstring("finalizer failed"))
150+
Expect(result.Updated).To(BeFalse())
151+
Expect(result.StatusUpdated).To(BeFalse())
152+
Expect(len(pod.Finalizers)).To(Equal(1))
153+
Expect(pod.Finalizers[0]).To(Equal("finalizers.sigs.k8s.io/testfinalizer"))
154+
})
155+
156+
It("should return expected result values and error values when registering multiple finalizers", func() {
157+
now := metav1.Now()
158+
pod.DeletionTimestamp = &now
159+
pod.Finalizers = []string{
160+
"finalizers.sigs.k8s.io/testfinalizer1",
161+
"finalizers.sigs.k8s.io/testfinalizer2",
162+
"finalizers.sigs.k8s.io/testfinalizer3",
163+
}
164+
165+
// registering multiple finalizers with different return values
166+
// test for Updated as true, and nil error
167+
f.result.Updated = true
168+
f.result.StatusUpdated = false
169+
f.err = nil
170+
err = finalizers.Register("finalizers.sigs.k8s.io/testfinalizer1", f)
171+
Expect(err).To(BeNil())
172+
173+
result, err := finalizers.Finalize(context.TODO(), pod)
174+
Expect(err).To(BeNil())
175+
Expect(result.Updated).To(BeTrue())
176+
Expect(result.StatusUpdated).To(BeFalse())
177+
// `finalizers.sigs.k8s.io/testfinalizer1` will be removed from the list
178+
// of finalizers, so length will be 2.
179+
Expect(len(pod.Finalizers)).To(Equal(2))
180+
Expect(pod.Finalizers[0]).To(Equal("finalizers.sigs.k8s.io/testfinalizer2"))
181+
Expect(pod.Finalizers[1]).To(Equal("finalizers.sigs.k8s.io/testfinalizer3"))
182+
183+
// test for Updated and StatusUpdated as false, and non-nil error
184+
f.result.Updated = false
185+
f.result.StatusUpdated = false
186+
f.err = fmt.Errorf("finalizer failed")
187+
err = finalizers.Register("finalizers.sigs.k8s.io/testfinalizer2", f)
188+
Expect(err).To(BeNil())
189+
190+
result, err = finalizers.Finalize(context.TODO(), pod)
191+
Expect(err).ToNot(BeNil())
192+
Expect(err.Error()).To(ContainSubstring("finalizer failed"))
193+
Expect(result.Updated).To(BeFalse())
194+
Expect(result.StatusUpdated).To(BeFalse())
195+
Expect(len(pod.Finalizers)).To(Equal(2))
196+
Expect(pod.Finalizers[0]).To(Equal("finalizers.sigs.k8s.io/testfinalizer2"))
197+
Expect(pod.Finalizers[1]).To(Equal("finalizers.sigs.k8s.io/testfinalizer3"))
198+
199+
// test for result as true, and non-nil error
200+
f.result.Updated = true
201+
f.result.StatusUpdated = true
202+
f.err = fmt.Errorf("finalizer failed")
203+
err = finalizers.Register("finalizers.sigs.k8s.io/testfinalizer3", f)
204+
Expect(err).To(BeNil())
205+
206+
result, err = finalizers.Finalize(context.TODO(), pod)
207+
Expect(err).ToNot(BeNil())
208+
Expect(err.Error()).To(ContainSubstring("finalizer failed"))
209+
Expect(result.Updated).To(BeTrue())
210+
Expect(result.StatusUpdated).To(BeTrue())
211+
Expect(len(pod.Finalizers)).To(Equal(2))
212+
Expect(pod.Finalizers[0]).To(Equal("finalizers.sigs.k8s.io/testfinalizer2"))
213+
Expect(pod.Finalizers[1]).To(Equal("finalizers.sigs.k8s.io/testfinalizer3"))
214+
})
215+
})
216+
})

pkg/finalizer/types.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
Copyright 2021 The Kubernetes Authors.
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
http://www.apache.org/licenses/LICENSE-2.0
7+
Unless required by applicable law or agreed to in writing, software
8+
distributed under the License is distributed on an "AS IS" BASIS,
9+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
See the License for the specific language governing permissions and
11+
limitations under the License.
12+
*/
13+
14+
package finalizer
15+
16+
import (
17+
"context"
18+
19+
"sigs.k8s.io/controller-runtime/pkg/client"
20+
)
21+
22+
// Registerer holds Register that will check if a key is already registered
23+
// and error out and it does; and if not registered, it will add the finalizer
24+
// to the finalizers map as the value for the provided key
25+
type Registerer interface {
26+
Register(key string, f Finalizer) error
27+
}
28+
29+
// Finalizer holds Finalize that will add/remove a finalizer based on the
30+
// deletion timestamp being set and return an indication of whether the
31+
// obj needs an update or not
32+
type Finalizer interface {
33+
Finalize(context.Context, client.Object) (Result, error)
34+
}
35+
36+
// Finalizers implements Registerer and Finalizer to finalize all registered
37+
// finalizers if the provided object has a deletion timestamp or set all
38+
// registered finalizers if it does not
39+
type Finalizers interface {
40+
Registerer
41+
Finalizer
42+
}

0 commit comments

Comments
 (0)