Skip to content

Commit 9b4c278

Browse files
committed
feat: implement health check logic
Signed-off-by: Matthew H. Irby <[email protected]>
1 parent 743d8ff commit 9b4c278

File tree

3 files changed

+162
-12
lines changed

3 files changed

+162
-12
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
# v2.3.2
2+
## Features
3+
- Add a `healthCheckIntervalSeconds` specification to Issuer / ClusterIssuer resources, allowing flexibility in the health check interval.
4+
15
# v2.3.1
26
## Fixes
37
- Add a manual dispatch of Helm chart release.

internal/controller/issuer_controller.go

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525

2626
commandissuer "github.com/Keyfactor/command-cert-manager-issuer/api/v1alpha1"
2727
"github.com/Keyfactor/command-cert-manager-issuer/internal/command"
28+
"github.com/go-logr/logr"
2829
corev1 "k8s.io/api/core/v1"
2930
"k8s.io/apimachinery/pkg/runtime"
3031
"k8s.io/apimachinery/pkg/types"
@@ -86,8 +87,6 @@ func (r *IssuerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (res
8687
return ctrl.Result{}, nil
8788
}
8889

89-
log.Info(fmt.Sprintf("Starting %s reconciliation run", issuer.GetObjectKind().GroupVersionKind().Kind))
90-
9190
// Always attempt to update the Ready condition
9291
defer func() {
9392
if err != nil {
@@ -99,6 +98,17 @@ func (r *IssuerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (res
9998
}
10099
}()
101100

101+
healthCheckInterval, err := getHealthCheckInterval(log, issuer)
102+
if err != nil {
103+
log.Error(err, "en error occurred while getting the health check interval")
104+
issuer.GetStatus().SetCondition(ctx, commandissuer.IssuerConditionReady, commandissuer.ConditionFalse, issuerReadyConditionReason, err.Error())
105+
issuer.GetStatus().SetCondition(ctx, commandissuer.IssuerConditionSupportsMetadata, commandissuer.ConditionUnknown, "", "")
106+
return ctrl.Result{}, nil
107+
}
108+
109+
log.Info(fmt.Sprintf("Starting %s reconciliation run", issuer.GetObjectKind().GroupVersionKind().Kind))
110+
log.Info(fmt.Sprintf("Issuer %s has been configured with a health check interval of %d seconds", issuer.GetObjectKind().GroupVersionKind().Kind, int(healthCheckInterval/time.Second)))
111+
102112
var secretNamespace string
103113

104114
switch {
@@ -142,7 +152,30 @@ func (r *IssuerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (res
142152
issuer.GetStatus().SetCondition(ctx, commandissuer.IssuerConditionSupportsMetadata, commandissuer.ConditionFalse, "Metadata fields are not defined", "Connected Command platform doesn't have the Command Issuer metadata fields defined.")
143153
}
144154

145-
return ctrl.Result{RequeueAfter: defaultHealthCheckInterval}, nil
155+
return ctrl.Result{RequeueAfter: healthCheckInterval}, nil
156+
}
157+
158+
func getHealthCheckInterval(log logr.Logger, issuer commandissuer.IssuerLike) (time.Duration, error) {
159+
spec := issuer.GetSpec()
160+
161+
if spec.HealthCheckIntervalSeconds == nil {
162+
log.Info(fmt.Sprintf("health check spec value is nil, using default: %d", int(defaultHealthCheckInterval/time.Second)))
163+
return defaultHealthCheckInterval, nil
164+
}
165+
166+
interval := *spec.HealthCheckIntervalSeconds
167+
168+
// Health check interval should not be negative
169+
if interval < 0 {
170+
return 0, fmt.Errorf("interval %d is invalid, must be greater than or equal to 0", interval)
171+
}
172+
173+
// Issuer may be configured to ignore future health checks
174+
if interval == 0 {
175+
log.Info("health check interval is configured to be 0. this will disable future health checks for issuer.")
176+
}
177+
178+
return time.Duration(interval) * time.Second, nil
146179
}
147180

148181
func commandConfigFromIssuer(ctx context.Context, c client.Client, issuer commandissuer.IssuerLike, secretNamespace string) (*command.Config, error) {

internal/controller/issuer_controller_test.go

Lines changed: 122 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
Copyright © 2024 Keyfactor
2+
Copyright © 2025 Keyfactor
33
44
Licensed under the Apache License, Version 2.0 (the "License");
55
you may not use this file except in compliance with the License.
@@ -20,7 +20,9 @@ import (
2020
"context"
2121
"errors"
2222
"testing"
23+
"time"
2324

25+
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
2426
commandissuerv1alpha1 "github.com/Keyfactor/command-cert-manager-issuer/api/v1alpha1"
2527
"github.com/Keyfactor/command-cert-manager-issuer/internal/command"
2628
logrtesting "github.com/go-logr/logr/testing"
@@ -52,19 +54,13 @@ func (f *fakeHealthChecker) CommandSupportsMetadata() (bool, error) {
5254
var newFakeHealthCheckerBuilder = func(builderErr error, checkerErr error, supportsMetadata bool) func(context.Context, *command.Config) (command.HealthChecker, error) {
5355
return func(context.Context, *command.Config) (command.HealthChecker, error) {
5456
return &fakeHealthChecker{
55-
errCheck: checkerErr,
57+
supportsMetadata: supportsMetadata,
58+
errCheck: checkerErr,
5659
}, builderErr
5760
}
5861
}
5962

6063
func TestIssuerReconcile(t *testing.T) {
61-
// caCert, rootKey := issueTestCertificate(t, "Root-CA", nil, nil)
62-
// caCertPem := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: caCert.Raw})
63-
64-
// serverCert, _ := issueTestCertificate(t, "Server", caCert, rootKey)
65-
// serverCertPem := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: serverCert.Raw})
66-
// caChain := append(serverCertPem, caCertPem...)
67-
6864
type testCase struct {
6965
kind string
7066
name types.NamespacedName
@@ -572,6 +568,123 @@ func TestIssuerReconcile(t *testing.T) {
572568
expectedReadyConditionStatus: commandissuerv1alpha1.ConditionFalse,
573569
expectedMetadataSupportedConditionStatus: commandissuerv1alpha1.ConditionFalse,
574570
},
571+
"success-custom-healthcheck-interval-issuer": {
572+
kind: "Issuer",
573+
name: types.NamespacedName{Namespace: "ns1", Name: "issuer1"},
574+
objects: []client.Object{
575+
&commandissuerv1alpha1.Issuer{
576+
ObjectMeta: metav1.ObjectMeta{
577+
Name: "issuer1",
578+
Namespace: "ns1",
579+
},
580+
Spec: commandissuerv1alpha1.IssuerSpec{
581+
SecretName: "issuer1-credentials",
582+
HealthCheckIntervalSeconds: to.Ptr(30),
583+
},
584+
Status: commandissuerv1alpha1.IssuerStatus{
585+
Conditions: []commandissuerv1alpha1.IssuerCondition{
586+
{
587+
Type: commandissuerv1alpha1.IssuerConditionReady,
588+
Status: commandissuerv1alpha1.ConditionUnknown,
589+
},
590+
},
591+
},
592+
},
593+
&corev1.Secret{
594+
Type: corev1.SecretTypeBasicAuth,
595+
ObjectMeta: metav1.ObjectMeta{
596+
Name: "issuer1-credentials",
597+
Namespace: "ns1",
598+
},
599+
Data: map[string][]byte{
600+
corev1.BasicAuthUsernameKey: []byte("username"),
601+
corev1.BasicAuthPasswordKey: []byte("password"),
602+
},
603+
},
604+
},
605+
healthCheckerBuilder: newFakeHealthCheckerBuilder(nil, nil, true),
606+
expectedReadyConditionStatus: commandissuerv1alpha1.ConditionTrue,
607+
expectedMetadataSupportedConditionStatus: commandissuerv1alpha1.ConditionTrue,
608+
expectedResult: ctrl.Result{RequeueAfter: time.Duration(30) * time.Second},
609+
},
610+
"success-custom-healthcheck-interval-clusterissuer": {
611+
kind: "ClusterIssuer",
612+
name: types.NamespacedName{Name: "clusterissuer1"},
613+
objects: []client.Object{
614+
&commandissuerv1alpha1.ClusterIssuer{
615+
ObjectMeta: metav1.ObjectMeta{
616+
Name: "clusterissuer1",
617+
},
618+
Spec: commandissuerv1alpha1.IssuerSpec{
619+
SecretName: "clusterissuer1-credentials",
620+
HealthCheckIntervalSeconds: to.Ptr(30),
621+
},
622+
Status: commandissuerv1alpha1.IssuerStatus{
623+
Conditions: []commandissuerv1alpha1.IssuerCondition{
624+
{
625+
Type: commandissuerv1alpha1.IssuerConditionReady,
626+
Status: commandissuerv1alpha1.ConditionUnknown,
627+
},
628+
},
629+
},
630+
},
631+
&corev1.Secret{
632+
Type: corev1.SecretTypeBasicAuth,
633+
ObjectMeta: metav1.ObjectMeta{
634+
Name: "clusterissuer1-credentials",
635+
Namespace: "kube-system",
636+
},
637+
Data: map[string][]byte{
638+
corev1.BasicAuthUsernameKey: []byte("username"),
639+
corev1.BasicAuthPasswordKey: []byte("password"),
640+
},
641+
},
642+
},
643+
healthCheckerBuilder: newFakeHealthCheckerBuilder(nil, nil, true),
644+
clusterResourceNamespace: "kube-system",
645+
expectedReadyConditionStatus: commandissuerv1alpha1.ConditionTrue,
646+
expectedMetadataSupportedConditionStatus: commandissuerv1alpha1.ConditionTrue,
647+
expectedResult: ctrl.Result{RequeueAfter: time.Duration(30) * time.Second},
648+
},
649+
"error-healthcheck-negative-value": {
650+
kind: "Issuer",
651+
name: types.NamespacedName{Namespace: "ns1", Name: "issuer1"},
652+
objects: []client.Object{
653+
&commandissuerv1alpha1.Issuer{
654+
ObjectMeta: metav1.ObjectMeta{
655+
Name: "issuer1",
656+
Namespace: "ns1",
657+
},
658+
Spec: commandissuerv1alpha1.IssuerSpec{
659+
SecretName: "issuer1-credentials",
660+
HealthCheckIntervalSeconds: to.Ptr(-30),
661+
},
662+
Status: commandissuerv1alpha1.IssuerStatus{
663+
Conditions: []commandissuerv1alpha1.IssuerCondition{
664+
{
665+
Type: commandissuerv1alpha1.IssuerConditionReady,
666+
Status: commandissuerv1alpha1.ConditionUnknown,
667+
},
668+
},
669+
},
670+
},
671+
&corev1.Secret{
672+
Type: corev1.SecretTypeBasicAuth,
673+
ObjectMeta: metav1.ObjectMeta{
674+
Name: "issuer1-credentials",
675+
Namespace: "ns1",
676+
},
677+
Data: map[string][]byte{
678+
corev1.BasicAuthUsernameKey: []byte("username"),
679+
corev1.BasicAuthPasswordKey: []byte("password"),
680+
},
681+
},
682+
},
683+
healthCheckerBuilder: newFakeHealthCheckerBuilder(nil, nil, false),
684+
expectedReadyConditionStatus: commandissuerv1alpha1.ConditionFalse,
685+
expectedMetadataSupportedConditionStatus: commandissuerv1alpha1.ConditionUnknown,
686+
expectedResult: ctrl.Result{},
687+
},
575688
}
576689

577690
scheme := runtime.NewScheme()

0 commit comments

Comments
 (0)