feat: implements Storage Version Migration API in-tree
Signed-off-by: Nilekh Chaudhari <1626598+nilekhc@users.noreply.github.com>
This commit is contained in:
227
pkg/apis/storagemigration/validation/validation.go
Normal file
227
pkg/apis/storagemigration/validation/validation.go
Normal file
@@ -0,0 +1,227 @@
|
||||
/*
|
||||
Copyright 2024 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package validation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apimachinery/pkg/util/validation"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
"k8s.io/kubernetes/pkg/apis/storagemigration"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
apimachineryvalidation "k8s.io/apimachinery/pkg/api/validation"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
metav1validation "k8s.io/apimachinery/pkg/apis/meta/v1/validation"
|
||||
apivalidation "k8s.io/kubernetes/pkg/apis/core/validation"
|
||||
)
|
||||
|
||||
func ValidateStorageVersionMigration(svm *storagemigration.StorageVersionMigration) field.ErrorList {
|
||||
allErrs := field.ErrorList{}
|
||||
allErrs = append(allErrs, apivalidation.ValidateObjectMeta(&svm.ObjectMeta, false, apimachineryvalidation.NameIsDNSSubdomain, field.NewPath("metadata"))...)
|
||||
|
||||
allErrs = checkAndAppendError(allErrs, field.NewPath("spec", "resource", "resource"), svm.Spec.Resource.Resource, "resource is required")
|
||||
allErrs = checkAndAppendError(allErrs, field.NewPath("spec", "resource", "version"), svm.Spec.Resource.Version, "version is required")
|
||||
|
||||
return allErrs
|
||||
}
|
||||
|
||||
func ValidateStorageVersionMigrationUpdate(newSVMBundle, oldSVMBundle *storagemigration.StorageVersionMigration) field.ErrorList {
|
||||
allErrs := ValidateStorageVersionMigration(newSVMBundle)
|
||||
allErrs = append(allErrs, apivalidation.ValidateObjectMetaUpdate(&newSVMBundle.ObjectMeta, &oldSVMBundle.ObjectMeta, field.NewPath("metadata"))...)
|
||||
|
||||
// prevent changes to the group, version and resource
|
||||
if newSVMBundle.Spec.Resource.Group != oldSVMBundle.Spec.Resource.Group {
|
||||
allErrs = append(allErrs, field.Invalid(field.NewPath("group"), newSVMBundle.Spec.Resource.Group, "field is immutable"))
|
||||
}
|
||||
if newSVMBundle.Spec.Resource.Version != oldSVMBundle.Spec.Resource.Version {
|
||||
allErrs = append(allErrs, field.Invalid(field.NewPath("version"), newSVMBundle.Spec.Resource.Version, "field is immutable"))
|
||||
}
|
||||
if newSVMBundle.Spec.Resource.Resource != oldSVMBundle.Spec.Resource.Resource {
|
||||
allErrs = append(allErrs, field.Invalid(field.NewPath("resource"), newSVMBundle.Spec.Resource.Resource, "field is immutable"))
|
||||
}
|
||||
|
||||
return allErrs
|
||||
}
|
||||
|
||||
func ValidateStorageVersionMigrationStatusUpdate(newSVMBundle, oldSVMBundle *storagemigration.StorageVersionMigration) field.ErrorList {
|
||||
allErrs := apivalidation.ValidateObjectMetaUpdate(&newSVMBundle.ObjectMeta, &oldSVMBundle.ObjectMeta, field.NewPath("metadata"))
|
||||
|
||||
fldPath := field.NewPath("status")
|
||||
|
||||
// resource version should be a non-negative integer
|
||||
rvInt, err := convertResourceVersionToInt(newSVMBundle.Status.ResourceVersion)
|
||||
if err != nil {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath.Child("resourceVersion"), newSVMBundle.Status.ResourceVersion, err.Error()))
|
||||
}
|
||||
allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(rvInt, fldPath.Child("resourceVersion"))...)
|
||||
|
||||
// TODO: after switching to metav1.Conditions in beta replace this validation with metav1.ValidateConditions
|
||||
allErrs = append(allErrs, validateConditions(newSVMBundle.Status.Conditions, fldPath.Child("conditions"))...)
|
||||
|
||||
// resource version should not change once it has been set
|
||||
if len(oldSVMBundle.Status.ResourceVersion) != 0 && oldSVMBundle.Status.ResourceVersion != newSVMBundle.Status.ResourceVersion {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath.Child("resourceVersion"), newSVMBundle.Status.ResourceVersion, "resourceVersion cannot be updated"))
|
||||
}
|
||||
|
||||
// at most one of success or failed may be true
|
||||
if isSuccessful(newSVMBundle) && isFailed(newSVMBundle) {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath.Child("conditions"), newSVMBundle.Status.Conditions, "Both success and failed conditions cannot be true at the same time"))
|
||||
}
|
||||
|
||||
// running must be false when success is true or failed is true
|
||||
if isSuccessful(newSVMBundle) && isRunning(newSVMBundle) {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath.Child("conditions"), newSVMBundle.Status.Conditions, "Running condition cannot be true when success condition is true"))
|
||||
}
|
||||
if isFailed(newSVMBundle) && isRunning(newSVMBundle) {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath.Child("conditions"), newSVMBundle.Status.Conditions, "Running condition cannot be true when failed condition is true"))
|
||||
}
|
||||
|
||||
// success cannot be set to false once it is true
|
||||
isOldSuccessful := isSuccessful(oldSVMBundle)
|
||||
if isOldSuccessful && !isSuccessful(newSVMBundle) {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath.Child("conditions"), newSVMBundle.Status.Conditions, "Success condition cannot be set to false once it is true"))
|
||||
}
|
||||
isOldFailed := isFailed(oldSVMBundle)
|
||||
if isOldFailed && !isFailed(newSVMBundle) {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath.Child("conditions"), newSVMBundle.Status.Conditions, "Failed condition cannot be set to false once it is true"))
|
||||
}
|
||||
|
||||
return allErrs
|
||||
}
|
||||
|
||||
func isSuccessful(svm *storagemigration.StorageVersionMigration) bool {
|
||||
successCondition := getCondition(svm, storagemigration.MigrationSucceeded)
|
||||
if successCondition != nil && successCondition.Status == corev1.ConditionTrue {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isFailed(svm *storagemigration.StorageVersionMigration) bool {
|
||||
failedCondition := getCondition(svm, storagemigration.MigrationFailed)
|
||||
if failedCondition != nil && failedCondition.Status == corev1.ConditionTrue {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isRunning(svm *storagemigration.StorageVersionMigration) bool {
|
||||
runningCondition := getCondition(svm, storagemigration.MigrationRunning)
|
||||
if runningCondition != nil && runningCondition.Status == corev1.ConditionTrue {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func getCondition(svm *storagemigration.StorageVersionMigration, conditionType storagemigration.MigrationConditionType) *storagemigration.MigrationCondition {
|
||||
for _, c := range svm.Status.Conditions {
|
||||
if c.Type == conditionType {
|
||||
return &c
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateConditions(conditions []storagemigration.MigrationCondition, fldPath *field.Path) field.ErrorList {
|
||||
var allErrs field.ErrorList
|
||||
|
||||
conditionTypeToFirstIndex := map[string]int{}
|
||||
for i, condition := range conditions {
|
||||
if _, ok := conditionTypeToFirstIndex[string(condition.Type)]; ok {
|
||||
allErrs = append(allErrs, field.Duplicate(fldPath.Index(i).Child("type"), condition.Type))
|
||||
} else {
|
||||
conditionTypeToFirstIndex[string(condition.Type)] = i
|
||||
}
|
||||
|
||||
allErrs = append(allErrs, validateCondition(condition, fldPath.Index(i))...)
|
||||
}
|
||||
|
||||
return allErrs
|
||||
}
|
||||
|
||||
func validateCondition(condition storagemigration.MigrationCondition, fldPath *field.Path) field.ErrorList {
|
||||
var allErrs field.ErrorList
|
||||
var validConditionStatuses = sets.NewString(string(metav1.ConditionTrue), string(metav1.ConditionFalse), string(metav1.ConditionUnknown))
|
||||
|
||||
// type is set and is a valid format
|
||||
allErrs = append(allErrs, metav1validation.ValidateLabelName(string(condition.Type), fldPath.Child("type"))...)
|
||||
|
||||
// status is set and is an accepted value
|
||||
if !validConditionStatuses.Has(string(condition.Status)) {
|
||||
allErrs = append(allErrs, field.NotSupported(fldPath.Child("status"), condition.Status, validConditionStatuses.List()))
|
||||
}
|
||||
|
||||
if condition.LastUpdateTime.IsZero() {
|
||||
allErrs = append(allErrs, field.Required(fldPath.Child("lastTransitionTime"), "must be set"))
|
||||
}
|
||||
|
||||
if len(condition.Reason) == 0 {
|
||||
allErrs = append(allErrs, field.Required(fldPath.Child("reason"), "must be set"))
|
||||
} else {
|
||||
for _, currErr := range isValidConditionReason(condition.Reason) {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath.Child("reason"), condition.Reason, currErr))
|
||||
}
|
||||
|
||||
const maxReasonLen int = 1 * 1024 // 1024
|
||||
if len(condition.Reason) > maxReasonLen {
|
||||
allErrs = append(allErrs, field.TooLong(fldPath.Child("reason"), condition.Reason, maxReasonLen))
|
||||
}
|
||||
}
|
||||
|
||||
const maxMessageLen int = 32 * 1024 // 32768
|
||||
if len(condition.Message) > maxMessageLen {
|
||||
allErrs = append(allErrs, field.TooLong(fldPath.Child("message"), condition.Message, maxMessageLen))
|
||||
}
|
||||
|
||||
return allErrs
|
||||
}
|
||||
func isValidConditionReason(value string) []string {
|
||||
const conditionReasonFmt string = "[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?"
|
||||
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 '_'"
|
||||
var conditionReasonRegexp = regexp.MustCompile("^" + conditionReasonFmt + "$")
|
||||
|
||||
if !conditionReasonRegexp.MatchString(value) {
|
||||
return []string{validation.RegexError(conditionReasonErrMsg, conditionReasonFmt, "my_name", "MY_NAME", "MyName", "ReasonA,ReasonB", "ReasonA:ReasonB")}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkAndAppendError(allErrs field.ErrorList, fieldPath *field.Path, value string, message string) field.ErrorList {
|
||||
if len(value) == 0 {
|
||||
allErrs = append(allErrs, field.Required(fieldPath, message))
|
||||
}
|
||||
return allErrs
|
||||
}
|
||||
|
||||
func convertResourceVersionToInt(rv string) (int64, error) {
|
||||
// initial value of RV is expected to be empty, which means the resource version is not set
|
||||
if len(rv) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
resourceVersion, err := strconv.ParseInt(rv, 10, 64)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to parse resource version %q: %w", rv, err)
|
||||
}
|
||||
|
||||
return resourceVersion, nil
|
||||
}
|
Reference in New Issue
Block a user