github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/apis/apps/v1alpha1/opsrequest_webhook.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 "encoding/json" 22 "fmt" 23 "reflect" 24 "strings" 25 26 "github.com/pkg/errors" 27 "golang.org/x/exp/slices" 28 corev1 "k8s.io/api/core/v1" 29 storagev1 "k8s.io/api/storage/v1" 30 apierrors "k8s.io/apimachinery/pkg/api/errors" 31 "k8s.io/apimachinery/pkg/api/resource" 32 "k8s.io/apimachinery/pkg/runtime" 33 "k8s.io/apimachinery/pkg/types" 34 ctrl "sigs.k8s.io/controller-runtime" 35 "sigs.k8s.io/controller-runtime/pkg/client" 36 logf "sigs.k8s.io/controller-runtime/pkg/log" 37 "sigs.k8s.io/controller-runtime/pkg/webhook" 38 "sigs.k8s.io/controller-runtime/pkg/webhook/admission" 39 40 "github.com/1aal/kubeblocks/pkg/constant" 41 ) 42 43 // log is for logging in this package. 44 var ( 45 opsRequestLog = logf.Log.WithName("opsrequest-resource") 46 opsRequestAnnotationKey = "kubeblocks.io/ops-request" 47 // OpsRequestBehaviourMapper records the opsRequest behaviour according to the OpsType. 48 OpsRequestBehaviourMapper = map[OpsType]OpsRequestBehaviour{} 49 ) 50 51 func (r *OpsRequest) SetupWebhookWithManager(mgr ctrl.Manager) error { 52 return ctrl.NewWebhookManagedBy(mgr). 53 For(r). 54 Complete() 55 } 56 57 // TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation. 58 // +kubebuilder:webhook:path=/validate-apps-kubeblocks-io-v1alpha1-opsrequest,mutating=false,failurePolicy=fail,sideEffects=None,groups=apps.kubeblocks.io,resources=opsrequests,verbs=create;update,versions=v1alpha1,name=vopsrequest.kb.io,admissionReviewVersions=v1 59 60 var _ webhook.Validator = &OpsRequest{} 61 62 // ValidateCreate implements webhook.Validator so a webhook will be registered for the type 63 func (r *OpsRequest) ValidateCreate() (admission.Warnings, error) { 64 opsRequestLog.Info("validate create", "name", r.Name) 65 return nil, r.validateEntry(true) 66 } 67 68 // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type 69 func (r *OpsRequest) ValidateUpdate(old runtime.Object) (admission.Warnings, error) { 70 opsRequestLog.Info("validate update", "name", r.Name) 71 lastOpsRequest := old.(*OpsRequest).DeepCopy() 72 // if no spec updated, we should skip validation. 73 // if not, we can not delete the OpsRequest when cluster has been deleted. 74 // because when cluster not existed, r.validate will report an error. 75 if reflect.DeepEqual(lastOpsRequest.Spec, r.Spec) { 76 return nil, nil 77 } 78 79 if r.IsComplete() { 80 return nil, fmt.Errorf("update OpsRequest: %s is forbidden when status.Phase is %s", r.Name, r.Status.Phase) 81 } 82 83 // Keep the cancel consistent between the two opsRequest for comparing the diff. 84 lastOpsRequest.Spec.Cancel = r.Spec.Cancel 85 if !reflect.DeepEqual(lastOpsRequest.Spec, r.Spec) && r.Status.Phase != "" { 86 return nil, fmt.Errorf("update OpsRequest: %s is forbidden except for cancel when status.Phase is %s", r.Name, r.Status.Phase) 87 } 88 return nil, r.validateEntry(false) 89 } 90 91 // ValidateDelete implements webhook.Validator so a webhook will be registered for the type 92 func (r *OpsRequest) ValidateDelete() (admission.Warnings, error) { 93 opsRequestLog.Info("validate delete", "name", r.Name) 94 return nil, nil 95 } 96 97 // IsComplete checks if opsRequest has been completed. 98 func (r *OpsRequest) IsComplete(phases ...OpsPhase) bool { 99 if len(phases) == 0 { 100 return slices.Contains([]OpsPhase{OpsCancelledPhase, OpsSucceedPhase, OpsFailedPhase}, r.Status.Phase) 101 } 102 return slices.Contains([]OpsPhase{OpsCancelledPhase, OpsSucceedPhase, OpsFailedPhase}, phases[0]) 103 } 104 105 // validateClusterPhase validates whether the current cluster state supports the OpsRequest 106 func (r *OpsRequest) validateClusterPhase(cluster *Cluster) error { 107 opsBehaviour := OpsRequestBehaviourMapper[r.Spec.Type] 108 // if the OpsType has no cluster phases, ignore it 109 if len(opsBehaviour.FromClusterPhases) == 0 { 110 return nil 111 } 112 // validate whether existing the same type OpsRequest 113 var ( 114 opsRequestValue string 115 opsRecorder []OpsRecorder 116 ok bool 117 ) 118 if opsRequestValue, ok = cluster.Annotations[opsRequestAnnotationKey]; ok { 119 // opsRequest annotation value in cluster to map 120 if err := json.Unmarshal([]byte(opsRequestValue), &opsRecorder); err != nil { 121 return err 122 } 123 } 124 125 opsNamesInQueue := make([]string, len(opsRecorder)) 126 for i, v := range opsRecorder { 127 // judge whether the opsRequest meets the following conditions: 128 // 1. the opsRequest is Reentrant. 129 // 2. the opsRequest supports concurrent execution of the same kind. 130 // 3. reconfiguring is a special case, it can be executed concurrently with other opsRequests. 131 if v.Name != r.Name && v.Type != ReconfiguringType { 132 return fmt.Errorf("existing OpsRequest: %s is running in Cluster: %s, handle this OpsRequest first", v.Name, cluster.Name) 133 } 134 opsNamesInQueue[i] = v.Name 135 } 136 // check if the opsRequest can be executed in the current cluster phase unless this opsRequest is reentrant. 137 if !slices.Contains(opsBehaviour.FromClusterPhases, cluster.Status.Phase) && 138 !slices.Contains(opsNamesInQueue, r.Name) { 139 // if TTLSecondsBeforeAbort is not set or 0, return error 140 if r.Spec.TTLSecondsBeforeAbort == nil || *r.Spec.TTLSecondsBeforeAbort == 0 { 141 return fmt.Errorf("OpsRequest.spec.type=%s is forbidden when Cluster.status.phase=%s", r.Spec.Type, cluster.Status.Phase) 142 } 143 } 144 return nil 145 } 146 147 // getCluster gets cluster with webhook client 148 func (r *OpsRequest) getCluster(ctx context.Context, k8sClient client.Client) (*Cluster, error) { 149 if k8sClient == nil { 150 return nil, nil 151 } 152 cluster := &Cluster{} 153 // get cluster resource 154 if err := k8sClient.Get(ctx, types.NamespacedName{Namespace: r.Namespace, Name: r.Spec.ClusterRef}, cluster); err != nil { 155 return nil, fmt.Errorf("get cluster: %s failed, err: %s", r.Spec.ClusterRef, err.Error()) 156 } 157 return cluster, nil 158 } 159 160 func (r *OpsRequest) getConfigMap(cmName string) (*corev1.ConfigMap, error) { 161 cmObj := &corev1.ConfigMap{} 162 cmKey := client.ObjectKey{ 163 Namespace: r.Namespace, 164 Name: cmName, 165 } 166 167 ctx := context.Background() 168 if err := webhookMgr.client.Get(ctx, cmKey, cmObj); err != nil { 169 return nil, err 170 } 171 return cmObj, nil 172 } 173 174 // Validate validates OpsRequest 175 func (r *OpsRequest) Validate(ctx context.Context, 176 k8sClient client.Client, 177 cluster *Cluster, 178 isCreate bool) error { 179 if isCreate { 180 if err := r.validateClusterPhase(cluster); err != nil { 181 return err 182 } 183 } 184 return r.validateOps(ctx, k8sClient, cluster) 185 } 186 187 // ValidateEntry OpsRequest webhook validate entry 188 func (r *OpsRequest) validateEntry(isCreate bool) error { 189 if webhookMgr == nil || webhookMgr.client == nil { 190 return nil 191 } 192 ctx := context.Background() 193 k8sClient := webhookMgr.client 194 cluster, err := r.getCluster(ctx, k8sClient) 195 if err != nil { 196 return err 197 } 198 return r.Validate(ctx, k8sClient, cluster, isCreate) 199 } 200 201 // validateOps validates ops attributes 202 func (r *OpsRequest) validateOps(ctx context.Context, 203 k8sClient client.Client, 204 cluster *Cluster) error { 205 if webhookMgr == nil { 206 return nil 207 } 208 // Check whether the corresponding attribute is legal according to the operation type 209 switch r.Spec.Type { 210 case UpgradeType: 211 return r.validateUpgrade(ctx, k8sClient) 212 case VerticalScalingType: 213 return r.validateVerticalScaling(cluster) 214 case HorizontalScalingType: 215 return r.validateHorizontalScaling(ctx, k8sClient, cluster) 216 case VolumeExpansionType: 217 return r.validateVolumeExpansion(ctx, k8sClient, cluster) 218 case RestartType: 219 return r.validateRestart(cluster) 220 case ReconfiguringType: 221 return r.validateReconfigure(cluster) 222 case SwitchoverType: 223 return r.validateSwitchover(ctx, k8sClient, cluster) 224 case DataScriptType: 225 return r.validateDataScript(ctx, k8sClient, cluster) 226 } 227 return nil 228 } 229 230 // validateUpgrade validates spec.restart 231 func (r *OpsRequest) validateRestart(cluster *Cluster) error { 232 restartList := r.Spec.RestartList 233 if len(restartList) == 0 { 234 return notEmptyError("spec.restart") 235 } 236 237 compNames := make([]string, len(restartList)) 238 for i, v := range restartList { 239 compNames[i] = v.ComponentName 240 } 241 return r.checkComponentExistence(cluster, compNames) 242 } 243 244 // validateUpgrade validates spec.clusterOps.upgrade 245 func (r *OpsRequest) validateUpgrade(ctx context.Context, 246 k8sClient client.Client) error { 247 if r.Spec.Upgrade == nil { 248 return notEmptyError("spec.upgrade") 249 } 250 251 clusterVersion := &ClusterVersion{} 252 clusterVersionRef := r.Spec.Upgrade.ClusterVersionRef 253 if err := k8sClient.Get(ctx, types.NamespacedName{Name: clusterVersionRef}, clusterVersion); err != nil { 254 return fmt.Errorf("get clusterVersion: %s failed, err: %s", clusterVersionRef, err.Error()) 255 } 256 return nil 257 } 258 259 // validateVerticalScaling validates api when spec.type is VerticalScaling 260 func (r *OpsRequest) validateVerticalScaling(cluster *Cluster) error { 261 verticalScalingList := r.Spec.VerticalScalingList 262 if len(verticalScalingList) == 0 { 263 return notEmptyError("spec.verticalScaling") 264 } 265 266 // validate resources is legal and get component name slice 267 componentNames := make([]string, len(verticalScalingList)) 268 for i, v := range verticalScalingList { 269 componentNames[i] = v.ComponentName 270 271 if invalidValue, err := validateVerticalResourceList(v.Requests); err != nil { 272 return invalidValueError(invalidValue, err.Error()) 273 } 274 if invalidValue, err := validateVerticalResourceList(v.Limits); err != nil { 275 return invalidValueError(invalidValue, err.Error()) 276 } 277 if invalidValue, err := compareRequestsAndLimits(v.ResourceRequirements); err != nil { 278 return invalidValueError(invalidValue, err.Error()) 279 } 280 } 281 return r.checkComponentExistence(cluster, componentNames) 282 } 283 284 // validateVerticalScaling validate api is legal when spec.type is VerticalScaling 285 func (r *OpsRequest) validateReconfigure(cluster *Cluster) error { 286 if webhookMgr == nil || webhookMgr.client == nil { 287 return nil 288 } 289 reconfigure := r.Spec.Reconfigure 290 if reconfigure == nil { 291 return notEmptyError("spec.reconfigure") 292 } 293 if cluster.Spec.GetComponentByName(reconfigure.ComponentName) == nil { 294 return fmt.Errorf("component %s not found", reconfigure.ComponentName) 295 } 296 for _, configuration := range reconfigure.Configurations { 297 cmObj, err := r.getConfigMap(fmt.Sprintf("%s-%s-%s", r.Spec.ClusterRef, reconfigure.ComponentName, configuration.Name)) 298 if err != nil { 299 return err 300 } 301 for _, key := range configuration.Keys { 302 // check add file 303 if _, ok := cmObj.Data[key.Key]; !ok && key.FileContent == "" { 304 return errors.Errorf("key %s not found in configmap %s", key.Key, configuration.Name) 305 } 306 if key.FileContent == "" && len(key.Parameters) == 0 { 307 return errors.New("key.fileContent and key.parameters cannot be empty at the same time") 308 } 309 } 310 } 311 return nil 312 } 313 314 // compareRequestsAndLimits compares the resource requests and limits 315 func compareRequestsAndLimits(resources corev1.ResourceRequirements) (string, error) { 316 requests := resources.Requests 317 limits := resources.Limits 318 if requests == nil || limits == nil { 319 return "", nil 320 } 321 for k, v := range requests { 322 if limitQuantity, ok := limits[k]; !ok { 323 continue 324 } else if compareQuantity(&v, &limitQuantity) { 325 return v.String(), errors.New(fmt.Sprintf(`must be less than or equal to %s limit`, k)) 326 } 327 } 328 return "", nil 329 } 330 331 // compareQuantity compares requests quantity and limits quantity 332 func compareQuantity(requestQuantity, limitQuantity *resource.Quantity) bool { 333 return requestQuantity != nil && limitQuantity != nil && requestQuantity.Cmp(*limitQuantity) > 0 334 } 335 336 // validateHorizontalScaling validates api when spec.type is HorizontalScaling 337 func (r *OpsRequest) validateHorizontalScaling(ctx context.Context, cli client.Client, cluster *Cluster) error { 338 horizontalScalingList := r.Spec.HorizontalScalingList 339 if len(horizontalScalingList) == 0 { 340 return notEmptyError("spec.horizontalScaling") 341 } 342 343 componentNames := make([]string, len(horizontalScalingList)) 344 for i, v := range horizontalScalingList { 345 componentNames[i] = v.ComponentName 346 } 347 return r.checkComponentExistence(cluster, componentNames) 348 } 349 350 // validateVolumeExpansion validates volumeExpansion api when spec.type is VolumeExpansion 351 func (r *OpsRequest) validateVolumeExpansion(ctx context.Context, cli client.Client, cluster *Cluster) error { 352 volumeExpansionList := r.Spec.VolumeExpansionList 353 if len(volumeExpansionList) == 0 { 354 return notEmptyError("spec.volumeExpansion") 355 } 356 357 componentNames := make([]string, len(volumeExpansionList)) 358 for i, v := range volumeExpansionList { 359 componentNames[i] = v.ComponentName 360 } 361 if err := r.checkComponentExistence(cluster, componentNames); err != nil { 362 return err 363 } 364 runningOpsList, err := GetRunningOpsByOpsType(ctx, cli, r.Spec.ClusterRef, r.Namespace, string(VolumeExpansionType)) 365 if err != nil { 366 return err 367 } 368 if len(runningOpsList) > 0 && runningOpsList[0].Name != r.Name { 369 return fmt.Errorf("existing other VolumeExpansion OpsRequest: %s is running in Cluster: %s, handle this OpsRequest first", runningOpsList[0].Name, cluster.Name) 370 } 371 return r.checkVolumesAllowExpansion(ctx, cli, cluster) 372 } 373 374 // validateSwitchover validates switchover api when spec.type is Switchover. 375 func (r *OpsRequest) validateSwitchover(ctx context.Context, cli client.Client, cluster *Cluster) error { 376 switchoverList := r.Spec.SwitchoverList 377 if len(switchoverList) == 0 { 378 return notEmptyError("spec.switchover") 379 } 380 componentNames := make([]string, len(switchoverList)) 381 for i, v := range switchoverList { 382 componentNames[i] = v.ComponentName 383 384 } 385 if err := r.checkComponentExistence(cluster, componentNames); err != nil { 386 return err 387 } 388 runningOpsList, err := GetRunningOpsByOpsType(ctx, cli, r.Spec.ClusterRef, r.Namespace, string(SwitchoverType)) 389 if err != nil { 390 return err 391 } 392 if len(runningOpsList) > 0 && runningOpsList[0].Name != r.Name { 393 return fmt.Errorf("existing other Switchover OpsRequest: %s is running in Cluster: %s, handle this OpsRequest first", runningOpsList[0].Name, cluster.Name) 394 } 395 return validateSwitchoverResourceList(ctx, cli, cluster, switchoverList) 396 } 397 398 // checkComponentExistence checks whether components to be operated exist in cluster spec. 399 func (r *OpsRequest) checkComponentExistence(cluster *Cluster, compNames []string) error { 400 compSpecNameMap := make(map[string]bool) 401 for _, compSpec := range cluster.Spec.ComponentSpecs { 402 compSpecNameMap[compSpec.Name] = true 403 } 404 405 var notFoundCompNames []string 406 for _, compName := range compNames { 407 if _, ok := compSpecNameMap[compName]; !ok { 408 notFoundCompNames = append(notFoundCompNames, compName) 409 } 410 } 411 412 if len(notFoundCompNames) > 0 { 413 return fmt.Errorf("components: %v not found, you can view the components by command: "+ 414 "kbcli cluster describe %s -n %s", notFoundCompNames, cluster.Name, r.Namespace) 415 } 416 return nil 417 } 418 419 func (r *OpsRequest) checkVolumesAllowExpansion(ctx context.Context, cli client.Client, cluster *Cluster) error { 420 type Entity struct { 421 existInSpec bool 422 storageClassName *string 423 allowExpansion bool 424 requestStorage resource.Quantity 425 } 426 427 // component name -> vct name -> entity 428 vols := make(map[string]map[string]Entity) 429 for _, comp := range r.Spec.VolumeExpansionList { 430 for _, vct := range comp.VolumeClaimTemplates { 431 if _, ok := vols[comp.ComponentName]; !ok { 432 vols[comp.ComponentName] = make(map[string]Entity) 433 } 434 vols[comp.ComponentName][vct.Name] = Entity{false, nil, false, vct.Storage} 435 } 436 } 437 // traverse the spec to update volumes 438 for _, comp := range cluster.Spec.ComponentSpecs { 439 if _, ok := vols[comp.Name]; !ok { 440 continue // ignore not-exist component 441 } 442 for _, vct := range comp.VolumeClaimTemplates { 443 e, ok := vols[comp.Name][vct.Name] 444 if !ok { 445 continue 446 } 447 e.existInSpec = true 448 e.storageClassName = vct.Spec.StorageClassName 449 vols[comp.Name][vct.Name] = e 450 } 451 } 452 453 // check all used storage classes 454 var err error 455 for cname, compVols := range vols { 456 for vname := range compVols { 457 e := vols[cname][vname] 458 if !e.existInSpec { 459 continue 460 } 461 e.storageClassName, err = r.getSCNameByPvcAndCheckStorageSize(ctx, cli, cname, vname, e.requestStorage) 462 if err != nil { 463 return err 464 } 465 allowExpansion, err := r.checkStorageClassAllowExpansion(ctx, cli, e.storageClassName) 466 if err != nil { 467 continue // ignore the error and take it as not-supported 468 } 469 e.allowExpansion = allowExpansion 470 vols[cname][vname] = e 471 } 472 } 473 474 for cname, compVols := range vols { 475 var ( 476 notFound []string 477 notSupport []string 478 notSupportSc []string 479 ) 480 for vct, e := range compVols { 481 if !e.existInSpec { 482 notFound = append(notFound, vct) 483 } 484 if !e.allowExpansion { 485 notSupport = append(notSupport, vct) 486 if e.storageClassName != nil { 487 notSupportSc = append(notSupportSc, *e.storageClassName) 488 } 489 } 490 } 491 if len(notFound) > 0 { 492 return fmt.Errorf("volumeClaimTemplates: %v not found in component: %s, you can view infos by command: "+ 493 "kbcli cluster describe %s -n %s", notFound, cname, cluster.Name, r.Namespace) 494 } 495 if len(notSupport) > 0 { 496 var notSupportScString string 497 if len(notSupportSc) > 0 { 498 notSupportScString = fmt.Sprintf("storageClass: %v of ", notSupportSc) 499 } 500 return fmt.Errorf(notSupportScString+"volumeClaimTemplate: %s not support volume expansion in component: %s, you can view infos by command: "+ 501 "kubectl get sc", notSupport, cname) 502 } 503 } 504 return nil 505 } 506 507 // checkStorageClassAllowExpansion checks whether the specified storage class supports volume expansion. 508 func (r *OpsRequest) checkStorageClassAllowExpansion(ctx context.Context, 509 cli client.Client, 510 storageClassName *string) (bool, error) { 511 if storageClassName == nil { 512 return false, nil 513 } 514 storageClass := &storagev1.StorageClass{} 515 // take not found error as unsupported 516 if err := cli.Get(ctx, types.NamespacedName{Name: *storageClassName}, storageClass); err != nil && !apierrors.IsNotFound(err) { 517 return false, err 518 } 519 if storageClass == nil || storageClass.AllowVolumeExpansion == nil { 520 return false, nil 521 } 522 return *storageClass.AllowVolumeExpansion, nil 523 } 524 525 // getSCNameByPvcAndCheckStorageSize gets the storageClassName by pvc and checks if the storage size is valid. 526 func (r *OpsRequest) getSCNameByPvcAndCheckStorageSize(ctx context.Context, 527 cli client.Client, 528 compName, 529 vctName string, 530 requestStorage resource.Quantity) (*string, error) { 531 pvcList := &corev1.PersistentVolumeClaimList{} 532 if err := cli.List(ctx, pvcList, client.InNamespace(r.Namespace), client.MatchingLabels{ 533 constant.AppInstanceLabelKey: r.Spec.ClusterRef, 534 constant.KBAppComponentLabelKey: compName, 535 }); err != nil { 536 return nil, err 537 } 538 if len(pvcList.Items) == 0 { 539 return nil, nil 540 } 541 var pvc *corev1.PersistentVolumeClaim 542 for _, v := range pvcList.Items { 543 // VolumeClaimTemplateNameLabelKeyForLegacy is deprecated: only compatible with version 0.5, will be removed in 0.7? 544 if v.Labels[constant.VolumeClaimTemplateNameLabelKey] == vctName || 545 v.Labels[constant.VolumeClaimTemplateNameLabelKeyForLegacy] == vctName { 546 pvc = &v 547 break 548 } 549 } 550 if pvc == nil { 551 return nil, nil 552 } 553 previousValue := *pvc.Status.Capacity.Storage() 554 if requestStorage.Cmp(previousValue) < 0 { 555 return nil, fmt.Errorf(`requested storage size of volumeClaimTemplate "%s" can not less than status.capacity.storage "%s" `, 556 vctName, previousValue.String()) 557 } 558 return pvc.Spec.StorageClassName, nil 559 } 560 561 // validateDataScript validates the data script. 562 func (r *OpsRequest) validateDataScript(ctx context.Context, cli client.Client, cluster *Cluster) error { 563 validateScript := func(spec *ScriptSpec) error { 564 rawScripts := spec.Script 565 scriptsFrom := spec.ScriptFrom 566 if len(rawScripts) == 0 && (scriptsFrom == nil) { 567 return fmt.Errorf("spec.scriptSpec.script and spec.scriptSpec.scriptFrom can not be empty at the same time") 568 } 569 if scriptsFrom != nil { 570 if scriptsFrom.ConfigMapRef == nil && scriptsFrom.SecretRef == nil { 571 return fmt.Errorf("spec.scriptSpec.scriptFrom.configMapRefs and spec.scriptSpec.scriptFrom.secretRefs can not be empty at the same time") 572 } 573 for _, configMapRef := range scriptsFrom.ConfigMapRef { 574 if err := cli.Get(ctx, types.NamespacedName{Name: configMapRef.Name, Namespace: r.Namespace}, &corev1.ConfigMap{}); err != nil { 575 return err 576 } 577 } 578 for _, secret := range scriptsFrom.SecretRef { 579 if err := cli.Get(ctx, types.NamespacedName{Name: secret.Name, Namespace: r.Namespace}, &corev1.Secret{}); err != nil { 580 return err 581 } 582 } 583 } 584 return nil 585 } 586 587 scriptSpec := r.Spec.ScriptSpec 588 if scriptSpec == nil { 589 return notEmptyError("spec.scriptSpec") 590 } 591 592 if err := r.checkComponentExistence(cluster, []string{scriptSpec.ComponentName}); err != nil { 593 return err 594 } 595 596 if err := validateScript(scriptSpec); err != nil { 597 return err 598 } 599 600 return nil 601 } 602 603 // validateVerticalResourceList checks if k8s resourceList is legal 604 func validateVerticalResourceList(resourceList map[corev1.ResourceName]resource.Quantity) (string, error) { 605 for k := range resourceList { 606 if k != corev1.ResourceCPU && k != corev1.ResourceMemory && !strings.HasPrefix(k.String(), corev1.ResourceHugePagesPrefix) { 607 return string(k), fmt.Errorf("resource key is not cpu or memory or hugepages- ") 608 } 609 } 610 611 return "", nil 612 } 613 614 func notEmptyError(target string) error { 615 return fmt.Errorf(`"%s" can not be empty`, target) 616 } 617 618 func invalidValueError(target string, value string) error { 619 return fmt.Errorf(`invalid value for "%s": %s`, target, value) 620 } 621 622 // GetRunningOpsByOpsType gets the running opsRequests by type. 623 func GetRunningOpsByOpsType(ctx context.Context, cli client.Client, 624 clusterName, namespace, opsType string) ([]OpsRequest, error) { 625 opsRequestList := &OpsRequestList{} 626 if err := cli.List(ctx, opsRequestList, client.MatchingLabels{ 627 constant.AppInstanceLabelKey: clusterName, 628 constant.OpsRequestTypeLabelKey: opsType, 629 }, client.InNamespace(namespace)); err != nil { 630 return nil, err 631 } 632 if len(opsRequestList.Items) == 0 { 633 return nil, nil 634 } 635 var runningOpsList []OpsRequest 636 for _, v := range opsRequestList.Items { 637 if v.Status.Phase == OpsRunningPhase { 638 runningOpsList = append(runningOpsList, v) 639 break 640 } 641 } 642 return runningOpsList, nil 643 } 644 645 // validateSwitchoverResourceList checks if switchover resourceList is legal. 646 func validateSwitchoverResourceList(ctx context.Context, cli client.Client, cluster *Cluster, switchoverList []Switchover) error { 647 for _, switchover := range switchoverList { 648 if switchover.InstanceName == "" { 649 return notEmptyError("switchover.instanceName") 650 } 651 652 // check clusterComponentDefinition whether support switchover 653 compDefObj, err := GetComponentDefByCluster(ctx, cli, *cluster, cluster.Spec.GetComponentDefRefName(switchover.ComponentName)) 654 if err != nil { 655 return err 656 } 657 if compDefObj == nil { 658 return fmt.Errorf("this cluster component %s is invalid", switchover.ComponentName) 659 } 660 if compDefObj.SwitchoverSpec == nil { 661 return fmt.Errorf("this cluster component %s does not support switchover", switchover.ComponentName) 662 } 663 switch switchover.InstanceName { 664 case constant.KBSwitchoverCandidateInstanceForAnyPod: 665 if compDefObj.SwitchoverSpec.WithoutCandidate == nil { 666 return fmt.Errorf("this cluster component %s does not support promote without specifying an instance. Please specify a specific instance for the promotion", switchover.ComponentName) 667 } 668 default: 669 if compDefObj.SwitchoverSpec.WithCandidate == nil { 670 return fmt.Errorf("this cluster component %s does not support specifying an instance for promote. If you want to perform a promote operation, please do not specify an instance", switchover.ComponentName) 671 } 672 } 673 674 // check switchover.InstanceName whether exist and role label is correct 675 if switchover.InstanceName == constant.KBSwitchoverCandidateInstanceForAnyPod { 676 return nil 677 } 678 pod := &corev1.Pod{} 679 if err := cli.Get(ctx, types.NamespacedName{Namespace: cluster.Namespace, Name: switchover.InstanceName}, pod); err != nil { 680 return fmt.Errorf("get instanceName %s failed, err: %s, and check the validity of the instanceName using \"kbcli cluster list-instances\"", switchover.InstanceName, err.Error()) 681 } 682 v, ok := pod.Labels[constant.RoleLabelKey] 683 if !ok || v == "" { 684 return fmt.Errorf("instanceName %s cannot be promoted because it had a invalid role label", switchover.InstanceName) 685 } 686 if v == constant.Primary || v == constant.Leader { 687 return fmt.Errorf("instanceName %s cannot be promoted because it is already the primary or leader instance", switchover.InstanceName) 688 } 689 if !strings.HasPrefix(pod.Name, fmt.Sprintf("%s-%s", cluster.Name, switchover.ComponentName)) { 690 return fmt.Errorf("instanceName %s does not belong to the current component, please check the validity of the instance using \"kbcli cluster list-instances\"", switchover.InstanceName) 691 } 692 } 693 return nil 694 }