github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/apis/apps/v1alpha1/clusterdefinition_webhook_test.go (about) 1 /* 2 Copyright (C) 2022-2023 ApeCloud Co., Ltd 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package v1alpha1 18 19 import ( 20 "context" 21 "fmt" 22 23 . "github.com/onsi/ginkgo/v2" 24 . "github.com/onsi/gomega" 25 26 "k8s.io/apimachinery/pkg/util/yaml" 27 "sigs.k8s.io/controller-runtime/pkg/client" 28 ) 29 30 var _ = Describe("clusterDefinition webhook", func() { 31 var ( 32 randomStr = testCtx.GetRandomStr() 33 clusterDefinitionName = "webhook-cd-" + randomStr 34 clusterDefinitionName2 = "webhook-cd2" + randomStr 35 clusterDefinitionName3 = "webhook-cd3" + randomStr 36 ) 37 cleanupObjects := func() { 38 // Add any setup steps that needs to be executed before each test 39 err := k8sClient.DeleteAllOf(ctx, &ClusterDefinition{}, client.HasLabels{testCtx.TestObjLabelKey}) 40 Expect(err).NotTo(HaveOccurred()) 41 } 42 BeforeEach(func() { 43 // Add any setup steps that needs to be executed before each test 44 cleanupObjects() 45 }) 46 47 AfterEach(func() { 48 // Add any teardown steps that needs to be executed after each test 49 cleanupObjects() 50 }) 51 52 Context("When clusterDefinition create and update", func() { 53 It("Should webhook validate passed", func() { 54 55 By("By creating a new clusterDefinition") 56 clusterDef, _ := createTestClusterDefinitionObj(clusterDefinitionName) 57 Expect(testCtx.CreateObj(ctx, clusterDef)).Should(Succeed()) 58 // wait until ClusterDefinition created 59 Expect(k8sClient.Get(context.Background(), client.ObjectKey{Name: clusterDefinitionName}, clusterDef)).Should(Succeed()) 60 61 By("By creating a new clusterDefinition") 62 clusterDef, _ = createTestClusterDefinitionObj3(clusterDefinitionName3) 63 Expect(testCtx.CreateObj(ctx, clusterDef)).Should(Succeed()) 64 // wait until ClusterDefinition created 65 Expect(k8sClient.Get(context.Background(), client.ObjectKey{Name: clusterDefinitionName3}, clusterDef)).Should(Succeed()) 66 67 By("By creating a new clusterDefinition with workloadType==Consensus but consensusSpec not present") 68 clusterDef, _ = createTestClusterDefinitionObj2(clusterDefinitionName2) 69 Expect(testCtx.CreateObj(ctx, clusterDef)).ShouldNot(Succeed()) 70 71 By("Set Leader.Replicas > 1") 72 clusterDef.Spec.ComponentDefs[0].ConsensusSpec = &ConsensusSetSpec{Leader: DefaultLeader} 73 replicas := int32(2) 74 clusterDef.Spec.ComponentDefs[0].ConsensusSpec.Leader.Replicas = &replicas 75 Expect(testCtx.CreateObj(ctx, clusterDef)).ShouldNot(Succeed()) 76 // restore clusterDef 77 clusterDef.Spec.ComponentDefs[0].ConsensusSpec.Leader.Replicas = nil 78 79 By("Set Followers.Replicas to odd") 80 followers := make([]ConsensusMember, 1) 81 rel := int32(3) 82 followers[0] = ConsensusMember{Name: "follower", AccessMode: "Readonly", Replicas: &rel} 83 clusterDef.Spec.ComponentDefs[0].ConsensusSpec.Followers = followers 84 Expect(testCtx.CreateObj(ctx, clusterDef)).ShouldNot(Succeed()) 85 }) 86 87 It("Validate Cluster Definition System Accounts", func() { 88 By("By creating a new clusterDefinition") 89 clusterDef, _ := createTestClusterDefinitionObj3(clusterDefinitionName3) 90 cmdExecConfig := &CmdExecutorConfig{ 91 CommandExecutorEnvItem: CommandExecutorEnvItem{ 92 Image: "mysql-8.0.30", 93 }, 94 CommandExecutorItem: CommandExecutorItem{ 95 Command: []string{"mysql", "-e", "$(KB_ACCOUNT_STATEMENT)"}, 96 }, 97 } 98 By("By creating a new clusterDefinition with duplicated accounts") 99 mockAccounts := []SystemAccountConfig{ 100 { 101 Name: AdminAccount, 102 ProvisionPolicy: ProvisionPolicy{ 103 Type: CreateByStmt, 104 Statements: &ProvisionStatements{ 105 CreationStatement: `CREATE USER IF NOT EXISTS $(USERNAME) IDENTIFIED BY "$(PASSWD)"; `, 106 DeletionStatement: "DROP USER IF EXISTS $(USERNAME);", 107 }, 108 }, 109 }, 110 { 111 Name: AdminAccount, 112 ProvisionPolicy: ProvisionPolicy{ 113 Type: CreateByStmt, 114 Statements: &ProvisionStatements{ 115 CreationStatement: `CREATE USER IF NOT EXISTS $(USERNAME) IDENTIFIED BY "$(PASSWD)"; `, 116 DeletionStatement: "DROP USER IF EXISTS $(USERNAME);", 117 }, 118 }, 119 }, 120 } 121 passwdConfig := PasswordConfig{ 122 Length: 10, 123 } 124 clusterDef.Spec.ComponentDefs[0].SystemAccounts = &SystemAccountSpec{ 125 CmdExecutorConfig: cmdExecConfig, 126 PasswordConfig: passwdConfig, 127 Accounts: mockAccounts, 128 } 129 Expect(testCtx.CreateObj(ctx, clusterDef)).ShouldNot(Succeed()) 130 131 // fix duplication error 132 mockAccounts[1].Name = ProbeAccount 133 By("By creating a new clusterDefinition with invalid password setting") 134 // test password config 135 invalidPasswdConfig := PasswordConfig{ 136 Length: 10, 137 NumDigits: 10, 138 NumSymbols: 10, 139 } 140 clusterDef.Spec.ComponentDefs[0].SystemAccounts = &SystemAccountSpec{ 141 CmdExecutorConfig: cmdExecConfig, 142 PasswordConfig: invalidPasswdConfig, 143 Accounts: mockAccounts, 144 } 145 Expect(testCtx.CreateObj(ctx, clusterDef)).ShouldNot(Succeed()) 146 147 By("By creating a new clusterDefinition with statements missing") 148 mockAccounts[0].ProvisionPolicy.Type = ReferToExisting 149 clusterDef.Spec.ComponentDefs[0].SystemAccounts = &SystemAccountSpec{ 150 CmdExecutorConfig: cmdExecConfig, 151 PasswordConfig: passwdConfig, 152 Accounts: mockAccounts, 153 } 154 Expect(testCtx.CreateObj(ctx, clusterDef)).ShouldNot(Succeed()) 155 // reset account setting 156 mockAccounts[0].ProvisionPolicy.Type = CreateByStmt 157 158 By("Create accounts with empty deletion and update statements, should fail") 159 deletionStmt := mockAccounts[1].ProvisionPolicy.Statements.DeletionStatement 160 mockAccounts[1].ProvisionPolicy.Statements.DeletionStatement = "" 161 clusterDef.Spec.ComponentDefs[0].SystemAccounts = &SystemAccountSpec{ 162 CmdExecutorConfig: cmdExecConfig, 163 PasswordConfig: passwdConfig, 164 Accounts: mockAccounts, 165 } 166 Expect(testCtx.CreateObj(ctx, clusterDef)).ShouldNot(Succeed()) 167 // reset account setting 168 mockAccounts[1].ProvisionPolicy.Statements.DeletionStatement = deletionStmt 169 170 By("By creating a new clusterDefinition with valid accounts") 171 Expect(testCtx.CreateObj(ctx, clusterDef)).Should(Succeed()) 172 // wait until ClusterDefinition created 173 Expect(k8sClient.Get(context.Background(), client.ObjectKey{Name: clusterDefinitionName3}, clusterDef)).Should(Succeed()) 174 175 }) 176 177 It("Validate Cluster Definition Component Refs", func() { 178 By("By creating a new clusterDefinition") 179 clusterDef, err := createMultiCompClusterDefObj(clusterDefinitionName3) 180 Expect(err).ShouldNot(HaveOccurred()) 181 componentRefs := []ComponentDefRef{ 182 { 183 ComponentRefEnvs: []ComponentRefEnv{ 184 { 185 Name: "INJECTED_ENV", 186 ValueFrom: &ComponentValueFrom{ 187 Type: FromHeadlessServiceRef, 188 }, 189 }, 190 }, 191 }, 192 } 193 By("By creating a new clusterDefinition with empty component name, should fail") 194 clusterDef.Spec.ComponentDefs[0].ComponentDefRef = componentRefs 195 Expect(testCtx.CreateObj(ctx, clusterDef)).ShouldNot(Succeed()) 196 197 By("By creating a new clusterDefinition with invalid component name, should fail") 198 componentRefs[0].ComponentDefName = "invalid-name" 199 clusterDef.Spec.ComponentDefs[0].ComponentDefRef = componentRefs 200 Expect(testCtx.CreateObj(ctx, clusterDef)).ShouldNot(Succeed()) 201 202 By("By creating a new clusterDefinition with invalid workload type, should fail") 203 componentRefs[0].ComponentDefName = "mysql-proxy" 204 clusterDef.Spec.ComponentDefs[0].ComponentDefRef = componentRefs 205 Expect(testCtx.CreateObj(ctx, clusterDef)).ShouldNot(Succeed()) 206 207 By("By creating a new clusterDefinition with valid valueFrom type, should succeed") 208 componentRefs[0].ComponentRefEnvs[0].ValueFrom = &ComponentValueFrom{ 209 Type: FromServiceRef, 210 } 211 clusterDef.Spec.ComponentDefs[0].ComponentDefRef = componentRefs 212 Expect(testCtx.CreateObj(ctx, clusterDef)).Should(Succeed()) 213 }) 214 215 It("Should webhook validate configSpec", func() { 216 clusterDef, _ := createTestClusterDefinitionObj(clusterDefinitionName + "-cfg-test") 217 tests := []struct { 218 name string 219 tpls []ComponentConfigSpec 220 wantErr bool 221 expectedErrMessage string 222 }{{ 223 name: "cm_duplicate_test", 224 tpls: []ComponentConfigSpec{ 225 { 226 ComponentTemplateSpec: ComponentTemplateSpec{ 227 Name: "tpl1", 228 TemplateRef: "cm1", 229 VolumeName: "volume1", 230 }, 231 ConfigConstraintRef: "constraint1", 232 }, 233 { 234 ComponentTemplateSpec: ComponentTemplateSpec{ 235 Name: "tpl2", 236 TemplateRef: "cm1", 237 VolumeName: "volume2", 238 }, 239 ConfigConstraintRef: "constraint1", 240 }, 241 }, 242 wantErr: true, 243 expectedErrMessage: "configmap[cm1] already existed.", 244 }, { 245 name: "name_duplicate_test", 246 tpls: []ComponentConfigSpec{ 247 { 248 ComponentTemplateSpec: ComponentTemplateSpec{ 249 Name: "tpl1", 250 TemplateRef: "cm1", 251 VolumeName: "volume1", 252 }, 253 ConfigConstraintRef: "constraint1", 254 }, 255 { 256 ComponentTemplateSpec: ComponentTemplateSpec{ 257 Name: "tpl1", 258 TemplateRef: "cm2", 259 VolumeName: "volume2", 260 }, 261 ConfigConstraintRef: "constraint2", 262 }, 263 }, 264 wantErr: true, 265 expectedErrMessage: "Duplicate value: map", 266 }, { 267 name: "volume_duplicate_test", 268 tpls: []ComponentConfigSpec{ 269 { 270 ComponentTemplateSpec: ComponentTemplateSpec{ 271 Name: "tpl1", 272 TemplateRef: "cm1", 273 VolumeName: "volume1", 274 }, 275 ConfigConstraintRef: "constraint1", 276 }, 277 { 278 ComponentTemplateSpec: ComponentTemplateSpec{ 279 Name: "tpl2", 280 TemplateRef: "cm2", 281 VolumeName: "volume1", 282 }, 283 ConfigConstraintRef: "constraint2", 284 }, 285 }, 286 wantErr: true, 287 expectedErrMessage: "volume[volume1] already existed.", 288 }, { 289 name: "normal_test", 290 tpls: []ComponentConfigSpec{ 291 { 292 ComponentTemplateSpec: ComponentTemplateSpec{ 293 Name: "tpl1", 294 TemplateRef: "cm1", 295 VolumeName: "volume1", 296 }, 297 ConfigConstraintRef: "constraint1", 298 }, 299 { 300 ComponentTemplateSpec: ComponentTemplateSpec{ 301 Name: "tpl2", 302 TemplateRef: "cm2", 303 VolumeName: "volume2", 304 }, 305 ConfigConstraintRef: "constraint1", 306 }, 307 }, 308 wantErr: false, 309 }} 310 311 for _, tt := range tests { 312 clusterDef.Spec.ComponentDefs[0].ConfigSpecs = tt.tpls 313 err := testCtx.CreateObj(ctx, clusterDef) 314 if tt.wantErr { 315 Expect(err).ShouldNot(Succeed()) 316 Expect(err.Error()).Should(ContainSubstring(tt.expectedErrMessage)) 317 } else { 318 Expect(err).Should(Succeed()) 319 } 320 } 321 }) 322 }) 323 324 It("test mutating webhook", func() { 325 clusterDef, _ := createTestClusterDefinitionObj3(clusterDefinitionName + "-mutating") 326 By("test set the default value to RoleProbeTimeoutAfterPodsReady when roleProbe is not nil") 327 clusterDef.Spec.ComponentDefs[0].Probes = &ClusterDefinitionProbes{ 328 RoleProbe: &ClusterDefinitionProbe{}, 329 } 330 Expect(testCtx.CreateObj(ctx, clusterDef)).Should(Succeed()) 331 Expect(k8sClient.Get(ctx, client.ObjectKey{Name: clusterDef.Name}, clusterDef)).Should(Succeed()) 332 Expect(clusterDef.Spec.ComponentDefs[0].Probes.RoleProbeTimeoutAfterPodsReady).Should(Equal(DefaultRoleProbeTimeoutAfterPodsReady)) 333 334 By("test set zero to RoleProbeTimeoutAfterPodsReady when roleProbe is nil") 335 clusterDef.Spec.ComponentDefs[0].Probes = &ClusterDefinitionProbes{ 336 RoleProbeTimeoutAfterPodsReady: 60, 337 } 338 Expect(k8sClient.Update(ctx, clusterDef)).Should(Succeed()) 339 Expect(k8sClient.Get(ctx, client.ObjectKey{Name: clusterDef.Name}, clusterDef)).Should(Succeed()) 340 Expect(clusterDef.Spec.ComponentDefs[0].Probes.RoleProbeTimeoutAfterPodsReady).Should(Equal(int32(0))) 341 342 By("set h-scale policy type to Snapshot") 343 clusterDef.Spec.ComponentDefs[0].HorizontalScalePolicy = &HorizontalScalePolicy{ 344 Type: HScaleDataClonePolicyFromSnapshot, 345 } 346 Expect(k8sClient.Update(ctx, clusterDef)).Should(Succeed()) 347 Expect(k8sClient.Get(ctx, client.ObjectKey{Name: clusterDef.Name}, clusterDef)).Should(Succeed()) 348 Expect(clusterDef.Spec.ComponentDefs[0].HorizontalScalePolicy.Type).Should(Equal(HScaleDataClonePolicyCloneVolume)) 349 }) 350 }) 351 352 // createTestClusterDefinitionObj other webhook_test called this function, carefully for modifying the function 353 func createTestClusterDefinitionObj(name string) (*ClusterDefinition, error) { 354 clusterDefYaml := fmt.Sprintf(` 355 apiVersion: apps.kubeblocks.io/v1alpha1 356 kind: ClusterDefinition 357 metadata: 358 name: %s 359 spec: 360 componentDefs: 361 - name: replicasets 362 workloadType: Stateful 363 podSpec: 364 containers: 365 - name: nginx 366 image: nginx:latest 367 - name: proxy 368 workloadType: Stateless 369 podSpec: 370 containers: 371 - name: nginx 372 image: nginx:latest 373 `, name) 374 clusterDefinition := &ClusterDefinition{} 375 err := yaml.Unmarshal([]byte(clusterDefYaml), clusterDefinition) 376 return clusterDefinition, err 377 } 378 379 // createTestClusterDefinitionObj2 create an invalid obj 380 func createTestClusterDefinitionObj2(name string) (*ClusterDefinition, error) { 381 clusterDefYaml := fmt.Sprintf(` 382 apiVersion: apps.kubeblocks.io/v1alpha1 383 kind: ClusterDefinition 384 metadata: 385 name: %s 386 spec: 387 componentDefs: 388 - name: mysql-rafted 389 workloadType: Consensus 390 podSpec: 391 containers: 392 - name: mysql 393 image: docker.io/apecloud/apecloud-mysql-server:latest 394 `, name) 395 clusterDefinition := &ClusterDefinition{} 396 err := yaml.Unmarshal([]byte(clusterDefYaml), clusterDefinition) 397 return clusterDefinition, err 398 } 399 400 func createTestClusterDefinitionObj3(name string) (*ClusterDefinition, error) { 401 clusterDefYaml := fmt.Sprintf(` 402 apiVersion: apps.kubeblocks.io/v1alpha1 403 kind: ClusterDefinition 404 metadata: 405 name: %s 406 spec: 407 componentDefs: 408 - name: replicasets 409 logConfig: 410 - name: error 411 filePathPattern: /data/mysql/log/mysqld.err 412 - name: slow 413 filePathPattern: /data/mysql/mysqld-slow.log 414 configSpecs: 415 - name: mysql-tree-node-template-8.0 416 templateRef: mysql-tree-node-template-8.0 417 volumeName: mysql-config 418 workloadType: Consensus 419 consensusSpec: 420 leader: 421 name: leader 422 accessMode: ReadWrite 423 followers: 424 - name: follower 425 accessMode: Readonly 426 podSpec: 427 containers: 428 - name: mysql 429 image: docker.io/apecloud/apecloud-mysql-server:latest 430 imagePullPolicy: IfNotPresent 431 ports: 432 - containerPort: 3306 433 protocol: TCP 434 name: mysql 435 - containerPort: 13306 436 protocol: TCP 437 name: paxos 438 volumeMounts: 439 - mountPath: /data 440 name: data 441 - mountPath: /log 442 name: log 443 - mountPath: /data/config/mysql 444 name: mysql-config 445 env: 446 - name: "MYSQL_ROOT_PASSWORD" 447 valueFrom: 448 secretKeyRef: 449 name: $(CONN_CREDENTIAL_SECRET_NAME) 450 key: password 451 command: ["/usr/bin/bash", "-c"] 452 `, name) 453 clusterDefinition := &ClusterDefinition{} 454 err := yaml.Unmarshal([]byte(clusterDefYaml), clusterDefinition) 455 return clusterDefinition, err 456 } 457 458 func createMultiCompClusterDefObj(name string) (*ClusterDefinition, error) { 459 clusterDefYaml := fmt.Sprintf(` 460 apiVersion: apps.kubeblocks.io/v1alpha1 461 kind: ClusterDefinition 462 metadata: 463 name: %s 464 spec: 465 componentDefs: 466 - name: mysql-rafted 467 workloadType: Stateful 468 podSpec: 469 containers: 470 - name: mysql-raft 471 command: ["/usr/bin/bash", "-c"] 472 - name: mysql-proxy 473 workloadType: Stateless 474 podSpec: 475 containers: 476 - name: mysql-proxy 477 command: ["/usr/bin/bash", "-c"] 478 `, name) 479 clusterDefinition := &ClusterDefinition{} 480 err := yaml.Unmarshal([]byte(clusterDefYaml), clusterDefinition) 481 return clusterDefinition, err 482 }