From 38955d6fc163454d7568a772bd23171aa2da1568 Mon Sep 17 00:00:00 2001 From: Amit Oren Date: Tue, 12 Aug 2025 13:56:51 +0300 Subject: [PATCH 1/7] Add redis standalone status Signed-off-by: Amit Oren --- api/redis/v1beta2/redis_types.go | 24 ++++++++++++++++- .../redis.redis.opstreelabs.in_redis.yaml | 21 ++++++++++++++- internal/cmd/manager/cmd.go | 5 ++-- internal/controller/redis/redis_controller.go | 27 ++++++++++++++++++- internal/k8sutils/status.go | 19 +++++++++++++ 5 files changed, 91 insertions(+), 5 deletions(-) diff --git a/api/redis/v1beta2/redis_types.go b/api/redis/v1beta2/redis_types.go index a42e8c5c1a..931ab33ea2 100644 --- a/api/redis/v1beta2/redis_types.go +++ b/api/redis/v1beta2/redis_types.go @@ -50,11 +50,33 @@ type RedisSpec struct { } // RedisStatus defines the observed state of Redis -type RedisStatus struct{} +// +kubebuilder:subresource:status +type RedisStatus struct { + State RedisState `json:"state,omitempty"` + Reason string `json:"reason,omitempty"` +} + +type RedisState string + +const ( + InitializingReason string = "Redis is initializing" + ReadyReason string = "Redis is ready" + FailedReason string = "Redis has failed" +) + +// Status Field of the Redis +const ( + RedisInitializing RedisState = "Initializing" + RedisReady RedisState = "Ready" + RedisFailed RedisState = "Failed" +) // +kubebuilder:object:root=true // +kubebuilder:subresource:status // +kubebuilder:storageversion +// +kubebuilder:printcolumn:name="State",type="string",JSONPath=".status.state",description="The current state of the Redis" +// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`,description="Age of Redis" +// +kubebuilder:printcolumn:name="Reason",type="string",JSONPath=".status.reason",description="The reason for the current state",priority=1 // Redis is the Schema for the redis API type Redis struct { diff --git a/config/crd/bases/redis.redis.opstreelabs.in_redis.yaml b/config/crd/bases/redis.redis.opstreelabs.in_redis.yaml index dc08082476..6cef37c7cb 100644 --- a/config/crd/bases/redis.redis.opstreelabs.in_redis.yaml +++ b/config/crd/bases/redis.redis.opstreelabs.in_redis.yaml @@ -14,7 +14,21 @@ spec: singular: redis scope: Namespaced versions: - - name: v1beta2 + - additionalPrinterColumns: + - description: The current state of the Redis + jsonPath: .status.state + name: State + type: string + - description: Age of Redis + jsonPath: .metadata.creationTimestamp + name: Age + type: date + - description: The reason for the current state + jsonPath: .status.reason + name: Reason + priority: 1 + type: string + name: v1beta2 schema: openAPIV3Schema: description: Redis is the Schema for the redis API @@ -5382,6 +5396,11 @@ spec: type: object status: description: RedisStatus defines the observed state of Redis + properties: + reason: + type: string + state: + type: string type: object required: - spec diff --git a/internal/cmd/manager/cmd.go b/internal/cmd/manager/cmd.go index 85a53c8e2e..7da62d15bb 100644 --- a/internal/cmd/manager/cmd.go +++ b/internal/cmd/manager/cmd.go @@ -232,8 +232,9 @@ func setupControllers(mgr ctrl.Manager, k8sClient kubernetes.Interface, maxConcu healer := redis.NewHealer(k8sClient) if err := (&rediscontroller.Reconciler{ - Client: mgr.GetClient(), - K8sClient: k8sClient, + Client: mgr.GetClient(), + K8sClient: k8sClient, + StatefulSet: k8sutils.NewStatefulSetService(k8sClient), }).SetupWithManager(mgr, controller.Options{MaxConcurrentReconciles: maxConcurrentReconciles}); err != nil { setupLog.Error(err, "unable to create controller", "controller", "Redis") return err diff --git a/internal/controller/redis/redis_controller.go b/internal/controller/redis/redis_controller.go index 0243d276b5..4afa8ae005 100644 --- a/internal/controller/redis/redis_controller.go +++ b/internal/controller/redis/redis_controller.go @@ -37,6 +37,7 @@ const ( // Reconciler reconciles a Redis object type Reconciler struct { client.Client + k8sutils.StatefulSet K8sClient kubernetes.Interface } @@ -59,15 +60,39 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu if err = k8sutils.AddFinalizer(ctx, instance, RedisFinalizer, r.Client); err != nil { return intctrlutil.RequeueE(ctx, err, "failed to add finalizer") } + + if instance.Status.State == "" || instance.Status.State == rvb2.RedisFailed { + if err = k8sutils.UpdateRedisStatus(ctx, instance, rvb2.RedisInitializing, rvb2.InitializingReason, r.Client); err != nil { + return intctrlutil.RequeueE(ctx, err, "failed to update status to initializing") + } + } + err = k8sutils.CreateStandaloneRedis(ctx, instance, r.K8sClient) if err != nil { + if statusErr := k8sutils.UpdateRedisStatus(ctx, instance, rvb2.RedisFailed, rvb2.FailedReason, r.Client); statusErr != nil { + return intctrlutil.RequeueE(ctx, statusErr, "failed to update status to failed") + } return intctrlutil.RequeueE(ctx, err, "failed to create redis") } + err = k8sutils.CreateStandaloneService(ctx, instance, r.K8sClient) if err != nil { + if statusErr := k8sutils.UpdateRedisStatus(ctx, instance, rvb2.RedisFailed, rvb2.FailedReason, r.Client); statusErr != nil { + return intctrlutil.RequeueE(ctx, statusErr, "failed to update status to failed") + } return intctrlutil.RequeueE(ctx, err, "failed to create service") } - return intctrlutil.RequeueAfter(ctx, time.Second*10, "requeue after 10 seconds") + + if r.IsStatefulSetReady(ctx, instance.Namespace, instance.Name) { + if instance.Status.State != rvb2.RedisReady { + if err = k8sutils.UpdateRedisStatus(ctx, instance, rvb2.RedisReady, rvb2.ReadyReason, r.Client); err != nil { + return intctrlutil.RequeueE(ctx, err, "failed to update status to ready") + } + } + return intctrlutil.RequeueAfter(ctx, time.Second*30, "requeue after 30 seconds") + } + + return intctrlutil.RequeueAfter(ctx, time.Second*10, "StatefulSet not ready, requeue after 10 seconds") } // SetupWithManager sets up the controller with the Manager. diff --git a/internal/k8sutils/status.go b/internal/k8sutils/status.go index d30fc67c91..17784eb791 100644 --- a/internal/k8sutils/status.go +++ b/internal/k8sutils/status.go @@ -4,6 +4,7 @@ import ( "context" "reflect" + rvb2 "github.com/OT-CONTAINER-KIT/redis-operator/api/redis/v1beta2" rcvb2 "github.com/OT-CONTAINER-KIT/redis-operator/api/rediscluster/v1beta2" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/log" @@ -28,3 +29,21 @@ func UpdateRedisClusterStatus(ctx context.Context, cr *rcvb2.RedisCluster, state } return nil } + +// UpdateRedisStatus will update the status of the Redis +func UpdateRedisStatus(ctx context.Context, cr *rvb2.Redis, state rvb2.RedisState, reason string, k8sClient client.Client) error { + newStatus := rvb2.RedisStatus{ + State: state, + Reason: reason, + } + if reflect.DeepEqual(cr.Status, newStatus) { + return nil + } + cr.Status = newStatus + + if err := k8sClient.Status().Update(ctx, cr); err != nil { + log.FromContext(ctx).Error(err, "Failed to update Redis status") + return err + } + return nil +} From 8d878ef7773c529bea4ce315a027396441a77600 Mon Sep 17 00:00:00 2001 From: Amit Oren Date: Wed, 13 Aug 2025 17:39:41 +0300 Subject: [PATCH 2/7] Handle recreation case Signed-off-by: Amit Oren --- internal/k8sutils/statefulset.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/internal/k8sutils/statefulset.go b/internal/k8sutils/statefulset.go index f2c5e46e69..5ca23eea6d 100644 --- a/internal/k8sutils/statefulset.go +++ b/internal/k8sutils/statefulset.go @@ -60,10 +60,15 @@ func (s *StatefulSetService) IsStatefulSetReady(ctx context.Context, namespace, if sts.Spec.Replicas != nil { replicas = int(*sts.Spec.Replicas) } - if expectedUpdateReplicas := replicas - partition; sts.Status.UpdatedReplicas < int32(expectedUpdateReplicas) { - log.FromContext(ctx).V(1).Info("StatefulSet is not ready", "Status.UpdatedReplicas", sts.Status.UpdatedReplicas, "ExpectedUpdateReplicas", expectedUpdateReplicas) - return false + if sts.GetGeneration() == 1 && int(sts.Status.ReadyReplicas) == replicas { + // When we cannot update statefulset due to immutability, we delete it with cascade=false and recreate it + // This causes UpdatedReplicas to be 0, despite pod being ready + log.FromContext(ctx).V(1).Info("StatefulSet has been recreated", "ObservedGeneration", sts.Status.ObservedGeneration, "Generation", sts.Generation) + } else { + log.FromContext(ctx).V(1).Info("StatefulSet is not ready", "Status.UpdatedReplicas", sts.Status.UpdatedReplicas, "ExpectedUpdateReplicas", expectedUpdateReplicas) + return false + } } if partition == 0 && sts.Status.CurrentRevision != sts.Status.UpdateRevision { log.FromContext(ctx).V(1).Info("StatefulSet is not ready", "Status.CurrentRevision", sts.Status.CurrentRevision, "Status.UpdateRevision", sts.Status.UpdateRevision) From a5c0fb215bd52365b615b93dc8e7b1898f55dbe2 Mon Sep 17 00:00:00 2001 From: Amit Oren Date: Thu, 14 Aug 2025 11:29:43 +0300 Subject: [PATCH 3/7] Remove generation check Signed-off-by: Amit Oren --- internal/k8sutils/statefulset.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/k8sutils/statefulset.go b/internal/k8sutils/statefulset.go index 5ca23eea6d..179606ac42 100644 --- a/internal/k8sutils/statefulset.go +++ b/internal/k8sutils/statefulset.go @@ -61,7 +61,7 @@ func (s *StatefulSetService) IsStatefulSetReady(ctx context.Context, namespace, replicas = int(*sts.Spec.Replicas) } if expectedUpdateReplicas := replicas - partition; sts.Status.UpdatedReplicas < int32(expectedUpdateReplicas) { - if sts.GetGeneration() == 1 && int(sts.Status.ReadyReplicas) == replicas { + if int(sts.Status.ReadyReplicas) == replicas { // When we cannot update statefulset due to immutability, we delete it with cascade=false and recreate it // This causes UpdatedReplicas to be 0, despite pod being ready log.FromContext(ctx).V(1).Info("StatefulSet has been recreated", "ObservedGeneration", sts.Status.ObservedGeneration, "Generation", sts.Generation) From e7379cec06db0f00161b8817ae1661fe0ea94824 Mon Sep 17 00:00:00 2001 From: Amit Oren Date: Thu, 14 Aug 2025 12:41:09 +0300 Subject: [PATCH 4/7] Codegen Signed-off-by: Amit Oren --- charts/redis-operator/crds/crds.yaml | 21 ++++++++++++++++++- .../CRD Reference/API Reference/_index.md | 2 ++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/charts/redis-operator/crds/crds.yaml b/charts/redis-operator/crds/crds.yaml index d342c8e38d..9107eb6f05 100644 --- a/charts/redis-operator/crds/crds.yaml +++ b/charts/redis-operator/crds/crds.yaml @@ -13,7 +13,21 @@ spec: singular: redis scope: Namespaced versions: - - name: v1beta2 + - additionalPrinterColumns: + - description: The current state of the Redis + jsonPath: .status.state + name: State + type: string + - description: Age of Redis + jsonPath: .metadata.creationTimestamp + name: Age + type: date + - description: The reason for the current state + jsonPath: .status.reason + name: Reason + priority: 1 + type: string + name: v1beta2 schema: openAPIV3Schema: description: Redis is the Schema for the redis API @@ -5381,6 +5395,11 @@ spec: type: object status: description: RedisStatus defines the observed state of Redis + properties: + reason: + type: string + state: + type: string type: object required: - spec diff --git a/docs/content/en/docs/CRD Reference/API Reference/_index.md b/docs/content/en/docs/CRD Reference/API Reference/_index.md index a84da865be..b9e280b6e4 100644 --- a/docs/content/en/docs/CRD Reference/API Reference/_index.md +++ b/docs/content/en/docs/CRD Reference/API Reference/_index.md @@ -527,6 +527,8 @@ _Appears in:_ | `hostPort` _integer_ | | | | + + #### Service From fa74f1e633d288266bf7bad745410a5c940e49d2 Mon Sep 17 00:00:00 2001 From: Amit Oren Date: Thu, 14 Aug 2025 13:28:34 +0300 Subject: [PATCH 5/7] Fix tests Signed-off-by: Amit Oren --- internal/controller/redis/redis_controller_suite_test.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/internal/controller/redis/redis_controller_suite_test.go b/internal/controller/redis/redis_controller_suite_test.go index 99f33699be..b037a1e761 100644 --- a/internal/controller/redis/redis_controller_suite_test.go +++ b/internal/controller/redis/redis_controller_suite_test.go @@ -23,6 +23,7 @@ import ( "time" rvb2 "github.com/OT-CONTAINER-KIT/redis-operator/api/redis/v1beta2" + "github.com/OT-CONTAINER-KIT/redis-operator/internal/k8sutils" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/onsi/gomega/gexec" @@ -94,8 +95,9 @@ var _ = BeforeSuite(func() { Expect(err).ToNot(HaveOccurred()) err = (&Reconciler{ - Client: k8sManager.GetClient(), - K8sClient: k8sClient, + Client: k8sManager.GetClient(), + StatefulSet: k8sutils.NewStatefulSetService(k8sClient), + K8sClient: k8sClient, }).SetupWithManager(k8sManager, controller.Options{}) Expect(err).ToNot(HaveOccurred()) From 531b57adc4394bebe499d7c4f77ebe5740e6d9b7 Mon Sep 17 00:00:00 2001 From: Amit Oren Date: Thu, 14 Aug 2025 13:52:36 +0300 Subject: [PATCH 6/7] Skip checks if sts is OnDelete Signed-off-by: Amit Oren --- internal/k8sutils/statefulset.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/internal/k8sutils/statefulset.go b/internal/k8sutils/statefulset.go index 179606ac42..e19343cc99 100644 --- a/internal/k8sutils/statefulset.go +++ b/internal/k8sutils/statefulset.go @@ -60,6 +60,14 @@ func (s *StatefulSetService) IsStatefulSetReady(ctx context.Context, namespace, if sts.Spec.Replicas != nil { replicas = int(*sts.Spec.Replicas) } + if sts.Spec.UpdateStrategy.Type == appsv1.OnDeleteStatefulSetStrategyType { + // For OnDelete, we just check if the pods are ready + if int(sts.Status.ReadyReplicas) != replicas { + log.FromContext(ctx).V(1).Info("StatefulSet is not ready", "Status.ReadyReplicas", sts.Status.ReadyReplicas, "Replicas", replicas) + return false + } + return true + } if expectedUpdateReplicas := replicas - partition; sts.Status.UpdatedReplicas < int32(expectedUpdateReplicas) { if int(sts.Status.ReadyReplicas) == replicas { // When we cannot update statefulset due to immutability, we delete it with cascade=false and recreate it From 04ec59f9b8ff32d0ac1f2b0b95b4bfe1877dd2fe Mon Sep 17 00:00:00 2001 From: Amit Oren Date: Mon, 18 Aug 2025 12:36:12 +0300 Subject: [PATCH 7/7] Don't requeue Signed-off-by: Amit Oren --- internal/controller/redis/redis_controller.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/controller/redis/redis_controller.go b/internal/controller/redis/redis_controller.go index 4afa8ae005..78377502cb 100644 --- a/internal/controller/redis/redis_controller.go +++ b/internal/controller/redis/redis_controller.go @@ -89,7 +89,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu return intctrlutil.RequeueE(ctx, err, "failed to update status to ready") } } - return intctrlutil.RequeueAfter(ctx, time.Second*30, "requeue after 30 seconds") + return intctrlutil.Reconciled() } return intctrlutil.RequeueAfter(ctx, time.Second*10, "StatefulSet not ready, requeue after 10 seconds")