1. 环境说明

源码链接:https://github.com/OT-CONTAINER-KIT/redis-operator 分支/Tag:v0.13.0Kubernetes版本:v1.23.0,该值从工程中的go.mod文件中得知
Controller-runtime版本:v0.11.0,该值从工程中的go.mod文件中得知

2. 架构

RedissonClient 对应的opsForHash操作_cluster

3. 目录结构

在分析源码之前,我们先来看可以看项目结构,如下所示,写过Operator的小伙伴肯定非常的熟悉,这个项目结构实际上就是通过kube-buidler/operator-sdk创建出来的标准工程结构,我们从上倒下简单过一遍

api目录中为CRDSpec定义,譬如这里是redis_types.go中。这里面定义的都是业务相关的元数据,也是用户使用这个operator可以提交的yaml中的字段定义。当执行make install的时候,会根据这里的定义以及config目录中的配置生成CRD以及相关的RBAC资源清单

config目录比较简单,这里面是通过kustomize组织的一系列配置,通过kube-buidler/operator-sdk创建工程时,会自动创建这个目录,里面的配置大多数都不需要更改,我们只需要改一些自己关心的信息,譬如Operatornamespace以及名字

controllers目录则是我们今天分析的重点,Operator.Reconciler就是在这个目录中被实现的,一旦Operator相关的资源清单发生改变,Informer就会通知Operator,最终会调用到Reconcile。显然,Reconcile中的逻辑就是部署Redis高可用集群

dashborad目录并非标注的Operator目录,大致看了一下文件内容,这个目录下保存就是Grafana配置

example目录中则是测试Operator的资源清单

k8sutils顾名思义,就是实现Redis-Operator的一些工具方法,该目录也并非标准kube-buidler/operator-sdk目录

static中为Redis-OperatorLogo以及架构图

