Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion api/redis/v1beta2/redis_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
21 changes: 20 additions & 1 deletion charts/redis-operator/crds/crds.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
21 changes: 20 additions & 1 deletion config/crd/bases/redis.redis.opstreelabs.in_redis.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions docs/content/en/docs/CRD Reference/API Reference/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,8 @@ _Appears in:_
| `hostPort` _integer_ | | | |




#### Service


Expand Down
5 changes: 3 additions & 2 deletions internal/cmd/manager/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 26 additions & 1 deletion internal/controller/redis/redis_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const (
// Reconciler reconciles a Redis object
type Reconciler struct {
client.Client
k8sutils.StatefulSet
K8sClient kubernetes.Interface
}

Expand All @@ -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.
Expand Down
6 changes: 4 additions & 2 deletions internal/controller/redis/redis_controller_suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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())

Expand Down
19 changes: 16 additions & 3 deletions internal/k8sutils/statefulset.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
19 changes: 19 additions & 0 deletions internal/k8sutils/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
}