@@ -28,6 +28,7 @@ import (
2828 hcmetrics "github.com/openshift/hypershift/hypershift-operator/controllers/hostedcluster/metrics"
2929 hcpmanifests "github.com/openshift/hypershift/hypershift-operator/controllers/manifests"
3030 "github.com/openshift/hypershift/hypershift-operator/controllers/manifests/controlplaneoperator"
31+ etcdrecoverymanifests "github.com/openshift/hypershift/hypershift-operator/controllers/manifests/etcdrecovery"
3132 kvinfra "github.com/openshift/hypershift/kubevirtexternalinfra"
3233 "github.com/openshift/hypershift/support/api"
3334 "github.com/openshift/hypershift/support/azureutil"
@@ -47,9 +48,11 @@ import (
4748 configv1 "github.com/openshift/api/config/v1"
4849
4950 appsv1 "k8s.io/api/apps/v1"
51+ batchv1 "k8s.io/api/batch/v1"
5052 corev1 "k8s.io/api/core/v1"
5153 "k8s.io/apimachinery/pkg/api/equality"
5254 errors2 "k8s.io/apimachinery/pkg/api/errors"
55+ "k8s.io/apimachinery/pkg/api/meta"
5356 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
5457 "k8s.io/apimachinery/pkg/types"
5558 "k8s.io/apimachinery/pkg/util/intstr"
@@ -6529,3 +6532,158 @@ func TestComputeEndpointServiceCondition(t *testing.T) {
65296532 })
65306533 }
65316534}
6535+
6536+ func TestReconcileETCDMemberRecovery (t * testing.T ) {
6537+ hcpNS := "clusters-test-hc"
6538+
6539+ healthyEtcdPods := func () []crclient.Object {
6540+ var pods []crclient.Object
6541+ for i := 0 ; i < 3 ; i ++ {
6542+ pods = append (pods , & corev1.Pod {
6543+ ObjectMeta : metav1.ObjectMeta {
6544+ Name : fmt .Sprintf ("etcd-%d" , i ),
6545+ Namespace : hcpNS ,
6546+ Labels : map [string ]string {"app" : "etcd" },
6547+ },
6548+ Status : corev1.PodStatus {
6549+ ContainerStatuses : []corev1.ContainerStatus {
6550+ {
6551+ Name : "etcd" ,
6552+ State : corev1.ContainerState {Running : & corev1.ContainerStateRunning {}},
6553+ },
6554+ },
6555+ },
6556+ })
6557+ }
6558+ return pods
6559+ }
6560+
6561+ healthyStatefulSet := & appsv1.StatefulSet {
6562+ ObjectMeta : metav1.ObjectMeta {
6563+ Name : "etcd" ,
6564+ Namespace : hcpNS ,
6565+ },
6566+ Status : appsv1.StatefulSetStatus {
6567+ ReadyReplicas : 3 ,
6568+ AvailableReplicas : 3 ,
6569+ },
6570+ }
6571+
6572+ unhealthyStatefulSet := & appsv1.StatefulSet {
6573+ ObjectMeta : metav1.ObjectMeta {
6574+ Name : "etcd" ,
6575+ Namespace : hcpNS ,
6576+ },
6577+ Status : appsv1.StatefulSetStatus {
6578+ ReadyReplicas : 2 ,
6579+ AvailableReplicas : 2 ,
6580+ },
6581+ }
6582+
6583+ staleCondition := metav1.Condition {
6584+ Type : string (hyperv1 .EtcdRecoveryActive ),
6585+ Status : metav1 .ConditionFalse ,
6586+ Reason : hyperv1 .EtcdRecoveryJobFailedReason ,
6587+ Message : "Error in Etcd Recovery job: the Etcd cluster requires manual intervention." ,
6588+ LastTransitionTime : metav1 .Now (),
6589+ }
6590+
6591+ failedJob := etcdrecoverymanifests .EtcdRecoveryJob (hcpNS )
6592+ failedJob .Status = batchv1.JobStatus {
6593+ Conditions : []batchv1.JobCondition {
6594+ {
6595+ Type : batchv1 .JobFailed ,
6596+ Status : corev1 .ConditionTrue ,
6597+ },
6598+ },
6599+ }
6600+
6601+ testCases := []struct {
6602+ name string
6603+ objects []crclient.Object
6604+ conditions []metav1.Condition
6605+ expectedReason string
6606+ conditionExists bool
6607+ }{
6608+ {
6609+ name : "When etcd is healthy and stale EtcdRecoveryJobFailed condition exists it should clear the condition" ,
6610+ conditions : []metav1.Condition {staleCondition },
6611+ objects : append (healthyEtcdPods (), healthyStatefulSet ),
6612+ expectedReason : hyperv1 .AsExpectedReason ,
6613+ conditionExists : true ,
6614+ },
6615+ {
6616+ name : "When etcd is healthy and no EtcdRecoveryActive condition exists it should not add one" ,
6617+ conditions : []metav1.Condition {},
6618+ objects : append (healthyEtcdPods (), healthyStatefulSet ),
6619+ conditionExists : false ,
6620+ },
6621+ {
6622+ name : "When failed job exists but etcd recovered it should cleanup job and clear condition" ,
6623+ conditions : []metav1.Condition {staleCondition },
6624+ objects : append (healthyEtcdPods (), healthyStatefulSet , failedJob ),
6625+ expectedReason : hyperv1 .AsExpectedReason ,
6626+ conditionExists : true ,
6627+ },
6628+ {
6629+ name : "When failed job exists and etcd is still unhealthy it should keep the failure condition" ,
6630+ conditions : []metav1.Condition {staleCondition },
6631+ objects : append (healthyEtcdPods (), unhealthyStatefulSet , failedJob ),
6632+ expectedReason : hyperv1 .EtcdRecoveryJobFailedReason ,
6633+ conditionExists : true ,
6634+ },
6635+ }
6636+
6637+ for _ , tc := range testCases {
6638+ t .Run (tc .name , func (t * testing.T ) {
6639+ g := NewGomegaWithT (t )
6640+
6641+ hcluster := & hyperv1.HostedCluster {
6642+ ObjectMeta : metav1.ObjectMeta {
6643+ Name : "test-hc" ,
6644+ Namespace : "clusters" ,
6645+ },
6646+ Spec : hyperv1.HostedClusterSpec {
6647+ Etcd : hyperv1.EtcdSpec {
6648+ ManagementType : hyperv1 .Managed ,
6649+ },
6650+ ControllerAvailabilityPolicy : hyperv1 .HighlyAvailable ,
6651+ },
6652+ Status : hyperv1.HostedClusterStatus {
6653+ Conditions : tc .conditions ,
6654+ },
6655+ }
6656+
6657+ objects := append ([]crclient.Object {hcluster }, tc .objects ... )
6658+ client := fake .NewClientBuilder ().
6659+ WithScheme (api .Scheme ).
6660+ WithObjects (objects ... ).
6661+ WithStatusSubresource (hcluster ).
6662+ Build ()
6663+
6664+ r := & HostedClusterReconciler {
6665+ Client : client ,
6666+ now : metav1 .Now ,
6667+ EnableEtcdRecovery : true ,
6668+ }
6669+
6670+ _ , err := r .reconcileETCDMemberRecovery (
6671+ ctrl .LoggerInto (t .Context (), zap .New (zap .UseDevMode (true ))),
6672+ hcluster ,
6673+ upsert .New (false ).CreateOrUpdate ,
6674+ )
6675+ g .Expect (err ).ToNot (HaveOccurred ())
6676+
6677+ updatedHC := & hyperv1.HostedCluster {}
6678+ g .Expect (client .Get (t .Context (), crclient .ObjectKeyFromObject (hcluster ), updatedHC )).To (Succeed ())
6679+
6680+ condition := meta .FindStatusCondition (updatedHC .Status .Conditions , string (hyperv1 .EtcdRecoveryActive ))
6681+ if tc .conditionExists {
6682+ g .Expect (condition ).ToNot (BeNil ())
6683+ g .Expect (condition .Reason ).To (Equal (tc .expectedReason ))
6684+ } else {
6685+ g .Expect (condition ).To (BeNil ())
6686+ }
6687+ })
6688+ }
6689+ }
0 commit comments