.
├── api
│   └── v1beta1
│       ├── common_types.go
│       ├── groupversion_info.go
│       ├── rediscluster_types.go
│       ├── redis_types.go
│       └── zz_generated.deepcopy.go
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── config
│   ├── certmanager
│   │   ├── certificate.yaml
│   │   ├── kustomization.yaml
│   │   └── kustomizeconfig.yaml
│   ├── crd
│   │   ├── bases
│   │   ├── kustomization.yaml
│   │   ├── kustomizeconfig.yaml
│   │   └── patches
│   ├── default
│   │   ├── kustomization.yaml
│   │   ├── manager_auth_proxy_patch.yaml
│   │   └── manager_config_patch.yaml
│   ├── manager
│   │   ├── controller_manager_config.yaml
│   │   ├── kustomization.yaml
│   │   └── manager.yaml
│   ├── prometheus
│   │   ├── kustomization.yaml
│   │   ├── monitor.yaml
│   │   └── redis-servicemonitor.yaml
│   ├── rbac
│   │   ├── auth_proxy_client_clusterrole.yaml
│   │   ├── auth_proxy_role_binding.yaml
│   │   ├── auth_proxy_role.yaml
│   │   ├── auth_proxy_service.yaml
│   │   ├── kustomization.yaml
│   │   ├── leader_election_role_binding.yaml
│   │   ├── leader_election_role.yaml
│   │   ├── rediscluster_editor_role.yaml
│   │   ├── rediscluster_viewer_role.yaml
│   │   ├── redis_editor_role.yaml
│   │   ├── redis_viewer_role.yaml
│   │   ├── role_binding.yaml
│   │   ├── role.yaml
│   │   └── serviceaccount.yaml
│   ├── samples
│   │   ├── kustomization.yaml
│   │   └── redis_v1beta1_redis.yaml
│   └── scorecard
│       ├── bases
│       ├── kustomization.yaml
│       └── patches
├── CONTRIBUTING.md
├── controllers
│   ├── rediscluster_controller.go
│   ├── redis_controller.go
│   └── suite_test.go
├── dashboards
│   └── redis-operator-cluster.json
├── Dockerfile
├── docs
│   ├── app.yaml
│   ├── assets
│   │   ├── icons
│   │   ├── scss
│   │   └── templates
│   ├── cloudbuild.yaml
│   ├── config.toml
│   ├── content
│   │   └── en
│   ├── credits
│   ├── gen-api-docs.sh
│   ├── go.mod
│   ├── go.sum
│   ├── handler.go
│   ├── handler_test.go
│   ├── htmltest.yaml
│   ├── layouts
│   │   ├── partials
│   │   └── shortcodes
│   ├── main.go
│   ├── netlify.toml
│   ├── package.json
│   ├── static
│   │   ├── css
│   │   ├── diagrams
│   │   ├── favicons
│   │   ├── images
│   │   ├── js
│   │   └── redis-operator.cast
│   ├── themes
│   │   └── docsy
│   └── vanity.yaml
├── example
│   ├── additional_config
│   │   ├── clusterd.yaml
│   │   ├── configmap.yaml
│   │   └── standalone.yaml
│   ├── advance_config
│   │   ├── clusterd.yaml
│   │   └── standalone.yaml
│   ├── affinity
│   │   ├── clusterd.yaml
│   │   └── standalone.yaml
│   ├── disruption_budget
│   │   └── clusterd.yaml
│   ├── eks-cluster.yaml
│   ├── external_service
│   │   ├── clusterd.yaml
│   │   ├── cluster-svc.yaml
│   │   ├── standalone-svc.yaml
│   │   └── standalone.yaml
│   ├── password_protected
│   │   ├── clusterd.yaml
│   │   └── standalone.yaml
│   ├── private_registry
│   │   ├── clusterd.yaml
│   │   └── standalone.yaml
│   ├── probes
│   │   ├── clusterd.yaml
│   │   └── standalone.yaml
│   ├── redis-cluster.yaml
│   ├── redis_monitoring
│   │   ├── clusterd.yaml
│   │   └── standalone.yaml
│   ├── redis-standalone.yaml
│   ├── tls_enabled
│   │   ├── redis-cluster.yaml
│   │   └── redis-standalone.yaml
│   ├── upgrade-strategy
│   │   ├── clusterd.yaml
│   │   └── standalone.yaml
│   └── volume_mount
│       ├── configmap.yaml
│       ├── redis-cluster.yaml
│       └── redis-standalone.yaml
├── go.mod
├── go.sum
├── hack
│   └── boilerplate.go.txt
├── install-operator.sh
├── k8sutils
│   ├── client.go
│   ├── finalizer.go
│   ├── labels.go
│   ├── poddisruption.go
│   ├── redis-cluster.go
│   ├── redis.go
│   ├── redis-standalone.go
│   ├── redis_test.go
│   ├── secrets.go
│   ├── services.go
│   └── statefulset.go
├── LICENSE
├── main.go
├── Makefile
├── PROJECT
├── README.md
├── SECURITY.md
├── static
│   ├── redis-operator-architecture.png
│   ├── redis-operator-logo.png
│   ├── redis-operator-logo.svg
│   ├── redis-operator-logo-wn.svg
│   └── redis-operator.png
└── USED_BY_ORGANIZATIONS.md

4. 手动搭建Redis集群

手动搭建Redis集群

目的:手动搭建Redis集群是为了能够理解搭建一个高可用的Redis需要哪些操作,如果使用Operator来搭建,Operator必然也需要实现对应的逻辑

5. 源码分析

5.1. KubernetesConfig

我们先来看看KubernetesConfig是如何定义的,定义如下,结合Redis-Operator的目标,该定义主要是为了获取Redis相关的配置,譬如你需要使用的Redis版本,这里通过Image来控制,镜像拉去策略,以及将来运行RedisContainer的资源限制。

  • Image:显然,Image表示的是Redis的镜像地址,通过该镜像地址,我们可以控制需要的Redis版本
  • ImagePullPolicy:这个字段并不陌生,接触过K8S一两天的人都知道该字段适用于控制镜像拉去策略
  • Resources:用于控制Redis.Container的资源限制
  • ExistingPasswordSecretRedis可以设置密码,如果设置了密码,通过该字段指定密码在哪个K8SSecret的哪个Key当中
  • ImagePullSecrets:这里应该是针对私仓的控制
  • UpdateStrategy:有状态应用的更新策略,从这个字段可以推测,Redis-Operator最终会使用StatefulSet来控制Redis集群
type KubernetesConfig struct {
	Image                  string                           `json:"image"`
	ImagePullPolicy        corev1.PullPolicy                `json:"imagePullPolicy,omitempty"`
	Resources              *corev1.ResourceRequirements     `json:"resources,omitempty"`
	ExistingPasswordSecret *ExistingPasswordSecret          `json:"redisSecret,omitempty"`
	ImagePullSecrets       *[]corev1.LocalObjectReference   `json:"imagePullSecrets,omitempty"`
	UpdateStrategy         appsv1.StatefulSetUpdateStrategy `json:"updateStrategy,omitempty"`
}

