1
16
17 package controllers
18
19 import (
20 "context"
21 "fmt"
22 "math/rand"
23 "testing"
24 "time"
25
26 cmapi "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"
27 cmmeta "github.com/cert-manager/cert-manager/pkg/apis/meta/v1"
28 logrtesting "github.com/go-logr/logr/testing"
29 "github.com/stretchr/testify/assert"
30 "github.com/stretchr/testify/require"
31 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
32 "k8s.io/apimachinery/pkg/runtime"
33 "k8s.io/apimachinery/pkg/runtime/schema"
34 "k8s.io/apimachinery/pkg/types"
35 "k8s.io/client-go/tools/record"
36 clocktesting "k8s.io/utils/clock/testing"
37 "k8s.io/utils/ptr"
38 "sigs.k8s.io/controller-runtime/pkg/client"
39 "sigs.k8s.io/controller-runtime/pkg/client/fake"
40 "sigs.k8s.io/controller-runtime/pkg/reconcile"
41 "sigs.k8s.io/controller-runtime/pkg/source"
42
43 "github.com/cert-manager/issuer-lib/api/v1alpha1"
44 "github.com/cert-manager/issuer-lib/controllers/signer"
45 "github.com/cert-manager/issuer-lib/internal/testapi/api"
46 "github.com/cert-manager/issuer-lib/internal/testapi/testutil"
47 "github.com/cert-manager/issuer-lib/internal/tests/errormatch"
48 )
49
50
51
52
53
54
55 func randomTime() time.Time {
56 min := time.Date(1970, 1, 0, 0, 0, 0, 0, time.UTC).Unix()
57 max := time.Date(2070, 1, 0, 0, 0, 0, 0, time.UTC).Unix()
58 delta := max - min
59
60 sec := rand.Int63n(delta) + min
61 return time.Unix(sec, 0)
62 }
63
64 func TestTestIssuerReconcilerReconcile(t *testing.T) {
65 t.Parallel()
66
67 fieldOwner := "test-simple-issuer-reconciler-reconcile"
68
69 type testCase struct {
70 name string
71 check signer.Check
72 objects []client.Object
73 eventSourceError error
74 validateError *errormatch.Matcher
75 expectedResult reconcile.Result
76 expectedStatusPatch *v1alpha1.IssuerStatus
77 expectedEvents []string
78 }
79
80 randTime := randomTime()
81
82 fakeTime1 := randTime.Truncate(time.Second)
83 fakeTimeObj1 := metav1.NewTime(fakeTime1)
84 fakeClock1 := clocktesting.NewFakeClock(fakeTime1)
85
86 fakeTime2 := randTime.Add(4 * time.Hour).Truncate(time.Second)
87 fakeTimeObj2 := metav1.NewTime(fakeTime2)
88 fakeClock2 := clocktesting.NewFakeClock(fakeTime2)
89
90 issuer1 := testutil.TestIssuer(
91 "issuer-1",
92 testutil.SetTestIssuerNamespace("ns1"),
93 )
94
95 staticChecker := func(err error) signer.Check {
96 return func(_ context.Context, _ v1alpha1.Issuer) error {
97 return err
98 }
99 }
100
101 tests := []testCase{
102
103 {
104 name: "ignore-issuer-not-found",
105 check: staticChecker(nil),
106 objects: []client.Object{},
107 expectedStatusPatch: nil,
108 },
109
110
111 {
112 name: "trigger-when-ready",
113 check: staticChecker(nil),
114 objects: []client.Object{
115 testutil.TestIssuerFrom(issuer1,
116 testutil.SetTestIssuerGeneration(80),
117 testutil.SetTestIssuerStatusCondition(
118 fakeClock1,
119 cmapi.IssuerConditionReady,
120 cmmeta.ConditionTrue,
121 v1alpha1.IssuerConditionReasonChecked,
122 "Succeeded checking the issuer",
123 ),
124 ),
125 },
126 expectedStatusPatch: &v1alpha1.IssuerStatus{
127 Conditions: []cmapi.IssuerCondition{
128 {
129 Type: cmapi.IssuerConditionReady,
130 Status: cmmeta.ConditionTrue,
131 Reason: v1alpha1.IssuerConditionReasonChecked,
132 Message: "Succeeded checking the issuer",
133 ObservedGeneration: 80,
134 LastTransitionTime: &fakeTimeObj1,
135 },
136 },
137 },
138 expectedEvents: []string{
139 "Normal Checked Succeeded checking the issuer",
140 },
141 },
142
143
144 {
145 name: "ignore-failed",
146 check: staticChecker(nil),
147 objects: []client.Object{
148 testutil.TestIssuerFrom(issuer1,
149 testutil.SetTestIssuerGeneration(80),
150 testutil.SetTestIssuerStatusCondition(
151 fakeClock1,
152 cmapi.IssuerConditionReady,
153 cmmeta.ConditionFalse,
154 v1alpha1.IssuerConditionReasonFailed,
155 "[error message]",
156 ),
157 ),
158 },
159 expectedStatusPatch: nil,
160 },
161
162
163 {
164 name: "failed-ignore-reported-error",
165 check: staticChecker(nil),
166 objects: []client.Object{
167 testutil.TestIssuerFrom(issuer1,
168 testutil.SetTestIssuerGeneration(80),
169 testutil.SetTestIssuerStatusCondition(
170 fakeClock1,
171 cmapi.IssuerConditionReady,
172 cmmeta.ConditionFalse,
173 v1alpha1.IssuerConditionReasonFailed,
174 "[error message]",
175 ),
176 ),
177 },
178 eventSourceError: fmt.Errorf("[specific error]"),
179 expectedStatusPatch: nil,
180 },
181
182
183 {
184 name: "ready-reported-error",
185 check: staticChecker(nil),
186 objects: []client.Object{
187 testutil.TestIssuerFrom(issuer1,
188 testutil.SetTestIssuerGeneration(80),
189 testutil.SetTestIssuerStatusCondition(
190 fakeClock1,
191 cmapi.IssuerConditionReady,
192 cmmeta.ConditionTrue,
193 v1alpha1.IssuerConditionReasonChecked,
194 "Succeeded checking the issuer",
195 ),
196 ),
197 },
198 eventSourceError: fmt.Errorf("[specific error]"),
199 expectedStatusPatch: &v1alpha1.IssuerStatus{
200 Conditions: []cmapi.IssuerCondition{
201 {
202 Type: cmapi.IssuerConditionReady,
203 Status: cmmeta.ConditionFalse,
204 Reason: v1alpha1.IssuerConditionReasonPending,
205 Message: "Not ready yet: [specific error]",
206 ObservedGeneration: 80,
207 LastTransitionTime: &fakeTimeObj2,
208 },
209 },
210 },
211 validateError: errormatch.ErrorContains("[specific error]"),
212 expectedEvents: []string{
213 "Warning RetryableError Not ready yet: [specific error]",
214 },
215 },
216
217
218 {
219 name: "recheck-outdated-ready",
220 check: staticChecker(nil),
221 objects: []client.Object{
222 testutil.TestIssuerFrom(issuer1,
223 testutil.SetTestIssuerGeneration(80),
224 testutil.SetTestIssuerStatusCondition(
225 fakeClock1,
226 cmapi.IssuerConditionReady,
227 cmmeta.ConditionTrue,
228 v1alpha1.IssuerConditionReasonChecked,
229 "Succeeded checking the issuer",
230 ),
231 testutil.SetTestIssuerGeneration(81),
232 ),
233 },
234 expectedStatusPatch: &v1alpha1.IssuerStatus{
235 Conditions: []cmapi.IssuerCondition{
236 {
237 Type: cmapi.IssuerConditionReady,
238 Status: cmmeta.ConditionTrue,
239 Reason: v1alpha1.IssuerConditionReasonChecked,
240 Message: "Succeeded checking the issuer",
241 LastTransitionTime: &fakeTimeObj1,
242 ObservedGeneration: 81,
243 },
244 },
245 },
246 expectedEvents: []string{
247 "Normal Checked Succeeded checking the issuer",
248 },
249 },
250
251
252 {
253 name: "initialize-ready-condition",
254 objects: []client.Object{
255 issuer1,
256 },
257 expectedStatusPatch: &v1alpha1.IssuerStatus{
258 Conditions: []cmapi.IssuerCondition{
259 {
260 Type: cmapi.IssuerConditionReady,
261 Status: cmmeta.ConditionUnknown,
262 Reason: v1alpha1.IssuerConditionReasonInitializing,
263 Message: fieldOwner + " has started reconciling this Issuer",
264 LastTransitionTime: &fakeTimeObj2,
265 },
266 },
267 },
268 },
269
270
271 {
272 name: "retry-on-error",
273 check: staticChecker(fmt.Errorf("[specific error]")),
274 objects: []client.Object{
275 testutil.TestIssuerFrom(issuer1,
276 testutil.SetTestIssuerStatusCondition(
277 fakeClock1,
278 cmapi.IssuerConditionReady,
279 cmmeta.ConditionUnknown,
280 v1alpha1.IssuerConditionReasonInitializing,
281 fieldOwner+" has started reconciling this Issuer",
282 ),
283 ),
284 },
285 expectedStatusPatch: &v1alpha1.IssuerStatus{
286 Conditions: []cmapi.IssuerCondition{
287 {
288 Type: cmapi.IssuerConditionReady,
289 Status: cmmeta.ConditionFalse,
290 Reason: v1alpha1.IssuerConditionReasonPending,
291 Message: "Not ready yet: [specific error]",
292 LastTransitionTime: &fakeTimeObj2,
293 },
294 },
295 },
296 validateError: errormatch.ErrorContains("[specific error]"),
297 expectedEvents: []string{
298 "Warning RetryableError Not ready yet: [specific error]",
299 },
300 },
301
302
303 {
304 name: "dont-retry-on-permanent-error",
305 check: staticChecker(signer.PermanentError{Err: fmt.Errorf("[specific error]")}),
306 objects: []client.Object{
307 testutil.TestIssuerFrom(issuer1,
308 testutil.SetTestIssuerStatusCondition(
309 fakeClock1,
310 cmapi.IssuerConditionReady,
311 cmmeta.ConditionUnknown,
312 v1alpha1.IssuerConditionReasonInitializing,
313 fieldOwner+" has started reconciling this Issuer",
314 ),
315 ),
316 },
317 expectedStatusPatch: &v1alpha1.IssuerStatus{
318 Conditions: []cmapi.IssuerCondition{
319 {
320 Type: cmapi.IssuerConditionReady,
321 Status: cmmeta.ConditionFalse,
322 Reason: v1alpha1.IssuerConditionReasonFailed,
323 Message: "Failed permanently: [specific error]",
324 LastTransitionTime: &fakeTimeObj2,
325 },
326 },
327 },
328 validateError: errormatch.ErrorContains("terminal error: [specific error]"),
329 expectedEvents: []string{
330 "Warning PermanentError Failed permanently: [specific error]",
331 },
332 },
333
334
335
336
337
338 {
339 name: "success-issuer",
340 check: staticChecker(nil),
341 objects: []client.Object{
342 testutil.TestIssuerFrom(issuer1,
343 testutil.SetTestIssuerStatusCondition(
344 fakeClock1,
345 cmapi.IssuerConditionReady,
346 cmmeta.ConditionUnknown,
347 v1alpha1.IssuerConditionReasonInitializing,
348 fieldOwner+" has started reconciling this Issuer",
349 ),
350 ),
351 },
352 expectedStatusPatch: &v1alpha1.IssuerStatus{
353 Conditions: []cmapi.IssuerCondition{
354 {
355 Type: cmapi.IssuerConditionReady,
356 Status: cmmeta.ConditionTrue,
357 Reason: v1alpha1.IssuerConditionReasonChecked,
358 Message: "Succeeded checking the issuer",
359 LastTransitionTime: &fakeTimeObj2,
360 },
361 },
362 },
363 expectedEvents: []string{
364 "Normal Checked Succeeded checking the issuer",
365 },
366 },
367
368
369 {
370 name: "success-recover",
371 check: staticChecker(nil),
372 objects: []client.Object{
373 testutil.TestIssuerFrom(issuer1,
374 testutil.SetTestIssuerGeneration(80),
375 testutil.SetTestIssuerStatusCondition(
376 fakeClock1,
377 cmapi.IssuerConditionReady,
378 cmmeta.ConditionFalse,
379 v1alpha1.IssuerConditionReasonInitializing,
380 fieldOwner+" has started reconciling this Issuer",
381 ),
382 testutil.SetTestIssuerGeneration(81),
383 ),
384 },
385 expectedStatusPatch: &v1alpha1.IssuerStatus{
386 Conditions: []cmapi.IssuerCondition{
387 {
388 Type: cmapi.IssuerConditionReady,
389 Status: cmmeta.ConditionTrue,
390 Reason: v1alpha1.IssuerConditionReasonChecked,
391 Message: "Succeeded checking the issuer",
392 LastTransitionTime: &fakeTimeObj2,
393 ObservedGeneration: 81,
394 },
395 },
396 },
397 expectedEvents: []string{
398 "Normal Checked Succeeded checking the issuer",
399 },
400 },
401 }
402
403 for _, tc := range tests {
404 tc := tc
405 t.Run(tc.name, func(t *testing.T) {
406 t.Parallel()
407
408 scheme := runtime.NewScheme()
409 require.NoError(t, api.AddToScheme(scheme))
410 fakeClient := fake.NewClientBuilder().
411 WithScheme(scheme).
412 WithObjects(tc.objects...).
413 Build()
414
415 req := reconcile.Request{
416 NamespacedName: types.NamespacedName{
417 Name: issuer1.Name,
418 Namespace: issuer1.Namespace,
419 },
420 }
421
422 var vciBefore api.TestIssuer
423 err := fakeClient.Get(context.TODO(), req.NamespacedName, &vciBefore)
424 require.NoError(t, client.IgnoreNotFound(err), "unexpected error from fake client")
425
426 logger := logrtesting.NewTestLoggerWithOptions(t, logrtesting.Options{LogTimestamp: true, Verbosity: 10})
427 fakeRecorder := record.NewFakeRecorder(100)
428
429 controller := IssuerReconciler{
430 ForObject: &api.TestIssuer{},
431 FieldOwner: fieldOwner,
432 EventSource: fakeEventSource{
433 err: tc.eventSourceError,
434 },
435 Client: fakeClient,
436 Check: tc.check,
437 EventRecorder: fakeRecorder,
438 Clock: fakeClock2,
439 }
440
441 res, issuerStatusPatch, reconcileErr := controller.reconcileStatusPatch(logger, context.TODO(), req)
442
443 assert.Equal(t, tc.expectedResult, res)
444 assert.Equal(t, tc.expectedStatusPatch, issuerStatusPatch)
445 ptr.Deref(tc.validateError, *errormatch.NoError())(t, reconcileErr)
446
447 allEvents := chanToSlice(fakeRecorder.Events)
448 if len(tc.expectedEvents) == 0 {
449 assert.Emptyf(t, allEvents, "expected no events to be recorded, but got: %#v", allEvents)
450 } else {
451 assert.Equal(t, tc.expectedEvents, allEvents)
452 }
453 })
454 }
455 }
456
457 type fakeEventSource struct {
458 err error
459 }
460
461 func (fakeEventSource) AddConsumer(gvk schema.GroupVersionKind) source.Source {
462 panic("not implemented")
463 }
464 func (fakeEventSource) ReportError(gvk schema.GroupVersionKind, namespacedName types.NamespacedName, err error) error {
465 panic("not implemented")
466 }
467
468 func (fes fakeEventSource) HasReportedError(gvk schema.GroupVersionKind, namespacedName types.NamespacedName) error {
469 return fes.err
470 }
471
View as plain text