Skip to content

Commit 6daa9ce

Browse files
Merge pull request #8247 from sdminonne/CNTRLPLANE-2204
CNTRLPLANE-2204: improve AllNodesHealthy and AllMachinesReady conditions
2 parents 1d9ea73 + e977a39 commit 6daa9ce

5 files changed

Lines changed: 1217 additions & 79 deletions

File tree

hypershift-operator/controllers/nodepool/conditions.go

Lines changed: 62 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -98,11 +98,24 @@ func FindStatusCondition(conditions []hyperv1.NodePoolCondition, conditionType s
9898
return nil
9999
}
100100

101-
// FindStatusCondition finds the conditionType in conditions.
102-
func findCAPIStatusCondition(conditions []capiv1.Condition, conditionType capiv1.ConditionType) *capiv1.Condition {
103-
for i := range conditions {
104-
if conditions[i].Type == conditionType {
105-
return &conditions[i]
101+
// machineConditionResult normalizes a CAPI Machine condition into a common struct.
102+
type machineConditionResult struct {
103+
Status corev1.ConditionStatus
104+
Reason string
105+
Message string
106+
}
107+
108+
// findMachineStatusCondition looks up a condition on a CAPI Machine from
109+
// Machine.Status.Conditions ([]capiv1.Condition).
110+
// Returns nil if the condition is not found.
111+
func findMachineStatusCondition(machine *capiv1.Machine, conditionType string) *machineConditionResult {
112+
for i := range machine.Status.Conditions {
113+
if string(machine.Status.Conditions[i].Type) == conditionType {
114+
return &machineConditionResult{
115+
Status: machine.Status.Conditions[i].Status,
116+
Reason: machine.Status.Conditions[i].Reason,
117+
Message: machine.Status.Conditions[i].Message,
118+
}
106119
}
107120
}
108121

@@ -604,29 +617,45 @@ func (r *NodePoolReconciler) setMachineAndNodeConditions(ctx context.Context, no
604617
func (r *NodePoolReconciler) setAllNodesHealthyCondition(nodePool *hyperv1.NodePool, machines []*capiv1.Machine) {
605618
status := corev1.ConditionTrue
606619
reason := hyperv1.AsExpectedReason
607-
var message string
620+
message := hyperv1.AllIsWellMessage
608621

609-
if len(machines) < 1 {
622+
if numMachines := len(machines); numMachines == 0 {
610623
status = corev1.ConditionFalse
611624
reason = hyperv1.NodePoolNotFoundReason
612625
message = "No Machines are created"
613626
if nodePool.Spec.Replicas != nil && *nodePool.Spec.Replicas == 0 {
614627
reason = hyperv1.AsExpectedReason
615628
message = "NodePool set to no replicas"
616629
}
617-
}
630+
} else {
631+
numNotHealthy := 0
632+
messageMap := make(map[string][]string)
618633

619-
for _, machine := range machines {
620-
condition := findCAPIStatusCondition(machine.Status.Conditions, capiv1.MachineNodeHealthyCondition)
621-
if condition != nil && condition.Status != corev1.ConditionTrue {
622-
status = corev1.ConditionFalse
623-
reason = condition.Reason
624-
message = message + fmt.Sprintf("Machine %s: %s\n", machine.Name, condition.Reason)
634+
for _, machine := range machines {
635+
condition := findMachineStatusCondition(machine, string(capiv1.MachineNodeHealthyCondition))
636+
if condition == nil {
637+
// NodeHealthy condition not yet reported; treat as not healthy.
638+
status = corev1.ConditionFalse
639+
numNotHealthy++
640+
mapReason := capiv1.WaitingForNodeRefReason
641+
mapMessage := fmt.Sprintf("Machine %s: %s\n", machine.Name, mapReason)
642+
messageMap[mapReason] = append(messageMap[mapReason], mapMessage)
643+
} else if condition.Status != corev1.ConditionTrue {
644+
status = corev1.ConditionFalse
645+
numNotHealthy++
646+
mapReason := condition.Reason
647+
var mapMessage string
648+
if condition.Message != "" {
649+
mapMessage = fmt.Sprintf("Machine %s: %s: %s\n", machine.Name, condition.Reason, condition.Message)
650+
} else {
651+
mapMessage = fmt.Sprintf("Machine %s: %s\n", machine.Name, condition.Reason)
652+
}
653+
messageMap[mapReason] = append(messageMap[mapReason], mapMessage)
654+
}
655+
}
656+
if numNotHealthy > 0 {
657+
reason, message = aggregateMachineReasonsAndMessages(messageMap, numMachines, numNotHealthy, aggregatorMachineStateHealthy)
625658
}
626-
}
627-
628-
if status == corev1.ConditionTrue {
629-
message = hyperv1.AllIsWellMessage
630659
}
631660

632661
allMachinesHealthyCondition := &hyperv1.NodePoolCondition{
@@ -666,11 +695,18 @@ func (r *NodePoolReconciler) setAllMachinesReadyCondition(nodePool *hyperv1.Node
666695
messageMap := make(map[string][]string)
667696

668697
for _, machine := range machines {
669-
readyCond := findCAPIStatusCondition(machine.Status.Conditions, capiv1.ReadyCondition)
670-
if readyCond != nil && readyCond.Status != corev1.ConditionTrue {
698+
readyCond := findMachineStatusCondition(machine, string(capiv1.ReadyCondition))
699+
if readyCond == nil {
700+
// Ready condition not yet reported; treat as not ready.
701+
status = corev1.ConditionFalse
702+
numNotReady++
703+
mapReason := capiv1.WaitingForInfrastructureFallbackReason
704+
mapMessage := fmt.Sprintf("Machine %s: %s\n", machine.Name, mapReason)
705+
messageMap[mapReason] = append(messageMap[mapReason], mapMessage)
706+
} else if readyCond.Status != corev1.ConditionTrue {
671707
status = corev1.ConditionFalse
672708
numNotReady++
673-
infraReadyCond := findCAPIStatusCondition(machine.Status.Conditions, capiv1.InfrastructureReadyCondition)
709+
infraReadyCond := findMachineStatusCondition(machine, string(capiv1.InfrastructureReadyCondition))
674710
// We append the reason as part of the higher Message, since the message is meaningless.
675711
// This is how a CAPI condition looks like in AWS for an instance deleted out of band failure.
676712
// - lastTransitionTime: "2022-11-28T15:14:28Z"
@@ -685,7 +721,11 @@ func (r *NodePoolReconciler) setAllMachinesReadyCondition(nodePool *hyperv1.Node
685721
mapMessage = fmt.Sprintf("Machine %s: %s: %s\n", machine.Name, infraReadyCond.Reason, infraReadyCond.Message)
686722
} else {
687723
mapReason = readyCond.Reason
688-
mapMessage = fmt.Sprintf("Machine %s: %s\n", machine.Name, readyCond.Reason)
724+
if readyCond.Message != "" && !isSetupCounterCondMessage.MatchString(readyCond.Message) {
725+
mapMessage = fmt.Sprintf("Machine %s: %s: %s\n", machine.Name, readyCond.Reason, readyCond.Message)
726+
} else {
727+
mapMessage = fmt.Sprintf("Machine %s: %s\n", machine.Name, readyCond.Reason)
728+
}
689729
}
690730

691731
messageMap[mapReason] = append(messageMap[mapReason], mapMessage)

hypershift-operator/controllers/nodepool/nodepool_controller.go

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1095,8 +1095,13 @@ func sortedByCreationTimestamp(machines []*capiv1.Machine) []*capiv1.Machine {
10951095

10961096
const (
10971097
endOfMessage = "... too many similar errors\n"
1098+
endOfGlobalMessage = "... message truncated\n"
1099+
endOfReasons = ",ReasonsTruncated"
10981100
maxMessageLength = 1000
1101+
maxGlobalMessageLength = 3000
1102+
maxReasonLength = 1024 // +kubebuilder:validation:MaxLength on NodePoolCondition.Reason
10991103
aggregatorMachineStateReady = "ready"
1104+
aggregatorMachineStateHealthy = "healthy"
11001105
aggregatorMachineStateLiveMigratable = "live migratable"
11011106
)
11021107

@@ -1117,10 +1122,46 @@ func aggregateMachineReasonsAndMessages(messageMap map[string][]string, numMachi
11171122
sort.Strings(reasons)
11181123

11191124
for _, reason := range reasons {
1120-
msgBuilder.WriteString(aggregateMachineMessages(messageMap[reason]))
1125+
// Sort messages within each reason bucket to ensure deterministic output
1126+
// regardless of Kubernetes list order, avoiding unnecessary status updates.
1127+
sort.Strings(messageMap[reason])
1128+
reasonBlock := aggregateMachineMessages(messageMap[reason])
1129+
if msgBuilder.Len()+len(reasonBlock)+len(endOfGlobalMessage) > maxGlobalMessageLength {
1130+
msgBuilder.WriteString(endOfGlobalMessage)
1131+
break
1132+
}
1133+
msgBuilder.WriteString(reasonBlock)
11211134
}
11221135

1123-
return strings.Join(reasons, ","), msgBuilder.String()
1136+
return truncateReasons(reasons), msgBuilder.String()
1137+
}
1138+
1139+
// truncateReasons joins reasons with commas and truncates the result to fit
1140+
// within the NodePoolCondition.Reason MaxLength=1024 validation limit.
1141+
// When truncation occurs, the suffix ",ReasonsTruncated" is appended.
1142+
func truncateReasons(reasons []string) string {
1143+
joined := strings.Join(reasons, ",")
1144+
if len(joined) <= maxReasonLength {
1145+
return joined
1146+
}
1147+
1148+
// Build the truncated reason string by adding reasons one at a time,
1149+
// reserving space for the endOfReasons suffix.
1150+
builder := strings.Builder{}
1151+
for i, reason := range reasons {
1152+
separator := ""
1153+
if i > 0 {
1154+
separator = ","
1155+
}
1156+
if builder.Len()+len(separator)+len(reason)+len(endOfReasons) > maxReasonLength {
1157+
builder.WriteString(endOfReasons)
1158+
break
1159+
}
1160+
builder.WriteString(separator)
1161+
builder.WriteString(reason)
1162+
}
1163+
1164+
return builder.String()
11241165
}
11251166

11261167
func aggregateMachineMessages(msgs []string) string {

0 commit comments

Comments
 (0)