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 }