1
16
17 package controllers
18
19 import (
20 "context"
21 "errors"
22 "fmt"
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 cmgen "github.com/cert-manager/cert-manager/test/unit/gen"
29 logrtesting "github.com/go-logr/logr/testing"
30 "github.com/stretchr/testify/assert"
31 "github.com/stretchr/testify/require"
32 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
33 "k8s.io/apimachinery/pkg/runtime"
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
42 "github.com/cert-manager/issuer-lib/api/v1alpha1"
43 "github.com/cert-manager/issuer-lib/conditions"
44 "github.com/cert-manager/issuer-lib/controllers/signer"
45 "github.com/cert-manager/issuer-lib/internal/kubeutil"
46 "github.com/cert-manager/issuer-lib/internal/testapi/api"
47 "github.com/cert-manager/issuer-lib/internal/testapi/testutil"
48 "github.com/cert-manager/issuer-lib/internal/tests/errormatch"
49 )
50
51 func TestCertificateRequestReconcilerReconcile(t *testing.T) {
52 t.Parallel()
53
54 fieldOwner := "test-certificate-request-reconciler-reconcile"
55
56 type testCase struct {
57 name string
58 sign signer.Sign
59 objects []client.Object
60 validateError *errormatch.Matcher
61 expectedResult reconcile.Result
62 expectedStatusPatch *cmapi.CertificateRequestStatus
63 expectedEvents []string
64 }
65
66 randTime := randomTime()
67
68 fakeTime1 := randTime.Truncate(time.Second)
69 fakeTimeObj1 := metav1.NewTime(fakeTime1)
70 fakeClock1 := clocktesting.NewFakeClock(fakeTime1)
71
72 fakeTime2 := randTime.Add(4 * time.Hour).Truncate(time.Second)
73 fakeTimeObj2 := metav1.NewTime(fakeTime2)
74 fakeClock2 := clocktesting.NewFakeClock(fakeTime2)
75
76 issuer1 := testutil.TestIssuer(
77 "issuer-1",
78 testutil.SetTestIssuerNamespace("ns1"),
79 testutil.SetTestIssuerGeneration(70),
80 testutil.SetTestIssuerStatusCondition(
81 fakeClock1,
82 cmapi.IssuerConditionReady,
83 cmmeta.ConditionTrue,
84 v1alpha1.IssuerConditionReasonChecked,
85 "Succeeded checking the issuer",
86 ),
87 )
88
89 clusterIssuer1 := testutil.TestClusterIssuer(
90 "cluster-issuer-1",
91 testutil.SetTestClusterIssuerGeneration(70),
92 testutil.SetTestClusterIssuerStatusCondition(
93 fakeClock1,
94 cmapi.IssuerConditionReady,
95 cmmeta.ConditionTrue,
96 v1alpha1.IssuerConditionReasonChecked,
97 "Succeeded checking the issuer",
98 ),
99 )
100
101 cr1 := cmgen.CertificateRequest(
102 "cr1",
103 cmgen.SetCertificateRequestNamespace("ns1"),
104 cmgen.SetCertificateRequestIssuer(cmmeta.ObjectReference{
105 Group: api.SchemeGroupVersion.Group,
106 }),
107 func(cr *cmapi.CertificateRequest) {
108 conditions.SetCertificateRequestStatusCondition(
109 fakeClock1,
110 cr.Status.Conditions,
111 &cr.Status.Conditions,
112 cmapi.CertificateRequestConditionReady,
113 cmmeta.ConditionUnknown,
114 v1alpha1.CertificateRequestConditionReasonInitializing,
115 fieldOwner+" has begun reconciling this CertificateRequest",
116 )
117 conditions.SetCertificateRequestStatusCondition(
118 fakeClock1,
119 cr.Status.Conditions,
120 &cr.Status.Conditions,
121 cmapi.CertificateRequestConditionApproved,
122 cmmeta.ConditionTrue,
123 "ApprovedReason",
124 "ApprovedMessage",
125 )
126 },
127 )
128
129 successSigner := func(cert string) signer.Sign {
130 return func(_ context.Context, _ signer.CertificateRequestObject, _ v1alpha1.Issuer) (signer.PEMBundle, error) {
131 return signer.PEMBundle{
132 ChainPEM: []byte(cert),
133 }, nil
134 }
135 }
136
137 tests := []testCase{
138
139
140
141
142 {
143 name: "ignore-certificaterequest-not-found",
144 objects: []client.Object{},
145 },
146
147
148 {
149 name: "ignore-unless-approved-or-denied",
150 objects: []client.Object{
151 cmgen.CertificateRequestFrom(cr1, func(cr *cmapi.CertificateRequest) {
152 cr.Status.Conditions = nil
153 }),
154 },
155 },
156
157
158 {
159 name: "issuer-ref-unknown-group",
160 objects: []client.Object{
161 cmgen.CertificateRequestFrom(cr1, func(cr *cmapi.CertificateRequest) {
162 cr.Spec.IssuerRef.Group = "unknown-group"
163 }),
164 },
165 },
166
167
168 {
169 name: "issuer-ref-unknown-kind",
170 objects: []client.Object{
171 cmgen.CertificateRequestFrom(cr1, func(cr *cmapi.CertificateRequest) {
172 cr.Spec.IssuerRef.Kind = "unknown-kind"
173 }),
174 },
175 },
176
177
178 {
179 name: "already-ready",
180 objects: []client.Object{
181 cmgen.CertificateRequestFrom(cr1,
182 cmgen.SetCertificateRequestStatusCondition(cmapi.CertificateRequestCondition{
183 Type: cmapi.CertificateRequestConditionReady,
184 Reason: cmapi.CertificateRequestReasonIssued,
185 Status: cmmeta.ConditionTrue,
186 }),
187 ),
188 },
189 },
190
191
192 {
193 name: "already-failed",
194 objects: []client.Object{
195 cmgen.CertificateRequestFrom(cr1,
196 cmgen.SetCertificateRequestStatusCondition(cmapi.CertificateRequestCondition{
197 Type: cmapi.CertificateRequestConditionReady,
198 Status: cmmeta.ConditionFalse,
199 Reason: cmapi.CertificateRequestReasonFailed,
200 }),
201 ),
202 },
203 },
204
205
206 {
207 name: "already-denied",
208 objects: []client.Object{
209 cmgen.CertificateRequestFrom(cr1,
210 cmgen.SetCertificateRequestStatusCondition(cmapi.CertificateRequestCondition{
211 Type: cmapi.CertificateRequestConditionReady,
212 Status: cmmeta.ConditionFalse,
213 Reason: cmapi.CertificateRequestReasonDenied,
214 }),
215 ),
216 },
217 },
218
219
220 {
221 name: "initialize-ready-condition",
222 objects: []client.Object{
223 cmgen.CertificateRequestFrom(cr1, func(cr *cmapi.CertificateRequest) {
224 removeCertificateRequestCondition(cr, cmapi.CertificateRequestConditionReady)
225 }),
226 },
227 expectedStatusPatch: &cmapi.CertificateRequestStatus{
228 Conditions: []cmapi.CertificateRequestCondition{
229 {
230 Type: cmapi.CertificateRequestConditionReady,
231 Status: cmmeta.ConditionUnknown,
232 Reason: v1alpha1.CertificateRequestConditionReasonInitializing,
233 Message: fieldOwner + " has started reconciling this CertificateRequest",
234 LastTransitionTime: &fakeTimeObj2,
235 },
236 },
237 },
238 },
239
240
241 {
242 name: "set-ready-denied",
243 objects: []client.Object{
244 cmgen.CertificateRequestFrom(cr1, cmgen.SetCertificateRequestStatusCondition(cmapi.CertificateRequestCondition{
245 Type: cmapi.CertificateRequestConditionDenied,
246 Status: cmmeta.ConditionTrue,
247 Reason: "",
248 })),
249 },
250 expectedStatusPatch: &cmapi.CertificateRequestStatus{
251 Conditions: []cmapi.CertificateRequestCondition{
252 {
253 Type: cmapi.CertificateRequestConditionReady,
254 Status: cmmeta.ConditionFalse,
255 Reason: cmapi.CertificateRequestReasonDenied,
256 Message: "Detected that the CertificateRequest is denied, so it will never be Ready.",
257 LastTransitionTime: &fakeTimeObj2,
258 },
259 },
260 FailureTime: &fakeTimeObj2,
261 },
262 expectedEvents: []string{
263 "Warning PermanentError Detected that the CertificateRequest is denied, so it will never be Ready.",
264 },
265 },
266
267
268 {
269 name: "set-ready-pending-missing-issuer",
270 objects: []client.Object{
271 cmgen.CertificateRequestFrom(cr1, func(cr *cmapi.CertificateRequest) {
272 cr.Spec.IssuerRef.Name = issuer1.Name
273 cr.Spec.IssuerRef.Kind = issuer1.Kind
274 }),
275 },
276 expectedStatusPatch: &cmapi.CertificateRequestStatus{
277 Conditions: []cmapi.CertificateRequestCondition{
278 {
279 Type: cmapi.CertificateRequestConditionReady,
280 Status: cmmeta.ConditionFalse,
281 Reason: cmapi.CertificateRequestReasonPending,
282 Message: "testissuers.testing.cert-manager.io \"issuer-1\" not found. Waiting for it to be created.",
283 LastTransitionTime: &fakeTimeObj2,
284 },
285 },
286 },
287 expectedEvents: []string{
288 "Normal WaitingForIssuerExist testissuers.testing.cert-manager.io \"issuer-1\" not found. Waiting for it to be created.",
289 },
290 },
291
292
293
294 {
295 name: "set-ready-pending-issuer-has-no-ready-condition",
296 objects: []client.Object{
297 cmgen.CertificateRequestFrom(cr1,
298 cmgen.SetCertificateRequestIssuer(cmmeta.ObjectReference{
299 Name: issuer1.Name,
300 Group: api.SchemeGroupVersion.Group,
301 }),
302 ),
303 testutil.TestIssuerFrom(issuer1,
304 func(si *api.TestIssuer) {
305 si.Status.Conditions = nil
306 },
307 ),
308 },
309 expectedStatusPatch: &cmapi.CertificateRequestStatus{
310 Conditions: []cmapi.CertificateRequestCondition{
311 {
312 Type: cmapi.CertificateRequestConditionReady,
313 Status: cmmeta.ConditionFalse,
314 Reason: cmapi.CertificateRequestReasonPending,
315 Message: "Waiting for issuer to become ready. Current issuer ready condition: <none>.",
316 LastTransitionTime: &fakeTimeObj2,
317 },
318 },
319 },
320 expectedEvents: []string{
321 "Normal WaitingForIssuerReady Waiting for issuer to become ready. Current issuer ready condition: <none>.",
322 },
323 },
324
325
326 {
327 name: "set-ready-pending-issuer-is-not-ready",
328 objects: []client.Object{
329 cmgen.CertificateRequestFrom(cr1,
330 cmgen.SetCertificateRequestIssuer(cmmeta.ObjectReference{
331 Name: issuer1.Name,
332 Group: api.SchemeGroupVersion.Group,
333 }),
334 ),
335 testutil.TestIssuerFrom(issuer1,
336 testutil.SetTestIssuerStatusCondition(
337 fakeClock1,
338 cmapi.IssuerConditionReady,
339 cmmeta.ConditionFalse,
340 "[REASON]",
341 "[MESSAGE]",
342 ),
343 ),
344 },
345 expectedStatusPatch: &cmapi.CertificateRequestStatus{
346 Conditions: []cmapi.CertificateRequestCondition{
347 {
348 Type: cmapi.CertificateRequestConditionReady,
349 Status: cmmeta.ConditionFalse,
350 Reason: cmapi.CertificateRequestReasonPending,
351 Message: "Waiting for issuer to become ready. Current issuer ready condition is \"[REASON]\": [MESSAGE].",
352 LastTransitionTime: &fakeTimeObj2,
353 },
354 },
355 },
356 expectedEvents: []string{
357 "Normal WaitingForIssuerReady Waiting for issuer to become ready. Current issuer ready condition is \"[REASON]\": [MESSAGE].",
358 },
359 },
360
361
362
363 {
364 name: "set-ready-pending-issuer-ready-outdated",
365 objects: []client.Object{
366 cmgen.CertificateRequestFrom(cr1,
367 cmgen.SetCertificateRequestIssuer(cmmeta.ObjectReference{
368 Name: issuer1.Name,
369 Group: api.SchemeGroupVersion.Group,
370 }),
371 ),
372 testutil.TestIssuerFrom(issuer1,
373 testutil.SetTestIssuerGeneration(issuer1.Generation+1),
374 ),
375 },
376 expectedStatusPatch: &cmapi.CertificateRequestStatus{
377 Conditions: []cmapi.CertificateRequestCondition{
378 {
379 Type: cmapi.CertificateRequestConditionReady,
380 Status: cmmeta.ConditionFalse,
381 Reason: cmapi.CertificateRequestReasonPending,
382 Message: "Waiting for issuer to become ready. Current issuer ready condition is outdated.",
383 LastTransitionTime: &fakeTimeObj2,
384 },
385 },
386 },
387 expectedEvents: []string{
388 "Normal WaitingForIssuerReady Waiting for issuer to become ready. Current issuer ready condition is outdated.",
389 },
390 },
391
392
393
394 {
395 name: "timeout-permanent-error",
396 sign: func(_ context.Context, cr signer.CertificateRequestObject, _ v1alpha1.Issuer) (signer.PEMBundle, error) {
397 return signer.PEMBundle{}, fmt.Errorf("a specific error")
398 },
399 objects: []client.Object{
400 cmgen.CertificateRequestFrom(cr1,
401 cmgen.SetCertificateRequestIssuer(cmmeta.ObjectReference{
402 Name: issuer1.Name,
403 Group: api.SchemeGroupVersion.Group,
404 }),
405 func(cr *cmapi.CertificateRequest) {
406 cr.CreationTimestamp = metav1.NewTime(fakeTimeObj2.Add(-2 * time.Minute))
407 },
408 ),
409 testutil.TestIssuerFrom(issuer1),
410 },
411 expectedStatusPatch: &cmapi.CertificateRequestStatus{
412 Conditions: []cmapi.CertificateRequestCondition{
413 {
414 Type: cmapi.CertificateRequestConditionReady,
415 Status: cmmeta.ConditionFalse,
416 Reason: cmapi.CertificateRequestReasonFailed,
417 Message: "Failed permanently to sign CertificateRequest: a specific error",
418 LastTransitionTime: &fakeTimeObj2,
419 },
420 },
421 FailureTime: &fakeTimeObj2,
422 },
423 validateError: errormatch.ErrorContains("terminal error: a specific error"),
424 expectedEvents: []string{
425 "Warning PermanentError Failed permanently to sign CertificateRequest: a specific error",
426 },
427 },
428
429
430
431 {
432 name: "retry-on-pending-error",
433 sign: func(_ context.Context, cr signer.CertificateRequestObject, _ v1alpha1.Issuer) (signer.PEMBundle, error) {
434 return signer.PEMBundle{}, signer.PendingError{Err: fmt.Errorf("reason for being pending")}
435 },
436 objects: []client.Object{
437 cmgen.CertificateRequestFrom(cr1,
438 cmgen.SetCertificateRequestIssuer(cmmeta.ObjectReference{
439 Name: issuer1.Name,
440 Group: api.SchemeGroupVersion.Group,
441 }),
442 func(cr *cmapi.CertificateRequest) {
443 cr.CreationTimestamp = metav1.NewTime(fakeTimeObj2.Add(-2 * time.Minute))
444 },
445 ),
446 testutil.TestIssuerFrom(issuer1),
447 },
448 expectedStatusPatch: &cmapi.CertificateRequestStatus{
449 Conditions: []cmapi.CertificateRequestCondition{
450 {
451 Type: cmapi.CertificateRequestConditionReady,
452 Status: cmmeta.ConditionFalse,
453 Reason: cmapi.CertificateRequestReasonPending,
454 Message: "Signing still in progress. Reason: Signing still in progress. Reason: reason for being pending",
455 LastTransitionTime: &fakeTimeObj2,
456 },
457 },
458 },
459 expectedResult: reconcile.Result{
460 Requeue: true,
461 },
462 expectedEvents: []string{
463 "Warning RetryableError Signing still in progress. Reason: Signing still in progress. Reason: reason for being pending",
464 },
465 },
466
467
468
469
470
471
472
473 {
474 name: "error-set-certificate-request-condition-should-add-new-condition-and-retry",
475 sign: func(_ context.Context, cr signer.CertificateRequestObject, _ v1alpha1.Issuer) (signer.PEMBundle, error) {
476 return signer.PEMBundle{}, signer.SetCertificateRequestConditionError{
477 Err: fmt.Errorf("test error"),
478 ConditionType: "[condition type]",
479 Status: cmmeta.ConditionTrue,
480 Reason: "[reason]",
481 }
482 },
483 objects: []client.Object{
484 cmgen.CertificateRequestFrom(cr1,
485 func(cr *cmapi.CertificateRequest) {
486 cr.CreationTimestamp = fakeTimeObj2
487 },
488 cmgen.SetCertificateRequestIssuer(cmmeta.ObjectReference{
489 Name: issuer1.Name,
490 Group: api.SchemeGroupVersion.Group,
491 }),
492 ),
493 testutil.TestIssuerFrom(issuer1),
494 },
495 expectedStatusPatch: &cmapi.CertificateRequestStatus{
496 Conditions: []cmapi.CertificateRequestCondition{
497 {
498 Type: "[condition type]",
499 Status: cmmeta.ConditionTrue,
500 Reason: "[reason]",
501 Message: "test error",
502 LastTransitionTime: &fakeTimeObj2,
503 },
504 {
505 Type: cmapi.CertificateRequestConditionReady,
506 Status: cmmeta.ConditionFalse,
507 Reason: cmapi.CertificateRequestReasonPending,
508 Message: "Failed to sign CertificateRequest, will retry: test error",
509 LastTransitionTime: &fakeTimeObj2,
510 },
511 },
512 },
513 validateError: errormatch.ErrorContains("terminal error: test error"),
514 expectedEvents: []string{
515 "Warning RetryableError Failed to sign CertificateRequest, will retry: test error",
516 },
517 },
518
519
520
521
522
523
524
525 {
526 name: "error-set-certificate-request-condition-should-update-existing-condition-and-retry",
527 sign: func(_ context.Context, cr signer.CertificateRequestObject, _ v1alpha1.Issuer) (signer.PEMBundle, error) {
528 return signer.PEMBundle{}, signer.SetCertificateRequestConditionError{
529 Err: fmt.Errorf("test error2"),
530 ConditionType: "[condition type]",
531 Status: cmmeta.ConditionTrue,
532 Reason: "[reason]",
533 }
534 },
535 objects: []client.Object{
536 cmgen.CertificateRequestFrom(cr1,
537 func(cr *cmapi.CertificateRequest) {
538 cr.CreationTimestamp = fakeTimeObj2
539 },
540 cmgen.SetCertificateRequestIssuer(cmmeta.ObjectReference{
541 Name: issuer1.Name,
542 Group: api.SchemeGroupVersion.Group,
543 }),
544 cmgen.AddCertificateRequestStatusCondition(cmapi.CertificateRequestCondition{
545 Type: "[condition type]",
546 Status: cmmeta.ConditionTrue,
547 Reason: "[reason]",
548 Message: "test error",
549 LastTransitionTime: &fakeTimeObj2,
550 }),
551 ),
552 testutil.TestIssuerFrom(issuer1),
553 },
554 expectedStatusPatch: &cmapi.CertificateRequestStatus{
555 Conditions: []cmapi.CertificateRequestCondition{
556 {
557 Type: "[condition type]",
558 Status: cmmeta.ConditionTrue,
559 Reason: "[reason]",
560 Message: "test error2",
561 LastTransitionTime: &fakeTimeObj2,
562 },
563 {
564 Type: cmapi.CertificateRequestConditionReady,
565 Status: cmmeta.ConditionFalse,
566 Reason: cmapi.CertificateRequestReasonPending,
567 Message: "Failed to sign CertificateRequest, will retry: test error2",
568 LastTransitionTime: &fakeTimeObj2,
569 },
570 },
571 },
572 validateError: errormatch.ErrorContains("test error2"),
573 expectedEvents: []string{
574 "Warning RetryableError Failed to sign CertificateRequest, will retry: test error2",
575 },
576 },
577
578
579
580
581
582
583
584 {
585 name: "error-set-certificate-request-condition-should-add-new-condition-and-timeout",
586 sign: func(_ context.Context, cr signer.CertificateRequestObject, _ v1alpha1.Issuer) (signer.PEMBundle, error) {
587 return signer.PEMBundle{}, signer.SetCertificateRequestConditionError{
588 Err: fmt.Errorf("test error"),
589 ConditionType: "[condition type]",
590 Status: cmmeta.ConditionTrue,
591 Reason: "[reason]",
592 }
593 },
594 objects: []client.Object{
595 cmgen.CertificateRequestFrom(cr1,
596 cmgen.SetCertificateRequestIssuer(cmmeta.ObjectReference{
597 Name: issuer1.Name,
598 Group: api.SchemeGroupVersion.Group,
599 }),
600 func(cr *cmapi.CertificateRequest) {
601 cr.CreationTimestamp = metav1.NewTime(fakeTimeObj2.Add(-2 * time.Minute))
602 },
603 ),
604 testutil.TestIssuerFrom(issuer1),
605 },
606 expectedStatusPatch: &cmapi.CertificateRequestStatus{
607 Conditions: []cmapi.CertificateRequestCondition{
608 {
609 Type: "[condition type]",
610 Status: cmmeta.ConditionTrue,
611 Reason: "[reason]",
612 Message: "test error",
613 LastTransitionTime: &fakeTimeObj2,
614 },
615 {
616 Type: cmapi.CertificateRequestConditionReady,
617 Status: cmmeta.ConditionFalse,
618 Reason: cmapi.CertificateRequestReasonFailed,
619 Message: "Failed permanently to sign CertificateRequest: test error",
620 LastTransitionTime: &fakeTimeObj2,
621 },
622 },
623 FailureTime: &fakeTimeObj2,
624 },
625 validateError: errormatch.ErrorContains("terminal error: test error"),
626 expectedEvents: []string{
627 "Warning PermanentError Failed permanently to sign CertificateRequest: test error",
628 },
629 },
630
631
632
633
634
635
636
637 {
638 name: "error-set-certificate-request-condition-should-update-existing-condition-and-timeout",
639 sign: func(_ context.Context, cr signer.CertificateRequestObject, _ v1alpha1.Issuer) (signer.PEMBundle, error) {
640 return signer.PEMBundle{}, signer.SetCertificateRequestConditionError{
641 Err: fmt.Errorf("test error2"),
642 ConditionType: "[condition type]",
643 Status: cmmeta.ConditionTrue,
644 Reason: "[reason]",
645 }
646 },
647 objects: []client.Object{
648 cmgen.CertificateRequestFrom(cr1,
649 cmgen.SetCertificateRequestIssuer(cmmeta.ObjectReference{
650 Name: issuer1.Name,
651 Group: api.SchemeGroupVersion.Group,
652 }),
653 func(cr *cmapi.CertificateRequest) {
654 cr.CreationTimestamp = metav1.NewTime(fakeTimeObj2.Add(-2 * time.Minute))
655 },
656 cmgen.AddCertificateRequestStatusCondition(cmapi.CertificateRequestCondition{
657 Type: "[condition type]",
658 Status: cmmeta.ConditionTrue,
659 Reason: "[reason]",
660 Message: "test error",
661 LastTransitionTime: &fakeTimeObj1,
662 }),
663 ),
664 testutil.TestIssuerFrom(issuer1),
665 },
666 expectedStatusPatch: &cmapi.CertificateRequestStatus{
667 Conditions: []cmapi.CertificateRequestCondition{
668 {
669 Type: "[condition type]",
670 Status: cmmeta.ConditionTrue,
671 Reason: "[reason]",
672 Message: "test error2",
673 LastTransitionTime: &fakeTimeObj1,
674 },
675 {
676 Type: cmapi.CertificateRequestConditionReady,
677 Status: cmmeta.ConditionFalse,
678 Reason: cmapi.CertificateRequestReasonFailed,
679 Message: "Failed permanently to sign CertificateRequest: test error2",
680 LastTransitionTime: &fakeTimeObj2,
681 },
682 },
683 FailureTime: &fakeTimeObj2,
684 },
685 validateError: errormatch.ErrorContains("terminal error: test error2"),
686 expectedEvents: []string{
687 "Warning PermanentError Failed permanently to sign CertificateRequest: test error2",
688 },
689 },
690
691
692
693
694
695
696 {
697 name: "error-set-certificate-request-condition-should-not-timeout-if-pending",
698 sign: func(_ context.Context, cr signer.CertificateRequestObject, _ v1alpha1.Issuer) (signer.PEMBundle, error) {
699 return signer.PEMBundle{}, signer.SetCertificateRequestConditionError{
700 Err: signer.PendingError{Err: fmt.Errorf("test error")},
701 ConditionType: "[condition type]",
702 Status: cmmeta.ConditionTrue,
703 Reason: "[reason]",
704 }
705 },
706 objects: []client.Object{
707 cmgen.CertificateRequestFrom(cr1,
708 cmgen.SetCertificateRequestIssuer(cmmeta.ObjectReference{
709 Name: issuer1.Name,
710 Group: api.SchemeGroupVersion.Group,
711 }),
712 func(cr *cmapi.CertificateRequest) {
713 cr.CreationTimestamp = metav1.NewTime(fakeTimeObj2.Add(-2 * time.Minute))
714 },
715 ),
716 testutil.TestIssuerFrom(issuer1),
717 },
718 expectedStatusPatch: &cmapi.CertificateRequestStatus{
719 Conditions: []cmapi.CertificateRequestCondition{
720 {
721 Type: "[condition type]",
722 Status: cmmeta.ConditionTrue,
723 Reason: "[reason]",
724 Message: "test error",
725 LastTransitionTime: &fakeTimeObj2,
726 },
727 {
728 Type: cmapi.CertificateRequestConditionReady,
729 Status: cmmeta.ConditionFalse,
730 Reason: cmapi.CertificateRequestReasonPending,
731 Message: "Signing still in progress. Reason: Signing still in progress. Reason: test error",
732 LastTransitionTime: &fakeTimeObj2,
733 },
734 },
735 },
736 expectedResult: reconcile.Result{
737 Requeue: false,
738 },
739 expectedEvents: []string{
740 "Warning RetryableError Signing still in progress. Reason: Signing still in progress. Reason: test error",
741 },
742 },
743
744
745
746
747
748
749 {
750 name: "error-set-certificate-request-condition-should-not-retry-on-permanent-error",
751 sign: func(_ context.Context, cr signer.CertificateRequestObject, _ v1alpha1.Issuer) (signer.PEMBundle, error) {
752 return signer.PEMBundle{}, signer.SetCertificateRequestConditionError{
753 Err: signer.PermanentError{Err: fmt.Errorf("test error")},
754 ConditionType: "[condition type]",
755 Status: cmmeta.ConditionTrue,
756 Reason: "[reason]",
757 }
758 },
759 objects: []client.Object{
760 cmgen.CertificateRequestFrom(cr1,
761 cmgen.SetCertificateRequestIssuer(cmmeta.ObjectReference{
762 Name: issuer1.Name,
763 Group: api.SchemeGroupVersion.Group,
764 }),
765 ),
766 testutil.TestIssuerFrom(issuer1),
767 },
768 expectedStatusPatch: &cmapi.CertificateRequestStatus{
769 Conditions: []cmapi.CertificateRequestCondition{
770 {
771 Type: "[condition type]",
772 Status: cmmeta.ConditionTrue,
773 Reason: "[reason]",
774 Message: "test error",
775 LastTransitionTime: &fakeTimeObj2,
776 },
777 {
778 Type: cmapi.CertificateRequestConditionReady,
779 Status: cmmeta.ConditionFalse,
780 Reason: cmapi.CertificateRequestReasonFailed,
781 Message: "Failed permanently to sign CertificateRequest: test error",
782 LastTransitionTime: &fakeTimeObj2,
783 },
784 },
785 FailureTime: &fakeTimeObj2,
786 },
787 validateError: errormatch.ErrorContains("terminal error: test error"),
788 expectedEvents: []string{
789 "Warning PermanentError Failed permanently to sign CertificateRequest: test error",
790 },
791 },
792
793
794 {
795 name: "fail-on-permanent-error",
796 sign: func(_ context.Context, cr signer.CertificateRequestObject, _ v1alpha1.Issuer) (signer.PEMBundle, error) {
797 return signer.PEMBundle{}, signer.PermanentError{Err: fmt.Errorf("a specific error")}
798 },
799 objects: []client.Object{
800 cmgen.CertificateRequestFrom(cr1,
801 cmgen.SetCertificateRequestIssuer(cmmeta.ObjectReference{
802 Name: issuer1.Name,
803 Group: api.SchemeGroupVersion.Group,
804 }),
805 ),
806 testutil.TestIssuerFrom(issuer1),
807 },
808 expectedStatusPatch: &cmapi.CertificateRequestStatus{
809 Conditions: []cmapi.CertificateRequestCondition{
810 {
811 Type: cmapi.CertificateRequestConditionReady,
812 Status: cmmeta.ConditionFalse,
813 Reason: cmapi.CertificateRequestReasonFailed,
814 Message: "Failed permanently to sign CertificateRequest: a specific error",
815 LastTransitionTime: &fakeTimeObj2,
816 },
817 },
818 FailureTime: &fakeTimeObj2,
819 },
820 validateError: errormatch.ErrorContains("terminal error: a specific error"),
821 expectedEvents: []string{
822 "Warning PermanentError Failed permanently to sign CertificateRequest: a specific error",
823 },
824 },
825
826
827
828 {
829 name: "retry-on-error",
830 sign: func(_ context.Context, cr signer.CertificateRequestObject, _ v1alpha1.Issuer) (signer.PEMBundle, error) {
831 return signer.PEMBundle{}, errors.New("waiting for approval")
832 },
833 objects: []client.Object{
834 cmgen.CertificateRequestFrom(cr1,
835 func(cr *cmapi.CertificateRequest) {
836 cr.CreationTimestamp = fakeTimeObj2
837 },
838 func(cr *cmapi.CertificateRequest) {
839 cr.Spec.IssuerRef.Name = issuer1.Name
840 cr.Spec.IssuerRef.Kind = issuer1.Kind
841 },
842 ),
843 testutil.TestIssuerFrom(issuer1),
844 },
845 expectedStatusPatch: &cmapi.CertificateRequestStatus{
846 Conditions: []cmapi.CertificateRequestCondition{
847 {
848 Type: cmapi.CertificateRequestConditionReady,
849 Status: cmmeta.ConditionFalse,
850 Reason: cmapi.CertificateRequestReasonPending,
851 Message: "Failed to sign CertificateRequest, will retry: waiting for approval",
852 LastTransitionTime: &fakeTimeObj2,
853 },
854 },
855 },
856 validateError: errormatch.ErrorContains("waiting for approval"),
857 expectedEvents: []string{
858 "Warning RetryableError Failed to sign CertificateRequest, will retry: waiting for approval",
859 },
860 },
861
862 {
863 name: "success-issuer",
864 sign: successSigner("a-signed-certificate"),
865 objects: []client.Object{
866 cmgen.CertificateRequestFrom(cr1, func(cr *cmapi.CertificateRequest) {
867 cr.Spec.IssuerRef.Name = issuer1.Name
868 cr.Spec.IssuerRef.Kind = issuer1.Kind
869 }),
870 testutil.TestIssuerFrom(issuer1),
871 },
872 expectedStatusPatch: &cmapi.CertificateRequestStatus{
873 Certificate: []byte("a-signed-certificate"),
874 Conditions: []cmapi.CertificateRequestCondition{
875 {
876 Type: cmapi.CertificateRequestConditionReady,
877 Status: cmmeta.ConditionTrue,
878 Reason: cmapi.CertificateRequestReasonIssued,
879 Message: "Succeeded signing the CertificateRequest",
880 LastTransitionTime: &fakeTimeObj2,
881 },
882 },
883 },
884 expectedEvents: []string{
885 "Normal Issued Succeeded signing the CertificateRequest",
886 },
887 },
888
889 {
890 name: "success-clusterissuer",
891 sign: successSigner("a-signed-certificate"),
892 objects: []client.Object{
893 cmgen.CertificateRequestFrom(cr1, func(cr *cmapi.CertificateRequest) {
894 cr.Spec.IssuerRef.Name = clusterIssuer1.Name
895 cr.Spec.IssuerRef.Kind = clusterIssuer1.Kind
896 }),
897 testutil.TestClusterIssuerFrom(clusterIssuer1),
898 },
899 expectedStatusPatch: &cmapi.CertificateRequestStatus{
900 Certificate: []byte("a-signed-certificate"),
901 Conditions: []cmapi.CertificateRequestCondition{
902 {
903 Type: cmapi.CertificateRequestConditionReady,
904 Status: cmmeta.ConditionTrue,
905 Reason: cmapi.CertificateRequestReasonIssued,
906 Message: "Succeeded signing the CertificateRequest",
907 LastTransitionTime: &fakeTimeObj2,
908 },
909 },
910 },
911 expectedEvents: []string{
912 "Normal Issued Succeeded signing the CertificateRequest",
913 },
914 },
915 }
916
917 for _, tc := range tests {
918 tc := tc
919 t.Run(tc.name, func(t *testing.T) {
920 t.Parallel()
921
922 scheme := runtime.NewScheme()
923 require.NoError(t, setupCertificateRequestReconcilerScheme(scheme))
924 require.NoError(t, api.AddToScheme(scheme))
925 fakeClient := fake.NewClientBuilder().
926 WithScheme(scheme).
927 WithObjects(tc.objects...).
928 Build()
929
930 req := reconcile.Request{
931 NamespacedName: types.NamespacedName{
932 Name: cr1.Name,
933 Namespace: cr1.Namespace,
934 },
935 }
936
937 var crBefore cmapi.CertificateRequest
938 err := fakeClient.Get(context.TODO(), req.NamespacedName, &crBefore)
939 require.NoError(t, client.IgnoreNotFound(err), "unexpected error from fake client")
940
941 logger := logrtesting.NewTestLoggerWithOptions(t, logrtesting.Options{LogTimestamp: true, Verbosity: 10})
942 fakeRecorder := record.NewFakeRecorder(100)
943
944 controller := (&CertificateRequestReconciler{
945 RequestController: RequestController{
946 IssuerTypes: []v1alpha1.Issuer{&api.TestIssuer{}},
947 ClusterIssuerTypes: []v1alpha1.Issuer{&api.TestClusterIssuer{}},
948 FieldOwner: fieldOwner,
949 MaxRetryDuration: time.Minute,
950 EventSource: kubeutil.NewEventStore(),
951 Client: fakeClient,
952 Sign: tc.sign,
953 EventRecorder: fakeRecorder,
954 Clock: fakeClock2,
955 },
956 }).Init()
957
958 err = controller.setAllIssuerTypesWithGroupVersionKind(scheme)
959 require.NoError(t, err)
960
961 res, statusPatch, reconcileErr := controller.reconcileStatusPatch(logger, context.TODO(), req)
962 var crStatusPatch *cmapi.CertificateRequestStatus
963 if statusPatch != nil {
964 crStatusPatch = statusPatch.(CertificateRequestPatch).CertificateRequestPatch()
965 }
966
967 assert.Equal(t, tc.expectedResult, res)
968 assert.Equal(t, tc.expectedStatusPatch, crStatusPatch)
969 ptr.Deref(tc.validateError, *errormatch.NoError())(t, reconcileErr)
970
971 allEvents := chanToSlice(fakeRecorder.Events)
972 if len(tc.expectedEvents) == 0 {
973 assert.Emptyf(t, allEvents, "expected no events to be recorded, but got: %#v", allEvents)
974 } else {
975 assert.Equal(t, tc.expectedEvents, allEvents)
976 }
977 })
978 }
979 }
980
981 func chanToSlice(ch <-chan string) []string {
982 out := make([]string, 0, len(ch))
983 for i := 0; i < len(ch); i++ {
984 out = append(out, <-ch)
985 }
986 return out
987 }
988
989 func removeCertificateRequestCondition(cr *cmapi.CertificateRequest, conditionType cmapi.CertificateRequestConditionType) {
990 for i, cond := range cr.Status.Conditions {
991 if cond.Type == conditionType {
992 cr.Status.Conditions = append(cr.Status.Conditions[:i], cr.Status.Conditions[i+1:]...)
993 return
994 }
995 }
996 }
997
998 func TestCertificateRequestMatchIssuerType(t *testing.T) {
999 t.Parallel()
1000
1001 type testcase struct {
1002 name string
1003
1004 issuerTypes []v1alpha1.Issuer
1005 clusterIssuerTypes []v1alpha1.Issuer
1006 cr *cmapi.CertificateRequest
1007
1008 expectedIssuerType v1alpha1.Issuer
1009 expectedIssuerName types.NamespacedName
1010 expectedError *errormatch.Matcher
1011 }
1012
1013 createCr := func(name string, namespace string, kind string, group string) *cmapi.CertificateRequest {
1014 return &cmapi.CertificateRequest{
1015 ObjectMeta: metav1.ObjectMeta{
1016 Namespace: namespace,
1017 },
1018 Spec: cmapi.CertificateRequestSpec{
1019 IssuerRef: cmmeta.ObjectReference{
1020 Name: name,
1021 Kind: kind,
1022 Group: group,
1023 },
1024 },
1025 }
1026 }
1027
1028 testcases := []testcase{
1029 {
1030 name: "empty",
1031 issuerTypes: nil,
1032 clusterIssuerTypes: nil,
1033 cr: nil,
1034
1035 expectedIssuerType: nil,
1036 expectedIssuerName: types.NamespacedName{},
1037 expectedError: errormatch.ErrorContains("invalid reference, CertificateRequest is nil"),
1038 },
1039 {
1040 name: "no issuers",
1041 issuerTypes: nil,
1042 clusterIssuerTypes: nil,
1043 cr: createCr("name", "namespace", "", "test"),
1044
1045 expectedIssuerType: nil,
1046 expectedIssuerName: types.NamespacedName{},
1047 expectedError: errormatch.ErrorContains("no issuer found for reference: [Group=\"test\", Kind=\"\", Name=\"name\"]"),
1048 },
1049 {
1050 name: "match issuer",
1051 issuerTypes: []v1alpha1.Issuer{&api.TestIssuer{}},
1052 clusterIssuerTypes: []v1alpha1.Issuer{&api.TestClusterIssuer{}},
1053 cr: createCr("name", "namespace", "TestIssuer", "testing.cert-manager.io"),
1054
1055 expectedIssuerType: &api.TestIssuer{},
1056 expectedIssuerName: types.NamespacedName{Name: "name", Namespace: "namespace"},
1057 },
1058 {
1059 name: "match cluster issuer",
1060 issuerTypes: []v1alpha1.Issuer{&api.TestIssuer{}},
1061 clusterIssuerTypes: []v1alpha1.Issuer{&api.TestClusterIssuer{}},
1062 cr: createCr("name", "namespace", "TestClusterIssuer", "testing.cert-manager.io"),
1063
1064 expectedIssuerType: &api.TestClusterIssuer{},
1065 expectedIssuerName: types.NamespacedName{Name: "name"},
1066 },
1067 {
1068 name: "select kind if empty",
1069 issuerTypes: []v1alpha1.Issuer{},
1070 clusterIssuerTypes: []v1alpha1.Issuer{&api.TestClusterIssuer{}},
1071 cr: createCr("name", "namespace", "", "testing.cert-manager.io"),
1072
1073 expectedIssuerType: &api.TestClusterIssuer{},
1074 expectedIssuerName: types.NamespacedName{Name: "name"},
1075 },
1076 {
1077 name: "prefer issuer over cluster issuer (v1)",
1078 issuerTypes: []v1alpha1.Issuer{&api.TestIssuer{}},
1079 clusterIssuerTypes: []v1alpha1.Issuer{&api.TestClusterIssuer{}},
1080 cr: createCr("name", "namespace", "", "testing.cert-manager.io"),
1081
1082 expectedIssuerType: &api.TestIssuer{},
1083 expectedIssuerName: types.NamespacedName{Name: "name", Namespace: "namespace"},
1084 },
1085 {
1086 name: "prefer issuer over cluster issuer (v2)",
1087 issuerTypes: []v1alpha1.Issuer{&api.TestIssuer{}},
1088 clusterIssuerTypes: []v1alpha1.Issuer{&api.TestIssuer{}},
1089 cr: createCr("name", "namespace", "", "testing.cert-manager.io"),
1090
1091 expectedIssuerType: &api.TestIssuer{},
1092 expectedIssuerName: types.NamespacedName{Name: "name", Namespace: "namespace"},
1093 },
1094 }
1095
1096 scheme := runtime.NewScheme()
1097 require.NoError(t, api.AddToScheme(scheme))
1098
1099 for _, tc := range testcases {
1100 tc := tc
1101 t.Run(tc.name, func(t *testing.T) {
1102 t.Parallel()
1103
1104 crr := &CertificateRequestReconciler{
1105 RequestController: RequestController{
1106 IssuerTypes: tc.issuerTypes,
1107 ClusterIssuerTypes: tc.clusterIssuerTypes,
1108 },
1109 }
1110
1111 require.NoError(t, crr.setAllIssuerTypesWithGroupVersionKind(scheme))
1112
1113 issuerType, issuerName, err := crr.matchIssuerType(tc.cr)
1114
1115 if tc.expectedIssuerType != nil {
1116 require.NoError(t, kubeutil.SetGroupVersionKind(scheme, tc.expectedIssuerType))
1117 }
1118
1119 assert.Equal(t, tc.expectedIssuerType, issuerType)
1120 assert.Equal(t, tc.expectedIssuerName, issuerName)
1121 if !ptr.Deref(tc.expectedError, *errormatch.NoError())(t, err) {
1122 t.Fail()
1123 }
1124 })
1125 }
1126 }
1127
View as plain text