5.2. RedisSpec

RedisSpec

我们继续看看RedsiSpec中是如何定义的,这里面的数据肯定非常的重要,因为它影响着Operator的行为。实际上通过阅读RedisController中的代码可以知道,Redis资源实际上是一个单节点定义。

RedisSpec中定义着Redis期望的状态,我们一起来看看这些字段的含义:

  • KubernetesConfig:这个在上面已经解释过了,这里面定义了Redis的镜像地址,镜像拉去策略,资源限制,密码,以及升级策略
  • RedisExporterRedis早于Prometheus,因此并没有暴露相关的指标,因此需要一个Exporter来暴露Redis的相关指标
  • RedisConfigRedis的扩展配置
  • StorageRedis的存储配置,其实就是一个PVC
  • NodeSelectorNode选择,如果需要把Redis部署在特定的节点上,可以使用这个标签
  • SecurityContext
  • PriorityClassName:定义Pod的优先级,这个涉及到kube-scheduler的抢占式调度
  • Affinity:亲和性的配置
  • Tolerations:容忍配置,即是否可以把Redis部署在打了污点的节点上
  • TLSRedisTLS配置
  • ReadinessProbe:就绪探针
  • LivenessProbe:存活探针
  • Sidecars:也就是当前流行的Sidecar,当用户想要在Redis POD中部署其它容器时可以使用此配置
  • ServiceAccountName:用于RBAC权限控制
// api/v1beta1/redis_types.go

type RedisSpec struct {
	KubernetesConfig  KubernetesConfig           `json:"kubernetesConfig"`
	RedisExporter     *RedisExporter             `json:"redisExporter,omitempty"`
	RedisConfig       *RedisConfig               `json:"redisConfig,omitempty"`
	Storage           *Storage                   `json:"storage,omitempty"`
	NodeSelector      map[string]string          `json:"nodeSelector,omitempty"`
	SecurityContext   *corev1.PodSecurityContext `json:"securityContext,omitempty"`
	PriorityClassName string                     `json:"priorityClassName,omitempty"`
	Affinity          *corev1.Affinity           `json:"affinity,omitempty"`
	Tolerations       *[]corev1.Toleration       `json:"tolerations,omitempty"`
	TLS               *TLSConfig                 `json:"TLS,omitempty"`
	ReadinessProbe *Probe `json:"readinessProbe,omitempty" protobuf:"bytes,11,opt,name=readinessProbe"`
	LivenessProbe      *Probe     `json:"livenessProbe,omitempty" protobuf:"bytes,11,opt,name=livenessProbe"`
	Sidecars           *[]Sidecar `json:"sidecars,omitempty"`
	ServiceAccountName *string    `json:"serviceAccountName,omitempty"`
}

5.3. RedisLeader

RedisLeader

既然是Redis集群,那么一定有LeaderFollower之分,我们一起来看看RedisLeader是如何定义的:

  • Replicas:用于定义Leader的数量,显然,根据常用的Raft选举算法, Leader的数量一般定义为计数个,且大于三个
  • RedisConfig:用于定义Leader的配置
  • Affinity:用于定义Leader的亲和性,由于LeaderFollower的特性不同,他们可能需要部署在特定的节点上
  • PodDisruptionBuget
  • ReadinessProbeLeader就绪探针
  • LivenessProbeLeader的存活探针
// api/v1beta1/rediscluster_types.go
type RedisLeader struct {
	Replicas            *int32                    `json:"replicas,omitempty"`
	RedisConfig         *RedisConfig              `json:"redisConfig,omitempty"`
	Affinity            *corev1.Affinity          `json:"affinity,omitempty"`
	PodDisruptionBudget *RedisPodDisruptionBudget `json:"pdb,omitempty"`
	ReadinessProbe *Probe `json:"readinessProbe,omitempty" protobuf:"bytes,11,opt,name=readinessProbe"`
	LivenessProbe *Probe `json:"livenessProbe,omitempty" protobuf:"bytes,11,opt,name=livenessProbe"`
}

5.4. RedisFollower

RedisFollower

看完RedisLeader的定义,在看到RedisFollower的定义,就不难理解了,定时是一摸一样的,只不过抽选的角色为Follower而已

