Skip to content

Commit 52bbccb

Browse files
committed
refactor and add unit tests for operator reconciler
1 parent 2d8dea8 commit 52bbccb

File tree

4 files changed

+366
-151
lines changed

4 files changed

+366
-151
lines changed

controllers/operator_controller.go

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -40,17 +40,8 @@ import (
4040
// OperatorReconciler reconciles a Operator object
4141
type OperatorReconciler struct {
4242
client.Client
43-
Scheme *runtime.Scheme
44-
45-
resolver *resolution.OperatorResolver
46-
}
47-
48-
func NewOperatorReconciler(c client.Client, s *runtime.Scheme, r *resolution.OperatorResolver) *OperatorReconciler {
49-
return &OperatorReconciler{
50-
Client: c,
51-
Scheme: s,
52-
resolver: r,
53-
}
43+
Scheme *runtime.Scheme
44+
Resolver *resolution.OperatorResolver
5445
}
5546

5647
//+kubebuilder:rbac:groups=operators.operatorframework.io,resources=operators,verbs=get;list;watch
@@ -120,9 +111,9 @@ func (r *OperatorReconciler) reconcile(ctx context.Context, op *operatorsv1alpha
120111
var message = "resolution was successful"
121112

122113
// run resolution
123-
solution, err := r.resolver.Resolve(ctx)
114+
solution, err := r.Resolver.Resolve(ctx)
124115
if err != nil {
125-
status = metav1.ConditionTrue
116+
status = metav1.ConditionFalse
126117
reason = operatorsv1alpha1.ReasonResolutionFailed
127118
message = err.Error()
128119
} else {
Lines changed: 344 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,344 @@
1+
package controllers_test
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
. "github.com/onsi/ginkgo/v2"
8+
. "github.com/onsi/gomega"
9+
"github.com/operator-framework/deppy/pkg/deppy"
10+
"github.com/operator-framework/deppy/pkg/deppy/input"
11+
rukpakv1alpha1 "github.com/operator-framework/rukpak/api/v1alpha1"
12+
apimeta "k8s.io/apimachinery/pkg/api/meta"
13+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
14+
"k8s.io/apimachinery/pkg/types"
15+
"k8s.io/apimachinery/pkg/util/rand"
16+
"k8s.io/utils/pointer"
17+
ctrl "sigs.k8s.io/controller-runtime"
18+
"sigs.k8s.io/controller-runtime/pkg/client"
19+
20+
operatorsv1alpha1 "github.com/operator-framework/operator-controller/api/v1alpha1"
21+
"github.com/operator-framework/operator-controller/controllers"
22+
"github.com/operator-framework/operator-controller/internal/resolution"
23+
operatorutil "github.com/operator-framework/operator-controller/internal/util"
24+
)
25+
26+
var _ = Describe("Reconcile Test", func() {
27+
var (
28+
ctx context.Context
29+
reconciler *controllers.OperatorReconciler
30+
)
31+
BeforeEach(func() {
32+
ctx = context.Background()
33+
reconciler = &controllers.OperatorReconciler{
34+
Client: cl,
35+
Scheme: sch,
36+
Resolver: resolution.NewOperatorResolver(cl, testEntitySource),
37+
}
38+
})
39+
When("the operator does not exist", func() {
40+
It("returns no error", func() {
41+
res, err := reconciler.Reconcile(context.Background(), ctrl.Request{NamespacedName: types.NamespacedName{Name: "non-existent"}})
42+
Expect(res).To(Equal(ctrl.Result{}))
43+
Expect(err).NotTo(HaveOccurred())
44+
})
45+
})
46+
When("the operator exists", func() {
47+
var (
48+
operator *operatorsv1alpha1.Operator
49+
opKey types.NamespacedName
50+
)
51+
BeforeEach(func() {
52+
opKey = types.NamespacedName{Name: fmt.Sprintf("operator-test-%s", rand.String(8))}
53+
})
54+
When("the operator specifies a non-existent package", func() {
55+
var pkgName string
56+
BeforeEach(func() {
57+
By("initializing cluster state")
58+
pkgName = fmt.Sprintf("non-existent-%s", rand.String(6))
59+
operator = &operatorsv1alpha1.Operator{
60+
ObjectMeta: metav1.ObjectMeta{Name: opKey.Name},
61+
Spec: operatorsv1alpha1.OperatorSpec{PackageName: pkgName},
62+
}
63+
err := cl.Create(ctx, operator)
64+
Expect(err).NotTo(HaveOccurred())
65+
})
66+
It("sets resolution failure status", func() {
67+
By("running reconcile")
68+
res, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: opKey})
69+
Expect(res).To(Equal(ctrl.Result{}))
70+
// TODO: should resolution failure return an error?
71+
Expect(err).NotTo(HaveOccurred())
72+
73+
By("fetching updated operator after reconcile")
74+
Expect(cl.Get(ctx, opKey, operator)).NotTo(HaveOccurred())
75+
76+
By("checking the expected conditions")
77+
cond := apimeta.FindStatusCondition(operator.Status.Conditions, operatorsv1alpha1.TypeReady)
78+
Expect(cond).NotTo(BeNil())
79+
Expect(cond.Status).To(Equal(metav1.ConditionFalse))
80+
Expect(cond.Reason).To(Equal(operatorsv1alpha1.ReasonResolutionFailed))
81+
Expect(cond.Message).To(Equal(fmt.Sprintf("package '%s' not found", pkgName)))
82+
})
83+
})
84+
When("the operator specifies a valid available package", func() {
85+
const pkgName = "prometheus"
86+
BeforeEach(func() {
87+
By("initializing cluster state")
88+
operator = &operatorsv1alpha1.Operator{
89+
ObjectMeta: metav1.ObjectMeta{Name: opKey.Name},
90+
Spec: operatorsv1alpha1.OperatorSpec{PackageName: pkgName},
91+
}
92+
err := cl.Create(ctx, operator)
93+
Expect(err).NotTo(HaveOccurred())
94+
})
95+
When("the BundleDeployment does not exist", func() {
96+
BeforeEach(func() {
97+
By("running reconcile")
98+
res, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: opKey})
99+
Expect(res).To(Equal(ctrl.Result{}))
100+
Expect(err).NotTo(HaveOccurred())
101+
102+
By("fetching updated operator after reconcile")
103+
Expect(cl.Get(ctx, opKey, operator)).NotTo(HaveOccurred())
104+
})
105+
It("results in the expected BundleDeployment", func() {
106+
bd := &rukpakv1alpha1.BundleDeployment{}
107+
err := cl.Get(ctx, types.NamespacedName{Name: opKey.Name}, bd)
108+
Expect(err).NotTo(HaveOccurred())
109+
Expect(bd.Spec.ProvisionerClassName).To(Equal("core-rukpak-io-plain"))
110+
Expect(bd.Spec.Template.Spec.ProvisionerClassName).To(Equal("core-rukpak-io-registry"))
111+
Expect(bd.Spec.Template.Spec.Source.Type).To(Equal(rukpakv1alpha1.SourceTypeImage))
112+
Expect(bd.Spec.Template.Spec.Source.Image).NotTo(BeNil())
113+
Expect(bd.Spec.Template.Spec.Source.Image.Ref).To(Equal("quay.io/operatorhubio/prometheus@sha256:5b04c49d8d3eff6a338b56ec90bdf491d501fe301c9cdfb740e5bff6769a21ed"))
114+
})
115+
It("sets resolution success status", func() {
116+
cond := apimeta.FindStatusCondition(operator.Status.Conditions, operatorsv1alpha1.TypeReady)
117+
Expect(cond).NotTo(BeNil())
118+
Expect(cond.Status).To(Equal(metav1.ConditionTrue))
119+
Expect(cond.Reason).To(Equal(operatorsv1alpha1.ReasonResolutionSucceeded))
120+
Expect(cond.Message).To(Equal("resolution was successful"))
121+
})
122+
})
123+
When("the expected BundleDeployment already exists", func() {
124+
BeforeEach(func() {
125+
By("patching the existing BD")
126+
err := cl.Create(ctx, &rukpakv1alpha1.BundleDeployment{
127+
ObjectMeta: metav1.ObjectMeta{
128+
Name: opKey.Name,
129+
OwnerReferences: []metav1.OwnerReference{
130+
{
131+
APIVersion: operatorsv1alpha1.GroupVersion.String(),
132+
Kind: "Operator",
133+
Name: operator.Name,
134+
UID: operator.UID,
135+
Controller: pointer.Bool(true),
136+
BlockOwnerDeletion: pointer.Bool(true),
137+
},
138+
},
139+
},
140+
Spec: rukpakv1alpha1.BundleDeploymentSpec{
141+
ProvisionerClassName: "core-rukpak-io-plain",
142+
Template: &rukpakv1alpha1.BundleTemplate{
143+
Spec: rukpakv1alpha1.BundleSpec{
144+
ProvisionerClassName: "core-rukpak-io-registry",
145+
Source: rukpakv1alpha1.BundleSource{
146+
Type: rukpakv1alpha1.SourceTypeImage,
147+
Image: &rukpakv1alpha1.ImageSource{
148+
Ref: "quay.io/operatorhubio/prometheus@sha256:5b04c49d8d3eff6a338b56ec90bdf491d501fe301c9cdfb740e5bff6769a21ed",
149+
},
150+
},
151+
},
152+
},
153+
},
154+
})
155+
Expect(err).NotTo(HaveOccurred())
156+
157+
By("running reconcile")
158+
res, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: opKey})
159+
Expect(res).To(Equal(ctrl.Result{}))
160+
Expect(err).NotTo(HaveOccurred())
161+
162+
By("fetching updated operator after reconcile")
163+
Expect(cl.Get(ctx, opKey, operator)).NotTo(HaveOccurred())
164+
})
165+
PIt("does not patch the BundleDeployment", func() {
166+
// TODO: verify that no patch call is made.
167+
})
168+
It("results in the expected BundleDeployment", func() {
169+
bd := &rukpakv1alpha1.BundleDeployment{}
170+
err := cl.Get(ctx, types.NamespacedName{Name: opKey.Name}, bd)
171+
Expect(err).NotTo(HaveOccurred())
172+
Expect(bd.Spec.ProvisionerClassName).To(Equal("core-rukpak-io-plain"))
173+
Expect(bd.Spec.Template.Spec.ProvisionerClassName).To(Equal("core-rukpak-io-registry"))
174+
Expect(bd.Spec.Template.Spec.Source.Type).To(Equal(rukpakv1alpha1.SourceTypeImage))
175+
Expect(bd.Spec.Template.Spec.Source.Image).NotTo(BeNil())
176+
Expect(bd.Spec.Template.Spec.Source.Image.Ref).To(Equal("quay.io/operatorhubio/prometheus@sha256:5b04c49d8d3eff6a338b56ec90bdf491d501fe301c9cdfb740e5bff6769a21ed"))
177+
})
178+
It("sets resolution success status", func() {
179+
cond := apimeta.FindStatusCondition(operator.Status.Conditions, operatorsv1alpha1.TypeReady)
180+
Expect(cond).NotTo(BeNil())
181+
Expect(cond.Status).To(Equal(metav1.ConditionTrue))
182+
Expect(cond.Reason).To(Equal(operatorsv1alpha1.ReasonResolutionSucceeded))
183+
Expect(cond.Message).To(Equal("resolution was successful"))
184+
})
185+
})
186+
When("an out-of-date BundleDeployment exists", func() {
187+
BeforeEach(func() {
188+
By("creating the expected BD")
189+
err := cl.Create(ctx, &rukpakv1alpha1.BundleDeployment{
190+
ObjectMeta: metav1.ObjectMeta{Name: opKey.Name},
191+
Spec: rukpakv1alpha1.BundleDeploymentSpec{
192+
ProvisionerClassName: "foo",
193+
Template: &rukpakv1alpha1.BundleTemplate{
194+
Spec: rukpakv1alpha1.BundleSpec{
195+
ProvisionerClassName: "bar",
196+
Source: rukpakv1alpha1.BundleSource{
197+
Type: rukpakv1alpha1.SourceTypeHTTP,
198+
HTTP: &rukpakv1alpha1.HTTPSource{
199+
URL: "http://localhost:8080/",
200+
},
201+
},
202+
},
203+
},
204+
},
205+
})
206+
Expect(err).NotTo(HaveOccurred())
207+
208+
By("running reconcile")
209+
res, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: opKey})
210+
Expect(res).To(Equal(ctrl.Result{}))
211+
Expect(err).NotTo(HaveOccurred())
212+
213+
By("fetching updated operator after reconcile")
214+
Expect(cl.Get(ctx, opKey, operator)).NotTo(HaveOccurred())
215+
})
216+
It("results in the expected BundleDeployment", func() {
217+
bd := &rukpakv1alpha1.BundleDeployment{}
218+
err := cl.Get(ctx, types.NamespacedName{Name: opKey.Name}, bd)
219+
Expect(err).NotTo(HaveOccurred())
220+
Expect(bd.Spec.ProvisionerClassName).To(Equal("core-rukpak-io-plain"))
221+
Expect(bd.Spec.Template.Spec.ProvisionerClassName).To(Equal("core-rukpak-io-registry"))
222+
Expect(bd.Spec.Template.Spec.Source.Type).To(Equal(rukpakv1alpha1.SourceTypeImage))
223+
Expect(bd.Spec.Template.Spec.Source.Image).NotTo(BeNil())
224+
Expect(bd.Spec.Template.Spec.Source.Image.Ref).To(Equal("quay.io/operatorhubio/prometheus@sha256:5b04c49d8d3eff6a338b56ec90bdf491d501fe301c9cdfb740e5bff6769a21ed"))
225+
})
226+
It("sets resolution success status", func() {
227+
cond := apimeta.FindStatusCondition(operator.Status.Conditions, operatorsv1alpha1.TypeReady)
228+
Expect(cond).NotTo(BeNil())
229+
Expect(cond.Status).To(Equal(metav1.ConditionTrue))
230+
Expect(cond.Reason).To(Equal(operatorsv1alpha1.ReasonResolutionSucceeded))
231+
Expect(cond.Message).To(Equal("resolution was successful"))
232+
})
233+
})
234+
})
235+
When("the selected bundle's image ref cannot be parsed", func() {
236+
const pkgName = "badimage"
237+
BeforeEach(func() {
238+
By("initializing cluster state")
239+
operator = &operatorsv1alpha1.Operator{
240+
ObjectMeta: metav1.ObjectMeta{Name: opKey.Name},
241+
Spec: operatorsv1alpha1.OperatorSpec{PackageName: pkgName},
242+
}
243+
err := cl.Create(ctx, operator)
244+
Expect(err).NotTo(HaveOccurred())
245+
})
246+
PIt("sets resolution failure status and returns an error", func() {
247+
By("running reconcile")
248+
res, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: opKey})
249+
Expect(res).To(Equal(ctrl.Result{}))
250+
Expect(err).To(MatchError(ContainSubstring(`error determining bundle path for entity`)))
251+
252+
By("fetching updated operator after reconcile")
253+
Expect(cl.Get(ctx, opKey, operator)).NotTo(HaveOccurred())
254+
255+
By("checking the expected conditions")
256+
// TODO: there should be a condition update that sets Ready false in this scenario
257+
})
258+
})
259+
When("the operator specifies a duplicate package", func() {
260+
const pkgName = "prometheus"
261+
BeforeEach(func() {
262+
By("initializing cluster state")
263+
err := cl.Create(ctx, &operatorsv1alpha1.Operator{
264+
ObjectMeta: metav1.ObjectMeta{Name: fmt.Sprintf("orig-%s", opKey.Name)},
265+
Spec: operatorsv1alpha1.OperatorSpec{PackageName: pkgName},
266+
})
267+
Expect(err).NotTo(HaveOccurred())
268+
269+
operator = &operatorsv1alpha1.Operator{
270+
ObjectMeta: metav1.ObjectMeta{Name: opKey.Name},
271+
Spec: operatorsv1alpha1.OperatorSpec{PackageName: pkgName},
272+
}
273+
err = cl.Create(ctx, operator)
274+
Expect(err).NotTo(HaveOccurred())
275+
})
276+
It("sets resolution failure status", func() {
277+
By("running reconcile")
278+
res, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: opKey})
279+
Expect(res).To(Equal(ctrl.Result{}))
280+
// TODO: should this scenario return an error?
281+
Expect(err).NotTo(HaveOccurred())
282+
283+
By("fetching updated operator after reconcile")
284+
Expect(cl.Get(ctx, opKey, operator)).NotTo(HaveOccurred())
285+
286+
By("checking the expected conditions")
287+
cond := apimeta.FindStatusCondition(operator.Status.Conditions, operatorsv1alpha1.TypeReady)
288+
Expect(cond).NotTo(BeNil())
289+
Expect(cond.Status).To(Equal(metav1.ConditionFalse))
290+
Expect(cond.Reason).To(Equal(operatorsv1alpha1.ReasonResolutionFailed))
291+
Expect(cond.Message).To(Equal(`duplicate identifier "required package prometheus" in input`))
292+
})
293+
})
294+
AfterEach(func() {
295+
verifyInvariants(ctx, operator)
296+
297+
err := cl.Delete(ctx, operator)
298+
Expect(err).To(Not(HaveOccurred()))
299+
})
300+
})
301+
})
302+
303+
func verifyInvariants(ctx context.Context, op *operatorsv1alpha1.Operator) {
304+
key := client.ObjectKeyFromObject(op)
305+
err := cl.Get(ctx, key, op)
306+
Expect(err).To(BeNil())
307+
308+
verifyConditionsInvariants(op)
309+
}
310+
311+
func verifyConditionsInvariants(op *operatorsv1alpha1.Operator) {
312+
// Expect that the operator's set of conditions contains all defined
313+
// condition types for the Operator API. Every reconcile should always
314+
// ensure every condition type's status/reason/message reflects the state
315+
// read during _this_ reconcile call.
316+
Expect(op.Status.Conditions).To(HaveLen(len(operatorutil.ConditionTypes)))
317+
for _, t := range operatorutil.ConditionTypes {
318+
cond := apimeta.FindStatusCondition(op.Status.Conditions, t)
319+
Expect(cond).To(Not(BeNil()))
320+
Expect(cond.Status).NotTo(BeEmpty())
321+
Expect(cond.Reason).To(BeElementOf(operatorutil.ConditionReasons))
322+
Expect(cond.ObservedGeneration).To(Equal(op.GetGeneration()))
323+
}
324+
}
325+
326+
var testEntitySource = input.NewCacheQuerier(map[deppy.Identifier]input.Entity{
327+
"operatorhub/prometheus/0.37.0": *input.NewEntity("operatorhub/prometheus/0.37.0", map[string]string{
328+
"olm.bundle.path": `"quay.io/operatorhubio/prometheus@sha256:3e281e587de3d03011440685fc4fb782672beab044c1ebadc42788ce05a21c35"`,
329+
"olm.channel": `{"channelName":"beta","priority":0}`,
330+
"olm.package": `{"packageName":"prometheus","version":"0.37.0"}`,
331+
"olm.gvk": `[]`,
332+
}),
333+
"operatorhub/prometheus/0.47.0": *input.NewEntity("operatorhub/prometheus/0.47.0", map[string]string{
334+
"olm.bundle.path": `"quay.io/operatorhubio/prometheus@sha256:5b04c49d8d3eff6a338b56ec90bdf491d501fe301c9cdfb740e5bff6769a21ed"`,
335+
"olm.channel": `{"channelName":"beta","priority":0,"replaces":"prometheusoperator.0.37.0"}`,
336+
"olm.package": `{"packageName":"prometheus","version":"0.47.0"}`,
337+
"olm.gvk": `[]`,
338+
}),
339+
"operatorhub/badimage/0.1.0": *input.NewEntity("operatorhub/badimage/0.1.0", map[string]string{
340+
"olm.bundle.path": `{"name": "quay.io/operatorhubio/badimage:v0.1.0"}`,
341+
"olm.package": `{"packageName":"badimage","version":"0.1.0"}`,
342+
"olm.gvk": `[]`,
343+
}),
344+
})

0 commit comments

Comments
 (0)