diff --git a/k8sutils/cluster-scaling.go b/k8sutils/cluster-scaling.go index 8304d0060..b0a384c02 100644 --- a/k8sutils/cluster-scaling.go +++ b/k8sutils/cluster-scaling.go @@ -32,7 +32,7 @@ func ReshardRedisCluster(client kubernetes.Interface, logger logr.Logger, cr *re if *cr.Spec.ClusterVersion == "v7" { cmd = append(cmd, getRedisHostname(transferPOD, cr, "leader")+":6379") } else { - cmd = append(cmd, getRedisServerIP(transferPOD)+":6379") + cmd = append(cmd, getRedisServerIP(client, logger, transferPOD)+":6379") } if cr.Spec.KubernetesConfig.ExistingPasswordSecret != nil { @@ -71,7 +71,7 @@ func ReshardRedisCluster(client kubernetes.Interface, logger logr.Logger, cr *re logger.V(1).Info("Skipped the execution of", "Cmd", cmd) return } - executeCommand(cr, cmd, cr.ObjectMeta.Name+"-leader-0") + executeCommand(client, logger, cr, cmd, cr.ObjectMeta.Name+"-leader-0") } func getRedisClusterSlots(ctx context.Context, client kubernetes.Interface, logger logr.Logger, cr *redisv1beta2.RedisCluster, nodeID string) string { @@ -153,7 +153,7 @@ func RebalanceRedisClusterEmptyMasters(client kubernetes.Interface, logger logr. if *cr.Spec.ClusterVersion == "v7" { cmd = append(cmd, getRedisHostname(pod, cr, "leader")+":6379") } else { - cmd = append(cmd, getRedisServerIP(pod)+":6379") + cmd = append(cmd, getRedisServerIP(client, logger, pod)+":6379") } cmd = append(cmd, "--cluster-use-empty-masters") @@ -170,7 +170,7 @@ func RebalanceRedisClusterEmptyMasters(client kubernetes.Interface, logger logr. cmd = append(cmd, getRedisTLSArgs(cr.Spec.TLS, cr.ObjectMeta.Name+"-leader-0")...) logger.V(1).Info("Redis cluster rebalance command is", "Command", cmd) - executeCommand(cr, cmd, cr.ObjectMeta.Name+"-leader-1") + executeCommand(client, logger, cr, cmd, cr.ObjectMeta.Name+"-leader-1") } func CheckIfEmptyMasters(ctx context.Context, client kubernetes.Interface, logger logr.Logger, cr *redisv1beta2.RedisCluster) { @@ -205,7 +205,7 @@ func RebalanceRedisCluster(client kubernetes.Interface, logger logr.Logger, cr * if *cr.Spec.ClusterVersion == "v7" { cmd = append(cmd, getRedisHostname(pod, cr, "leader")+":6379") } else { - cmd = append(cmd, getRedisServerIP(pod)+":6379") + cmd = append(cmd, getRedisServerIP(client, logger, pod)+":6379") } if cr.Spec.KubernetesConfig.ExistingPasswordSecret != nil { @@ -220,7 +220,7 @@ func RebalanceRedisCluster(client kubernetes.Interface, logger logr.Logger, cr * cmd = append(cmd, getRedisTLSArgs(cr.Spec.TLS, cr.ObjectMeta.Name+"-leader-0")...) logger.V(1).Info("Redis cluster rebalance command is", "Command", cmd) - executeCommand(cr, cmd, cr.ObjectMeta.Name+"-leader-1") + executeCommand(client, logger, cr, cmd, cr.ObjectMeta.Name+"-leader-1") } // Add redis cluster node would add a node to the existing redis cluster using redis-cli @@ -243,8 +243,8 @@ func AddRedisNodeToCluster(ctx context.Context, client kubernetes.Interface, log cmd = append(cmd, getRedisHostname(newPod, cr, "leader")+":6379") cmd = append(cmd, getRedisHostname(existingPod, cr, "leader")+":6379") } else { - cmd = append(cmd, getRedisServerIP(newPod)+":6379") - cmd = append(cmd, getRedisServerIP(existingPod)+":6379") + cmd = append(cmd, getRedisServerIP(client, logger, newPod)+":6379") + cmd = append(cmd, getRedisServerIP(client, logger, existingPod)+":6379") } if cr.Spec.KubernetesConfig.ExistingPasswordSecret != nil { @@ -259,7 +259,7 @@ func AddRedisNodeToCluster(ctx context.Context, client kubernetes.Interface, log cmd = append(cmd, getRedisTLSArgs(cr.Spec.TLS, cr.ObjectMeta.Name+"-leader-0")...) logger.V(1).Info("Redis cluster add-node command is", "Command", cmd) - executeCommand(cr, cmd, cr.ObjectMeta.Name+"-leader-0") + executeCommand(client, logger, cr, cmd, cr.ObjectMeta.Name+"-leader-0") } // getAttachedFollowerNodeIDs would return a slice of redis followers attached to a redis leader @@ -323,14 +323,14 @@ func RemoveRedisFollowerNodesFromCluster(ctx context.Context, client kubernetes. if *cr.Spec.ClusterVersion == "v7" { cmd = append(cmd, getRedisHostname(existingPod, cr, "leader")+":6379") } else { - cmd = append(cmd, getRedisServerIP(existingPod)+":6379") + cmd = append(cmd, getRedisServerIP(client, logger, existingPod)+":6379") } for _, followerNodeID := range followerNodeIDs { cmd = append(cmd, followerNodeID) logger.V(1).Info("Redis cluster follower remove command is", "Command", cmd) - executeCommand(cr, cmd, cr.ObjectMeta.Name+"-leader-0") + executeCommand(client, logger, cr, cmd, cr.ObjectMeta.Name+"-leader-0") cmd = cmd[:len(cmd)-1] } } @@ -354,7 +354,7 @@ func RemoveRedisNodeFromCluster(ctx context.Context, client kubernetes.Interface if *cr.Spec.ClusterVersion == "v7" { cmd = append(cmd, getRedisHostname(existingPod, cr, "leader")+":6379") } else { - cmd = append(cmd, getRedisServerIP(existingPod)+":6379") + cmd = append(cmd, getRedisServerIP(client, logger, existingPod)+":6379") } removePodNodeID := getRedisNodeID(ctx, client, logger, cr, removePod) @@ -375,7 +375,7 @@ func RemoveRedisNodeFromCluster(ctx context.Context, client kubernetes.Interface if getRedisClusterSlots(ctx, client, logger, cr, removePodNodeID) != "0" { logger.V(1).Info("Skipping execution remove leader not empty", "cmd", cmd) } - executeCommand(cr, cmd, cr.ObjectMeta.Name+"-leader-0") + executeCommand(client, logger, cr, cmd, cr.ObjectMeta.Name+"-leader-0") } // verifyLeaderPod return true if the pod is leader/master @@ -415,7 +415,7 @@ func ClusterFailover(ctx context.Context, client kubernetes.Interface, logger lo if *cr.Spec.ClusterVersion == "v7" { cmd = append(cmd, getRedisHostname(pod, cr, "leader")+":6379") } else { - cmd = append(cmd, getRedisServerIP(pod)+":6379") + cmd = append(cmd, getRedisServerIP(client, logger, pod)+":6379") } if cr.Spec.KubernetesConfig.ExistingPasswordSecret != nil { @@ -430,5 +430,5 @@ func ClusterFailover(ctx context.Context, client kubernetes.Interface, logger lo cmd = append(cmd, getRedisTLSArgs(cr.Spec.TLS, slavePodName)...) logger.V(1).Info("Redis cluster failover command is", "Command", cmd) - executeCommand(cr, cmd, slavePodName) + executeCommand(client, logger, cr, cmd, slavePodName) } diff --git a/k8sutils/redis-sentinel.go b/k8sutils/redis-sentinel.go index 42e96b38b..9977fa644 100644 --- a/k8sutils/redis-sentinel.go +++ b/k8sutils/redis-sentinel.go @@ -336,6 +336,6 @@ func getRedisReplicationMasterIP(ctx context.Context, client kubernetes.Interfac Namespace: replicationNamespace, } - realMasterPodIP := getRedisServerIP(realMasterInfo) + realMasterPodIP := getRedisServerIP(client, logger, realMasterInfo) return realMasterPodIP } diff --git a/k8sutils/redis.go b/k8sutils/redis.go index a34a7934a..51cb0a4e3 100644 --- a/k8sutils/redis.go +++ b/k8sutils/redis.go @@ -26,28 +26,31 @@ type RedisDetails struct { } // getRedisServerIP will return the IP of redis service -func getRedisServerIP(redisInfo RedisDetails) string { - logger := generateRedisManagerLogger(redisInfo.Namespace, redisInfo.PodName) - client, err := GenerateK8sClient(GenerateK8sConfig) - if err != nil { - logger.Error(err, "Error in getting k8s client") - return "" - } +func getRedisServerIP(client kubernetes.Interface, logger logr.Logger, redisInfo RedisDetails) string { + logger.V(1).Info("Fetching Redis pod", "namespace", redisInfo.Namespace, "podName", redisInfo.PodName) + redisPod, err := client.CoreV1().Pods(redisInfo.Namespace).Get(context.TODO(), redisInfo.PodName, metav1.GetOptions{}) if err != nil { - logger.Error(err, "Error in getting redis pod IP") + logger.Error(err, "Error in getting Redis pod IP", "namespace", redisInfo.Namespace, "podName", redisInfo.PodName) + return "" } redisIP := redisPod.Status.PodIP - // If we're NOT IPv4, assume were IPv6.. - if redisIP != "" { - if net.ParseIP(redisIP).To4() == nil { - logger.V(1).Info("Redis is IPv6", "ip", redisIP, "ipv6", net.ParseIP(redisIP).To16()) - redisIP = fmt.Sprintf("[%s]", redisIP) - } + logger.V(1).Info("Fetched Redis pod IP", "ip", redisIP) + + // Check if IP is empty + if redisIP == "" { + logger.V(1).Info("Redis pod IP is empty", "namespace", redisInfo.Namespace, "podName", redisInfo.PodName) + return "" + } + + // If we're NOT IPv4, assume we're IPv6.. + if net.ParseIP(redisIP).To4() == nil { + logger.V(1).Info("Redis is using IPv6", "ip", redisIP) + redisIP = fmt.Sprintf("[%s]", redisIP) } - logger.V(1).Info("Successfully got the ip for redis", "ip", redisIP) + logger.V(1).Info("Successfully got the IP for Redis", "ip", redisIP) return redisIP } @@ -58,37 +61,37 @@ func getRedisHostname(redisInfo RedisDetails, cr *redisv1beta2.RedisCluster, rol } // CreateSingleLeaderRedisCommand will create command for single leader cluster creation -func CreateSingleLeaderRedisCommand(cr *redisv1beta2.RedisCluster) []string { - logger := generateRedisManagerLogger(cr.Namespace, cr.ObjectMeta.Name) +func CreateSingleLeaderRedisCommand(logger logr.Logger, cr *redisv1beta2.RedisCluster) []string { cmd := []string{"redis-cli", "CLUSTER", "ADDSLOTS"} for i := 0; i < 16384; i++ { cmd = append(cmd, strconv.Itoa(i)) } + logger.V(1).Info("Generating Redis Add Slots command for single node cluster", + "BaseCommand", cmd[:3], + "SlotsRange", "0-16383", + "TotalSlots", 16384) - logger.V(1).Info("Redis Add Slots command for single node cluster is", "Command", cmd) return cmd } // CreateMultipleLeaderRedisCommand will create command for single leader cluster creation -func CreateMultipleLeaderRedisCommand(cr *redisv1beta2.RedisCluster) []string { - logger := generateRedisManagerLogger(cr.Namespace, cr.ObjectMeta.Name) +func CreateMultipleLeaderRedisCommand(client kubernetes.Interface, logger logr.Logger, cr *redisv1beta2.RedisCluster) []string { cmd := []string{"redis-cli", "--cluster", "create"} replicas := cr.Spec.GetReplicaCounts("leader") - for podCount := 0; podCount <= int(replicas)-1; podCount++ { - pod := RedisDetails{ - PodName: cr.ObjectMeta.Name + "-leader-" + strconv.Itoa(podCount), - Namespace: cr.Namespace, - } - if *cr.Spec.ClusterVersion == "v7" { - cmd = append(cmd, getRedisHostname(pod, cr, "leader")+":6379") + for podCount := 0; podCount < int(replicas); podCount++ { + podName := cr.ObjectMeta.Name + "-leader-" + strconv.Itoa(podCount) + var address string + if cr.Spec.ClusterVersion != nil && *cr.Spec.ClusterVersion == "v7" { + address = getRedisHostname(RedisDetails{PodName: podName, Namespace: cr.Namespace}, cr, "leader") + ":6379" } else { - cmd = append(cmd, getRedisServerIP(pod)+":6379") + address = getRedisServerIP(client, logger, RedisDetails{PodName: podName, Namespace: cr.Namespace}) + ":6379" } + cmd = append(cmd, address) } cmd = append(cmd, "--cluster-yes") - logger.V(1).Info("Redis Add Slots command for single node cluster is", "Command", cmd) + logger.V(1).Info("Redis cluster creation command", "CommandBase", cmd[:3], "Replicas", replicas) return cmd } @@ -102,9 +105,9 @@ func ExecuteRedisClusterCommand(ctx context.Context, client kubernetes.Interface if err != nil { logger.Error(err, "error executing failover command") } - cmd = CreateSingleLeaderRedisCommand(cr) + cmd = CreateSingleLeaderRedisCommand(logger, cr) default: - cmd = CreateMultipleLeaderRedisCommand(cr) + cmd = CreateMultipleLeaderRedisCommand(client, logger, cr) } if cr.Spec.KubernetesConfig.ExistingPasswordSecret != nil { @@ -117,7 +120,7 @@ func ExecuteRedisClusterCommand(ctx context.Context, client kubernetes.Interface } cmd = append(cmd, getRedisTLSArgs(cr.Spec.TLS, cr.ObjectMeta.Name+"-leader-0")...) logger.V(1).Info("Redis cluster creation command is", "Command", cmd) - executeCommand(cr, cmd, cr.ObjectMeta.Name+"-leader-0") + executeCommand(client, logger, cr, cmd, cr.ObjectMeta.Name+"-leader-0") } func getRedisTLSArgs(tlsConfig *redisv1beta2.TLSConfig, clientHost string) []string { @@ -135,25 +138,32 @@ func getRedisTLSArgs(tlsConfig *redisv1beta2.TLSConfig, clientHost string) []str // createRedisReplicationCommand will create redis replication creation command func createRedisReplicationCommand(client kubernetes.Interface, logger logr.Logger, cr *redisv1beta2.RedisCluster, leaderPod RedisDetails, followerPod RedisDetails) []string { cmd := []string{"redis-cli", "--cluster", "add-node"} - if *cr.Spec.ClusterVersion == "v7" { - cmd = append(cmd, getRedisHostname(followerPod, cr, "follower")+":6379") - cmd = append(cmd, getRedisHostname(leaderPod, cr, "leader")+":6379") + var followerAddress, leaderAddress string + + if cr.Spec.ClusterVersion != nil && *cr.Spec.ClusterVersion == "v7" { + followerAddress = getRedisHostname(followerPod, cr, "follower") + ":6379" + leaderAddress = getRedisHostname(leaderPod, cr, "leader") + ":6379" } else { - cmd = append(cmd, getRedisServerIP(followerPod)+":6379") - cmd = append(cmd, getRedisServerIP(leaderPod)+":6379") + followerAddress = getRedisServerIP(client, logger, followerPod) + ":6379" + leaderAddress = getRedisServerIP(client, logger, leaderPod) + ":6379" } - cmd = append(cmd, "--cluster-slave") + + cmd = append(cmd, followerAddress, leaderAddress, "--cluster-slave") if cr.Spec.KubernetesConfig.ExistingPasswordSecret != nil { pass, err := getRedisPassword(client, logger, cr.Namespace, *cr.Spec.KubernetesConfig.ExistingPasswordSecret.Name, *cr.Spec.KubernetesConfig.ExistingPasswordSecret.Key) if err != nil { - logger.Error(err, "Error in getting redis password") + logger.Error(err, "Failed to retrieve Redis password", "Secret", *cr.Spec.KubernetesConfig.ExistingPasswordSecret.Name) } - cmd = append(cmd, "-a") - cmd = append(cmd, pass) + cmd = append(cmd, "-a", pass) } + cmd = append(cmd, getRedisTLSArgs(cr.Spec.TLS, leaderPod.PodName)...) - logger.V(2).Info("Redis replication creation command is", "Command", cmd) + + logger.V(1).Info("Generated Redis replication command", + "FollowerAddress", followerAddress, "LeaderAddress", leaderAddress, + "Command", cmd) + return cmd } @@ -175,7 +185,7 @@ func ExecuteRedisReplicationCommand(ctx context.Context, client kubernetes.Inter PodName: cr.ObjectMeta.Name + "-leader-" + strconv.Itoa(int(followerIdx)%int(leaderCounts)), Namespace: cr.Namespace, } - podIP = getRedisServerIP(followerPod) + podIP = getRedisServerIP(client, logger, followerPod) if !checkRedisNodePresence(cr, nodes, podIP) { logger.V(1).Info("Adding node to cluster.", "Node.IP", podIP, "Follower.Pod", followerPod) cmd := createRedisReplicationCommand(client, logger, cr, leaderPod, followerPod) @@ -187,7 +197,7 @@ func ExecuteRedisReplicationCommand(ctx context.Context, client kubernetes.Inter continue } if pong == "PONG" { - executeCommand(cr, cmd, cr.ObjectMeta.Name+"-leader-0") + executeCommand(client, logger, cr, cmd, cr.ObjectMeta.Name+"-leader-0") } else { logger.V(1).Info("Skipping execution of command due to failed Redis ping", "Follower.Pod", followerPod) } @@ -330,14 +340,14 @@ func configureRedisClient(client kubernetes.Interface, logger logr.Logger, cr *r logger.Error(err, "Error in getting redis password") } redisClient = redis.NewClient(&redis.Options{ - Addr: getRedisServerIP(redisInfo) + ":6379", + Addr: getRedisServerIP(client, logger, redisInfo) + ":6379", Password: pass, DB: 0, TLSConfig: getRedisTLSConfig(client, logger, cr, redisInfo), }) } else { redisClient = redis.NewClient(&redis.Options{ - Addr: getRedisServerIP(redisInfo) + ":6379", + Addr: getRedisServerIP(client, logger, redisInfo) + ":6379", Password: "", DB: 0, TLSConfig: getRedisTLSConfig(client, logger, cr, redisInfo), @@ -347,23 +357,17 @@ func configureRedisClient(client kubernetes.Interface, logger logr.Logger, cr *r } // executeCommand will execute the commands in pod -func executeCommand(cr *redisv1beta2.RedisCluster, cmd []string, podName string) { +func executeCommand(client kubernetes.Interface, logger logr.Logger, cr *redisv1beta2.RedisCluster, cmd []string, podName string) { var ( execOut bytes.Buffer execErr bytes.Buffer ) - logger := generateRedisManagerLogger(cr.Namespace, cr.ObjectMeta.Name) - client, err := GenerateK8sClient(GenerateK8sConfig) - if err != nil { - logger.Error(err, "Could not generate kubernetes client") - return - } config, err := GenerateK8sConfig() if err != nil { logger.Error(err, "Could not find pod to execute") return } - targetContainer, pod := getContainerID(cr, podName) + targetContainer, pod := getContainerID(client, logger, cr, podName) if targetContainer < 0 { logger.Error(err, "Could not find pod to execute") return @@ -395,26 +399,30 @@ func executeCommand(cr *redisv1beta2.RedisCluster, cmd []string, podName string) } // getContainerID will return the id of container from pod -func getContainerID(cr *redisv1beta2.RedisCluster, podName string) (int, *corev1.Pod) { - logger := generateRedisManagerLogger(cr.Namespace, cr.ObjectMeta.Name) - client, err := GenerateK8sClient(GenerateK8sConfig) - if err != nil { - logger.Error(err, "Could not generate kubernetes client") - return -1, nil - } +func getContainerID(client kubernetes.Interface, logger logr.Logger, cr *redisv1beta2.RedisCluster, podName string) (int, *corev1.Pod) { pod, err := client.CoreV1().Pods(cr.Namespace).Get(context.TODO(), podName, metav1.GetOptions{}) if err != nil { - logger.Error(err, "Could not get pod info") + logger.Error(err, "Could not get pod info", "Pod Name", podName, "Namespace", cr.Namespace) + return -1, nil } + logger.Info("Pod info retrieved successfully", "Pod Name", podName, "Namespace", cr.Namespace) + targetContainer := -1 for containerID, tr := range pod.Spec.Containers { - logger.V(1).Info("Pod Counted successfully", "Count", containerID, "Container Name", tr.Name) + logger.V(1).Info("Inspecting container", "Pod Name", podName, "Container ID", containerID, "Container Name", tr.Name) if tr.Name == cr.ObjectMeta.Name+"-leader" { targetContainer = containerID + logger.Info("Leader container found", "Container ID", containerID, "Container Name", tr.Name) break } } + + if targetContainer == -1 { + logger.Info("Leader container not found in pod", "Pod Name", podName) + return -1, nil + } + return targetContainer, pod } @@ -451,14 +459,14 @@ func configureRedisReplicationClient(client kubernetes.Interface, logger logr.Lo logger.Error(err, "Error in getting redis password") } redisClient = redis.NewClient(&redis.Options{ - Addr: getRedisServerIP(redisInfo) + ":6379", + Addr: getRedisServerIP(client, logger, redisInfo) + ":6379", Password: pass, DB: 0, TLSConfig: getRedisReplicationTLSConfig(client, logger, cr, redisInfo), }) } else { redisClient = redis.NewClient(&redis.Options{ - Addr: getRedisServerIP(redisInfo) + ":6379", + Addr: getRedisServerIP(client, logger, redisInfo) + ":6379", Password: "", DB: 0, TLSConfig: getRedisReplicationTLSConfig(client, logger, cr, redisInfo), @@ -556,7 +564,7 @@ func CreateMasterSlaveReplication(ctx context.Context, client kubernetes.Interfa Namespace: cr.Namespace, } - realMasterPodIP := getRedisServerIP(realMasterInfo) + realMasterPodIP := getRedisServerIP(client, logger, realMasterInfo) for i := 0; i < len(masterPods); i++ { if masterPods[i] != realMasterPod { diff --git a/k8sutils/redis_test.go b/k8sutils/redis_test.go index 3028796dd..26628290d 100644 --- a/k8sutils/redis_test.go +++ b/k8sutils/redis_test.go @@ -1,4 +1,3 @@ -// checkRedisNodePresence package k8sutils import ( @@ -8,6 +7,13 @@ import ( "testing" redisv1beta2 "github.com/OT-CONTAINER-KIT/redis-operator/api/v1beta2" + mock_utils "github.com/OT-CONTAINER-KIT/redis-operator/mocks/utils" + "github.com/go-logr/logr/testr" + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + k8sClientFake "k8s.io/client-go/kubernetes/fake" + "k8s.io/utils/pointer" ) func TestCheckRedisNodePresence(t *testing.T) { @@ -38,3 +44,402 @@ func TestCheckRedisNodePresence(t *testing.T) { }) } } + +func TestGetRedisServerIP(t *testing.T) { + tests := []struct { + name string + setup func() *k8sClientFake.Clientset + redisInfo RedisDetails + expectedIP string + expectEmpty bool + }{ + { + name: "Successfully retrieve IPv4 address", + setup: func() *k8sClientFake.Clientset { + return k8sClientFake.NewSimpleClientset(&corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "redis-pod", + Namespace: "default", + }, + Status: corev1.PodStatus{ + PodIP: "192.168.1.1", + }, + }) + }, + redisInfo: RedisDetails{ + PodName: "redis-pod", + Namespace: "default", + }, + expectedIP: "192.168.1.1", + expectEmpty: false, + }, + { + name: "Successfully retrieve IPv6 address", + setup: func() *k8sClientFake.Clientset { + return k8sClientFake.NewSimpleClientset(&corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "redis-pod", + Namespace: "default", + }, + Status: corev1.PodStatus{ + PodIP: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", + }, + }) + }, + redisInfo: RedisDetails{ + PodName: "redis-pod", + Namespace: "default", + }, + expectedIP: "[2001:0db8:85a3:0000:0000:8a2e:0370:7334]", + expectEmpty: false, + }, + { + name: "Error retrieving pod results in empty IP", + setup: func() *k8sClientFake.Clientset { + client := k8sClientFake.NewSimpleClientset() + return client + }, + redisInfo: RedisDetails{ + PodName: "nonexistent-pod", + Namespace: "default", + }, + expectEmpty: true, + }, + { + name: "Empty results in empty IP", + setup: func() *k8sClientFake.Clientset { + return k8sClientFake.NewSimpleClientset(&corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "redis-pod", + Namespace: "default", + }, + Status: corev1.PodStatus{ + PodIP: "", + }, + }) + }, + redisInfo: RedisDetails{ + PodName: "redis-pod", + Namespace: "default", + }, + expectEmpty: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + client := tt.setup() + logger := testr.New(t) + redisIP := getRedisServerIP(client, logger, tt.redisInfo) + + if tt.expectEmpty { + assert.Empty(t, redisIP, "Expected an empty IP address") + } else { + assert.Equal(t, tt.expectedIP, redisIP, "Expected and actual IP do not match") + } + }) + } +} + +func TestGetRedisHostname(t *testing.T) { + tests := []struct { + name string + redisInfo RedisDetails + redisCluster *redisv1beta2.RedisCluster + role string + expected string + }{ + { + name: "standard configuration", + redisInfo: RedisDetails{ + PodName: "redis-pod", + Namespace: "default", + }, + redisCluster: &redisv1beta2.RedisCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "mycluster", + Namespace: "default", + }, + }, + role: "master", + expected: "redis-pod.mycluster-master-headless.default.svc", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fqdn := getRedisHostname(tt.redisInfo, tt.redisCluster, tt.role) + assert.Equal(t, tt.expected, fqdn, "FQDN should match the expected output") + }) + } +} + +func TestCreateSingleLeaderRedisCommand(t *testing.T) { + logger := testr.New(t) + cr := &redisv1beta2.RedisCluster{} + cmd := CreateSingleLeaderRedisCommand(logger, cr) + + assert.Equal(t, "redis-cli", cmd[0]) + assert.Equal(t, "CLUSTER", cmd[1]) + assert.Equal(t, "ADDSLOTS", cmd[2]) + + expectedLength := 16384 + 3 + + assert.Equal(t, expectedLength, len(cmd)) + assert.Equal(t, "0", cmd[3]) + assert.Equal(t, "16383", cmd[expectedLength-1]) +} + +func TestCreateMultipleLeaderRedisCommand(t *testing.T) { + tests := []struct { + name string + redisCluster *redisv1beta2.RedisCluster + expectedCommands []string + }{ + { + name: "Multiple leaders cluster version v7", + redisCluster: &redisv1beta2.RedisCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "mycluster", + Namespace: "default", + }, + Spec: redisv1beta2.RedisClusterSpec{ + Size: pointer.Int32(3), + ClusterVersion: pointer.String("v7"), + }, + }, + expectedCommands: []string{ + "redis-cli", "--cluster", "create", + "mycluster-leader-0.mycluster-leader-headless.default.svc:6379", + "mycluster-leader-1.mycluster-leader-headless.default.svc:6379", + "mycluster-leader-2.mycluster-leader-headless.default.svc:6379", + "--cluster-yes", + }, + }, + { + name: "Multiple leaders cluster without version v7", + redisCluster: &redisv1beta2.RedisCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "mycluster", + Namespace: "default", + }, + Spec: redisv1beta2.RedisClusterSpec{ + Size: pointer.Int32(3), + }, + }, + expectedCommands: []string{ + "redis-cli", "--cluster", "create", + "192.168.1.1:6379", + "192.168.1.2:6379", + "192.168.1.3:6379", + "--cluster-yes", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + client := mock_utils.CreateFakeClientWithPodIPs_LeaderPods(tt.redisCluster) + logger := testr.New(t) + + cmd := CreateMultipleLeaderRedisCommand(client, logger, tt.redisCluster) + assert.Equal(t, tt.expectedCommands, cmd) + }) + } +} + +func TestGetRedisTLSArgs(t *testing.T) { + tests := []struct { + name string + tlsConfig *redisv1beta2.TLSConfig + clientHost string + expected []string + }{ + { + name: "with TLS configuration", + tlsConfig: &redisv1beta2.TLSConfig{}, + clientHost: "redis-host", + expected: []string{"--tls", "--cacert", "/tls/ca.crt", "-h", "redis-host"}, + }, + { + name: "without TLS configuration", + tlsConfig: nil, + clientHost: "redis-host", + expected: []string{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := getRedisTLSArgs(tt.tlsConfig, tt.clientHost) + assert.Equal(t, tt.expected, cmd, "Expected command arguments do not match") + }) + } +} + +func TestCreateRedisReplicationCommand(t *testing.T) { + tests := []struct { + name string + redisCluster *redisv1beta2.RedisCluster + leaderPod RedisDetails + followerPod RedisDetails + expectedCommand []string + }{ + { + name: "Test case with cluster version v7", + redisCluster: &redisv1beta2.RedisCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "redis-cluster", + Namespace: "default", + }, + Spec: redisv1beta2.RedisClusterSpec{ + Size: pointer.Int32(3), + ClusterVersion: pointer.String("v7"), + }, + }, + leaderPod: RedisDetails{ + PodName: "redis-cluster-leader-0", + Namespace: "default", + }, + followerPod: RedisDetails{ + PodName: "redis-cluster-follower-0", + Namespace: "default", + }, + expectedCommand: []string{ + "redis-cli", "--cluster", "add-node", + "redis-cluster-follower-0.redis-cluster-follower-headless.default.svc:6379", + "redis-cluster-leader-0.redis-cluster-leader-headless.default.svc:6379", + "--cluster-slave", + }, + }, + { + name: "Test case without cluster version v7", + redisCluster: &redisv1beta2.RedisCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "redis-cluster", + Namespace: "default", + }, + Spec: redisv1beta2.RedisClusterSpec{ + Size: pointer.Int32(3), + }, + }, + leaderPod: RedisDetails{ + PodName: "redis-cluster-leader-0", + Namespace: "default", + }, + followerPod: RedisDetails{ + PodName: "redis-cluster-follower-0", + Namespace: "default", + }, + expectedCommand: []string{ + "redis-cli", "--cluster", "add-node", + "192.168.2.1:6379", + "192.168.1.1:6379", + "--cluster-slave", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + client := mock_utils.CreateFakeClientWithPodIPs(tt.redisCluster) + logger := testr.New(t) + + cmd := createRedisReplicationCommand(client, logger, tt.redisCluster, tt.leaderPod, tt.followerPod) + + // Assert the command is as expected using testify + assert.Equal(t, tt.expectedCommand, cmd) + }) + } +} + +func TestGetContainerID(t *testing.T) { + tests := []struct { + name string + setupPod *corev1.Pod + redisCluster *redisv1beta2.RedisCluster + expectedID int + expectError bool + }{ + { + name: "Successful retrieval of leader container", + setupPod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "redis-cluster-leader-0", + Namespace: "default", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "redis-cluster-leader", + }, + { + Name: "another-container", + }, + }, + }, + }, + redisCluster: &redisv1beta2.RedisCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "redis-cluster", + Namespace: "default", + }, + }, + expectedID: 0, + expectError: false, + }, + { + name: "Pod not found", + setupPod: &corev1.Pod{}, + redisCluster: &redisv1beta2.RedisCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "redis-cluster", + Namespace: "default", + }, + }, + expectedID: -1, + expectError: true, + }, + { + name: "Leader container not found in the pod", + setupPod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "redis-cluster-leader-0", + Namespace: "default", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "non-leader-container", + }, + }, + }, + }, + redisCluster: &redisv1beta2.RedisCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "redis-cluster", + Namespace: "default", + }, + }, + expectedID: -1, + expectError: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + client := k8sClientFake.NewSimpleClientset(test.setupPod) + logger := testr.New(t) + id, pod := getContainerID(client, logger, test.redisCluster, test.setupPod.Name) + if test.expectError { + assert.Nil(t, pod, "Expected no pod but got one") + assert.Equal(t, test.expectedID, id, "Expected ID does not match") + } else { + assert.NotNil(t, pod, "Expected a pod but got none") + assert.Equal(t, test.expectedID, id, "Expected ID does not match") + assert.Equal(t, test.setupPod.Name, pod.GetName(), "Pod names do not match") + assert.Equal(t, test.setupPod.Namespace, pod.GetNamespace(), "Pod namespaces do not match") + } + }) + } +} diff --git a/mocks/utils/utils.go b/mocks/utils/utils.go new file mode 100644 index 000000000..6f9c33b99 --- /dev/null +++ b/mocks/utils/utils.go @@ -0,0 +1,107 @@ +package utils + +import ( + "fmt" + "strconv" + + redisv1beta2 "github.com/OT-CONTAINER-KIT/redis-operator/api/v1beta2" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes/fake" +) + +func CreateFakeClientWithPodIPs_LeaderPods(cr *redisv1beta2.RedisCluster) *fake.Clientset { + replicas := cr.Spec.GetReplicaCounts("leader") + pods := make([]runtime.Object, replicas) + + for i := 0; i < int(replicas); i++ { + podName := cr.ObjectMeta.Name + "-leader-" + strconv.Itoa(i) + pods[i] = &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: podName, + Namespace: cr.Namespace, + }, + Status: corev1.PodStatus{ + PodIP: fmt.Sprintf("192.168.1.%d", i+1), + }, + } + } + return fake.NewSimpleClientset(pods...) +} + +func CreateFakeClientWithPodIPs(cr *redisv1beta2.RedisCluster) *fake.Clientset { + leaderReplicas := cr.Spec.GetReplicaCounts("leader") + followerReplicas := cr.Spec.GetReplicaCounts("follower") + pods := make([]runtime.Object, leaderReplicas+followerReplicas) + + for i := 0; i < int(leaderReplicas); i++ { + podName := cr.ObjectMeta.Name + "-leader-" + strconv.Itoa(i) + pods[i] = &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: podName, + Namespace: cr.Namespace, + }, + Status: corev1.PodStatus{ + PodIP: fmt.Sprintf("192.168.1.%d", i+1), + }, + } + } + for i := 0; i < int(followerReplicas); i++ { + podName := cr.ObjectMeta.Name + "-follower-" + strconv.Itoa(i) + pods[i+int(leaderReplicas)] = &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: podName, + Namespace: cr.Namespace, + }, + Status: corev1.PodStatus{ + PodIP: fmt.Sprintf("192.168.2.%d", i+1), + }, + } + } + + return fake.NewSimpleClientset(pods...) +} + +func CreateFakeClientWithSecrets(cr *redisv1beta2.RedisCluster, secretName, secretKey, secretValue string) *fake.Clientset { + leaderReplicas := cr.Spec.GetReplicaCounts("leader") + followerReplicas := cr.Spec.GetReplicaCounts("follower") + pods := make([]runtime.Object, leaderReplicas+followerReplicas) + + for i := 0; i < int(leaderReplicas); i++ { + podName := cr.ObjectMeta.Name + "-leader-" + strconv.Itoa(i) + pods[i] = &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: podName, + Namespace: cr.Namespace, + }, + Status: corev1.PodStatus{ + PodIP: fmt.Sprintf("192.168.1.%d", i+1), + }, + } + } + for i := 0; i < int(followerReplicas); i++ { + podName := cr.ObjectMeta.Name + "-follower-" + strconv.Itoa(i) + pods[i+int(leaderReplicas)] = &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: podName, + Namespace: cr.Namespace, + }, + Status: corev1.PodStatus{ + PodIP: fmt.Sprintf("192.168.2.%d", i+1), + }, + } + } + + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: cr.Namespace, + }, + Data: map[string][]byte{ + secretKey: []byte(secretValue), + }, + } + + return fake.NewSimpleClientset(append(pods, secret)...) +}