github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/apis/apps/v1alpha1/cluster_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  	"fmt"
    22  	"reflect"
    23  
    24  	corev1 "k8s.io/api/core/v1"
    25  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    26  	"k8s.io/apimachinery/pkg/runtime"
    27  	"k8s.io/apimachinery/pkg/runtime/schema"
    28  	"k8s.io/apimachinery/pkg/types"
    29  	"k8s.io/apimachinery/pkg/util/validation/field"
    30  	ctrl "sigs.k8s.io/controller-runtime"
    31  	logf "sigs.k8s.io/controller-runtime/pkg/log"
    32  	"sigs.k8s.io/controller-runtime/pkg/webhook"
    33  	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
    34  )
    35  
    36  // log is for logging in this package.
    37  var clusterlog = logf.Log.WithName("cluster-resource")
    38  
    39  func (r *Cluster) SetupWebhookWithManager(mgr ctrl.Manager) error {
    40  	return ctrl.NewWebhookManagedBy(mgr).
    41  		For(r).
    42  		Complete()
    43  }
    44  
    45  // TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation.
    46  // +kubebuilder:webhook:path=/validate-apps-kubeblocks-io-v1alpha1-cluster,mutating=false,failurePolicy=fail,sideEffects=None,groups=apps.kubeblocks.io,resources=clusters,verbs=create;update,versions=v1alpha1,name=vcluster.kb.io,admissionReviewVersions=v1
    47  
    48  var _ webhook.Validator = &Cluster{}
    49  
    50  // ValidateCreate implements webhook.Validator so a webhook will be registered for the type
    51  func (r *Cluster) ValidateCreate() (admission.Warnings, error) {
    52  	clusterlog.Info("validate create", "name", r.Name)
    53  	return nil, r.validate()
    54  }
    55  
    56  // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type
    57  func (r *Cluster) ValidateUpdate(old runtime.Object) (admission.Warnings, error) {
    58  	clusterlog.Info("validate update", "name", r.Name)
    59  	lastCluster := old.(*Cluster)
    60  	if lastCluster.Spec.ClusterDefRef != r.Spec.ClusterDefRef {
    61  		return nil, newInvalidError(ClusterKind, r.Name, "spec.clusterDefinitionRef", "clusterDefinitionRef is immutable, you can not update it. ")
    62  	}
    63  	if err := r.validate(); err != nil {
    64  		return nil, err
    65  	}
    66  	return nil, r.validateVolumeClaimTemplates(lastCluster)
    67  }
    68  
    69  // ValidateDelete implements webhook.Validator so a webhook will be registered for the type
    70  func (r *Cluster) ValidateDelete() (admission.Warnings, error) {
    71  	clusterlog.Info("validate delete", "name", r.Name)
    72  	if r.Spec.TerminationPolicy == DoNotTerminate {
    73  		return nil, fmt.Errorf("the deletion for a cluster with DoNotTerminate termination policy is denied")
    74  	}
    75  	return nil, nil
    76  }
    77  
    78  // validateVolumeClaimTemplates volumeClaimTemplates is forbidden modification except for storage size.
    79  func (r *Cluster) validateVolumeClaimTemplates(lastCluster *Cluster) error {
    80  	var allErrs field.ErrorList
    81  	for i, component := range r.Spec.ComponentSpecs {
    82  		lastComponent := getLastComponentByName(lastCluster, component.Name)
    83  		if lastComponent == nil {
    84  			continue
    85  		}
    86  		setVolumeClaimStorageSizeZero(component.VolumeClaimTemplates)
    87  		setVolumeClaimStorageSizeZero(lastComponent.VolumeClaimTemplates)
    88  		if !reflect.DeepEqual(component.VolumeClaimTemplates, lastComponent.VolumeClaimTemplates) {
    89  			path := fmt.Sprintf("spec.components[%d].volumeClaimTemplates", i)
    90  			allErrs = append(allErrs, field.Invalid(field.NewPath(path),
    91  				nil, "volumeClaimTemplates is forbidden modification except for storage size."))
    92  		}
    93  	}
    94  	if len(allErrs) > 0 {
    95  		return apierrors.NewInvalid(
    96  			schema.GroupKind{Group: APIVersion, Kind: ClusterKind},
    97  			r.Name, allErrs)
    98  	}
    99  	return nil
   100  }
   101  
   102  // getLastComponentByName the cluster maybe delete or add a component, so we get the component by name.
   103  func getLastComponentByName(lastCluster *Cluster, componentName string) *ClusterComponentSpec {
   104  	for _, component := range lastCluster.Spec.ComponentSpecs {
   105  		if component.Name == componentName {
   106  			return &component
   107  		}
   108  	}
   109  	return nil
   110  }
   111  
   112  // setVolumeClaimStorageSizeZero set the volumeClaimTemplates storage size to zero. then we can diff last/current volumeClaimTemplates.
   113  func setVolumeClaimStorageSizeZero(volumeClaimTemplates []ClusterComponentVolumeClaimTemplate) {
   114  	for i := range volumeClaimTemplates {
   115  		volumeClaimTemplates[i].Spec.Resources = corev1.ResourceRequirements{}
   116  	}
   117  }
   118  
   119  // Validate Cluster.spec is legal
   120  func (r *Cluster) validate() error {
   121  	var (
   122  		allErrs    field.ErrorList
   123  		ctx        = context.Background()
   124  		clusterDef = &ClusterDefinition{}
   125  	)
   126  	if webhookMgr == nil {
   127  		return nil
   128  	}
   129  
   130  	r.validateClusterVersionRef(&allErrs)
   131  
   132  	err := webhookMgr.client.Get(ctx, types.NamespacedName{Name: r.Spec.ClusterDefRef}, clusterDef)
   133  
   134  	if err != nil {
   135  		allErrs = append(allErrs, field.Invalid(field.NewPath("spec.clusterDefinitionRef"),
   136  			r.Spec.ClusterDefRef, err.Error()))
   137  	} else {
   138  		r.validateComponents(&allErrs, clusterDef)
   139  	}
   140  
   141  	if len(allErrs) > 0 {
   142  		return apierrors.NewInvalid(
   143  			schema.GroupKind{Group: APIVersion, Kind: ClusterKind},
   144  			r.Name, allErrs)
   145  	}
   146  	return nil
   147  }
   148  
   149  // ValidateClusterVersionRef validate spec.clusterVersionRef is legal
   150  func (r *Cluster) validateClusterVersionRef(allErrs *field.ErrorList) {
   151  	clusterVersion := &ClusterVersion{}
   152  	err := webhookMgr.client.Get(context.Background(), types.NamespacedName{
   153  		Namespace: r.Namespace,
   154  		Name:      r.Spec.ClusterVersionRef,
   155  	}, clusterVersion)
   156  	if err != nil {
   157  		*allErrs = append(*allErrs, field.Invalid(field.NewPath("spec.clusterVersionRef"),
   158  			r.Spec.ClusterDefRef, err.Error()))
   159  	}
   160  }
   161  
   162  // ValidateComponents validate spec.components is legal
   163  func (r *Cluster) validateComponents(allErrs *field.ErrorList, clusterDef *ClusterDefinition) {
   164  	var (
   165  		// invalid component slice
   166  		invalidComponentDefs = make([]string, 0)
   167  		componentNameMap     = make(map[string]struct{})
   168  		componentDefMap      = make(map[string]struct{})
   169  		componentMap         = make(map[string]ClusterComponentDefinition)
   170  	)
   171  
   172  	for _, v := range clusterDef.Spec.ComponentDefs {
   173  		componentDefMap[v.Name] = struct{}{}
   174  		componentMap[v.Name] = v
   175  	}
   176  
   177  	for i, v := range r.Spec.ComponentSpecs {
   178  		if _, ok := componentDefMap[v.ComponentDefRef]; !ok {
   179  			invalidComponentDefs = append(invalidComponentDefs, v.ComponentDefRef)
   180  		}
   181  
   182  		componentNameMap[v.Name] = struct{}{}
   183  		r.validateComponentResources(allErrs, v.Resources, i)
   184  	}
   185  
   186  	r.validateComponentTLSSettings(allErrs)
   187  
   188  	if len(invalidComponentDefs) > 0 {
   189  		*allErrs = append(*allErrs, field.NotFound(field.NewPath("spec.components[*].type"),
   190  			getComponentDefNotFoundMsg(invalidComponentDefs, r.Spec.ClusterDefRef)))
   191  	}
   192  }
   193  
   194  // validateComponentResources validate component resources
   195  func (r *Cluster) validateComponentResources(allErrs *field.ErrorList, resources corev1.ResourceRequirements, index int) {
   196  	if invalidValue, err := validateVerticalResourceList(resources.Requests); err != nil {
   197  		*allErrs = append(*allErrs, field.Invalid(field.NewPath(fmt.Sprintf("spec.components[%d].resources.requests", index)), invalidValue, err.Error()))
   198  	}
   199  	if invalidValue, err := validateVerticalResourceList(resources.Limits); err != nil {
   200  		*allErrs = append(*allErrs, field.Invalid(field.NewPath(fmt.Sprintf("spec.components[%d].resources.limits", index)), invalidValue, err.Error()))
   201  	}
   202  	if invalidValue, err := compareRequestsAndLimits(resources); err != nil {
   203  		*allErrs = append(*allErrs, field.Invalid(field.NewPath(fmt.Sprintf("spec.components[%d].resources.requests", index)), invalidValue, err.Error()))
   204  	}
   205  }
   206  
   207  func (r *Cluster) validateComponentTLSSettings(allErrs *field.ErrorList) {
   208  	for index, component := range r.Spec.ComponentSpecs {
   209  		if !component.TLS {
   210  			continue
   211  		}
   212  		if component.Issuer == nil {
   213  			*allErrs = append(*allErrs, field.Required(field.NewPath(fmt.Sprintf("spec.components[%d].issuer", index)), "Issuer must be set when Tls enabled"))
   214  			continue
   215  		}
   216  		if component.Issuer.Name == IssuerUserProvided && component.Issuer.SecretRef == nil {
   217  			*allErrs = append(*allErrs, field.Required(field.NewPath(fmt.Sprintf("spec.components[%d].issuer.secretRef", index)), "Secret must provide when issuer name is UserProvided"))
   218  		}
   219  	}
   220  }