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/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/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/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 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..78377502cb 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.Reconciled() + } + + 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/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()) diff --git a/internal/k8sutils/statefulset.go b/internal/k8sutils/statefulset.go index f2c5e46e69..e19343cc99 100644 --- a/internal/k8sutils/statefulset.go +++ b/internal/k8sutils/statefulset.go @@ -60,10 +60,23 @@ 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) { - log.FromContext(ctx).V(1).Info("StatefulSet is not ready", "Status.UpdatedReplicas", sts.Status.UpdatedReplicas, "ExpectedUpdateReplicas", expectedUpdateReplicas) - return false + 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) + } 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) 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 +}