// api/v1beta1/rediscluster_types.go

type RedisFollower struct {
	Replicas            *int32                    `json:"replicas,omitempty"`
	RedisConfig         *RedisConfig              `json:"redisConfig,omitempty"`
	Affinity            *corev1.Affinity          `json:"affinity,omitempty"`
	PodDisruptionBudget *RedisPodDisruptionBudget `json:"pdb,omitempty"`
	ReadinessProbe *Probe `json:"readinessProbe,omitempty" protobuf:"bytes,11,opt,name=readinessProbe"`
	LivenessProbe *Probe `json:"livenessProbe,omitempty" protobuf:"bytes,11,opt,name=livenessProbe"`
}

5.5. RedisClusterSpec

RedisClusterSpec

我们再来看看RedisClusterSpec,该数据结构定义了我们期望的Redisg集群

  • Size:集群的大小
  • KubernetesConfig:这个在上面已经解释过了,这里面定义了Redis的镜像地址,镜像拉去策略,资源限制,密码,以及升级策略
  • RedisLeader:用于定义RedisLeader相关元数据
  • RedisFollower:定义RedisFollowder相关元数据
  • RedisExporterRedisExporter指标采集,用于导出给Prometheus
  • Storage:存储定义,就是一个PVC
  • NodeSelectorNode标签选择器
  • SecurityContext
  • PriorityClassName
  • Tolerations:容忍
  • Resource:资源限制
  • TLSRedis TLS配置
  • Sicecarssidecar模式
  • ServiceAccountName:用于RBAC权限控制
  • PersistenceEnabled:是否需要持久化数据
// api/v1beta1/rediscluster_types.go

type RedisClusterSpec struct {
	Size             *int32           `json:"clusterSize"`
	KubernetesConfig KubernetesConfig `json:"kubernetesConfig"`
	ClusterVersion *string `json:"clusterVersion,omitempty"`
	RedisLeader RedisLeader `json:"redisLeader,omitempty"`
	RedisFollower      RedisFollower                `json:"redisFollower,omitempty"`
	RedisExporter      *RedisExporter               `json:"redisExporter,omitempty"`
	Storage            *Storage                     `json:"storage,omitempty"`
	NodeSelector       map[string]string            `json:"nodeSelector,omitempty"`
	SecurityContext    *corev1.PodSecurityContext   `json:"securityContext,omitempty"`
	PriorityClassName  string                       `json:"priorityClassName,omitempty"`
	Tolerations        *[]corev1.Toleration         `json:"tolerations,omitempty"`
	Resources          *corev1.ResourceRequirements `json:"resources,omitempty"`
	TLS                *TLSConfig                   `json:"TLS,omitempty"`
	Sidecars           *[]Sidecar                   `json:"sidecars,omitempty"`
	ServiceAccountName *string                      `json:"serviceAccountName,omitempty"`
	PersistenceEnabled *bool                        `json:"persistenceEnabled,omitempty"`
}

5.6. RedisController

RedisController

可以看到RedisController非常的简短,具体逻辑如下:

  • 1、通过ApiServer获取当前的Redis资源
  • 2、如果发现Redis资源打了redis.opstreelabs.in/skip-reconcile注解,那么就直接退出,由此可以推测出redis.opstreelabs.in/skip-reconcile注解的用法,如果我们不希望Redis-Operator管理Redis资源,就打上这个注解
  • 3、如果发现Redis资源需要删除,那么先删除Service, 然后是PVC,最后是StatefulSet,删除之后再删除finalizer
  • 注意,只有先通过第四步,给Redis资源打上了FinallzerRedis的删除流程才会被Redis-Operator接管,Finalizer可以认为是Kubernetes给用户开放的资源清理的Hook
  • 4、给Redis资源添加Finalizer
  • 5、创建Redis单节点实例
  • 5.1、生成Redis标签,标签为:app=<cr.Name>, redis_setup_type:standalone, role=standalone,这个标签会打在StatefulSet
  • 5.2、生成Redis注解,注解为:redis.opstreelabs.in=true, redis.opstreelabs.instance=<cr.Name>,这个注解将来也会打在StatefulSet
  • 5.3、根据Redis资源清单,生成StatefulSet配置
  • 5.4、生成OwnerReference信息
  • 5.5、根据Redis资源清单,生成Redis Container配置
  • 5.6、通过apiserver查询是否StatefulSet,如果没查询到,说明当前需要创建Redis单节点实例
  • 5.7、如果找到了通过Update接口,更新StatefuleSet,我们来看一下具体的更新过程,看看如果Redis单实例节点已经创建的情况下,我们需要如何更新。
  • 5.7.1、通过k8s-objectmatcher库计算出current, modified之间的差别
  • 5.7.2、如果Redis资源的PVC容量发生改变,需要更改每个节点的PVC容量,由于这里RedisController组建的是单实例节点,因此这里只需要更新一个NodePVC容量
  • 注意,由于PVC的容量只能增加,不能减少,因此这里一定是扩容
  • 5.7.3、把旧的Redis中的Annotation拷贝给当前新提交的Redis资源
  • 5.7.4、调用Update接口更新资源
  • 6、创建Service
  • Service一共创建了两种,一种是普通的Service,一种是Headless Service
