@@ -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
8284func (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
390574func isCertManagerCRDPresent (mgr ctrl.Manager ) bool {
391575 gvk := certv1 .SchemeGroupVersion .WithKind ("Certificate" )
0 commit comments