...

Source file src/k8s.io/kubernetes/pkg/apis/storagemigration/validation/validation.go

Documentation: k8s.io/kubernetes/pkg/apis/storagemigration/validation

     1  /*
     2  Copyright 2024 The Kubernetes Authors.
     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 validation
    18  
    19  import (
    20  	"fmt"
    21  	"regexp"
    22  	"strconv"
    23  
    24  	"k8s.io/apimachinery/pkg/util/sets"
    25  	"k8s.io/apimachinery/pkg/util/validation"
    26  	"k8s.io/apimachinery/pkg/util/validation/field"
    27  	"k8s.io/kubernetes/pkg/apis/storagemigration"
    28  
    29  	corev1 "k8s.io/api/core/v1"
    30  	apimachineryvalidation "k8s.io/apimachinery/pkg/api/validation"
    31  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    32  	metav1validation "k8s.io/apimachinery/pkg/apis/meta/v1/validation"
    33  	apivalidation "k8s.io/kubernetes/pkg/apis/core/validation"
    34  )
    35  
    36  func ValidateStorageVersionMigration(svm *storagemigration.StorageVersionMigration) field.ErrorList {
    37  	allErrs := field.ErrorList{}
    38  	allErrs = append(allErrs, apivalidation.ValidateObjectMeta(&svm.ObjectMeta, false, apimachineryvalidation.NameIsDNSSubdomain, field.NewPath("metadata"))...)
    39  
    40  	allErrs = checkAndAppendError(allErrs, field.NewPath("spec", "resource", "resource"), svm.Spec.Resource.Resource, "resource is required")
    41  	allErrs = checkAndAppendError(allErrs, field.NewPath("spec", "resource", "version"), svm.Spec.Resource.Version, "version is required")
    42  
    43  	return allErrs
    44  }
    45  
    46  func ValidateStorageVersionMigrationUpdate(newSVMBundle, oldSVMBundle *storagemigration.StorageVersionMigration) field.ErrorList {
    47  	allErrs := ValidateStorageVersionMigration(newSVMBundle)
    48  	allErrs = append(allErrs, apivalidation.ValidateObjectMetaUpdate(&newSVMBundle.ObjectMeta, &oldSVMBundle.ObjectMeta, field.NewPath("metadata"))...)
    49  
    50  	// prevent changes to the group, version and resource
    51  	if newSVMBundle.Spec.Resource.Group != oldSVMBundle.Spec.Resource.Group {
    52  		allErrs = append(allErrs, field.Invalid(field.NewPath("group"), newSVMBundle.Spec.Resource.Group, "field is immutable"))
    53  	}
    54  	if newSVMBundle.Spec.Resource.Version != oldSVMBundle.Spec.Resource.Version {
    55  		allErrs = append(allErrs, field.Invalid(field.NewPath("version"), newSVMBundle.Spec.Resource.Version, "field is immutable"))
    56  	}
    57  	if newSVMBundle.Spec.Resource.Resource != oldSVMBundle.Spec.Resource.Resource {
    58  		allErrs = append(allErrs, field.Invalid(field.NewPath("resource"), newSVMBundle.Spec.Resource.Resource, "field is immutable"))
    59  	}
    60  
    61  	return allErrs
    62  }
    63  
    64  func ValidateStorageVersionMigrationStatusUpdate(newSVMBundle, oldSVMBundle *storagemigration.StorageVersionMigration) field.ErrorList {
    65  	allErrs := apivalidation.ValidateObjectMetaUpdate(&newSVMBundle.ObjectMeta, &oldSVMBundle.ObjectMeta, field.NewPath("metadata"))
    66  
    67  	fldPath := field.NewPath("status")
    68  
    69  	// resource version should be a non-negative integer
    70  	rvInt, err := convertResourceVersionToInt(newSVMBundle.Status.ResourceVersion)
    71  	if err != nil {
    72  		allErrs = append(allErrs, field.Invalid(fldPath.Child("resourceVersion"), newSVMBundle.Status.ResourceVersion, err.Error()))
    73  	}
    74  	allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(rvInt, fldPath.Child("resourceVersion"))...)
    75  
    76  	// TODO: after switching to metav1.Conditions in beta replace this validation with metav1.ValidateConditions
    77  	allErrs = append(allErrs, validateConditions(newSVMBundle.Status.Conditions, fldPath.Child("conditions"))...)
    78  
    79  	// resource version should not change once it has been set
    80  	if len(oldSVMBundle.Status.ResourceVersion) != 0 && oldSVMBundle.Status.ResourceVersion != newSVMBundle.Status.ResourceVersion {
    81  		allErrs = append(allErrs, field.Invalid(fldPath.Child("resourceVersion"), newSVMBundle.Status.ResourceVersion, "resourceVersion cannot be updated"))
    82  	}
    83  
    84  	// at most one of success or failed may be true
    85  	if isSuccessful(newSVMBundle) && isFailed(newSVMBundle) {
    86  		allErrs = append(allErrs, field.Invalid(fldPath.Child("conditions"), newSVMBundle.Status.Conditions, "Both success and failed conditions cannot be true at the same time"))
    87  	}
    88  
    89  	// running must be false when success is true or failed is true
    90  	if isSuccessful(newSVMBundle) && isRunning(newSVMBundle) {
    91  		allErrs = append(allErrs, field.Invalid(fldPath.Child("conditions"), newSVMBundle.Status.Conditions, "Running condition cannot be true when success condition is true"))
    92  	}
    93  	if isFailed(newSVMBundle) && isRunning(newSVMBundle) {
    94  		allErrs = append(allErrs, field.Invalid(fldPath.Child("conditions"), newSVMBundle.Status.Conditions, "Running condition cannot be true when failed condition is true"))
    95  	}
    96  
    97  	// success cannot be set to false once it is true
    98  	isOldSuccessful := isSuccessful(oldSVMBundle)
    99  	if isOldSuccessful && !isSuccessful(newSVMBundle) {
   100  		allErrs = append(allErrs, field.Invalid(fldPath.Child("conditions"), newSVMBundle.Status.Conditions, "Success condition cannot be set to false once it is true"))
   101  	}
   102  	isOldFailed := isFailed(oldSVMBundle)
   103  	if isOldFailed && !isFailed(newSVMBundle) {
   104  		allErrs = append(allErrs, field.Invalid(fldPath.Child("conditions"), newSVMBundle.Status.Conditions, "Failed condition cannot be set to false once it is true"))
   105  	}
   106  
   107  	return allErrs
   108  }
   109  
   110  func isSuccessful(svm *storagemigration.StorageVersionMigration) bool {
   111  	successCondition := getCondition(svm, storagemigration.MigrationSucceeded)
   112  	if successCondition != nil && successCondition.Status == corev1.ConditionTrue {
   113  		return true
   114  	}
   115  	return false
   116  }
   117  
   118  func isFailed(svm *storagemigration.StorageVersionMigration) bool {
   119  	failedCondition := getCondition(svm, storagemigration.MigrationFailed)
   120  	if failedCondition != nil && failedCondition.Status == corev1.ConditionTrue {
   121  		return true
   122  	}
   123  	return false
   124  }
   125  
   126  func isRunning(svm *storagemigration.StorageVersionMigration) bool {
   127  	runningCondition := getCondition(svm, storagemigration.MigrationRunning)
   128  	if runningCondition != nil && runningCondition.Status == corev1.ConditionTrue {
   129  		return true
   130  	}
   131  	return false
   132  }
   133  
   134  func getCondition(svm *storagemigration.StorageVersionMigration, conditionType storagemigration.MigrationConditionType) *storagemigration.MigrationCondition {
   135  	for _, c := range svm.Status.Conditions {
   136  		if c.Type == conditionType {
   137  			return &c
   138  		}
   139  	}
   140  
   141  	return nil
   142  }
   143  
   144  func validateConditions(conditions []storagemigration.MigrationCondition, fldPath *field.Path) field.ErrorList {
   145  	var allErrs field.ErrorList
   146  
   147  	conditionTypeToFirstIndex := map[string]int{}
   148  	for i, condition := range conditions {
   149  		if _, ok := conditionTypeToFirstIndex[string(condition.Type)]; ok {
   150  			allErrs = append(allErrs, field.Duplicate(fldPath.Index(i).Child("type"), condition.Type))
   151  		} else {
   152  			conditionTypeToFirstIndex[string(condition.Type)] = i
   153  		}
   154  
   155  		allErrs = append(allErrs, validateCondition(condition, fldPath.Index(i))...)
   156  	}
   157  
   158  	return allErrs
   159  }
   160  
   161  func validateCondition(condition storagemigration.MigrationCondition, fldPath *field.Path) field.ErrorList {
   162  	var allErrs field.ErrorList
   163  	var validConditionStatuses = sets.NewString(string(metav1.ConditionTrue), string(metav1.ConditionFalse), string(metav1.ConditionUnknown))
   164  
   165  	// type is set and is a valid format
   166  	allErrs = append(allErrs, metav1validation.ValidateLabelName(string(condition.Type), fldPath.Child("type"))...)
   167  
   168  	// status is set and is an accepted value
   169  	if !validConditionStatuses.Has(string(condition.Status)) {
   170  		allErrs = append(allErrs, field.NotSupported(fldPath.Child("status"), condition.Status, validConditionStatuses.List()))
   171  	}
   172  
   173  	if condition.LastUpdateTime.IsZero() {
   174  		allErrs = append(allErrs, field.Required(fldPath.Child("lastTransitionTime"), "must be set"))
   175  	}
   176  
   177  	if len(condition.Reason) == 0 {
   178  		allErrs = append(allErrs, field.Required(fldPath.Child("reason"), "must be set"))
   179  	} else {
   180  		for _, currErr := range isValidConditionReason(condition.Reason) {
   181  			allErrs = append(allErrs, field.Invalid(fldPath.Child("reason"), condition.Reason, currErr))
   182  		}
   183  
   184  		const maxReasonLen int = 1 * 1024 // 1024
   185  		if len(condition.Reason) > maxReasonLen {
   186  			allErrs = append(allErrs, field.TooLong(fldPath.Child("reason"), condition.Reason, maxReasonLen))
   187  		}
   188  	}
   189  
   190  	const maxMessageLen int = 32 * 1024 // 32768
   191  	if len(condition.Message) > maxMessageLen {
   192  		allErrs = append(allErrs, field.TooLong(fldPath.Child("message"), condition.Message, maxMessageLen))
   193  	}
   194  
   195  	return allErrs
   196  }
   197  func isValidConditionReason(value string) []string {
   198  	const conditionReasonFmt string = "[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?"
   199  	const conditionReasonErrMsg string = "a condition reason must start with alphabetic character, optionally followed by a string of alphanumeric characters or '_,:', and must end with an alphanumeric character or '_'"
   200  	var conditionReasonRegexp = regexp.MustCompile("^" + conditionReasonFmt + "$")
   201  
   202  	if !conditionReasonRegexp.MatchString(value) {
   203  		return []string{validation.RegexError(conditionReasonErrMsg, conditionReasonFmt, "my_name", "MY_NAME", "MyName", "ReasonA,ReasonB", "ReasonA:ReasonB")}
   204  	}
   205  	return nil
   206  }
   207  
   208  func checkAndAppendError(allErrs field.ErrorList, fieldPath *field.Path, value string, message string) field.ErrorList {
   209  	if len(value) == 0 {
   210  		allErrs = append(allErrs, field.Required(fieldPath, message))
   211  	}
   212  	return allErrs
   213  }
   214  
   215  func convertResourceVersionToInt(rv string) (int64, error) {
   216  	// initial value of RV is expected to be empty, which means the resource version is not set
   217  	if len(rv) == 0 {
   218  		return 0, nil
   219  	}
   220  
   221  	resourceVersion, err := strconv.ParseInt(rv, 10, 64)
   222  	if err != nil {
   223  		return 0, fmt.Errorf("failed to parse resource version %q: %w", rv, err)
   224  	}
   225  
   226  	return resourceVersion, nil
   227  }
   228  

View as plain text