type RedisReconciler struct {
	client.Client
	Log    logr.Logger
	Scheme *runtime.Scheme
}

// Reconcile is part of the main kubernetes reconciliation loop which aims
func (r *RedisReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	reqLogger := r.Log.WithValues("Request.Namespace", req.Namespace, "Request.Name", req.Name)
	reqLogger.Info("Reconciling opstree redis controller")
	instance := &redisv1beta1.Redis{}

	err := r.Client.Get(context.TODO(), req.NamespacedName, instance)
	if err != nil {
		if errors.IsNotFound(err) {
			return ctrl.Result{}, nil
		}
		return ctrl.Result{}, err
	}
	if _, found := instance.ObjectMeta.GetAnnotations()["redis.opstreelabs.in/skip-reconcile"]; found {
		reqLogger.Info("Found annotations redis.opstreelabs.in/skip-reconcile, so skipping reconcile")
		return ctrl.Result{RequeueAfter: time.Second * 10}, nil
	}
	if err := k8sutils.HandleRedisFinalizer(instance, r.Client); err != nil {
		return ctrl.Result{}, err
	}

	if err := k8sutils.AddRedisFinalizer(instance, r.Client); err != nil {
		return ctrl.Result{}, err
	}

	err = k8sutils.CreateStandaloneRedis(instance)
	if err != nil {
		return ctrl.Result{}, err
	}
	err = k8sutils.CreateStandaloneService(instance)
	if err != nil {
		return ctrl.Result{}, err
	}

	reqLogger.Info("Will reconcile redis operator in again 10 seconds")
	return ctrl.Result{RequeueAfter: time.Second * 10}, nil
}

// SetupWithManager sets up the controller with the Manager.
func (r *RedisReconciler) SetupWithManager(mgr ctrl.Manager) error {
	return ctrl.NewControllerManagedBy(mgr).
		For(&redisv1beta1.Redis{}).
		Complete(r)
}

func CreateStandaloneRedis(cr *redisv1beta1.Redis) error {
	logger := statefulSetLogger(cr.Namespace, cr.ObjectMeta.Name)
	labels := getRedisLabels(cr.ObjectMeta.Name, "standalone", "standalone", cr.ObjectMeta.Labels)
	annotations := generateStatefulSetsAnots(cr.ObjectMeta)
	objectMetaInfo := generateObjectMetaInformation(cr.ObjectMeta.Name, cr.Namespace, labels, annotations)
	err := CreateOrUpdateStateFul(cr.Namespace,
		objectMetaInfo,
		generateRedisStandaloneParams(cr),
		redisAsOwner(cr),
		generateRedisStandaloneContainerParams(cr),
		cr.Spec.Sidecars,
	)
	if err != nil {
		logger.Error(err, "Cannot create standalone statefulset for Redis")
		return err
	}
	return nil
}

5.7. RedisClusterController

RedisClusterController

