Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: enhance RedisReplication controller and CRD with additional status #1154

Merged
merged 10 commits into from
Dec 11, 2024
Merged
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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ permissions:
contents: read

env:
GOLANG_VERSION: 1.22
GOLANG_VERSION: 1.23.4
APPLICATION_NAME: redis-operator
DockerImagName: docker.io/opstree/redis-operator
BuildDocs: true
Expand All @@ -32,7 +32,7 @@ jobs:
- name: Run GolangCI-Lint
uses: golangci/golangci-lint-action@v6
with:
version: v1.54.0
version: v1.62.2

gotest:
needs:
Expand Down
4 changes: 1 addition & 3 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ linters:
- gofmt
- gofumpt
- goprintffuncname
- gosec
- gosimple
- govet
- grouper
Expand All @@ -39,15 +38,14 @@ linters:
- tenv
- thelper
- tparallel
- typecheck
- unconvert
- unused
- wastedassign
- whitespace

run:
timeout: 15m
go: "1.22"
go: "1.23.4"
tests: true
show-stats: true
skip-files:
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Build the manager binary
FROM golang:1.22 as builder
FROM golang:1.23-alpine as builder

Check warning on line 2 in Dockerfile

View workflow job for this annotation

GitHub Actions / build_scan_container_image

The 'as' keyword should match the case of the 'from' keyword

FromAsCasing: 'as' and 'FROM' keywords' casing do not match More info: https://docs.docker.com/go/dockerfile/rule/from-as-casing/
ARG BUILDOS
ARG BUILDPLATFORM
ARG BUILDARCH
Expand Down
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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/OT-CONTAINER-KIT/redis-operator

go 1.22
go 1.23.4

require (
github.com/avast/retry-go v3.0.0+incompatible
Expand Down
155 changes: 113 additions & 42 deletions pkg/controllers/redisreplication/redisreplication_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,64 +28,52 @@
}

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: "finalizer", rec: r.reconcileFinalizer},
{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 57 in pkg/controllers/redisreplication/redisreplication_controller.go

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L56 - L57 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 76 in pkg/controllers/redisreplication/redisreplication_controller.go

View check run for this annotation

Codecov / codecov/patch

pkg/controllers/redisreplication/redisreplication_controller.go#L71-L76

Added lines #L71 - L76 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 +106,89 @@
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 119 in pkg/controllers/redisreplication/redisreplication_controller.go

View check run for this annotation

Codecov / codecov/patch

pkg/controllers/redisreplication/redisreplication_controller.go#L116-L119

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

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#L122-L123

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

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 130 in pkg/controllers/redisreplication/redisreplication_controller.go

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L129 - L130 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 137 in pkg/controllers/redisreplication/redisreplication_controller.go

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L136 - L137 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 144 in pkg/controllers/redisreplication/redisreplication_controller.go

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L143 - L144 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 171 in pkg/controllers/redisreplication/redisreplication_controller.go

View check run for this annotation

Codecov / codecov/patch

pkg/controllers/redisreplication/redisreplication_controller.go#L160-L171

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

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

View check run for this annotation

Codecov / codecov/patch

pkg/controllers/redisreplication/redisreplication_controller.go#L173

Added line #L173 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 189 in pkg/controllers/redisreplication/redisreplication_controller.go

View check run for this annotation

Codecov / codecov/patch

pkg/controllers/redisreplication/redisreplication_controller.go#L177-L189

Added lines #L177 - L189 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
}
41 changes: 21 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,10 @@
# 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.
# 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 +14,20 @@ 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_POD_FROM_STATUS=$(kubectl -n ${NAMESPACE} get redisreplication redis-replication -o jsonpath='{.status.masterNode}');
export MASTER_IP_FROM_STATUS=$(kubectl -n ${NAMESPACE} get pod ${MASTER_POD_FROM_STATUS} -o jsonpath='{.status.podIP}');
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,19 @@ 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: 120s

- 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_POD_FROM_STATUS=$(kubectl -n ${NAMESPACE} get redisreplication redis-replication -o jsonpath='{.status.masterNode}');
export MASTER_IP_FROM_STATUS=$(kubectl -n ${NAMESPACE} get pod ${MASTER_POD_FROM_STATUS} -o jsonpath='{.status.podIP}');
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
Loading
Loading