Skip to content

Commit ddb834e

Browse files
authored
Add status reporting to EtcdCluster (#350)
Signed-off-by: Guna K Kambalimath <Guna.Kambalimath@ibm.com>
1 parent 0a4c8bf commit ddb834e

1 file changed

Lines changed: 187 additions & 3 deletions

File tree

internal/controller/etcdcluster_controller.go

Lines changed: 187 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ import (
2626
appsv1 "k8s.io/api/apps/v1"
2727
corev1 "k8s.io/api/core/v1"
2828
"k8s.io/apimachinery/pkg/api/errors"
29+
"k8s.io/apimachinery/pkg/api/meta"
30+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2931
"k8s.io/apimachinery/pkg/runtime"
3032
"k8s.io/client-go/tools/events"
3133
ctrl "sigs.k8s.io/controller-runtime"
@@ -80,13 +82,27 @@ type reconcileState struct {
8082
// For more details on the controller-runtime Reconcile contract see:
8183
// https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/reconcile
8284
func (r *EtcdClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
83-
state, res, err := r.fetchAndValidateState(ctx, req)
85+
var state *reconcileState
86+
var res ctrl.Result
87+
var err error
88+
89+
// Defer status update to ensure it's called regardless of return path
90+
defer func() {
91+
if state != nil {
92+
if statusErr := r.updateStatus(ctx, state); statusErr != nil {
93+
// Log but don't override the main reconciliation error
94+
log.FromContext(ctx).Error(statusErr, "Failed to update status")
95+
}
96+
}
97+
}()
98+
99+
state, res, err = r.fetchAndValidateState(ctx, req)
84100
if state == nil || err != nil {
85101
return res, err
86102
}
87103

88-
if bootstrapRes, err := r.bootstrapStatefulSet(ctx, state); err != nil || !bootstrapRes.IsZero() {
89-
return bootstrapRes, err
104+
if res, err = r.bootstrapStatefulSet(ctx, state); err != nil || !res.IsZero() {
105+
return res, err
90106
}
91107

92108
if err = r.performHealthChecks(ctx, state); err != nil {
@@ -386,6 +402,174 @@ func (r *EtcdClusterReconciler) reconcileClusterState(ctx context.Context, s *re
386402
return ctrl.Result{}, nil
387403
}
388404

405+
// updateStatus updates the EtcdCluster status based on observed state.
406+
// It is called at the end of each reconciliation cycle.
407+
func (r *EtcdClusterReconciler) updateStatus(ctx context.Context, s *reconcileState) error {
408+
logger := log.FromContext(ctx)
409+
410+
// Update ObservedGeneration
411+
s.cluster.Status.ObservedGeneration = s.cluster.Generation
412+
413+
// Update replica counts from StatefulSet
414+
if s.sts != nil {
415+
if s.sts.Spec.Replicas != nil {
416+
s.cluster.Status.CurrentReplicas = *s.sts.Spec.Replicas
417+
}
418+
s.cluster.Status.ReadyReplicas = s.sts.Status.ReadyReplicas
419+
}
420+
421+
// Update member count from etcd cluster
422+
if s.memberListResp != nil {
423+
s.cluster.Status.MemberCount = int32(len(s.memberListResp.Members))
424+
425+
// Update individual member statuses
426+
s.cluster.Status.Members = make([]ecv1alpha1.MemberStatus, 0, len(s.memberListResp.Members))
427+
for i, member := range s.memberListResp.Members {
428+
memberStatus := ecv1alpha1.MemberStatus{
429+
ID: fmt.Sprintf("%x", member.ID),
430+
Name: member.Name,
431+
}
432+
433+
// Find health info for this member
434+
if i < len(s.memberHealth) {
435+
health := s.memberHealth[i]
436+
memberStatus.IsHealthy = health.Health
437+
if health.Status != nil {
438+
memberStatus.Version = health.Status.Version
439+
memberStatus.IsLeader = health.Status.Header.MemberId == health.Status.Leader
440+
}
441+
}
442+
443+
memberStatus.IsLearner = member.IsLearner
444+
s.cluster.Status.Members = append(s.cluster.Status.Members, memberStatus)
445+
}
446+
447+
// Update leader ID
448+
_, leaderStatus := etcdutils.FindLeaderStatus(s.memberHealth, logger)
449+
if leaderStatus != nil {
450+
s.cluster.Status.LeaderID = fmt.Sprintf("%x", leaderStatus.Leader)
451+
}
452+
453+
// Update current version from leader or first healthy member
454+
if leaderStatus != nil {
455+
s.cluster.Status.CurrentVersion = leaderStatus.Version
456+
} else if len(s.memberHealth) > 0 && s.memberHealth[0].Status != nil {
457+
s.cluster.Status.CurrentVersion = s.memberHealth[0].Status.Version
458+
}
459+
}
460+
461+
// Update conditions
462+
r.updateConditions(s)
463+
464+
// Persist status update
465+
if err := r.Status().Update(ctx, s.cluster); err != nil {
466+
logger.Error(err, "Failed to update EtcdCluster status")
467+
return err
468+
}
469+
470+
return nil
471+
}
472+
473+
// updateConditions sets the standard Kubernetes conditions based on observed state
474+
func (r *EtcdClusterReconciler) updateConditions(s *reconcileState) {
475+
now := metav1.Now()
476+
477+
// Determine if cluster is available (has quorum and healthy members)
478+
availableCondition := metav1.Condition{
479+
Type: "Available",
480+
Status: metav1.ConditionFalse,
481+
ObservedGeneration: s.cluster.Generation,
482+
LastTransitionTime: now,
483+
Reason: "ClusterNotReady",
484+
Message: "Etcd cluster is not yet available",
485+
}
486+
487+
if s.memberListResp != nil && len(s.memberListResp.Members) > 0 {
488+
healthyCount := 0
489+
for _, health := range s.memberHealth {
490+
if health.Health {
491+
healthyCount++
492+
}
493+
}
494+
495+
quorum := (len(s.memberListResp.Members) / 2) + 1
496+
if healthyCount >= quorum {
497+
availableCondition.Status = metav1.ConditionTrue
498+
availableCondition.Reason = "ClusterAvailable"
499+
availableCondition.Message = fmt.Sprintf("Etcd cluster has %d/%d healthy members with quorum", healthyCount, len(s.memberListResp.Members))
500+
} else {
501+
availableCondition.Message = fmt.Sprintf("Etcd cluster has %d/%d healthy members, quorum requires %d", healthyCount, len(s.memberListResp.Members), quorum)
502+
}
503+
}
504+
505+
// Determine if cluster is progressing (scaling or upgrading)
506+
progressingCondition := metav1.Condition{
507+
Type: "Progressing",
508+
Status: metav1.ConditionFalse,
509+
ObservedGeneration: s.cluster.Generation,
510+
LastTransitionTime: now,
511+
Reason: "ClusterStable",
512+
Message: "Etcd cluster is stable",
513+
}
514+
515+
if s.sts != nil && s.sts.Spec.Replicas != nil {
516+
currentReplicas := *s.sts.Spec.Replicas
517+
desiredSize := int32(s.cluster.Spec.Size)
518+
519+
if currentReplicas != desiredSize {
520+
progressingCondition.Status = metav1.ConditionTrue
521+
progressingCondition.Reason = "ScalingInProgress"
522+
progressingCondition.Message = fmt.Sprintf("Scaling from %d to %d replicas", currentReplicas, desiredSize)
523+
} else if s.memberListResp != nil && int32(len(s.memberListResp.Members)) != desiredSize {
524+
progressingCondition.Status = metav1.ConditionTrue
525+
progressingCondition.Reason = "MembershipChanging"
526+
progressingCondition.Message = fmt.Sprintf("Etcd membership changing: %d members, target %d", len(s.memberListResp.Members), desiredSize)
527+
}
528+
529+
// Check for learners (indicates scaling in progress)
530+
if s.memberListResp != nil {
531+
for _, member := range s.memberListResp.Members {
532+
if member.IsLearner {
533+
progressingCondition.Status = metav1.ConditionTrue
534+
progressingCondition.Reason = "LearnerPromotion"
535+
progressingCondition.Message = "Waiting for learner member to be promoted"
536+
break
537+
}
538+
}
539+
}
540+
}
541+
542+
// Determine if cluster is degraded
543+
degradedCondition := metav1.Condition{
544+
Type: "Degraded",
545+
Status: metav1.ConditionFalse,
546+
ObservedGeneration: s.cluster.Generation,
547+
LastTransitionTime: now,
548+
Reason: "ClusterHealthy",
549+
Message: "All etcd members are healthy",
550+
}
551+
552+
if s.memberListResp != nil && len(s.memberHealth) > 0 {
553+
unhealthyMembers := []string{}
554+
for _, health := range s.memberHealth {
555+
if !health.Health && health.Status != nil {
556+
unhealthyMembers = append(unhealthyMembers, fmt.Sprintf("%x", health.Status.Header.MemberId))
557+
}
558+
}
559+
560+
if len(unhealthyMembers) > 0 {
561+
degradedCondition.Status = metav1.ConditionTrue
562+
degradedCondition.Reason = "UnhealthyMembers"
563+
degradedCondition.Message = fmt.Sprintf("Unhealthy members: %s", strings.Join(unhealthyMembers, ", "))
564+
}
565+
}
566+
567+
// Update or append conditions
568+
meta.SetStatusCondition(&s.cluster.Status.Conditions, availableCondition)
569+
meta.SetStatusCondition(&s.cluster.Status.Conditions, progressingCondition)
570+
meta.SetStatusCondition(&s.cluster.Status.Conditions, degradedCondition)
571+
}
572+
389573
// isCertManagerCRDPresent checks if cert-manager CRDs are installed in the cluster
390574
func isCertManagerCRDPresent(mgr ctrl.Manager) bool {
391575
gvk := certv1.SchemeGroupVersion.WithKind("Certificate")

0 commit comments

Comments
 (0)