github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/controllers/apps/systemaccount_util_test.go (about)

     1  /*
     2  Copyright (C) 2022-2023 ApeCloud Co., Ltd
     3  
     4  This file is part of KubeBlocks project
     5  
     6  This program is free software: you can redistribute it and/or modify
     7  it under the terms of the GNU Affero General Public License as published by
     8  the Free Software Foundation, either version 3 of the License, or
     9  (at your option) any later version.
    10  
    11  This program is distributed in the hope that it will be useful
    12  but WITHOUT ANY WARRANTY; without even the implied warranty of
    13  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    14  GNU Affero General Public License for more details.
    15  
    16  You should have received a copy of the GNU Affero General Public License
    17  along with this program.  If not, see <http://www.gnu.org/licenses/>.
    18  */
    19  
    20  package apps
    21  
    22  import (
    23  	"math/rand"
    24  	"reflect"
    25  	"strings"
    26  	"testing"
    27  
    28  	"github.com/stretchr/testify/assert"
    29  	corev1 "k8s.io/api/core/v1"
    30  
    31  	appsv1alpha1 "github.com/1aal/kubeblocks/apis/apps/v1alpha1"
    32  	"github.com/1aal/kubeblocks/pkg/constant"
    33  	testapps "github.com/1aal/kubeblocks/pkg/testutil/apps"
    34  	viper "github.com/1aal/kubeblocks/pkg/viperx"
    35  )
    36  
    37  func mockSystemAccountsSpec() *appsv1alpha1.SystemAccountSpec {
    38  	var (
    39  		mysqlClientImage = "docker.io/mysql:8.0.30"
    40  		mysqlCmdConfig   = appsv1alpha1.CmdExecutorConfig{
    41  			CommandExecutorEnvItem: appsv1alpha1.CommandExecutorEnvItem{
    42  				Image: mysqlClientImage,
    43  			},
    44  			CommandExecutorItem: appsv1alpha1.CommandExecutorItem{
    45  				Command: []string{"mysql"},
    46  				Args:    []string{"-h$(KB_ACCOUNT_ENDPOINT)", "-e $(KB_ACCOUNT_STATEMENT)"},
    47  			},
    48  		}
    49  		pwdConfig = appsv1alpha1.PasswordConfig{
    50  			Length:     10,
    51  			NumDigits:  5,
    52  			NumSymbols: 0,
    53  		}
    54  	)
    55  
    56  	spec := &appsv1alpha1.SystemAccountSpec{
    57  		CmdExecutorConfig: &mysqlCmdConfig,
    58  		PasswordConfig:    pwdConfig,
    59  		Accounts:          []appsv1alpha1.SystemAccountConfig{},
    60  	}
    61  
    62  	var account appsv1alpha1.SystemAccountConfig
    63  	var scope appsv1alpha1.ProvisionScope
    64  	for idx, name := range getAllSysAccounts() {
    65  		if idx%2 == 0 {
    66  			scope = appsv1alpha1.AnyPods
    67  		} else {
    68  			scope = appsv1alpha1.AllPods
    69  		}
    70  		if idx%3 == 0 {
    71  			account = mockCreateByRefSystemAccount(name, scope)
    72  		} else {
    73  			account = mockCreateByStmtSystemAccount(name)
    74  		}
    75  		spec.Accounts = append(spec.Accounts, account)
    76  	}
    77  	return spec
    78  }
    79  
    80  func mockCreateByStmtSystemAccount(name appsv1alpha1.AccountName) appsv1alpha1.SystemAccountConfig {
    81  	return appsv1alpha1.SystemAccountConfig{
    82  		Name: name,
    83  		ProvisionPolicy: appsv1alpha1.ProvisionPolicy{
    84  			Type: appsv1alpha1.CreateByStmt,
    85  			Statements: &appsv1alpha1.ProvisionStatements{
    86  				CreationStatement: "CREATE USER IF NOT EXISTS $(USERNAME) IDENTIFIED BY \"$(PASSWD)\";",
    87  				UpdateStatement:   "ALTER USER $(USERNAME) IDENTIFIED BY \"$(PASSWD)\";",
    88  				DeletionStatement: "DROP USER IF EXISTS $(USERNAME);",
    89  			},
    90  		},
    91  	}
    92  }
    93  
    94  func mockCreateByRefSystemAccount(name appsv1alpha1.AccountName, scope appsv1alpha1.ProvisionScope) appsv1alpha1.SystemAccountConfig {
    95  	return appsv1alpha1.SystemAccountConfig{
    96  		Name: name,
    97  		ProvisionPolicy: appsv1alpha1.ProvisionPolicy{
    98  			Type:  appsv1alpha1.ReferToExisting,
    99  			Scope: scope,
   100  			SecretRef: &appsv1alpha1.ProvisionSecretRef{
   101  				Namespace: testCtx.DefaultNamespace,
   102  				Name:      "$(CONN_CREDENTIAL_SECRET_NAME)",
   103  			},
   104  		},
   105  	}
   106  }
   107  
   108  func TestUpdateFacts(t *testing.T) {
   109  	type testCase struct {
   110  		// accounts
   111  		accounts []appsv1alpha1.AccountName
   112  		// expectation
   113  		expect appsv1alpha1.KBAccountType
   114  	}
   115  	testCases := []testCase{
   116  		{
   117  			accounts: []appsv1alpha1.AccountName{appsv1alpha1.AdminAccount},
   118  			expect:   appsv1alpha1.KBAccountAdmin,
   119  		},
   120  		{
   121  			accounts: []appsv1alpha1.AccountName{appsv1alpha1.AdminAccount, appsv1alpha1.DataprotectionAccount},
   122  			expect:   appsv1alpha1.KBAccountAdmin | appsv1alpha1.KBAccountDataprotection,
   123  		},
   124  		{
   125  			accounts: []appsv1alpha1.AccountName{appsv1alpha1.AdminAccount, appsv1alpha1.DataprotectionAccount, appsv1alpha1.ProbeAccount},
   126  			expect:   appsv1alpha1.KBAccountAdmin | appsv1alpha1.KBAccountDataprotection | appsv1alpha1.KBAccountProbe,
   127  		},
   128  		{
   129  			accounts: []appsv1alpha1.AccountName{appsv1alpha1.AdminAccount, appsv1alpha1.DataprotectionAccount, appsv1alpha1.ProbeAccount, appsv1alpha1.MonitorAccount},
   130  			expect:   appsv1alpha1.KBAccountAdmin | appsv1alpha1.KBAccountDataprotection | appsv1alpha1.KBAccountProbe | appsv1alpha1.KBAccountMonitor,
   131  		},
   132  		{
   133  			accounts: []appsv1alpha1.AccountName{appsv1alpha1.AdminAccount, appsv1alpha1.DataprotectionAccount, appsv1alpha1.ProbeAccount, appsv1alpha1.MonitorAccount, appsv1alpha1.ReplicatorAccount},
   134  			expect:   appsv1alpha1.KBAccountAdmin | appsv1alpha1.KBAccountDataprotection | appsv1alpha1.KBAccountProbe | appsv1alpha1.KBAccountMonitor | appsv1alpha1.KBAccountReplicator,
   135  		},
   136  	}
   137  
   138  	var facts appsv1alpha1.KBAccountType
   139  	for _, test := range testCases {
   140  		facts = 0
   141  		for _, acc := range test.accounts {
   142  			updateFacts(acc, &facts)
   143  		}
   144  		assert.Equal(t, test.expect, facts)
   145  	}
   146  }
   147  
   148  func TestRenderJob(t *testing.T) {
   149  	var (
   150  		clusterDefName     = "test-clusterdef"
   151  		clusterVersionName = "test-clusterversion"
   152  		clusterNamePrefix  = "test-cluster"
   153  		mysqlCompDefName   = "replicasets"
   154  		mysqlCompName      = "mysql"
   155  	)
   156  
   157  	systemAccount := mockSystemAccountsSpec()
   158  	clusterDef := testapps.NewClusterDefFactory(clusterDefName).
   159  		AddComponentDef(testapps.StatefulMySQLComponent, mysqlCompDefName).
   160  		AddSystemAccountSpec(systemAccount).
   161  		GetObject()
   162  	assert.NotNil(t, clusterDef)
   163  	assert.NotNil(t, clusterDef.Spec.ComponentDefs[0].SystemAccounts)
   164  
   165  	cluster := testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterNamePrefix, clusterDef.Name, clusterVersionName).
   166  		AddComponent(mysqlCompDefName, mysqlCompName).GetObject()
   167  	assert.NotNil(t, cluster)
   168  	if cluster.Annotations == nil {
   169  		cluster.Annotations = make(map[string]string, 0)
   170  	}
   171  
   172  	accountsSetting := clusterDef.Spec.ComponentDefs[0].SystemAccounts
   173  	replaceEnvsValues(cluster.Name, accountsSetting)
   174  	cmdExecutorConfig := accountsSetting.CmdExecutorConfig
   175  
   176  	engine := newCustomizedEngine(cmdExecutorConfig, cluster, mysqlCompName)
   177  	assert.NotNil(t, engine)
   178  
   179  	compKey := componentUniqueKey{
   180  		namespace:     cluster.Namespace,
   181  		clusterName:   cluster.Name,
   182  		componentName: mysqlCompName,
   183  	}
   184  
   185  	generateToleration := func() corev1.Toleration {
   186  		operators := []corev1.TolerationOperator{corev1.TolerationOpEqual, corev1.TolerationOpExists}
   187  		effects := []corev1.TaintEffect{corev1.TaintEffectNoSchedule, corev1.TaintEffectPreferNoSchedule, corev1.TaintEffectNoExecute}
   188  
   189  		toleration := corev1.Toleration{
   190  			Key:   testCtx.GetRandomStr(),
   191  			Value: testCtx.GetRandomStr(),
   192  		}
   193  		toss := rand.Intn(10)
   194  		toleration.Operator = operators[toss%len(operators)]
   195  		toleration.Effect = effects[toss%len(effects)]
   196  		return toleration
   197  	}
   198  
   199  	for _, acc := range accountsSetting.Accounts {
   200  		switch acc.ProvisionPolicy.Type {
   201  		case appsv1alpha1.CreateByStmt:
   202  			creationStmt, secrets := getCreationStmtForAccount(compKey, accountsSetting.PasswordConfig, acc, reCreate)
   203  			// make sure all variables have been replaced
   204  			for _, stmt := range creationStmt {
   205  				assert.False(t, strings.Contains(stmt, "$(USERNAME)"))
   206  				assert.False(t, strings.Contains(stmt, "$(PASSWD)"))
   207  			}
   208  			// render job with debug mode off
   209  			endpoint := "10.0.0.1"
   210  			mockJobName := "mock-job" + testCtx.GetRandomStr()
   211  			job := renderJob(mockJobName, engine, compKey, creationStmt, endpoint)
   212  			assert.NotNil(t, job)
   213  			_ = calibrateJobMetaAndSpec(job, cluster, compKey, acc.Name)
   214  			assert.NotNil(t, job.Spec.TTLSecondsAfterFinished)
   215  			assert.Equal(t, (int32)(1), *job.Spec.TTLSecondsAfterFinished)
   216  			envList := job.Spec.Template.Spec.Containers[0].Env
   217  			assert.GreaterOrEqual(t, len(envList), 1)
   218  			assert.Equal(t, job.Spec.Template.Spec.Containers[0].Image, cmdExecutorConfig.Image)
   219  			// render job with debug mode on
   220  			job = renderJob(mockJobName, engine, compKey, creationStmt, endpoint)
   221  			assert.NotNil(t, job)
   222  			// set debug mode on
   223  			cluster.Annotations[debugClusterAnnotationKey] = "True"
   224  			_ = calibrateJobMetaAndSpec(job, cluster, compKey, acc.Name)
   225  			assert.Nil(t, job.Spec.TTLSecondsAfterFinished)
   226  			assert.NotNil(t, secrets)
   227  			// set debug mode off
   228  			cluster.Annotations[debugClusterAnnotationKey] = "False"
   229  			// add toleration to cluster
   230  			toleration := make([]corev1.Toleration, 0)
   231  			toleration = append(toleration, generateToleration())
   232  			cluster.Spec.Tolerations = toleration
   233  			job = renderJob(mockJobName, engine, compKey, creationStmt, endpoint)
   234  			assert.NotNil(t, job)
   235  			_ = calibrateJobMetaAndSpec(job, cluster, compKey, acc.Name)
   236  			jobToleration := job.Spec.Template.Spec.Tolerations
   237  			assert.Equal(t, 2, len(jobToleration))
   238  			// make sure the toleration is added to job and contains our built-in toleration
   239  			tolerationKeys := make([]string, 0)
   240  			for _, t := range jobToleration {
   241  				tolerationKeys = append(tolerationKeys, t.Key)
   242  			}
   243  			assert.Contains(t, tolerationKeys, testDataPlaneTolerationKey)
   244  			assert.Contains(t, tolerationKeys, toleration[0].Key)
   245  		case appsv1alpha1.ReferToExisting:
   246  			assert.False(t, strings.Contains(acc.ProvisionPolicy.SecretRef.Name, constant.KBConnCredentialPlaceHolder))
   247  		}
   248  	}
   249  }
   250  
   251  func TestAccountNum(t *testing.T) {
   252  	totalAccounts := getAllSysAccounts()
   253  	accountNum := len(totalAccounts)
   254  	assert.Greater(t, accountNum, 0)
   255  	expectedMaxKBAccountType := 1 << (accountNum - 1)
   256  	assert.Equal(t, expectedMaxKBAccountType, appsv1alpha1.KBAccountMAX)
   257  }
   258  
   259  func TestAccountDebugMode(t *testing.T) {
   260  	type testCase struct {
   261  		viperEnvOn       bool
   262  		annotatedStrings []string
   263  		expectedR        bool
   264  	}
   265  
   266  	trueStrings := []string{"1", "t", "T", "TRUE", "true", "True"}            // should be parsed to true
   267  	falseStrings := []string{"0", "f", "F", "FALSE", "false", "False"}        // should be parsed to false
   268  	randomString := []string{"", "badCase", "invalidSettings", "TTT", "test"} // should be parsed to false
   269  
   270  	testCases := []testCase{
   271  		{
   272  			viperEnvOn:       false,
   273  			annotatedStrings: falseStrings,
   274  			expectedR:        false,
   275  		},
   276  		{
   277  			viperEnvOn:       false,
   278  			annotatedStrings: trueStrings,
   279  			expectedR:        true,
   280  		},
   281  		{
   282  			viperEnvOn:       true,
   283  			annotatedStrings: falseStrings,
   284  			expectedR:        true,
   285  		},
   286  		{
   287  			viperEnvOn:       true,
   288  			annotatedStrings: trueStrings,
   289  			expectedR:        true,
   290  		},
   291  		{
   292  			viperEnvOn:       false,
   293  			annotatedStrings: randomString,
   294  			expectedR:        false,
   295  		},
   296  	}
   297  
   298  	for _, test := range testCases {
   299  		if test.viperEnvOn {
   300  			viper.Set(systemAccountsDebugMode, true)
   301  		} else {
   302  			viper.Set(systemAccountsDebugMode, false)
   303  		}
   304  
   305  		for _, annotation := range test.annotatedStrings {
   306  			debugOn := getDebugMode(annotation)
   307  			assert.Equal(t, test.expectedR, debugOn)
   308  		}
   309  	}
   310  }
   311  
   312  func TestRenderCreationStmt(t *testing.T) {
   313  	var (
   314  		clusterDefName   = "test-clusterdef"
   315  		clusterName      = "test-cluster"
   316  		mysqlCompDefName = "replicasets"
   317  		mysqlCompName    = "mysql"
   318  	)
   319  
   320  	systemAccount := mockSystemAccountsSpec()
   321  	clusterDef := testapps.NewClusterDefFactory(clusterDefName).
   322  		AddComponentDef(testapps.StatefulMySQLComponent, mysqlCompDefName).
   323  		AddSystemAccountSpec(systemAccount).
   324  		GetObject()
   325  	assert.NotNil(t, clusterDef)
   326  
   327  	compDef := clusterDef.GetComponentDefByName(mysqlCompDefName)
   328  	assert.NotNil(t, compDef.SystemAccounts)
   329  
   330  	accountsSetting := compDef.SystemAccounts
   331  	replaceEnvsValues(clusterName, accountsSetting)
   332  
   333  	compKey := componentUniqueKey{
   334  		namespace:     testCtx.DefaultNamespace,
   335  		clusterName:   clusterName,
   336  		componentName: mysqlCompName,
   337  	}
   338  
   339  	for _, account := range accountsSetting.Accounts {
   340  		// for each account, we randomly remove deletion stmt
   341  		if account.ProvisionPolicy.Type == appsv1alpha1.CreateByStmt {
   342  			toss := rand.Intn(10) % 2
   343  			if toss == 1 {
   344  				// mock optional deletion statement
   345  				account.ProvisionPolicy.Statements.DeletionStatement = ""
   346  			}
   347  
   348  			stmts, secret := getCreationStmtForAccount(compKey, compDef.SystemAccounts.PasswordConfig, account, reCreate)
   349  			if toss == 1 {
   350  				assert.Equal(t, 1, len(stmts))
   351  			} else {
   352  				assert.Equal(t, 2, len(stmts))
   353  			}
   354  			assert.NotNil(t, secret)
   355  
   356  			stmts, secret = getCreationStmtForAccount(compKey, compDef.SystemAccounts.PasswordConfig, account, inPlaceUpdate)
   357  			assert.Equal(t, 1, len(stmts))
   358  			assert.NotNil(t, secret)
   359  		}
   360  	}
   361  }
   362  
   363  func TestMergeSystemAccountConfig(t *testing.T) {
   364  	systemAccount := mockSystemAccountsSpec()
   365  	// Make sure env is not empty
   366  	if systemAccount.CmdExecutorConfig.Env == nil {
   367  		systemAccount.CmdExecutorConfig.Env = []corev1.EnvVar{}
   368  	}
   369  
   370  	if len(systemAccount.CmdExecutorConfig.Env) == 0 {
   371  		systemAccount.CmdExecutorConfig.Env = append(systemAccount.CmdExecutorConfig.Env, corev1.EnvVar{
   372  			Name:  "cluster-def-env",
   373  			Value: "cluster-def-env-value",
   374  		})
   375  	}
   376  	// nil spec
   377  	componentVersion := &appsv1alpha1.ClusterComponentVersion{
   378  		SystemAccountSpec: nil,
   379  	}
   380  	accountConfig := systemAccount.CmdExecutorConfig.DeepCopy()
   381  	completeExecConfig(accountConfig, componentVersion)
   382  	assert.Equal(t, systemAccount.CmdExecutorConfig.Image, accountConfig.Image)
   383  	assert.Len(t, accountConfig.Env, len(systemAccount.CmdExecutorConfig.Env))
   384  	if len(systemAccount.CmdExecutorConfig.Env) > 0 {
   385  		assert.True(t, reflect.DeepEqual(accountConfig.Env, systemAccount.CmdExecutorConfig.Env))
   386  	}
   387  
   388  	// empty spec
   389  	accountConfig = systemAccount.CmdExecutorConfig.DeepCopy()
   390  	componentVersion.SystemAccountSpec = &appsv1alpha1.SystemAccountShortSpec{
   391  		CmdExecutorConfig: &appsv1alpha1.CommandExecutorEnvItem{},
   392  	}
   393  
   394  	completeExecConfig(accountConfig, componentVersion)
   395  	assert.Equal(t, systemAccount.CmdExecutorConfig.Image, accountConfig.Image)
   396  	assert.Len(t, accountConfig.Env, len(systemAccount.CmdExecutorConfig.Env))
   397  	if len(systemAccount.CmdExecutorConfig.Env) > 0 {
   398  		assert.True(t, reflect.DeepEqual(accountConfig.Env, systemAccount.CmdExecutorConfig.Env))
   399  	}
   400  
   401  	// spec with image
   402  	mockImageName := "test-image"
   403  	accountConfig = systemAccount.CmdExecutorConfig.DeepCopy()
   404  	componentVersion.SystemAccountSpec = &appsv1alpha1.SystemAccountShortSpec{
   405  		CmdExecutorConfig: &appsv1alpha1.CommandExecutorEnvItem{
   406  			Image: mockImageName,
   407  			Env:   nil,
   408  		},
   409  	}
   410  	completeExecConfig(accountConfig, componentVersion)
   411  	assert.NotEqual(t, systemAccount.CmdExecutorConfig.Image, accountConfig.Image)
   412  	assert.Equal(t, mockImageName, accountConfig.Image)
   413  	assert.Len(t, accountConfig.Env, len(systemAccount.CmdExecutorConfig.Env))
   414  	if len(systemAccount.CmdExecutorConfig.Env) > 0 {
   415  		assert.True(t, reflect.DeepEqual(accountConfig.Env, systemAccount.CmdExecutorConfig.Env))
   416  	}
   417  	// spec with empty envs
   418  	accountConfig = systemAccount.CmdExecutorConfig.DeepCopy()
   419  	componentVersion.SystemAccountSpec = &appsv1alpha1.SystemAccountShortSpec{
   420  		CmdExecutorConfig: &appsv1alpha1.CommandExecutorEnvItem{
   421  			Image: mockImageName,
   422  			Env:   []corev1.EnvVar{},
   423  		},
   424  	}
   425  	completeExecConfig(accountConfig, componentVersion)
   426  	assert.NotEqual(t, systemAccount.CmdExecutorConfig.Image, accountConfig.Image)
   427  	assert.Equal(t, mockImageName, accountConfig.Image)
   428  	assert.Len(t, accountConfig.Env, 0)
   429  
   430  	// spec with envs
   431  	testEnv := corev1.EnvVar{
   432  		Name:  "test-env",
   433  		Value: "test-value",
   434  	}
   435  	accountConfig = systemAccount.CmdExecutorConfig.DeepCopy()
   436  	componentVersion.SystemAccountSpec = &appsv1alpha1.SystemAccountShortSpec{
   437  		CmdExecutorConfig: &appsv1alpha1.CommandExecutorEnvItem{
   438  			Image: mockImageName,
   439  			Env:   []corev1.EnvVar{testEnv},
   440  		},
   441  	}
   442  	completeExecConfig(accountConfig, componentVersion)
   443  	assert.NotEqual(t, systemAccount.CmdExecutorConfig.Image, accountConfig.Image)
   444  	assert.Equal(t, mockImageName, accountConfig.Image)
   445  	assert.Len(t, accountConfig.Env, 1)
   446  	assert.Contains(t, accountConfig.Env, testEnv)
   447  }