Skip to content

Commit

Permalink
feat: enhance RedisReplication controller and CRD with additional sta…
Browse files Browse the repository at this point in the history
…tus columns and refactor reconciliation logic

- Added new printer columns "Master" and "Age" to the RedisReplication CRD for better visibility of the master node and resource age.
- Refactored the reconciliation logic in the RedisReplication controller to improve clarity and maintainability by introducing a reconciler struct for handling different reconciliation tasks.
- Updated the e2e tests to validate the HA setup of Redis Replication and Sentinel, ensuring consistency in master IP across different sources.
- Removed obsolete test files and replaced them with a new HA setup configuration.

This update improves the usability and reliability of the Redis replication feature.

Signed-off-by: drivebyer <[email protected]>
  • Loading branch information
drivebyer committed Dec 10, 2024
1 parent e9ec33f commit 25aab8a
Show file tree
Hide file tree
Showing 8 changed files with 174 additions and 101 deletions.
2 changes: 2 additions & 0 deletions api/v1beta2/redisreplication_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ type RedisReplicationStatus struct {
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:storageversion
// +kubebuilder:printcolumn:name="Master",type="string",JSONPath=".status.masterNode"
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"

// Redis is the Schema for the redis API
type RedisReplication struct {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4397,7 +4397,14 @@ spec:
storage: false
subresources:
status: {}
- name: v1beta2
- additionalPrinterColumns:
- jsonPath: .status.masterNode
name: Master
type: string
- jsonPath: .metadata.creationTimestamp
name: Age
type: date
name: v1beta2
schema:
openAPIV3Schema:
description: Redis is the Schema for the redis API
Expand Down
154 changes: 112 additions & 42 deletions pkg/controllers/redisreplication/redisreplication_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,64 +28,51 @@ type Reconciler struct {
}

func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
logger := log.FromContext(ctx, "Request.Namespace", req.Namespace, "Request.Name", req.Name)
instance := &redisv1beta2.RedisReplication{}

err := r.Client.Get(context.TODO(), req.NamespacedName, instance)
err := r.Client.Get(ctx, req.NamespacedName, instance)
if err != nil {
return intctrlutil.RequeueWithErrorChecking(ctx, err, "")
}
if instance.ObjectMeta.GetDeletionTimestamp() != nil {
if err = k8sutils.HandleRedisReplicationFinalizer(ctx, r.Client, r.K8sClient, instance); err != nil {
return intctrlutil.RequeueWithError(ctx, err, "")
}
return intctrlutil.Reconciled()
}
if _, found := instance.ObjectMeta.GetAnnotations()["redisreplication.opstreelabs.in/skip-reconcile"]; found {
return intctrlutil.RequeueAfter(ctx, time.Second*10, "found skip reconcile annotation")
}
if err = k8sutils.AddFinalizer(ctx, instance, k8sutils.RedisReplicationFinalizer, r.Client); err != nil {
return intctrlutil.RequeueWithError(ctx, err, "")
}
err = k8sutils.CreateReplicationRedis(ctx, instance, r.K8sClient)
if err != nil {
return intctrlutil.RequeueWithError(ctx, err, "")
}
err = k8sutils.CreateReplicationService(ctx, instance, r.K8sClient)
if err != nil {
return intctrlutil.RequeueWithError(ctx, err, "")
}
if !r.IsStatefulSetReady(ctx, instance.Namespace, instance.Name) {
return intctrlutil.Reconciled()
return intctrlutil.RequeueWithErrorChecking(ctx, err, "failed to get RedisReplication instance")

Check warning on line 35 in pkg/controllers/redisreplication/redisreplication_controller.go

View check run for this annotation

Codecov / codecov/patch

pkg/controllers/redisreplication/redisreplication_controller.go#L35

Added line #L35 was not covered by tests
}

var realMaster string
masterNodes := k8sutils.GetRedisNodesByRole(ctx, r.K8sClient, instance, "master")
if len(masterNodes) > 1 {
logger.Info("Creating redis replication by executing replication creation commands")
slaveNodes := k8sutils.GetRedisNodesByRole(ctx, r.K8sClient, instance, "slave")
realMaster = k8sutils.GetRedisReplicationRealMaster(ctx, r.K8sClient, instance, masterNodes)
if len(slaveNodes) == 0 {
realMaster = masterNodes[0]
var reconcilers []reconciler
if k8sutils.IsDeleted(instance) {
reconcilers = []reconciler{
{typ: "finalizer", rec: r.reconcileFinalizer},

Check warning on line 41 in pkg/controllers/redisreplication/redisreplication_controller.go

View check run for this annotation

Codecov / codecov/patch

pkg/controllers/redisreplication/redisreplication_controller.go#L40-L41

Added lines #L40 - L41 were not covered by tests
}
if err = k8sutils.CreateMasterSlaveReplication(ctx, r.K8sClient, instance, masterNodes, realMaster); err != nil {
return intctrlutil.RequeueAfter(ctx, time.Second*60, "")
} else {
reconcilers = []reconciler{
{typ: "annotation", rec: r.reconcileAnnotation},
{typ: "statefulset", rec: r.reconcileStatefulSet},
{typ: "service", rec: r.reconcileService},
{typ: "redis", rec: r.reconcileRedis},
{typ: "status", rec: r.reconcileStatus},
}
}
realMaster = k8sutils.GetRedisReplicationRealMaster(ctx, r.K8sClient, instance, masterNodes)
if err = r.UpdateRedisReplicationMaster(ctx, instance, realMaster); err != nil {
return intctrlutil.RequeueWithError(ctx, err, "")
}
if err = r.UpdateRedisPodRoleLabel(ctx, instance, realMaster); err != nil {
return intctrlutil.RequeueWithError(ctx, err, "")
for _, reconciler := range reconcilers {
result, err := reconciler.rec(ctx, instance)
if err != nil {
return intctrlutil.RequeueWithError(ctx, err, "")
}

Check warning on line 56 in pkg/controllers/redisreplication/redisreplication_controller.go

View check run for this annotation

Codecov / codecov/patch

pkg/controllers/redisreplication/redisreplication_controller.go#L55-L56

Added lines #L55 - L56 were not covered by tests
if result.Requeue {
return result, nil
}
}

return intctrlutil.RequeueAfter(ctx, time.Second*10, "")
}

func (r *Reconciler) UpdateRedisReplicationMaster(ctx context.Context, instance *redisv1beta2.RedisReplication, masterNode string) error {
if instance.Status.MasterNode == masterNode {
return nil
}

if instance.Status.MasterNode != masterNode {
logger := log.FromContext(ctx)
logger.Info("Updating master node",
"previous", instance.Status.MasterNode,
"new", masterNode)
}

Check warning on line 75 in pkg/controllers/redisreplication/redisreplication_controller.go

View check run for this annotation

Codecov / codecov/patch

pkg/controllers/redisreplication/redisreplication_controller.go#L70-L75

Added lines #L70 - L75 were not covered by tests
instance.Status.MasterNode = masterNode
if err := r.Client.Status().Update(ctx, instance); err != nil {
return err
Expand Down Expand Up @@ -118,6 +105,89 @@ func (r *Reconciler) UpdateRedisPodRoleLabel(ctx context.Context, cr *redisv1bet
return nil
}

type reconciler struct {
typ string
rec func(ctx context.Context, instance *redisv1beta2.RedisReplication) (ctrl.Result, error)
}

func (r *Reconciler) reconcileFinalizer(ctx context.Context, instance *redisv1beta2.RedisReplication) (ctrl.Result, error) {
if k8sutils.IsDeleted(instance) {
if err := k8sutils.HandleRedisReplicationFinalizer(ctx, r.Client, r.K8sClient, instance); err != nil {
return intctrlutil.RequeueWithError(ctx, err, "")
}
return intctrlutil.Reconciled()

Check warning on line 118 in pkg/controllers/redisreplication/redisreplication_controller.go

View check run for this annotation

Codecov / codecov/patch

pkg/controllers/redisreplication/redisreplication_controller.go#L113-L118

Added lines #L113 - L118 were not covered by tests
}
if err := k8sutils.AddFinalizer(ctx, instance, k8sutils.RedisReplicationFinalizer, r.Client); err != nil {
return intctrlutil.RequeueWithError(ctx, err, "")
}
return intctrlutil.Reconciled()

Check warning on line 123 in pkg/controllers/redisreplication/redisreplication_controller.go

View check run for this annotation

Codecov / codecov/patch

pkg/controllers/redisreplication/redisreplication_controller.go#L120-L123

Added lines #L120 - L123 were not covered by tests
}

func (r *Reconciler) reconcileAnnotation(ctx context.Context, instance *redisv1beta2.RedisReplication) (ctrl.Result, error) {
if _, found := instance.ObjectMeta.GetAnnotations()["redisreplication.opstreelabs.in/skip-reconcile"]; found {
return intctrlutil.RequeueAfter(ctx, time.Second*10, "found skip reconcile annotation")
}

Check warning on line 129 in pkg/controllers/redisreplication/redisreplication_controller.go

View check run for this annotation

Codecov / codecov/patch

pkg/controllers/redisreplication/redisreplication_controller.go#L128-L129

Added lines #L128 - L129 were not covered by tests
return intctrlutil.Reconciled()
}

func (r *Reconciler) reconcileStatefulSet(ctx context.Context, instance *redisv1beta2.RedisReplication) (ctrl.Result, error) {
if err := k8sutils.CreateReplicationRedis(ctx, instance, r.K8sClient); err != nil {
return intctrlutil.RequeueAfter(ctx, time.Second*60, "")
}

Check warning on line 136 in pkg/controllers/redisreplication/redisreplication_controller.go

View check run for this annotation

Codecov / codecov/patch

pkg/controllers/redisreplication/redisreplication_controller.go#L135-L136

Added lines #L135 - L136 were not covered by tests
return intctrlutil.Reconciled()
}

func (r *Reconciler) reconcileService(ctx context.Context, instance *redisv1beta2.RedisReplication) (ctrl.Result, error) {
if err := k8sutils.CreateReplicationService(ctx, instance, r.K8sClient); err != nil {
return intctrlutil.RequeueAfter(ctx, time.Second*60, "")
}

Check warning on line 143 in pkg/controllers/redisreplication/redisreplication_controller.go

View check run for this annotation

Codecov / codecov/patch

pkg/controllers/redisreplication/redisreplication_controller.go#L142-L143

Added lines #L142 - L143 were not covered by tests
return intctrlutil.Reconciled()
}

func (r *Reconciler) reconcileRedis(ctx context.Context, instance *redisv1beta2.RedisReplication) (ctrl.Result, error) {
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
logger := log.FromContext(ctx)

if !r.IsStatefulSetReady(ctx, instance.Namespace, instance.Name) {
logger.Info("StatefulSet not ready yet, requeuing",
"namespace", instance.Namespace,
"name", instance.Name)
return intctrlutil.RequeueAfter(ctx, time.Second*60, "")
}

var realMaster string
masterNodes := k8sutils.GetRedisNodesByRole(ctx, r.K8sClient, instance, "master")
if len(masterNodes) > 1 {
log.FromContext(ctx).Info("Creating redis replication by executing replication creation commands")
slaveNodes := k8sutils.GetRedisNodesByRole(ctx, r.K8sClient, instance, "slave")
realMaster = k8sutils.GetRedisReplicationRealMaster(ctx, r.K8sClient, instance, masterNodes)
if len(slaveNodes) == 0 {
realMaster = masterNodes[0]
}
if err := k8sutils.CreateMasterSlaveReplication(ctx, r.K8sClient, instance, masterNodes, realMaster); err != nil {
return intctrlutil.RequeueAfter(ctx, time.Second*60, "")
}

Check warning on line 170 in pkg/controllers/redisreplication/redisreplication_controller.go

View check run for this annotation

Codecov / codecov/patch

pkg/controllers/redisreplication/redisreplication_controller.go#L159-L170

Added lines #L159 - L170 were not covered by tests
}
return intctrlutil.Reconciled()

Check warning on line 172 in pkg/controllers/redisreplication/redisreplication_controller.go

View check run for this annotation

Codecov / codecov/patch

pkg/controllers/redisreplication/redisreplication_controller.go#L172

Added line #L172 was not covered by tests
}

// reconcileStatus update status and label.
func (r *Reconciler) reconcileStatus(ctx context.Context, instance *redisv1beta2.RedisReplication) (ctrl.Result, error) {
var err error
var realMaster string

masterNodes := k8sutils.GetRedisNodesByRole(ctx, r.K8sClient, instance, "master")
realMaster = k8sutils.GetRedisReplicationRealMaster(ctx, r.K8sClient, instance, masterNodes)
if err = r.UpdateRedisReplicationMaster(ctx, instance, realMaster); err != nil {
return intctrlutil.RequeueWithError(ctx, err, "")
}
if err = r.UpdateRedisPodRoleLabel(ctx, instance, realMaster); err != nil {
return intctrlutil.RequeueWithError(ctx, err, "")
}
return intctrlutil.Reconciled()

Check warning on line 188 in pkg/controllers/redisreplication/redisreplication_controller.go

View check run for this annotation

Codecov / codecov/patch

pkg/controllers/redisreplication/redisreplication_controller.go#L176-L188

Added lines #L176 - L188 were not covered by tests
}

// SetupWithManager sets up the controller with the Manager.
func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
Expand Down
9 changes: 9 additions & 0 deletions pkg/k8sutils/kube.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package k8sutils

import (
"sigs.k8s.io/controller-runtime/pkg/client"
)

func IsDeleted(obj client.Object) bool {
return obj.GetDeletionTimestamp() != nil

Check warning on line 8 in pkg/k8sutils/kube.go

View check run for this annotation

Codecov / codecov/patch

pkg/k8sutils/kube.go#L7-L8

Added lines #L7 - L8 were not covered by tests
}
40 changes: 20 additions & 20 deletions tests/e2e-chainsaw/v1beta2/setup/ha/chainsaw-test.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
# This case is to test the HA setup of the Redis Replication and Sentinel
# It will create a Redis Replication and Sentinel, then terminate the Redis Replication master pod
# and check if the Sentinel can promote a new master pod.
#

Check failure on line 4 in tests/e2e-chainsaw/v1beta2/setup/ha/chainsaw-test.yaml

View workflow job for this annotation

GitHub Actions / Validate YAML

4:2 [trailing-spaces] trailing spaces
# It check three place the same pod IP:
# 1. Status from RedisReplication
# 2. Label from RedisReplication
# 3. get-master-addr-by-name from Sentinel
---
apiVersion: chainsaw.kyverno.io/v1alpha1
kind: Test
Expand All @@ -7,25 +15,19 @@ spec:
steps:
- try:
- apply:
file: replication.yaml
- apply:
file: sentinel.yaml
- create:
file: cli-pod.yaml
file: ha.yaml

- name: Sleep for 3 minutes
- name: Test Master IP consistency
try:
- sleep:
duration: 3m

- name: Test sentinel monitoring
try:
duration: 180s
- script:
timeout: 10s
content: |
export MASTER_IP_FROM_SENTINEL=$(kubectl exec --namespace ${NAMESPACE} redis-sentinel-sentinel-0 -- redis-cli -p 26379 sentinel get-master-addr-by-name myMaster | head -n 1);
export MASTER_IP_FROM_STATUS=$(kubectl -n ${NAMESPACE} get pod $(kubectl -n ${NAMESPACE} get redisreplication redis-replication -o jsonpath='{.status.masterNode}') -o jsonpath='{.status.podIP}');

Check warning on line 27 in tests/e2e-chainsaw/v1beta2/setup/ha/chainsaw-test.yaml

View workflow job for this annotation

GitHub Actions / Validate YAML

27:201 [line-length] line too long (209 > 200 characters)
export MASTER_IP_FROM_SENTINEL=$(kubectl -n ${NAMESPACE} exec redis-sentinel-sentinel-0 -- redis-cli -p 26379 sentinel get-master-addr-by-name myMaster | head -n 1);
export MASTER_IP_FROM_LABEL=$(kubectl -n ${NAMESPACE} get pod -l app=redis-replication,redis-role=master,redis_setup_type=replication -o jsonpath='{.items[0].status.podIP}');
if [ "$MASTER_IP_FROM_SENTINEL" = "$MASTER_IP_FROM_LABEL" ]; then echo "OK"; else echo "FAIL"; fi
if [ "$MASTER_IP_FROM_SENTINEL" = "$MASTER_IP_FROM_LABEL" ] && [ "$MASTER_IP_FROM_SENTINEL" = "$MASTER_IP_FROM_STATUS" ]; then echo "OK"; else echo "FAIL"; fi
check:
(contains($stdout, 'OK')): true

Expand All @@ -35,20 +37,18 @@ spec:
- script:
timeout: 10s
content: |
kubectl --namespace ${NAMESPACE} delete pod redis-replication-0
- name: Sleep for 5 minutes
try:
kubectl -n ${NAMESPACE} delete pod redis-replication-0
- sleep:
duration: 5m
duration: 30s

- name: Test sentinel monitoring
- name: Test Master IP consistency
try:
- script:
timeout: 10s
content: |
export MASTER_IP_FROM_SENTINEL=$(kubectl exec --namespace ${NAMESPACE} redis-sentinel-sentinel-0 -- redis-cli -p 26379 sentinel get-master-addr-by-name myMaster | head -n 1);
export MASTER_IP_FROM_STATUS=$(kubectl -n ${NAMESPACE} get pod $(kubectl -n ${NAMESPACE} get redisreplication redis-replication -o jsonpath='{.status.masterNode}') -o jsonpath='{.status.podIP}');

Check warning on line 49 in tests/e2e-chainsaw/v1beta2/setup/ha/chainsaw-test.yaml

View workflow job for this annotation

GitHub Actions / Validate YAML

49:201 [line-length] line too long (209 > 200 characters)
export MASTER_IP_FROM_SENTINEL=$(kubectl -n ${NAMESPACE} exec redis-sentinel-sentinel-0 -- redis-cli -p 26379 sentinel get-master-addr-by-name myMaster | head -n 1);
export MASTER_IP_FROM_LABEL=$(kubectl -n ${NAMESPACE} get pod -l app=redis-replication,redis-role=master,redis_setup_type=replication -o jsonpath='{.items[0].status.podIP}');
if [ $MASTER_IP_FROM_SENTINEL = $MASTER_IP_FROM_LABEL ]; then echo "OK"; else echo "FAIL"; fi
if [ "$MASTER_IP_FROM_SENTINEL" = "$MASTER_IP_FROM_LABEL" ] && [ "$MASTER_IP_FROM_SENTINEL" = "$MASTER_IP_FROM_STATUS" ]; then echo "OK"; else echo "FAIL"; fi
check:
(contains($stdout, 'OK')): true
15 changes: 0 additions & 15 deletions tests/e2e-chainsaw/v1beta2/setup/ha/cli-pod.yaml

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,26 @@ spec:
resources:
requests:
storage: 1Gi
---
apiVersion: redis.redis.opstreelabs.in/v1beta2
kind: RedisSentinel
metadata:
name: redis-sentinel
spec:
clusterSize: 1
podSecurityContext:
runAsUser: 1000
fsGroup: 1000
redisSentinelConfig:
redisReplicationName: redis-replication
quorum: '1'
kubernetesConfig:
image: quay.io/opstree/redis-sentinel:latest
imagePullPolicy: Always
resources:
requests:
cpu: 101m
memory: 128Mi
limits:
cpu: 101m
memory: 128Mi
23 changes: 0 additions & 23 deletions tests/e2e-chainsaw/v1beta2/setup/ha/sentinel.yaml

This file was deleted.

0 comments on commit 25aab8a

Please sign in to comment.