357 lines
15 KiB
Go
357 lines
15 KiB
Go
/*
|
|
Copyright 2021 The Karmada 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"
|
|
|
|
"github.com/go-openapi/jsonpointer"
|
|
corev1 "k8s.io/api/core/v1"
|
|
apivalidation "k8s.io/apimachinery/pkg/api/validation"
|
|
metav1validation "k8s.io/apimachinery/pkg/apis/meta/v1/validation"
|
|
"k8s.io/apimachinery/pkg/util/validation"
|
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
|
"k8s.io/utils/ptr"
|
|
|
|
policyv1alpha1 "github.com/karmada-io/karmada/pkg/apis/policy/v1alpha1"
|
|
"github.com/karmada-io/karmada/pkg/util"
|
|
)
|
|
|
|
// ValidatePropagationSpec validates a PropagationSpec before creation or update.
|
|
func ValidatePropagationSpec(spec policyv1alpha1.PropagationSpec) field.ErrorList {
|
|
var allErrs field.ErrorList
|
|
allErrs = append(allErrs, ValidatePlacement(spec.Placement, field.NewPath("spec").Child("placement"))...)
|
|
if spec.Failover != nil && spec.Failover.Application != nil && !spec.PropagateDeps {
|
|
allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("propagateDeps"), spec.PropagateDeps, "application failover is set, propagateDeps must be true"))
|
|
}
|
|
allErrs = append(allErrs, ValidateFailover(spec.Failover, field.NewPath("spec").Child("failover"))...)
|
|
allErrs = append(allErrs, validateResourceSelectorsIfPreemptionEnabled(spec, field.NewPath("spec").Child("resourceSelectors"))...)
|
|
allErrs = append(allErrs, validateSuspension(spec.Suspension, field.NewPath("spec").Child("suspension"))...)
|
|
return allErrs
|
|
}
|
|
|
|
// validateResourceSelectorsIfPreemptionEnabled validates ResourceSelectors if Preemption is Always.
|
|
func validateResourceSelectorsIfPreemptionEnabled(spec policyv1alpha1.PropagationSpec, fldPath *field.Path) field.ErrorList {
|
|
if spec.Preemption != policyv1alpha1.PreemptAlways {
|
|
return nil
|
|
}
|
|
|
|
var allErrs field.ErrorList
|
|
for index, resourceSelector := range spec.ResourceSelectors {
|
|
if len(resourceSelector.Name) == 0 {
|
|
allErrs = append(allErrs, field.Invalid(fldPath.Index(index).Child("name"), resourceSelector.Name, "name cannot be empty if preemption is Always, the empty name may cause unexpected resources preemption"))
|
|
}
|
|
}
|
|
return allErrs
|
|
}
|
|
|
|
// validateSuspension validates no conflicts between dispatching and dispatchingOnClusters.
|
|
func validateSuspension(suspension *policyv1alpha1.Suspension, fldPath *field.Path) field.ErrorList {
|
|
if suspension == nil {
|
|
return nil
|
|
}
|
|
|
|
if (suspension.Dispatching != nil && *suspension.Dispatching) &&
|
|
(suspension.DispatchingOnClusters != nil && len(suspension.DispatchingOnClusters.ClusterNames) > 0) {
|
|
return field.ErrorList{
|
|
field.Invalid(fldPath.Child("suspension"), suspension, "suspension dispatching cannot co-exist with dispatchingOnClusters.clusterNames"),
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ValidatePlacement validates a placement before creation or update.
|
|
func ValidatePlacement(placement policyv1alpha1.Placement, fldPath *field.Path) field.ErrorList {
|
|
var allErrs field.ErrorList
|
|
|
|
if placement.ClusterAffinity != nil && placement.ClusterAffinities != nil {
|
|
allErrs = append(allErrs, field.Invalid(fldPath, placement, "clusterAffinities cannot co-exist with clusterAffinity"))
|
|
}
|
|
|
|
allErrs = append(allErrs, ValidateClusterAffinity(placement.ClusterAffinity, fldPath.Child("clusterAffinity"))...)
|
|
allErrs = append(allErrs, ValidateClusterAffinities(placement.ClusterAffinities, fldPath.Child("clusterAffinities"))...)
|
|
allErrs = append(allErrs, ValidateSpreadConstraint(placement.SpreadConstraints, fldPath.Child("spreadConstraints"))...)
|
|
return allErrs
|
|
}
|
|
|
|
// ValidateClusterAffinity validates a clusterAffinity before creation or update.
|
|
func ValidateClusterAffinity(affinity *policyv1alpha1.ClusterAffinity, fldPath *field.Path) field.ErrorList {
|
|
if affinity == nil {
|
|
return nil
|
|
}
|
|
|
|
var allErrs field.ErrorList
|
|
err := ValidatePolicyFieldSelector(affinity.FieldSelector)
|
|
if err != nil {
|
|
allErrs = append(allErrs, field.Invalid(fldPath.Child("fieldSelector"), affinity.FieldSelector, err.Error()))
|
|
}
|
|
return allErrs
|
|
}
|
|
|
|
// ValidateClusterAffinities validates clusterAffinities before creation or update.
|
|
func ValidateClusterAffinities(affinities []policyv1alpha1.ClusterAffinityTerm, fldPath *field.Path) field.ErrorList {
|
|
var allErrs field.ErrorList
|
|
|
|
affinityNames := make(map[string]bool)
|
|
for index := range affinities {
|
|
for _, err := range validation.IsQualifiedName(affinities[index].AffinityName) {
|
|
allErrs = append(allErrs, field.Invalid(fldPath.Index(index), affinities[index].AffinityName, err))
|
|
}
|
|
if _, exist := affinityNames[affinities[index].AffinityName]; exist {
|
|
allErrs = append(allErrs, field.Invalid(fldPath, affinities, "each affinity term in a policy must have a unique name"))
|
|
} else {
|
|
affinityNames[affinities[index].AffinityName] = true
|
|
}
|
|
|
|
allErrs = append(allErrs, ValidateClusterAffinity(&affinities[index].ClusterAffinity, fldPath.Index(index))...)
|
|
}
|
|
return allErrs
|
|
}
|
|
|
|
// ValidatePolicyFieldSelector tests if the fieldSelector of propagation policy or override policy is valid.
|
|
func ValidatePolicyFieldSelector(fieldSelector *policyv1alpha1.FieldSelector) error {
|
|
if fieldSelector == nil {
|
|
return nil
|
|
}
|
|
|
|
for _, matchExpression := range fieldSelector.MatchExpressions {
|
|
switch matchExpression.Key {
|
|
case util.ProviderField, util.RegionField, util.ZoneField:
|
|
default:
|
|
return fmt.Errorf("unsupported key %q, must be provider, region, or zone", matchExpression.Key)
|
|
}
|
|
|
|
switch matchExpression.Operator {
|
|
case corev1.NodeSelectorOpIn, corev1.NodeSelectorOpNotIn:
|
|
default:
|
|
return fmt.Errorf("unsupported operator %q, must be In or NotIn", matchExpression.Operator)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ValidateSpreadConstraint tests if the constraints is valid.
|
|
func ValidateSpreadConstraint(spreadConstraints []policyv1alpha1.SpreadConstraint, fldPath *field.Path) field.ErrorList {
|
|
var allErrs field.ErrorList
|
|
|
|
spreadByFieldsWithErrorMark := make(map[policyv1alpha1.SpreadFieldValue]*bool)
|
|
for index, constraint := range spreadConstraints {
|
|
// SpreadByField and SpreadByLabel should not co-exist
|
|
if len(constraint.SpreadByField) > 0 && len(constraint.SpreadByLabel) > 0 {
|
|
allErrs = append(allErrs, field.Invalid(fldPath.Index(index), constraint, "spreadByLabel should not co-exist with spreadByField"))
|
|
}
|
|
|
|
// If MinGroups provided, it should not be lower than 0.
|
|
if constraint.MinGroups < 0 {
|
|
allErrs = append(allErrs, field.Invalid(fldPath.Index(index), constraint, "minGroups lower than 0 is not allowed"))
|
|
}
|
|
|
|
// If MaxGroups provided, it should not be lower than 0.
|
|
if constraint.MaxGroups < 0 {
|
|
allErrs = append(allErrs, field.Invalid(fldPath.Index(index), constraint, "maxGroups lower than 0 is not allowed"))
|
|
}
|
|
|
|
// If MaxGroups provided, it should greater or equal than MinGroups.
|
|
if constraint.MaxGroups > 0 && constraint.MaxGroups < constraint.MinGroups {
|
|
allErrs = append(allErrs, field.Invalid(fldPath.Index(index), constraint, "maxGroups lower than minGroups is not allowed"))
|
|
}
|
|
|
|
if len(constraint.SpreadByField) > 0 {
|
|
marked := spreadByFieldsWithErrorMark[constraint.SpreadByField]
|
|
if !ptr.Deref[bool](marked, true) {
|
|
allErrs = append(allErrs, field.Invalid(fldPath, spreadConstraints, fmt.Sprintf("multiple %s spread constraints are not allowed", constraint.SpreadByField)))
|
|
*marked = true
|
|
}
|
|
if marked == nil {
|
|
spreadByFieldsWithErrorMark[constraint.SpreadByField] = ptr.To[bool](false)
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(spreadByFieldsWithErrorMark) > 0 {
|
|
// If one of spread constraints are using 'SpreadByField', the 'SpreadByFieldCluster' must be included.
|
|
// For example, when using 'SpreadByFieldRegion' to specify region groups, at the meantime, you must use
|
|
// 'SpreadByFieldCluster' to specify how many clusters should be selected.
|
|
if _, ok := spreadByFieldsWithErrorMark[policyv1alpha1.SpreadByFieldCluster]; !ok {
|
|
allErrs = append(allErrs, field.Invalid(fldPath, spreadConstraints, "the cluster spread constraint must be enabled in one of the constraints in case of SpreadByField is enabled"))
|
|
}
|
|
}
|
|
|
|
return allErrs
|
|
}
|
|
|
|
// ValidateFailover validates that the failoverBehavior is correctly defined.
|
|
func ValidateFailover(failoverBehavior *policyv1alpha1.FailoverBehavior, fldPath *field.Path) field.ErrorList {
|
|
var allErrs field.ErrorList
|
|
|
|
if failoverBehavior == nil {
|
|
return nil
|
|
}
|
|
|
|
allErrs = append(allErrs, ValidateApplicationFailover(failoverBehavior.Application, fldPath.Child("application"))...)
|
|
return allErrs
|
|
}
|
|
|
|
// ValidateApplicationFailover validates that the application failover is correctly defined.
|
|
func ValidateApplicationFailover(applicationFailoverBehavior *policyv1alpha1.ApplicationFailoverBehavior, fldPath *field.Path) field.ErrorList {
|
|
var allErrs field.ErrorList
|
|
|
|
if applicationFailoverBehavior == nil {
|
|
return nil
|
|
}
|
|
|
|
if *applicationFailoverBehavior.DecisionConditions.TolerationSeconds < 0 {
|
|
allErrs = append(allErrs, field.Invalid(fldPath.Child("decisionConditions").Child("tolerationSeconds"), *applicationFailoverBehavior.DecisionConditions.TolerationSeconds, "must be greater than or equal to 0"))
|
|
}
|
|
|
|
if applicationFailoverBehavior.PurgeMode != policyv1alpha1.Graciously && applicationFailoverBehavior.GracePeriodSeconds != nil {
|
|
allErrs = append(allErrs, field.Invalid(fldPath.Child("gracePeriodSeconds"), *applicationFailoverBehavior.GracePeriodSeconds, "only takes effect when purgeMode is graciously"))
|
|
}
|
|
|
|
if applicationFailoverBehavior.PurgeMode == policyv1alpha1.Graciously && applicationFailoverBehavior.GracePeriodSeconds == nil {
|
|
allErrs = append(allErrs, field.Invalid(fldPath.Child("gracePeriodSeconds"), applicationFailoverBehavior.GracePeriodSeconds, "should not be empty when purgeMode is graciously"))
|
|
}
|
|
|
|
if applicationFailoverBehavior.GracePeriodSeconds != nil && *applicationFailoverBehavior.GracePeriodSeconds <= 0 {
|
|
allErrs = append(allErrs, field.Invalid(fldPath.Child("gracePeriodSeconds"), *applicationFailoverBehavior.GracePeriodSeconds, "must be greater than 0"))
|
|
}
|
|
|
|
return allErrs
|
|
}
|
|
|
|
// ValidateOverrideSpec validates that the overrider specification is correctly defined.
|
|
func ValidateOverrideSpec(overrideSpec *policyv1alpha1.OverrideSpec) field.ErrorList {
|
|
var allErrs field.ErrorList
|
|
if overrideSpec == nil {
|
|
return nil
|
|
}
|
|
specPath := field.NewPath("spec")
|
|
//nolint:staticcheck
|
|
// disable `deprecation` check for backward compatibility.
|
|
if overrideSpec.TargetCluster != nil {
|
|
allErrs = append(allErrs, ValidateClusterAffinity(overrideSpec.TargetCluster, specPath.Child("targetCluster"))...)
|
|
}
|
|
//nolint:staticcheck
|
|
// disable `deprecation` check for backward compatibility.
|
|
if overrideSpec.TargetCluster != nil && overrideSpec.OverrideRules != nil {
|
|
allErrs = append(allErrs, field.Invalid(specPath.Child("targetCluster"), overrideSpec.TargetCluster, "overrideRules and targetCluster can't co-exist"))
|
|
}
|
|
//nolint:staticcheck
|
|
// disable `deprecation` check for backward compatibility.
|
|
if !emptyOverrides(overrideSpec.Overriders) && overrideSpec.OverrideRules != nil {
|
|
allErrs = append(allErrs, field.Invalid(specPath.Child("overriders"), overrideSpec.Overriders, "overrideRules and overriders can't co-exist"))
|
|
}
|
|
allErrs = append(allErrs, ValidateOverrideRules(overrideSpec.OverrideRules, specPath)...)
|
|
return allErrs
|
|
}
|
|
|
|
// emptyOverrides check if the overriders of override policy is empty.
|
|
func emptyOverrides(overriders policyv1alpha1.Overriders) bool {
|
|
if len(overriders.Plaintext) != 0 {
|
|
return false
|
|
}
|
|
if len(overriders.ImageOverrider) != 0 {
|
|
return false
|
|
}
|
|
if len(overriders.CommandOverrider) != 0 {
|
|
return false
|
|
}
|
|
if len(overriders.ArgsOverrider) != 0 {
|
|
return false
|
|
}
|
|
if len(overriders.LabelsOverrider) != 0 {
|
|
return false
|
|
}
|
|
if len(overriders.AnnotationsOverrider) != 0 {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// ValidateOverrideRules validates the overrideRules of override policy.
|
|
func ValidateOverrideRules(overrideRules []policyv1alpha1.RuleWithCluster, fldPath *field.Path) field.ErrorList {
|
|
var allErrs field.ErrorList
|
|
for overrideRuleIndex, rule := range overrideRules {
|
|
rulePath := fldPath.Child("overrideRules").Index(overrideRuleIndex)
|
|
|
|
// validates provided annotations.
|
|
for annotationIndex, annotation := range rule.Overriders.AnnotationsOverrider {
|
|
annotationPath := rulePath.Child("overriders").Child("annotationsOverrider").Index(annotationIndex)
|
|
allErrs = append(allErrs, apivalidation.ValidateAnnotations(annotation.Value, annotationPath.Child("value"))...)
|
|
}
|
|
|
|
// validates provided labels.
|
|
for labelIndex, label := range rule.Overriders.LabelsOverrider {
|
|
labelPath := rulePath.Child("overriders").Child("labelsOverrider").Index(labelIndex)
|
|
allErrs = append(allErrs, metav1validation.ValidateLabels(label.Value, labelPath.Child("value"))...)
|
|
}
|
|
|
|
// validates predicate path.
|
|
for imageIndex, image := range rule.Overriders.ImageOverrider {
|
|
imagePath := rulePath.Child("overriders").Child("imageOverrider").Index(imageIndex)
|
|
if image.Predicate != nil {
|
|
if _, err := jsonpointer.New(image.Predicate.Path); err != nil {
|
|
allErrs = append(allErrs, field.Invalid(imagePath.Child("predicate").Child("path"), image.Predicate.Path, err.Error()))
|
|
}
|
|
}
|
|
}
|
|
|
|
for fieldIndex, fieldOverrider := range rule.Overriders.FieldOverrider {
|
|
fieldPath := rulePath.Child("overriders").Child("fieldOverrider").Index(fieldIndex)
|
|
// validates that either YAML or JSON is selected for each field overrider.
|
|
if len(fieldOverrider.YAML) > 0 && len(fieldOverrider.JSON) > 0 {
|
|
allErrs = append(allErrs, field.Invalid(fieldPath, fieldOverrider, "FieldOverrider has both YAML and JSON set. Only one is allowed"))
|
|
}
|
|
// validates the field path.
|
|
if _, err := jsonpointer.New(fieldOverrider.FieldPath); err != nil {
|
|
allErrs = append(allErrs, field.Invalid(fieldPath.Child("fieldPath"), fieldOverrider.FieldPath, err.Error()))
|
|
}
|
|
// validates the JSON patch operations sub path.
|
|
allErrs = append(allErrs, validateJSONPatchSubPaths(fieldOverrider.JSON, fieldPath.Child("json"))...)
|
|
// validates the YAML patch operations sub path.
|
|
allErrs = append(allErrs, validateYAMLPatchSubPaths(fieldOverrider.YAML, fieldPath.Child("yaml"))...)
|
|
}
|
|
|
|
// validates the targetCluster.
|
|
allErrs = append(allErrs, ValidateClusterAffinity(rule.TargetCluster, rulePath.Child("targetCluster"))...)
|
|
}
|
|
return allErrs
|
|
}
|
|
|
|
func validateJSONPatchSubPaths(patches []policyv1alpha1.JSONPatchOperation, fieldPath *field.Path) field.ErrorList {
|
|
var allErrs field.ErrorList
|
|
for index, patch := range patches {
|
|
patchPath := fieldPath.Index(index)
|
|
if _, err := jsonpointer.New(patch.SubPath); err != nil {
|
|
allErrs = append(allErrs, field.Invalid(patchPath.Child("subPath"), patch.SubPath, err.Error()))
|
|
}
|
|
}
|
|
return allErrs
|
|
}
|
|
|
|
func validateYAMLPatchSubPaths(patches []policyv1alpha1.YAMLPatchOperation, fieldPath *field.Path) field.ErrorList {
|
|
var allErrs field.ErrorList
|
|
for index, patch := range patches {
|
|
patchPath := fieldPath.Index(index)
|
|
if _, err := jsonpointer.New(patch.SubPath); err != nil {
|
|
allErrs = append(allErrs, field.Invalid(patchPath.Child("subPath"), patch.SubPath, err.Error()))
|
|
}
|
|
}
|
|
return allErrs
|
|
}
|