真正组件集群的控制器是通过RedisClusterController来实现的,RedisController仅仅是开胃菜,我们来看看RedisClusterController是如何组件集群的,具体逻辑如下:

  • 1、根据RedisCluster资源名以及所在名称空间查询RedisCluster资源,如果没找到,则直接退出
  • 因为RedisCluster中包含了如何创建Redis集群的信息,因此如果这个资源如果找不到,RedisClusterController就陷入了巧妇难为无米之炊的困境了。不过,既然Informer都已经检测到了RedisCluster资源的变更,一般来说肯定是能够查到的。
  • 2、如果RedisCluster资源标注了rediscluster.opstreelabs.in/skip-reconcile注解,说明用户不想创建Redis集群,直接退出
  • 3、根据Redis Leader的数量以及Redis Follower的数量计算出总的副本数
  • 4、如果发现RedisCluster资源被删除了,那么开始执行删除流程,即先删除Service,然后删除PVC,最后在删除StaetfulSet,所有资源删除完成之后,再删除RedisClusterFinallzer资源
  • 5、如果发现不是RedisCluster资源的删除动作,那么判断该资源是否存在Finallzer,如果没有,则添加Finallzer
  • 6、创建Redis ClusterLeader,其实就是创建了名为redis-leaderStatefulSet,并且只创建了这一个资源,其它资源都没有创建
  • 7、创建名为redis-leader, redis-leader-headlessServcie
  • 8、创建名为redis-leaderPodDisruptionBudget资源,可以保证Redis集群更强的高可用性
  • 9、如果Redis Leader的所有Pod已经全部启动完成,那么就创建Follower,和Leader一样,先创建名为redis-followerStatefuleSet资源,然后创建名为redis-follower, redis-follower-headlessService资源,最后创建名为redis-followderPodDisruptionBudget资源
  • 10、判断RedisLeader, Follower的所有Pod是否已经全部启动完成,如果没有,那么两分钟之后再进来
  • 11、通过执行cluster nodes命令,看看当前有几个节点
  • 11.1、如果查询到的节点数量,不等于总数量,那么执行后续步骤:
  • 11.1.1、执行cluster nodes命令,看看当前又几个master节点
  • 11.1.2、如果查询到的master的数量和预期数量不符,说明master还未组建好,通过执行-- cluaster create <ip:port> --cluster-yes命令创建master
  • 11.1.3、如果相等,说明master已经组建好了,此时可以通过执行cluster add node命令把Follower一个一个添加给Leader
  • 11.2、如果查询到的节点数量,等于总数量,那么之后后续步骤:
  • 11.2.1、执行cluster nodes命令,拿到当前所有的node节点
  • 11.2.2、如果每个节点的状态没有包含fail,说明Redis集群创建成功,但凡有一个节点出现fail,说明集群组建失败,此时需要执行cluster reset命令
// controllers/rediscluster_controller.go
type RedisClusterReconciler struct {
	client.Client
	Log    logr.Logger
	Scheme *runtime.Scheme
}

