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  }