diff --git a/Makefile b/Makefile index def2e629..e80d7c13 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ #include tests/e2e/Makefile -VERSION ?= 1.3.0 +VERSION ?= 1.3.1 # CHANNELS define the bundle channels used in the bundle. # Add a new line here if you would like to change its default config. (E.g CHANNELS = "candidate,fast,stable") diff --git a/api/v1alpha1/uffizzicluster_types.go b/api/v1alpha1/uffizzicluster_types.go index ae0bbc3a..af586568 100644 --- a/api/v1alpha1/uffizzicluster_types.go +++ b/api/v1alpha1/uffizzicluster_types.go @@ -160,6 +160,7 @@ type UffizziClusterStatus struct { Host *string `json:"host,omitempty"` LastAppliedConfiguration *string `json:"lastAppliedConfiguration,omitempty"` LastAppliedHelmReleaseSpec *string `json:"lastAppliedHelmReleaseSpec,omitempty"` + LastAwakeTime *string `json:"lastAwakeTime,omitempty"` } // VClusterKubeConfig is the KubeConfig SecretReference of the related VCluster diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index e9a4bea4..8b39fbcc 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -372,6 +372,11 @@ func (in *UffizziClusterStatus) DeepCopyInto(out *UffizziClusterStatus) { *out = new(string) **out = **in } + if in.LastAwakeTime != nil { + in, out := &in.LastAwakeTime, &out.LastAwakeTime + *out = new(string) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UffizziClusterStatus. diff --git a/chart/Chart.yaml b/chart/Chart.yaml index 430bfcd8..56d03840 100644 --- a/chart/Chart.yaml +++ b/chart/Chart.yaml @@ -5,12 +5,12 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 1.3.0 +version: 1.3.1 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "v1.3.0" +appVersion: "v1.3.1" dependencies: - name: common repository: https://charts.bitnami.com/bitnami diff --git a/chart/templates/uffizziclusters.uffizzi.com_customresourcedefinition.yaml b/chart/templates/uffizziclusters.uffizzi.com_customresourcedefinition.yaml index 687a24ae..0e424fae 100644 --- a/chart/templates/uffizziclusters.uffizzi.com_customresourcedefinition.yaml +++ b/chart/templates/uffizziclusters.uffizzi.com_customresourcedefinition.yaml @@ -264,6 +264,8 @@ spec: type: string lastAppliedHelmReleaseSpec: type: string + lastAwakeTime: + type: string type: object type: object served: true diff --git a/chart/values.yaml b/chart/values.yaml index f1b7950f..9e5012e6 100644 --- a/chart/values.yaml +++ b/chart/values.yaml @@ -4,7 +4,7 @@ image: repository: docker.io/uffizzi/uffizzi-cluster-operator - tag: v1.3.0 + tag: v1.3.1 # `flux` dependency values flux: diff --git a/config/crd/bases/uffizzi.com_uffizziclusters.yaml b/config/crd/bases/uffizzi.com_uffizziclusters.yaml index 896bfea7..7e637e04 100644 --- a/config/crd/bases/uffizzi.com_uffizziclusters.yaml +++ b/config/crd/bases/uffizzi.com_uffizziclusters.yaml @@ -302,6 +302,8 @@ spec: type: string lastAppliedHelmReleaseSpec: type: string + lastAwakeTime: + type: string type: object type: object served: true diff --git a/controllers/conditions.go b/controllers/conditions.go index bef5e9af..280a05fd 100644 --- a/controllers/conditions.go +++ b/controllers/conditions.go @@ -1,15 +1,100 @@ package controllers import ( + uclusteruffizzicomv1alpha1 "github.com/UffizziCloud/uffizzi-cluster-operator/api/v1alpha1" + fluxhelmv2beta1 "github.com/fluxcd/helm-controller/api/v2beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -func buildInitializingCondition() metav1.Condition { +// Condition types. +const ( + // TypeReady resources are believed to be ready to handle work. + TypeReady = "Ready" + TypeSleep = "Sleep" +) + +// Reasons a resource is or is not ready. +const ( + ReasonInitializing = "Initializing" + ReasonSleeping = "Sleeping" + ReasonAwoken = "Awoken" +) + +func Initializing() metav1.Condition { return metav1.Condition{ - Type: "Ready", + Type: TypeReady, Status: metav1.ConditionUnknown, - Reason: "Initializing", + Reason: ReasonInitializing, LastTransitionTime: metav1.Now(), Message: "UffizziCluster is being initialized", } } + +func Sleeping(time metav1.Time) metav1.Condition { + return metav1.Condition{ + Type: TypeSleep, + Status: metav1.ConditionTrue, + Reason: ReasonSleeping, + LastTransitionTime: time, + Message: "UffizziCluster put to sleep manually", + } +} + +func Awoken(time metav1.Time) metav1.Condition { + return metav1.Condition{ + Type: TypeSleep, + Status: metav1.ConditionFalse, + Reason: ReasonAwoken, + LastTransitionTime: time, + Message: "UffizziCluster awoken manually", + } +} + +func mirrorHelmReleaseConditions(helmRelease *fluxhelmv2beta1.HelmRelease, uCluster *uclusteruffizzicomv1alpha1.UffizziCluster) { + uClusterConditions := []metav1.Condition{} + for _, c := range helmRelease.Status.Conditions { + helmMessage := "[HelmRelease] " + c.Message + uClusterCondition := c + uClusterCondition.Message = helmMessage + uClusterConditions = append(uClusterConditions, uClusterCondition) + } + setConditions(uCluster, uClusterConditions...) +} + +// setConditions sets the supplied conditions, replacing any existing conditions +// of the same type. This is a no-op if all supplied conditions are identical, +// ignoring the last transition time, to those already set. +func setConditions(uCluster *uclusteruffizzicomv1alpha1.UffizziCluster, c ...metav1.Condition) { + for _, new := range c { + exists := false + for i, existing := range uCluster.Status.Conditions { + if existing.Type != new.Type { + continue + } + if conditionsEqual(existing, new) { + exists = true + continue + } + uCluster.Status.Conditions[i] = new + exists = true + } + if !exists { + uCluster.Status.Conditions = append(uCluster.Status.Conditions, new) + } + } +} + +func setCondition(uCluster *uclusteruffizzicomv1alpha1.UffizziCluster, c metav1.Condition) { + setConditions(uCluster, c) +} + +// Equal returns true if the condition is identical to the supplied condition, +// ignoring the LastTransitionTime. +// +//nolint:gocritic // just a few bytes too heavy +func conditionsEqual(c, other metav1.Condition) bool { + return c.Type == other.Type && + c.Status == other.Status && + c.Reason == other.Reason && + c.Message == other.Message +} diff --git a/controllers/conditions_test.go b/controllers/conditions_test.go index 5bdeb0a7..ee10987b 100644 --- a/controllers/conditions_test.go +++ b/controllers/conditions_test.go @@ -5,7 +5,7 @@ import ( ) func TestBuildInitializingCondition(t *testing.T) { - initCondition := buildInitializingCondition() + initCondition := Initializing() if initCondition.Type != "Ready" { t.Errorf("Expected Ready, got %s", initCondition.Type) } diff --git a/controllers/uffizzicluster_controller.go b/controllers/uffizzicluster_controller.go index 82082849..e24204e8 100644 --- a/controllers/uffizzicluster_controller.go +++ b/controllers/uffizzicluster_controller.go @@ -145,19 +145,21 @@ func (r *UffizziClusterReconciler) Reconcile(ctx context.Context, req ctrl.Reque if len(uCluster.Status.Conditions) == 0 { var ( intialConditions = []metav1.Condition{ - buildInitializingCondition(), + Initializing(), } helmReleaseRef = "" host = "" kubeConfig = uclusteruffizzicomv1alpha1.VClusterKubeConfig{ SecretRef: &meta.SecretKeyReference{}, } + lastAwakeTime = metav1.Now().String() ) uCluster.Status = uclusteruffizzicomv1alpha1.UffizziClusterStatus{ Conditions: intialConditions, HelmReleaseRef: &helmReleaseRef, Host: &host, KubeConfig: kubeConfig, + LastAwakeTime: &lastAwakeTime, } if err := r.Status().Update(ctx, uCluster); err != nil { logger.Error(err, "Failed to update the default UffizziCluster status") @@ -228,14 +230,7 @@ func (r *UffizziClusterReconciler) Reconcile(ctx context.Context, req ctrl.Reque } else { lifecycleOpType = LIFECYCLE_OP_TYPE_UPDATE // if helm release already exists then replicate the status conditions onto the uffizzicluster object - uClusterConditions := []metav1.Condition{} - for _, c := range helmRelease.Status.Conditions { - helmMessage := "[HelmRelease] " + c.Message - uClusterCondition := c - uClusterCondition.Message = helmMessage - uClusterConditions = append(uClusterConditions, uClusterCondition) - } - uCluster.Status.Conditions = uClusterConditions + mirrorHelmReleaseConditions(helmRelease, uCluster) if err := r.Status().Update(ctx, uCluster); err != nil { //logger.Error(err, "Failed to update UffizziCluster status") return ctrl.Result{RequeueAfter: time.Second * 5}, err @@ -286,39 +281,55 @@ func (r *UffizziClusterReconciler) Reconcile(ctx context.Context, req ctrl.Reque // ---------------------- // UCLUSTER SLEEP // ---------------------- + if err := r.reconcileSleepState(ctx, uCluster); err != nil { + if k8serrors.IsNotFound(err) { + logger.Info("vcluster statefulset not found, requeueing") + return ctrl.Result{Requeue: true, RequeueAfter: time.Second * 5}, nil + } + logger.Error(err, "Failed to reconcile sleep state") + return ctrl.Result{Requeue: true, RequeueAfter: time.Second * 5}, nil + } + + // Requeue the request to check the helm release status + return ctrl.Result{Requeue: true}, nil +} + +func (r *UffizziClusterReconciler) reconcileSleepState(ctx context.Context, uCluster *uclusteruffizzicomv1alpha1.UffizziCluster) error { // get the stateful set created by the helm chart ucStatefulSet := &appsv1.StatefulSet{} if err := r.Get(ctx, types.NamespacedName{ Name: BuildVClusterHelmReleaseName(uCluster), - Namespace: req.NamespacedName.Namespace}, ucStatefulSet); err != nil { - logger.Error(err, "Failed to get UffizziCluster StatefulSet") - return ctrl.Result{}, err + Namespace: uCluster.Namespace}, ucStatefulSet); err != nil { + return err } // get the current replicas currentReplicas := ucStatefulSet.Spec.Replicas // scale the vcluster instance to 0 if the sleep flag is true if uCluster.Spec.Sleep && *currentReplicas > 0 { if err := r.scaleStatefulSet(ctx, ucStatefulSet, 0); err != nil { - logger.Error(err, "Failed to scale down UffizziCluster StatefulSet") - return ctrl.Result{}, err + return err } - logger.Info("UffizziCluster StatefulSet scaled down to 0") err := r.deleteWorkloads(ctx, uCluster) if err != nil { - logger.Error(err, "Failed to delete vcluster workloads") - return ctrl.Result{}, err + return err } + sleepingTime := metav1.Now() + setCondition(uCluster, Sleeping(sleepingTime)) // if the current replicas is 0, then do nothing } else if !uCluster.Spec.Sleep && *currentReplicas == 0 { if err := r.scaleStatefulSet(ctx, ucStatefulSet, 1); err != nil { - logger.Error(err, "Failed to scale up UffizziCluster StatefulSet") - return ctrl.Result{}, err + return err } - logger.Info("UffizziCluster StatefulSet scaled up to 1") + // set status for vcluster waking up + lastAwakeTime := metav1.Now() + lastAwakeTimeString := lastAwakeTime.String() + uCluster.Status.LastAwakeTime = &lastAwakeTimeString + setCondition(uCluster, Awoken(lastAwakeTime)) } - - // Requeue the request to check the status - return ctrl.Result{Requeue: true}, nil + if err := r.Status().Update(ctx, uCluster); err != nil { + return err + } + return nil } // scaleStatefulSet scales the stateful set to the given scale