Skip to content

Commit df2741d

Browse files
Merge pull request #1938 from joelanford/fix/zombie-operator
Bug 1899588: Only re-create operator resource if it has existing components
2 parents 0373c9b + 035683f commit df2741d

File tree

3 files changed

+125
-6
lines changed

3 files changed

+125
-6
lines changed

pkg/controller/operators/operator_controller.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
rbacv1 "k8s.io/api/rbac/v1"
1212
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
1313
apierrors "k8s.io/apimachinery/pkg/api/errors"
14+
"k8s.io/apimachinery/pkg/api/meta"
1415
"k8s.io/apimachinery/pkg/labels"
1516
"k8s.io/apimachinery/pkg/runtime"
1617
"k8s.io/apimachinery/pkg/types"
@@ -119,6 +120,12 @@ func (r *OperatorReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c
119120
in := &operatorsv1.Operator{}
120121
if err := r.Get(ctx, req.NamespacedName, in); err != nil {
121122
if apierrors.IsNotFound(err) {
123+
// If the Operator instance is not found, we're likely reconciling because
124+
// of a DELETE event. Only recreate the Operator if any of its components
125+
// still exist.
126+
if exists, err := r.hasExistingComponents(ctx, name); err != nil || !exists {
127+
return reconcile.Result{}, err
128+
}
122129
create = true
123130
in.SetName(name)
124131
} else {
@@ -219,6 +226,33 @@ func (r *OperatorReconciler) listComponents(ctx context.Context, selector labels
219226
return componentLists, nil
220227
}
221228

229+
func (r *OperatorReconciler) hasExistingComponents(ctx context.Context, name string) (bool, error) {
230+
op := &operatorsv1.Operator{}
231+
op.SetName(name)
232+
operator := decorators.Operator{Operator: op}
233+
234+
selector, err := operator.ComponentSelector()
235+
if err != nil {
236+
return false, err
237+
}
238+
239+
components, err := r.listComponents(ctx, selector)
240+
if err != nil {
241+
return false, err
242+
}
243+
244+
for _, list := range components {
245+
items, err := meta.ExtractList(list)
246+
if err != nil {
247+
return false, fmt.Errorf("Unable to extract list from runtime.Object")
248+
}
249+
if len(items) > 0 {
250+
return true, nil
251+
}
252+
}
253+
return false, nil
254+
}
255+
222256
func (r *OperatorReconciler) getLastResourceVersion(name types.NamespacedName) (string, bool) {
223257
r.mu.RLock()
224258
defer r.mu.RUnlock()

pkg/controller/operators/operator_controller_test.go

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
. "github.com/onsi/gomega"
88
corev1 "k8s.io/api/core/v1"
99
rbacv1 "k8s.io/api/rbac/v1"
10+
apierrors "k8s.io/apimachinery/pkg/api/errors"
1011
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1112
"k8s.io/apimachinery/pkg/runtime"
1213
"k8s.io/apimachinery/pkg/types"
@@ -43,9 +44,52 @@ var _ = Describe("Operator Controller", func() {
4344
Expect(k8sClient.Create(ctx, operator)).To(Succeed())
4445
})
4546

46-
AfterEach(func() {
47-
Expect(k8sClient.Get(ctx, name, operator)).To(Succeed())
48-
Expect(k8sClient.Delete(ctx, operator, deleteOpts)).To(Succeed())
47+
Describe("operator deletion", func() {
48+
JustBeforeEach(func() {
49+
Expect(k8sClient.Delete(ctx, operator)).To(Succeed())
50+
Eventually(func() bool {
51+
err := k8sClient.Get(ctx, name, operator)
52+
return apierrors.IsNotFound(err)
53+
}, timeout, interval).Should(BeTrue())
54+
})
55+
Context("with components bearing its label", func() {
56+
var (
57+
objs []runtime.Object
58+
namespace string
59+
)
60+
61+
BeforeEach(func() {
62+
namespace = genName("ns-")
63+
objs = testobj.WithLabel(expectedKey, "",
64+
testobj.WithName(namespace, &corev1.Namespace{}),
65+
)
66+
67+
for _, obj := range objs {
68+
Expect(k8sClient.Create(ctx, obj.(client.Object))).To(Succeed())
69+
}
70+
})
71+
72+
AfterEach(func() {
73+
for _, obj := range objs {
74+
Expect(k8sClient.Delete(ctx, obj.(client.Object), deleteOpts)).To(Succeed())
75+
}
76+
Expect(k8sClient.Delete(ctx, operator, deleteOpts)).To(Succeed())
77+
})
78+
79+
It("should re-create it", func() {
80+
Eventually(func() error {
81+
return k8sClient.Get(ctx, name, operator)
82+
}).Should(Succeed())
83+
})
84+
})
85+
Context("with no components bearing its label", func() {
86+
It("should not re-create it", func() {
87+
Consistently(func() bool {
88+
err := k8sClient.Get(ctx, name, operator)
89+
return apierrors.IsNotFound(err)
90+
}, timeout, interval).Should(BeTrue())
91+
})
92+
})
4993
})
5094

5195
Describe("component selection", func() {
@@ -61,6 +105,10 @@ var _ = Describe("Operator Controller", func() {
61105
}, timeout, interval).Should(Equal(expectedComponentLabelSelector))
62106
})
63107

108+
AfterEach(func() {
109+
Expect(k8sClient.Delete(ctx, operator, deleteOpts)).To(Succeed())
110+
})
111+
64112
Context("with no components bearing its label", func() {
65113
Specify("a status containing no component references", func() {
66114
Consistently(func() ([]operatorsv1.RichReference, error) {
@@ -92,7 +140,6 @@ var _ = Describe("Operator Controller", func() {
92140

93141
AfterEach(func() {
94142
for _, obj := range objs {
95-
Expect(k8sClient.Get(ctx, testobj.NamespacedName(obj), obj.(client.Object))).To(Succeed())
96143
Expect(k8sClient.Delete(ctx, obj.(client.Object), deleteOpts)).To(Succeed())
97144
}
98145
})

test/e2e/operator_test.go

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,12 @@ var _ = Describe("Operator", func() {
6464
// 8. Ensure o's status.components.refs field eventually contains references to sa-a and sa-b
6565
// 9. Remove the component label from sa-b
6666
// 10. Ensure the reference to sa-b is eventually removed from o's status.components.refs field
67-
// 11. Delete ns-a
68-
// 12. Ensure the reference to ns-a is eventually removed from o's status.components.refs field
67+
// 11. Delete o
68+
// 12. Ensure o is re-created
69+
// 13. Delete ns-a
70+
// 14. Ensure the reference to ns-a is eventually removed from o's status.components.refs field
71+
// 15. Delete o
72+
// 16. Ensure o is not re-created
6973
It("should surface components in its status", func() {
7074
o := &operatorsv1.Operator{}
7175
o.SetName(genName("o-"))
@@ -184,6 +188,21 @@ var _ = Describe("Operator", func() {
184188
By("removing a component's reference when it no longer bears the component label")
185189
componentRefEventuallyExists(w, false, getReference(scheme, saB))
186190

191+
// Delete o
192+
Eventually(func() error {
193+
err := client.Delete(clientCtx, o)
194+
if err != nil && !apierrors.IsNotFound(err) {
195+
return err
196+
}
197+
return nil
198+
}).Should(Succeed())
199+
200+
// Ensure that o is eventually recreated (because some of its components still exist).
201+
By("recreating the Operator when any components still exist")
202+
Eventually(func() error {
203+
return client.Get(clientCtx, types.NamespacedName{Name: o.GetName()}, o)
204+
}).Should(Succeed())
205+
187206
// Delete ns-a
188207
Eventually(func() error {
189208
err := client.Delete(clientCtx, nsA)
@@ -196,6 +215,25 @@ var _ = Describe("Operator", func() {
196215
// Ensure the reference to ns-a is eventually removed from o's status.components.refs field
197216
By("removing a component's reference when it no longer exists")
198217
componentRefEventuallyExists(w, false, getReference(scheme, nsA))
218+
219+
// Delete o
220+
Eventually(func() error {
221+
err := client.Delete(clientCtx, o)
222+
if apierrors.IsNotFound(err) {
223+
return nil
224+
}
225+
return err
226+
}).Should(Succeed())
227+
228+
// Ensure that o is consistently not found
229+
By("verifying the Operator is permanently deleted if it has no components")
230+
Consistently(func() error {
231+
err := client.Get(clientCtx, types.NamespacedName{Name: o.GetName()}, o)
232+
if apierrors.IsNotFound(err) {
233+
return nil
234+
}
235+
return err
236+
}).Should(Succeed())
199237
})
200238

201239
Context("when a subscription to a package exists", func() {

0 commit comments

Comments
 (0)