// Reconcile is part of the main kubernetes reconciliation loop
func (r *RedisClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	reqLogger := r.Log.WithValues("Request.Namespace", req.Namespace, "Request.Name", req.Name)
	reqLogger.Info("Reconciling opstree redis Cluster controller")
	instance := &redisv1beta1.RedisCluster{}

	err := r.Client.Get(context.TODO(), req.NamespacedName, instance)
	if err != nil {
		if errors.IsNotFound(err) {
			return ctrl.Result{}, nil
		}
		return ctrl.Result{}, err
	}

	if _, found := instance.ObjectMeta.GetAnnotations()["rediscluster.opstreelabs.in/skip-reconcile"]; found {
		reqLogger.Info("Found annotations rediscluster.opstreelabs.in/skip-reconcile, so skipping reconcile")
		return ctrl.Result{RequeueAfter: time.Second * 10}, nil
	}

	leaderReplicas := instance.Spec.GetReplicaCounts("leader")
	followerReplicas := instance.Spec.GetReplicaCounts("follower")
	totalReplicas := leaderReplicas + followerReplicas

	if err := k8sutils.HandleRedisClusterFinalizer(instance, r.Client); err != nil {
		return ctrl.Result{RequeueAfter: time.Second * 60}, err
	}

	if err := k8sutils.AddRedisClusterFinalizer(instance, r.Client); err != nil {
		return ctrl.Result{RequeueAfter: time.Second * 60}, err
	}

	err = k8sutils.CreateRedisLeader(instance)
	if err != nil {
		return ctrl.Result{RequeueAfter: time.Second * 60}, err
	}
	if leaderReplicas != 0 {
		err = k8sutils.CreateRedisLeaderService(instance)
		if err != nil {
			return ctrl.Result{RequeueAfter: time.Second * 60}, err
		}
	}

	err = k8sutils.ReconcileRedisPodDisruptionBudget(instance, "leader", instance.Spec.RedisLeader.PodDisruptionBudget)
	if err != nil {
		return ctrl.Result{RequeueAfter: time.Second * 60}, err
	}

	redisLeaderInfo, err := k8sutils.GetStatefulSet(instance.Namespace, instance.ObjectMeta.Name+"-leader")
	if err != nil {
		return ctrl.Result{RequeueAfter: time.Second * 60}, err
	}

	if int32(redisLeaderInfo.Status.ReadyReplicas) == leaderReplicas {
		err = k8sutils.CreateRedisFollower(instance)
		if err != nil {
			return ctrl.Result{RequeueAfter: time.Second * 60}, err
		}
		// if we have followers create their service.
		if followerReplicas != 0 {
			err = k8sutils.CreateRedisFollowerService(instance)
			if err != nil {
				return ctrl.Result{RequeueAfter: time.Second * 60}, err
			}
		}
		err = k8sutils.ReconcileRedisPodDisruptionBudget(instance, "follower", instance.Spec.RedisFollower.PodDisruptionBudget)
		if err != nil {
			return ctrl.Result{RequeueAfter: time.Second * 60}, err
		}
	}
	redisFollowerInfo, err := k8sutils.GetStatefulSet(instance.Namespace, instance.ObjectMeta.Name+"-follower")
	if err != nil {
		return ctrl.Result{RequeueAfter: time.Second * 60}, err
	}

	if leaderReplicas == 0 {
		reqLogger.Info("Redis leaders Cannot be 0", "Ready.Replicas", strconv.Itoa(int(redisLeaderInfo.Status.ReadyReplicas)), "Expected.Replicas", leaderReplicas)
		return ctrl.Result{RequeueAfter: time.Second * 120}, nil
	}

	if int32(redisLeaderInfo.Status.ReadyReplicas) != leaderReplicas && int32(redisFollowerInfo.Status.ReadyReplicas) != followerReplicas {
		reqLogger.Info("Redis leader and follower nodes are not ready yet", "Ready.Replicas", strconv.Itoa(int(redisLeaderInfo.Status.ReadyReplicas)), "Expected.Replicas", leaderReplicas)
		return ctrl.Result{RequeueAfter: time.Second * 120}, nil
	}
	reqLogger.Info("Creating redis cluster by executing cluster creation commands", "Leaders.Ready", strconv.Itoa(int(redisLeaderInfo.Status.ReadyReplicas)), "Followers.Ready", strconv.Itoa(int(redisFollowerInfo.Status.ReadyReplicas)))
	if k8sutils.CheckRedisNodeCount(instance, "") != totalReplicas {
		leaderCount := k8sutils.CheckRedisNodeCount(instance, "leader")
		if leaderCount != leaderReplicas {
			reqLogger.Info("Not all leader are part of the cluster...", "Leaders.Count", leaderCount, "Instance.Size", leaderReplicas)
			k8sutils.ExecuteRedisClusterCommand(instance)
		} else {
			if followerReplicas > 0 {
				reqLogger.Info("All leader are part of the cluster, adding follower/replicas", "Leaders.Count", leaderCount, "Instance.Size", leaderReplicas, "Follower.Replicas", followerReplicas)
				k8sutils.ExecuteRedisReplicationCommand(instance)
			} else {
				reqLogger.Info("no follower/replicas configured, skipping replication configuration", "Leaders.Count", leaderCount, "Leader.Size", leaderReplicas, "Follower.Replicas", followerReplicas)
			}
		}
	} else {
		reqLogger.Info("Redis leader count is desired")
		if k8sutils.CheckRedisClusterState(instance) >= int(totalReplicas)-1 {
			reqLogger.Info("Redis leader is not desired, executing failover operation")
			err = k8sutils.ExecuteFailoverOperation(instance)
			if err != nil {
				return ctrl.Result{RequeueAfter: time.Second * 10}, err
			}
		}
		return ctrl.Result{RequeueAfter: time.Second * 120}, nil
	}
	reqLogger.Info("Will reconcile redis cluster operator in again 10 seconds")
	return ctrl.Result{RequeueAfter: time.Second * 10}, nil
}

// SetupWithManager sets up the controller with the Manager.
func (r *RedisClusterReconciler) SetupWithManager(mgr ctrl.Manager) error {
	return ctrl.NewControllerManagedBy(mgr).
		For(&redisv1beta1.RedisCluster{}).
		Complete(r)
}