github.com/spotahome/redis-operator@v1.2.4/test/integration/redisfailover/creation_test.go (about)

     1  //go:build integration
     2  // +build integration
     3  
     4  package redisfailover_test
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"net"
    10  	"path/filepath"
    11  	"testing"
    12  	"time"
    13  
    14  	rediscli "github.com/go-redis/redis/v8"
    15  
    16  	"github.com/stretchr/testify/assert"
    17  	"github.com/stretchr/testify/require"
    18  	corev1 "k8s.io/api/core/v1"
    19  	v1 "k8s.io/api/core/v1"
    20  	apiextensionsclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
    21  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    22  	"k8s.io/apimachinery/pkg/labels"
    23  	"k8s.io/client-go/kubernetes"
    24  
    25  	_ "k8s.io/client-go/plugin/pkg/client/auth/oidc"
    26  	"k8s.io/client-go/util/homedir"
    27  
    28  	redisfailoverv1 "github.com/spotahome/redis-operator/api/redisfailover/v1"
    29  	redisfailoverclientset "github.com/spotahome/redis-operator/client/k8s/clientset/versioned"
    30  	"github.com/spotahome/redis-operator/cmd/utils"
    31  	"github.com/spotahome/redis-operator/log"
    32  	"github.com/spotahome/redis-operator/metrics"
    33  	"github.com/spotahome/redis-operator/operator/redisfailover"
    34  	"github.com/spotahome/redis-operator/service/k8s"
    35  	"github.com/spotahome/redis-operator/service/redis"
    36  )
    37  
    38  const (
    39  	name           = "testing"
    40  	namespace      = "rf-integration-tests"
    41  	redisSize      = int32(3)
    42  	sentinelSize   = int32(3)
    43  	authSecretPath = "redis-auth"
    44  	testPass       = "test-pass"
    45  	redisAddr      = "redis://127.0.0.1:6379"
    46  )
    47  
    48  type clients struct {
    49  	k8sClient   kubernetes.Interface
    50  	rfClient    redisfailoverclientset.Interface
    51  	aeClient    apiextensionsclientset.Interface
    52  	redisClient redis.Client
    53  }
    54  
    55  func (c *clients) prepareNS() error {
    56  	ns := &corev1.Namespace{
    57  		ObjectMeta: metav1.ObjectMeta{
    58  			Name: namespace,
    59  		},
    60  	}
    61  	_, err := c.k8sClient.CoreV1().Namespaces().Create(context.Background(), ns, metav1.CreateOptions{})
    62  	return err
    63  }
    64  
    65  func (c *clients) cleanup(stopC chan struct{}) {
    66  	c.k8sClient.CoreV1().Namespaces().Delete(context.Background(), namespace, metav1.DeleteOptions{})
    67  	close(stopC)
    68  }
    69  
    70  func TestRedisFailover(t *testing.T) {
    71  	require := require.New(t)
    72  
    73  	// Create signal channels.
    74  	stopC := make(chan struct{})
    75  	errC := make(chan error)
    76  
    77  	flags := &utils.CMDFlags{
    78  		KubeConfig:  filepath.Join(homedir.HomeDir(), ".kube", "config"),
    79  		Development: true,
    80  	}
    81  
    82  	// Kubernetes clients.
    83  	k8sClient, customClient, aeClientset, err := utils.CreateKubernetesClients(flags)
    84  	require.NoError(err)
    85  
    86  	// Create the redis clients
    87  	redisClient := redis.New(metrics.Dummy)
    88  
    89  	clients := clients{
    90  		k8sClient:   k8sClient,
    91  		rfClient:    customClient,
    92  		aeClient:    aeClientset,
    93  		redisClient: redisClient,
    94  	}
    95  
    96  	// Create kubernetes service.
    97  	k8sservice := k8s.New(k8sClient, customClient, aeClientset, log.Dummy, metrics.Dummy)
    98  
    99  	// Prepare namespace
   100  	prepErr := clients.prepareNS()
   101  	require.NoError(prepErr)
   102  
   103  	// Give time to the namespace to be ready
   104  	time.Sleep(15 * time.Second)
   105  
   106  	// Create operator and run.
   107  	redisfailoverOperator, err := redisfailover.New(redisfailover.Config{}, k8sservice, k8sClient, namespace, redisClient, metrics.Dummy, log.Dummy)
   108  	require.NoError(err)
   109  
   110  	go func() {
   111  		errC <- redisfailoverOperator.Run(context.Background())
   112  	}()
   113  
   114  	// Prepare cleanup for when the test ends
   115  	defer clients.cleanup(stopC)
   116  
   117  	// Give time to the operator to start
   118  	time.Sleep(15 * time.Second)
   119  
   120  	// Create secret
   121  	secret := &v1.Secret{
   122  		ObjectMeta: metav1.ObjectMeta{
   123  			Name:      authSecretPath,
   124  			Namespace: namespace,
   125  		},
   126  		Data: map[string][]byte{
   127  			"password": []byte(testPass),
   128  		},
   129  	}
   130  	_, err = k8sClient.CoreV1().Secrets(namespace).Create(context.Background(), secret, metav1.CreateOptions{})
   131  	require.NoError(err)
   132  
   133  	// Check that if we create a RedisFailover, it is certainly created and we can get it
   134  	ok := t.Run("Check Custom Resource Creation", clients.testCRCreation)
   135  	require.True(ok, "the custom resource has to be created to continue")
   136  
   137  	// Giving time to the operator to create the resources
   138  	time.Sleep(3 * time.Minute)
   139  
   140  	// Verify that auth is set and actually working
   141  	t.Run("Check that auth is set in sentinel and redis configs", clients.testAuth)
   142  
   143  	// Check custom config is set
   144  	t.Run("Check that custom config is behave expected", clients.testCustomConfig)
   145  
   146  	// Check that a Redis Statefulset is created and the size of it is the one defined by the
   147  	// Redis Failover definition created before.
   148  	t.Run("Check Redis Statefulset existing and size", clients.testRedisStatefulSet)
   149  
   150  	// Check that a Sentinel Deployment is created and the size of it is the one defined by the
   151  	// Redis Failover definition created before.
   152  	t.Run("Check Sentinel Deployment existing and size", clients.testSentinelDeployment)
   153  
   154  	// Connect to all the Redis pods and, asking to the Redis running inside them, check
   155  	// that only one of them is the master of the failover.
   156  	t.Run("Check Only One Redis Master", clients.testRedisMaster)
   157  
   158  	// Connect to all the Sentinel pods and, asking to the Sentinel running inside them,
   159  	// check that all of them are connected to the same Redis node, and also that that node
   160  	// is the master.
   161  	t.Run("Check Sentinels Checking the Redis Master", clients.testSentinelMonitoring)
   162  }
   163  
   164  func (c *clients) testCRCreation(t *testing.T) {
   165  	assert := assert.New(t)
   166  	toCreate := &redisfailoverv1.RedisFailover{
   167  		ObjectMeta: metav1.ObjectMeta{
   168  			Name:      name,
   169  			Namespace: namespace,
   170  		},
   171  		Spec: redisfailoverv1.RedisFailoverSpec{
   172  			Redis: redisfailoverv1.RedisSettings{
   173  				Replicas: redisSize,
   174  				Exporter: redisfailoverv1.Exporter{
   175  					Enabled: true,
   176  				},
   177  				CustomConfig: []string{`save ""`},
   178  			},
   179  			Sentinel: redisfailoverv1.SentinelSettings{
   180  				Replicas: sentinelSize,
   181  			},
   182  			Auth: redisfailoverv1.AuthSettings{
   183  				SecretPath: authSecretPath,
   184  			},
   185  		},
   186  	}
   187  
   188  	c.rfClient.DatabasesV1().RedisFailovers(namespace).Create(context.Background(), toCreate, metav1.CreateOptions{})
   189  	gotRF, err := c.rfClient.DatabasesV1().RedisFailovers(namespace).Get(context.Background(), name, metav1.GetOptions{})
   190  
   191  	assert.NoError(err)
   192  	assert.Equal(toCreate.Spec, gotRF.Spec)
   193  }
   194  
   195  func (c *clients) testRedisStatefulSet(t *testing.T) {
   196  	assert := assert.New(t)
   197  	redisSS, err := c.k8sClient.AppsV1().StatefulSets(namespace).Get(context.Background(), fmt.Sprintf("rfr-%s", name), metav1.GetOptions{})
   198  	assert.NoError(err)
   199  	assert.Equal(redisSize, int32(redisSS.Status.Replicas))
   200  }
   201  
   202  func (c *clients) testSentinelDeployment(t *testing.T) {
   203  	assert := assert.New(t)
   204  	sentinelD, err := c.k8sClient.AppsV1().Deployments(namespace).Get(context.Background(), fmt.Sprintf("rfs-%s", name), metav1.GetOptions{})
   205  	assert.NoError(err)
   206  	assert.Equal(3, int(sentinelD.Status.Replicas))
   207  }
   208  
   209  func (c *clients) testRedisMaster(t *testing.T) {
   210  	assert := assert.New(t)
   211  	masters := []string{}
   212  
   213  	redisSS, err := c.k8sClient.AppsV1().StatefulSets(namespace).Get(context.Background(), fmt.Sprintf("rfr-%s", name), metav1.GetOptions{})
   214  	assert.NoError(err)
   215  
   216  	listOptions := metav1.ListOptions{
   217  		LabelSelector: labels.FormatLabels(redisSS.Spec.Selector.MatchLabels),
   218  	}
   219  
   220  	redisPodList, err := c.k8sClient.CoreV1().Pods(namespace).List(context.Background(), listOptions)
   221  
   222  	assert.NoError(err)
   223  
   224  	for _, pod := range redisPodList.Items {
   225  		ip := pod.Status.PodIP
   226  		if ok, _ := c.redisClient.IsMaster(ip, "6379", testPass); ok {
   227  			masters = append(masters, ip)
   228  		}
   229  	}
   230  
   231  	assert.Equal(1, len(masters), "only one master expected")
   232  }
   233  
   234  func (c *clients) testSentinelMonitoring(t *testing.T) {
   235  	assert := assert.New(t)
   236  	masters := []string{}
   237  
   238  	sentinelD, err := c.k8sClient.AppsV1().Deployments(namespace).Get(context.Background(), fmt.Sprintf("rfs-%s", name), metav1.GetOptions{})
   239  	assert.NoError(err)
   240  
   241  	listOptions := metav1.ListOptions{
   242  		LabelSelector: labels.FormatLabels(sentinelD.Spec.Selector.MatchLabels),
   243  	}
   244  	sentinelPodList, err := c.k8sClient.CoreV1().Pods(namespace).List(context.Background(), listOptions)
   245  	assert.NoError(err)
   246  
   247  	for _, pod := range sentinelPodList.Items {
   248  		ip := pod.Status.PodIP
   249  		master, _, _ := c.redisClient.GetSentinelMonitor(ip)
   250  		masters = append(masters, master)
   251  	}
   252  
   253  	for _, masterIP := range masters {
   254  		assert.Equal(masters[0], masterIP, "all master ip monitoring should equal")
   255  	}
   256  
   257  	isMaster, err := c.redisClient.IsMaster(masters[0], "6379", testPass)
   258  	assert.NoError(err)
   259  	assert.True(isMaster, "Sentinel should monitor the Redis master")
   260  }
   261  
   262  func (c *clients) testAuth(t *testing.T) {
   263  	assert := assert.New(t)
   264  
   265  	redisCfg, err := c.k8sClient.CoreV1().ConfigMaps(namespace).Get(context.Background(), fmt.Sprintf("rfr-%s", name), metav1.GetOptions{})
   266  	assert.NoError(err)
   267  	assert.Contains(redisCfg.Data["redis.conf"], "requirepass "+testPass)
   268  	assert.Contains(redisCfg.Data["redis.conf"], "masterauth "+testPass)
   269  
   270  	redisSS, err := c.k8sClient.AppsV1().StatefulSets(namespace).Get(context.Background(), fmt.Sprintf("rfr-%s", name), metav1.GetOptions{})
   271  	assert.NoError(err)
   272  
   273  	assert.Len(redisSS.Spec.Template.Spec.Containers, 2)
   274  	assert.Equal(redisSS.Spec.Template.Spec.Containers[1].Env[1].Name, "REDIS_ADDR")
   275  	assert.Equal(redisSS.Spec.Template.Spec.Containers[1].Env[1].Value, redisAddr)
   276  	assert.Equal(redisSS.Spec.Template.Spec.Containers[1].Env[2].Name, "REDIS_PORT")
   277  	assert.Equal(redisSS.Spec.Template.Spec.Containers[1].Env[2].Value, "6379")
   278  	assert.Equal(redisSS.Spec.Template.Spec.Containers[1].Env[3].Name, "REDIS_USER")
   279  	assert.Equal(redisSS.Spec.Template.Spec.Containers[1].Env[3].Value, "default")
   280  	assert.Equal(redisSS.Spec.Template.Spec.Containers[1].Env[4].Name, "REDIS_PASSWORD")
   281  	assert.Equal(redisSS.Spec.Template.Spec.Containers[1].Env[4].ValueFrom.SecretKeyRef.Key, "password")
   282  	assert.Equal(redisSS.Spec.Template.Spec.Containers[1].Env[4].ValueFrom.SecretKeyRef.LocalObjectReference.Name, authSecretPath)
   283  }
   284  
   285  func (c *clients) testCustomConfig(t *testing.T) {
   286  	assert := assert.New(t)
   287  
   288  	redisSS, err := c.k8sClient.AppsV1().StatefulSets(namespace).Get(context.Background(), fmt.Sprintf("rfr-%s", name), metav1.GetOptions{})
   289  	assert.NoError(err)
   290  
   291  	listOptions := metav1.ListOptions{
   292  		LabelSelector: labels.FormatLabels(redisSS.Spec.Selector.MatchLabels),
   293  	}
   294  	redisPodList, err := c.k8sClient.CoreV1().Pods(namespace).List(context.Background(), listOptions)
   295  	assert.NoError(err)
   296  
   297  	rClient := rediscli.NewClient(&rediscli.Options{
   298  		Addr:     net.JoinHostPort(redisPodList.Items[0].Status.PodIP, "6379"),
   299  		Password: testPass,
   300  		DB:       0,
   301  	})
   302  	defer rClient.Close()
   303  
   304  	result := rClient.ConfigGet(context.TODO(), "save")
   305  	assert.NoError(result.Err())
   306  
   307  	values, err := result.Result()
   308  	assert.NoError(err)
   309  
   310  	assert.Len(values, 2)
   311  	assert.Empty(values[1])
   312  }