Skip to content

Commit 6e75203

Browse files
committed
Add event for certificate in KubeadmControlPlane
Signed-off-by: nayuta-ai <[email protected]>
1 parent 5e6992d commit 6e75203

File tree

5 files changed

+346
-2
lines changed

5 files changed

+346
-2
lines changed

controlplane/kubeadm/internal/controllers/consts.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,12 @@ const (
3030
// dependentCertRequeueAfter is how long to wait before checking again to see if
3131
// dependent certificates have been created.
3232
dependentCertRequeueAfter = 30 * time.Second
33+
34+
// CertificatesExpiringReason is the event reason when certificates are expiring soon
35+
// and rollout is triggered (RolloutBefore).
36+
CertificatesExpiringReason = "CertificatesExpiring"
37+
38+
// CertificatesExpiredReason is the event reason when certificates have already expired
39+
// and rollout is triggered (RolloutAfter).
40+
CertificatesExpiredReason = "CertificatesExpired"
3341
)

controlplane/kubeadm/internal/controllers/controller.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,10 @@ func (r *KubeadmControlPlaneReconciler) reconcile(ctx context.Context, controlPl
515515
for machine, machineUpToDateResult := range machinesUpToDateResults {
516516
allMessages = append(allMessages, fmt.Sprintf("Machine %s needs rollout: %s", machine, strings.Join(machineUpToDateResult.LogMessages, ",")))
517517
}
518+
519+
// Emit CertificateRenewalTriggered event (only on first detection)
520+
r.emitCertificateRenewalTriggeredEvent(ctx, controlPlane, machinesUpToDateResults)
521+
518522
log.Info(fmt.Sprintf("Rolling out Control Plane machines: %s", strings.Join(allMessages, ",")), "machinesNeedingRollout", machinesNeedingRollout.Names())
519523
v1beta1conditions.MarkFalse(controlPlane.KCP, controlplanev1.MachinesSpecUpToDateV1Beta1Condition, controlplanev1.RollingUpdateInProgressV1Beta1Reason, clusterv1.ConditionSeverityWarning, "Rolling %d replicas with outdated spec (%d replicas up to date)", len(machinesNeedingRollout), len(controlPlane.Machines)-len(machinesNeedingRollout))
520524
return r.updateControlPlane(ctx, controlPlane, machinesNeedingRollout, machinesUpToDateResults)

controlplane/kubeadm/internal/controllers/helpers.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import (
3939
"sigs.k8s.io/cluster-api/internal/util/ssa"
4040
"sigs.k8s.io/cluster-api/util"
4141
"sigs.k8s.io/cluster-api/util/certs"
42+
"sigs.k8s.io/cluster-api/util/conditions"
4243
v1beta1conditions "sigs.k8s.io/cluster-api/util/conditions/deprecated/v1beta1"
4344
"sigs.k8s.io/cluster-api/util/kubeconfig"
4445
"sigs.k8s.io/cluster-api/util/patch"
@@ -326,3 +327,85 @@ func (r *KubeadmControlPlaneReconciler) updateMachine(ctx context.Context, machi
326327
}
327328
return updatedMachine, nil
328329
}
330+
331+
// emitCertificateRenewalTriggeredEvent detects certificate renewal triggers and emits an event.
332+
// This function checks ConditionMessages for certificate expiry and uses condition state transitions
333+
// to ensure the event is emitted only once per rollout cycle.
334+
// Note: The message format "Certificates will expire soon" is set in filters.go.
335+
func (r *KubeadmControlPlaneReconciler) emitCertificateRenewalTriggeredEvent(
336+
ctx context.Context,
337+
controlPlane *internal.ControlPlane,
338+
machinesUpToDateResults map[string]internal.UpToDateResult,
339+
) {
340+
// Check if certificate renewal is the reason for rollout
341+
certificatesExpiring := []string{}
342+
certificatesExpired := []string{}
343+
344+
for machineName, result := range machinesUpToDateResults {
345+
for _, msg := range result.ConditionMessages {
346+
if strings.Contains(msg, internal.CertificatesExpirySoonMessage) {
347+
certificatesExpiring = append(certificatesExpiring, machineName)
348+
}
349+
if strings.Contains(msg, internal.RolloutAfterExpiredMessage) {
350+
certificatesExpired = append(certificatesExpired, machineName)
351+
}
352+
}
353+
}
354+
355+
// No certificate renewal detected
356+
if len(certificatesExpiring) == 0 && len(certificatesExpired) == 0 {
357+
return
358+
}
359+
360+
// Check v2 RollingOutCondition (if exists)
361+
rollingOutCondition := conditions.Get(controlPlane.KCP, controlplanev1.KubeadmControlPlaneRollingOutCondition)
362+
if rollingOutCondition != nil && rollingOutCondition.Status == metav1.ConditionTrue {
363+
// Already rolling out, don't emit duplicate event
364+
return
365+
}
366+
367+
// Check v1beta1 MachinesSpecUpToDateCondition (fallback for backward compatibility)
368+
v1beta1Condition := v1beta1conditions.Get(controlPlane.KCP, controlplanev1.MachinesSpecUpToDateV1Beta1Condition)
369+
if v1beta1Condition != nil && v1beta1Condition.Status == corev1.ConditionFalse {
370+
// Already rolling out, don't emit duplicate event
371+
return
372+
}
373+
374+
// Emit event for expiring certificates (RolloutBefore)
375+
if len(certificatesExpiring) > 0 {
376+
daysThreshold := controlPlane.KCP.Spec.Rollout.Before.CertificatesExpiryDays
377+
378+
if len(certificatesExpiring) == 1 {
379+
r.recorder.Eventf(controlPlane.KCP, corev1.EventTypeWarning, CertificatesExpiringReason,
380+
"Machine %s has certificates expiring within %d days and will be rolled out",
381+
certificatesExpiring[0], daysThreshold)
382+
} else if len(certificatesExpiring) <= 3 {
383+
r.recorder.Eventf(controlPlane.KCP, corev1.EventTypeWarning, CertificatesExpiringReason,
384+
"%d Machines have certificates expiring within %d days and will be rolled out: %s",
385+
len(certificatesExpiring), daysThreshold, strings.Join(certificatesExpiring, ", "))
386+
} else {
387+
r.recorder.Eventf(controlPlane.KCP, corev1.EventTypeWarning, CertificatesExpiringReason,
388+
"%d Machines have certificates expiring within %d days and will be rolled out: %s, ... (%d more)",
389+
len(certificatesExpiring), daysThreshold,
390+
strings.Join(certificatesExpiring[:3], ", "), len(certificatesExpiring)-3)
391+
}
392+
}
393+
394+
// Emit event for expired certificates (RolloutAfter)
395+
if len(certificatesExpired) > 0 {
396+
if len(certificatesExpired) == 1 {
397+
r.recorder.Eventf(controlPlane.KCP, corev1.EventTypeWarning, CertificatesExpiredReason,
398+
"Machine %s has expired certificates and will be rolled out",
399+
certificatesExpired[0])
400+
} else if len(certificatesExpired) <= 3 {
401+
r.recorder.Eventf(controlPlane.KCP, corev1.EventTypeWarning, CertificatesExpiredReason,
402+
"%d Machines have expired certificates and will be rolled out: %s",
403+
len(certificatesExpired), strings.Join(certificatesExpired, ", "))
404+
} else {
405+
r.recorder.Eventf(controlPlane.KCP, corev1.EventTypeWarning, CertificatesExpiredReason,
406+
"%d Machines have expired certificates and will be rolled out: %s, ... (%d more)",
407+
len(certificatesExpired),
408+
strings.Join(certificatesExpired[:3], ", "), len(certificatesExpired)-3)
409+
}
410+
}
411+
}

controlplane/kubeadm/internal/controllers/helpers_test.go

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package controllers
1818

1919
import (
2020
"context"
21+
"fmt"
2122
"strings"
2223
"testing"
2324

@@ -914,3 +915,246 @@ func trimSpaces(s string) string {
914915
s = strings.ReplaceAll(s, "\t", "")
915916
return s
916917
}
918+
919+
func TestEmitCertificateRenewalTriggeredEvent(t *testing.T) {
920+
tests := []struct {
921+
name string
922+
machinesUpToDateResults map[string]internal.UpToDateResult
923+
rollingOutCondition *metav1.Condition
924+
v1beta1Condition *clusterv1.Condition
925+
certificatesExpiryDays int32
926+
expectedEventCount int
927+
expectedEventReason string
928+
expectedMachinesInEvent []string
929+
expectedDaysInEvent int32
930+
}{
931+
{
932+
name: "should emit event when certificate renewal is detected for the first time",
933+
machinesUpToDateResults: map[string]internal.UpToDateResult{
934+
"machine-1": {
935+
ConditionMessages: []string{
936+
"Certificates will expire soon",
937+
},
938+
},
939+
},
940+
rollingOutCondition: nil, // No RollingOut condition exists yet
941+
v1beta1Condition: nil, // No v1beta1 condition exists yet
942+
certificatesExpiryDays: 30,
943+
expectedEventCount: 1,
944+
expectedEventReason: "CertificatesExpiring",
945+
expectedMachinesInEvent: []string{"machine-1"},
946+
expectedDaysInEvent: 30,
947+
},
948+
{
949+
name: "should emit event for multiple machines",
950+
machinesUpToDateResults: map[string]internal.UpToDateResult{
951+
"machine-1": {
952+
ConditionMessages: []string{
953+
"Certificates will expire soon",
954+
},
955+
},
956+
"machine-2": {
957+
ConditionMessages: []string{
958+
"Certificates will expire soon",
959+
},
960+
},
961+
},
962+
rollingOutCondition: nil,
963+
v1beta1Condition: nil,
964+
certificatesExpiryDays: 30,
965+
expectedEventCount: 1,
966+
expectedEventReason: "CertificatesExpiring",
967+
expectedMachinesInEvent: []string{"machine-1", "machine-2"},
968+
expectedDaysInEvent: 30,
969+
},
970+
{
971+
name: "should not emit event when already rolling out (v2 RollingOutCondition=True)",
972+
machinesUpToDateResults: map[string]internal.UpToDateResult{
973+
"machine-1": {
974+
ConditionMessages: []string{
975+
"Certificates will expire soon",
976+
},
977+
},
978+
},
979+
rollingOutCondition: &metav1.Condition{
980+
Type: controlplanev1.KubeadmControlPlaneRollingOutCondition,
981+
Status: metav1.ConditionTrue,
982+
},
983+
v1beta1Condition: nil,
984+
certificatesExpiryDays: 30,
985+
expectedEventCount: 0,
986+
},
987+
{
988+
name: "should not emit event when already rolling out (v1beta1 MachinesSpecUpToDate=False)",
989+
machinesUpToDateResults: map[string]internal.UpToDateResult{
990+
"machine-1": {
991+
ConditionMessages: []string{
992+
"Certificates will expire soon",
993+
},
994+
},
995+
},
996+
rollingOutCondition: nil,
997+
v1beta1Condition: &clusterv1.Condition{
998+
Type: controlplanev1.MachinesSpecUpToDateV1Beta1Condition,
999+
Status: corev1.ConditionFalse,
1000+
},
1001+
certificatesExpiryDays: 30,
1002+
expectedEventCount: 0,
1003+
},
1004+
{
1005+
name: "should not emit event when no certificate renewal detected",
1006+
machinesUpToDateResults: map[string]internal.UpToDateResult{
1007+
"machine-1": {
1008+
ConditionMessages: []string{
1009+
"Version mismatch",
1010+
},
1011+
},
1012+
},
1013+
rollingOutCondition: nil,
1014+
v1beta1Condition: nil,
1015+
certificatesExpiryDays: 30,
1016+
expectedEventCount: 0,
1017+
},
1018+
{
1019+
name: "should not emit event when no machines need rollout",
1020+
machinesUpToDateResults: map[string]internal.UpToDateResult{},
1021+
rollingOutCondition: nil,
1022+
v1beta1Condition: nil,
1023+
certificatesExpiryDays: 30,
1024+
expectedEventCount: 0,
1025+
},
1026+
{
1027+
name: "should emit CertificatesExpired event when certificates have already expired",
1028+
machinesUpToDateResults: map[string]internal.UpToDateResult{
1029+
"machine-1": {
1030+
ConditionMessages: []string{
1031+
"KubeadmControlPlane spec.rolloutAfter expired",
1032+
},
1033+
},
1034+
},
1035+
rollingOutCondition: nil,
1036+
v1beta1Condition: nil,
1037+
certificatesExpiryDays: 30,
1038+
expectedEventCount: 1,
1039+
expectedEventReason: "CertificatesExpired",
1040+
expectedMachinesInEvent: []string{"machine-1"},
1041+
},
1042+
{
1043+
name: "should emit CertificatesExpired event for multiple machines with expired certificates",
1044+
machinesUpToDateResults: map[string]internal.UpToDateResult{
1045+
"machine-1": {
1046+
ConditionMessages: []string{
1047+
"KubeadmControlPlane spec.rolloutAfter expired",
1048+
},
1049+
},
1050+
"machine-2": {
1051+
ConditionMessages: []string{
1052+
"KubeadmControlPlane spec.rolloutAfter expired",
1053+
},
1054+
},
1055+
},
1056+
rollingOutCondition: nil,
1057+
v1beta1Condition: nil,
1058+
certificatesExpiryDays: 30,
1059+
expectedEventCount: 1,
1060+
expectedEventReason: "CertificatesExpired",
1061+
expectedMachinesInEvent: []string{"machine-1", "machine-2"},
1062+
},
1063+
{
1064+
name: "should emit both events when some certificates are expiring and some have expired",
1065+
machinesUpToDateResults: map[string]internal.UpToDateResult{
1066+
"machine-1": {
1067+
ConditionMessages: []string{
1068+
"Certificates will expire soon",
1069+
},
1070+
},
1071+
"machine-2": {
1072+
ConditionMessages: []string{
1073+
"KubeadmControlPlane spec.rolloutAfter expired",
1074+
},
1075+
},
1076+
},
1077+
rollingOutCondition: nil,
1078+
v1beta1Condition: nil,
1079+
certificatesExpiryDays: 30,
1080+
expectedEventCount: 2,
1081+
expectedEventReason: "CertificatesExpiring", // First event reason
1082+
expectedMachinesInEvent: []string{"machine-1", "machine-2"},
1083+
expectedDaysInEvent: 30,
1084+
},
1085+
}
1086+
1087+
for _, tt := range tests {
1088+
t.Run(tt.name, func(t *testing.T) {
1089+
g := NewWithT(t)
1090+
1091+
// Create a fake event recorder
1092+
fakeRecorder := record.NewFakeRecorder(10)
1093+
1094+
// Create KCP with certificate expiry days
1095+
kcp := &controlplanev1.KubeadmControlPlane{
1096+
ObjectMeta: metav1.ObjectMeta{
1097+
Name: "test-kcp",
1098+
Namespace: metav1.NamespaceDefault,
1099+
},
1100+
Spec: controlplanev1.KubeadmControlPlaneSpec{
1101+
Rollout: controlplanev1.KubeadmControlPlaneRolloutSpec{
1102+
Before: controlplanev1.KubeadmControlPlaneRolloutBeforeSpec{
1103+
CertificatesExpiryDays: tt.certificatesExpiryDays,
1104+
},
1105+
},
1106+
},
1107+
}
1108+
1109+
if tt.rollingOutCondition != nil {
1110+
kcp.Status.Conditions = []metav1.Condition{*tt.rollingOutCondition}
1111+
}
1112+
if tt.v1beta1Condition != nil {
1113+
kcp.SetV1Beta1Conditions([]clusterv1.Condition{*tt.v1beta1Condition})
1114+
}
1115+
1116+
// Create control plane
1117+
controlPlane := &internal.ControlPlane{
1118+
KCP: kcp,
1119+
}
1120+
1121+
// Create reconciler with fake recorder
1122+
r := &KubeadmControlPlaneReconciler{
1123+
recorder: fakeRecorder,
1124+
}
1125+
1126+
// Call the function
1127+
r.emitCertificateRenewalTriggeredEvent(context.Background(), controlPlane, tt.machinesUpToDateResults)
1128+
1129+
// Verify events
1130+
events := []string{}
1131+
close(fakeRecorder.Events)
1132+
for event := range fakeRecorder.Events {
1133+
events = append(events, event)
1134+
}
1135+
1136+
g.Expect(events).To(HaveLen(tt.expectedEventCount))
1137+
if tt.expectedEventCount > 0 {
1138+
// Verify event reason
1139+
g.Expect(events[0]).To(ContainSubstring(tt.expectedEventReason))
1140+
1141+
// For CertificatesExpiring events, verify days threshold
1142+
if tt.expectedEventReason == "CertificatesExpiring" {
1143+
g.Expect(events[0]).To(ContainSubstring("certificates expiring within"))
1144+
g.Expect(events[0]).To(ContainSubstring(fmt.Sprintf("%d days", tt.expectedDaysInEvent)))
1145+
}
1146+
1147+
// For CertificatesExpired events, verify expired message
1148+
if tt.expectedEventReason == "CertificatesExpired" {
1149+
g.Expect(events[0]).To(ContainSubstring("expired certificates"))
1150+
}
1151+
1152+
// Verify machines are mentioned in events
1153+
allEvents := strings.Join(events, " ")
1154+
for _, machine := range tt.expectedMachinesInEvent {
1155+
g.Expect(allEvents).To(ContainSubstring(machine))
1156+
}
1157+
}
1158+
})
1159+
}
1160+
}

0 commit comments

Comments
 (0)