228 lines
9.5 KiB
Go
228 lines
9.5 KiB
Go
/*
|
|
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
|
|
}
|