karmada/pkg/util/validation/validation_test.go

648 lines
19 KiB
Go

package validation
import (
"strings"
"testing"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/utils/pointer"
policyv1alpha1 "github.com/karmada-io/karmada/pkg/apis/policy/v1alpha1"
"github.com/karmada-io/karmada/pkg/util"
)
func TestValidateOverrideSpec(t *testing.T) {
var tests = []struct {
name string
overrideSpec policyv1alpha1.OverrideSpec
expectError bool
}{
{
name: "overrideRules is set, overriders and targetCluster aren't set",
overrideSpec: policyv1alpha1.OverrideSpec{
OverrideRules: []policyv1alpha1.RuleWithCluster{
{
TargetCluster: &policyv1alpha1.ClusterAffinity{
ClusterNames: []string{"cluster-name"},
},
},
},
},
expectError: false,
},
{
name: "overriders and targetCluster are set, overrideRules isn't set",
overrideSpec: policyv1alpha1.OverrideSpec{
TargetCluster: &policyv1alpha1.ClusterAffinity{
ClusterNames: []string{"cluster-name"},
},
Overriders: policyv1alpha1.Overriders{
Plaintext: []policyv1alpha1.PlaintextOverrider{
{
Path: "spec/image",
},
},
},
},
expectError: false,
},
{
name: "overrideRules and targetCluster can't co-exist",
overrideSpec: policyv1alpha1.OverrideSpec{
OverrideRules: []policyv1alpha1.RuleWithCluster{
{
TargetCluster: &policyv1alpha1.ClusterAffinity{
ClusterNames: []string{"cluster-name"},
},
},
},
TargetCluster: &policyv1alpha1.ClusterAffinity{
ClusterNames: []string{"cluster-name"},
},
},
expectError: true,
},
{
name: "overrideRules and overriders can't co-exist",
overrideSpec: policyv1alpha1.OverrideSpec{
OverrideRules: []policyv1alpha1.RuleWithCluster{
{
TargetCluster: &policyv1alpha1.ClusterAffinity{
ClusterNames: []string{"cluster-name"},
},
},
},
Overriders: policyv1alpha1.Overriders{
Plaintext: []policyv1alpha1.PlaintextOverrider{
{
Path: "spec/image",
},
},
},
},
expectError: true,
},
{
name: "overrideRules, targetCluster and overriders can't co-exist",
overrideSpec: policyv1alpha1.OverrideSpec{
OverrideRules: []policyv1alpha1.RuleWithCluster{
{
TargetCluster: &policyv1alpha1.ClusterAffinity{
ClusterNames: []string{"cluster-name"},
},
},
},
TargetCluster: &policyv1alpha1.ClusterAffinity{
ClusterNames: []string{"cluster-name"},
},
Overriders: policyv1alpha1.Overriders{
Plaintext: []policyv1alpha1.PlaintextOverrider{
{
Path: "spec/image",
},
},
},
},
expectError: true,
},
{
name: "invalid annotation should not be allowed",
overrideSpec: policyv1alpha1.OverrideSpec{
OverrideRules: []policyv1alpha1.RuleWithCluster{
{
TargetCluster: &policyv1alpha1.ClusterAffinity{
ClusterNames: []string{"cluster-name"},
},
Overriders: policyv1alpha1.Overriders{
AnnotationsOverrider: []policyv1alpha1.LabelAnnotationOverrider{
{
Operator: "add",
Value: map[string]string{"testannotation~projectId": "c-m-lfx9lk92p-v86cf"},
},
},
},
},
},
},
expectError: true,
},
{
name: "invalid label should not be allowed",
overrideSpec: policyv1alpha1.OverrideSpec{
OverrideRules: []policyv1alpha1.RuleWithCluster{
{
TargetCluster: &policyv1alpha1.ClusterAffinity{
ClusterNames: []string{"cluster-name"},
},
Overriders: policyv1alpha1.Overriders{
LabelsOverrider: []policyv1alpha1.LabelAnnotationOverrider{
{
Operator: "add",
Value: map[string]string{"testannotation~projectId": "c-m-lfx9lk92p-v86cf"},
},
},
},
},
},
},
expectError: true,
},
{
name: "overrideSpec.targetCluster.fieldSelector has unsupported key",
overrideSpec: policyv1alpha1.OverrideSpec{
TargetCluster: &policyv1alpha1.ClusterAffinity{
FieldSelector: &policyv1alpha1.FieldSelector{
MatchExpressions: []corev1.NodeSelectorRequirement{
{
Key: "foo",
Operator: corev1.NodeSelectorOpIn,
Values: []string{"fooCloud"},
}}}},
},
expectError: true,
},
{
name: "overrideSpec.targetCluster.fieldSelector has unsupported operator",
overrideSpec: policyv1alpha1.OverrideSpec{
TargetCluster: &policyv1alpha1.ClusterAffinity{
FieldSelector: &policyv1alpha1.FieldSelector{
MatchExpressions: []corev1.NodeSelectorRequirement{
{
Key: util.ProviderField,
Operator: corev1.NodeSelectorOpGt,
Values: []string{"fooCloud"},
}}}},
},
expectError: true,
},
{
name: "overrideRules.[index].targetCluster.fieldSelector has unsupported key",
overrideSpec: policyv1alpha1.OverrideSpec{
OverrideRules: []policyv1alpha1.RuleWithCluster{
{
TargetCluster: &policyv1alpha1.ClusterAffinity{
FieldSelector: &policyv1alpha1.FieldSelector{
MatchExpressions: []corev1.NodeSelectorRequirement{
{
Key: "foo",
Operator: corev1.NodeSelectorOpIn,
Values: []string{"fooCloud"},
}}},
},
},
},
},
expectError: true,
},
{
name: "overrideRules.[index].targetCluster.fieldSelector has unsupported operator",
overrideSpec: policyv1alpha1.OverrideSpec{
OverrideRules: []policyv1alpha1.RuleWithCluster{
{
TargetCluster: &policyv1alpha1.ClusterAffinity{
FieldSelector: &policyv1alpha1.FieldSelector{
MatchExpressions: []corev1.NodeSelectorRequirement{
{
Key: util.ProviderField,
Operator: corev1.NodeSelectorOpGt,
Values: []string{"fooCloud"},
}}},
},
},
},
},
expectError: true,
},
}
for _, test := range tests {
tc := test
err := ValidateOverrideSpec(&tc.overrideSpec)
if err != nil && tc.expectError != true {
t.Fatalf("expect no error but got: %v", err)
}
if err == nil && tc.expectError == true {
t.Fatalf("expect an error but got none")
}
}
}
func TestEmptyOverrides(t *testing.T) {
tests := []struct {
name string
overriders policyv1alpha1.Overriders
want bool
}{
{
name: "empty overrides",
overriders: policyv1alpha1.Overriders{},
want: true,
},
{
name: "non-empty overrides",
overriders: policyv1alpha1.Overriders{
Plaintext: []policyv1alpha1.PlaintextOverrider{
{
Path: "spec/image",
},
},
},
want: false,
},
{
name: "non-empty overrides",
overriders: policyv1alpha1.Overriders{
Plaintext: []policyv1alpha1.PlaintextOverrider{
{
Path: "spec/image",
},
},
ImageOverrider: []policyv1alpha1.ImageOverrider{
{
Component: "Registry",
Operator: "remove",
Value: "fictional.registry.us",
},
},
CommandOverrider: []policyv1alpha1.CommandArgsOverrider{
{
ContainerName: "nginx",
Operator: "add",
Value: []string{"echo 'hello karmada'"},
},
},
},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := emptyOverrides(tt.overriders); got != tt.want {
t.Errorf("EmptyOverrides() = %v, want %v", got, tt.want)
}
})
}
}
func TestValidatePropagationSpec(t *testing.T) {
tests := []struct {
name string
spec policyv1alpha1.PropagationSpec
expectedErr string
}{
{
name: "valid spec",
spec: policyv1alpha1.PropagationSpec{
Placement: policyv1alpha1.Placement{
ClusterAffinity: &policyv1alpha1.ClusterAffinity{
FieldSelector: &policyv1alpha1.FieldSelector{
MatchExpressions: []corev1.NodeSelectorRequirement{
{
Key: util.ProviderField,
Operator: corev1.NodeSelectorOpIn,
Values: []string{"fooCloud"},
},
{
Key: util.RegionField,
Operator: corev1.NodeSelectorOpNotIn,
Values: []string{"fooCloud"},
},
{
Key: util.ZoneField,
Operator: corev1.NodeSelectorOpNotIn,
Values: []string{"fooCloud"},
},
}}},
SpreadConstraints: []policyv1alpha1.SpreadConstraint{
{
MaxGroups: 2,
MinGroups: 1,
SpreadByField: policyv1alpha1.SpreadByFieldRegion,
},
{
SpreadByField: policyv1alpha1.SpreadByFieldCluster,
}}}},
expectedErr: "",
},
{
name: "clusterAffinity.fieldSelector has unsupported key",
spec: policyv1alpha1.PropagationSpec{
Placement: policyv1alpha1.Placement{
ClusterAffinity: &policyv1alpha1.ClusterAffinity{
FieldSelector: &policyv1alpha1.FieldSelector{
MatchExpressions: []corev1.NodeSelectorRequirement{
{
Key: "foo",
Operator: corev1.NodeSelectorOpIn,
Values: []string{"fooCloud"},
}}}}}},
expectedErr: "unsupported key \"foo\", must be provider, region, or zone",
},
{
name: "clusterAffinity.fieldSelector has unsupported operator",
spec: policyv1alpha1.PropagationSpec{
Placement: policyv1alpha1.Placement{
ClusterAffinity: &policyv1alpha1.ClusterAffinity{
FieldSelector: &policyv1alpha1.FieldSelector{
MatchExpressions: []corev1.NodeSelectorRequirement{
{
Key: util.ProviderField,
Operator: corev1.NodeSelectorOpExists,
Values: []string{"fooCloud"},
}}}}}},
expectedErr: "unsupported operator \"Exists\", must be In or NotIn",
},
{
name: "clusterAffinities can not co-exist with clusterAffinity",
spec: policyv1alpha1.PropagationSpec{
Placement: policyv1alpha1.Placement{
ClusterAffinity: &policyv1alpha1.ClusterAffinity{
ClusterNames: []string{"m1"},
},
ClusterAffinities: []policyv1alpha1.ClusterAffinityTerm{
{
AffinityName: "group1",
ClusterAffinity: policyv1alpha1.ClusterAffinity{
ClusterNames: []string{"m1"},
}}}}},
expectedErr: "clusterAffinities can not co-exist with clusterAffinity",
},
{
name: "clusterAffinities different affinities have the same affinityName",
spec: policyv1alpha1.PropagationSpec{
Placement: policyv1alpha1.Placement{
ClusterAffinities: []policyv1alpha1.ClusterAffinityTerm{
{
AffinityName: "group1",
ClusterAffinity: policyv1alpha1.ClusterAffinity{
ClusterNames: []string{"m1"},
}},
{
AffinityName: "group1",
ClusterAffinity: policyv1alpha1.ClusterAffinity{
ClusterNames: []string{"m2"},
}}}}},
expectedErr: "each affinity term in a policy must have a unique name",
},
{
name: "clusterAffinities.[index].clusterAffinity.fieldSelector has unsupported key",
spec: policyv1alpha1.PropagationSpec{
Placement: policyv1alpha1.Placement{
ClusterAffinities: []policyv1alpha1.ClusterAffinityTerm{
{
AffinityName: "group1",
ClusterAffinity: policyv1alpha1.ClusterAffinity{
FieldSelector: &policyv1alpha1.FieldSelector{
MatchExpressions: []corev1.NodeSelectorRequirement{
{
Key: "foo",
Operator: corev1.NodeSelectorOpIn,
Values: []string{"fooCloud"},
}}}}}}}},
expectedErr: "unsupported key \"foo\", must be provider, region, or zone",
},
{
name: "clusterAffinities.[index].clusterAffinity.fieldSelector has unsupported operator",
spec: policyv1alpha1.PropagationSpec{
Placement: policyv1alpha1.Placement{
ClusterAffinities: []policyv1alpha1.ClusterAffinityTerm{
{
AffinityName: "group1",
ClusterAffinity: policyv1alpha1.ClusterAffinity{
FieldSelector: &policyv1alpha1.FieldSelector{
MatchExpressions: []corev1.NodeSelectorRequirement{
{
Key: util.ProviderField,
Operator: corev1.NodeSelectorOpExists,
Values: []string{"fooCloud"},
}}}}}}}},
expectedErr: "unsupported operator \"Exists\", must be In or NotIn",
},
{
name: "spreadConstraint spreadByLabel co-exist with spreadByField",
spec: policyv1alpha1.PropagationSpec{
Placement: policyv1alpha1.Placement{
SpreadConstraints: []policyv1alpha1.SpreadConstraint{
{
SpreadByField: policyv1alpha1.SpreadByFieldCluster,
SpreadByLabel: "foo",
},
},
}},
expectedErr: "spreadByLabel should not co-exist with spreadByField",
},
{
name: "spreadConstraint maxGroups lower than minGroups",
spec: policyv1alpha1.PropagationSpec{
Placement: policyv1alpha1.Placement{
SpreadConstraints: []policyv1alpha1.SpreadConstraint{
{
MaxGroups: 1,
MinGroups: 2,
},
},
}},
expectedErr: "maxGroups lower than minGroups is not allowed",
},
{
name: "spreadConstraint maxGroups lower than 0",
spec: policyv1alpha1.PropagationSpec{
Placement: policyv1alpha1.Placement{
SpreadConstraints: []policyv1alpha1.SpreadConstraint{
{
MaxGroups: -1,
MinGroups: 1,
},
},
}},
expectedErr: "maxGroups lower than 0 is not allowed",
},
{
name: "spreadConstraint minGroups lower than 0",
spec: policyv1alpha1.PropagationSpec{
Placement: policyv1alpha1.Placement{
SpreadConstraints: []policyv1alpha1.SpreadConstraint{
{
MaxGroups: 2,
MinGroups: -2,
},
},
}},
expectedErr: "minGroups lower than 0 is not allowed",
},
{
name: "spreadConstraint has two cluster spread constraints",
spec: policyv1alpha1.PropagationSpec{
Placement: policyv1alpha1.Placement{
SpreadConstraints: []policyv1alpha1.SpreadConstraint{
{
SpreadByField: policyv1alpha1.SpreadByFieldCluster,
MaxGroups: 2,
MinGroups: -2,
},
{
SpreadByField: policyv1alpha1.SpreadByFieldCluster,
MaxGroups: 5,
MinGroups: 3,
},
{
SpreadByLabel: "grouped-by-net",
MaxGroups: 5,
MinGroups: 3,
},
},
}},
expectedErr: "multiple cluster spread constraints are not allowed",
},
{
name: "spreadConstraint has multiple region spread constraints",
spec: policyv1alpha1.PropagationSpec{
Placement: policyv1alpha1.Placement{
SpreadConstraints: []policyv1alpha1.SpreadConstraint{
{
SpreadByField: policyv1alpha1.SpreadByFieldRegion,
MaxGroups: 1,
MinGroups: 3,
},
{
SpreadByField: policyv1alpha1.SpreadByFieldRegion,
MaxGroups: 4,
MinGroups: 2,
},
{
SpreadByField: policyv1alpha1.SpreadByFieldRegion,
MaxGroups: 6,
MinGroups: 5,
},
{
SpreadByField: policyv1alpha1.SpreadByFieldCluster,
MaxGroups: 10,
MinGroups: 5,
},
},
}},
expectedErr: "multiple region spread constraints are not allowed",
},
{
name: "spreadConstraint spreadByFieldCluster must be included if using spreadByField",
spec: policyv1alpha1.PropagationSpec{
Placement: policyv1alpha1.Placement{
SpreadConstraints: []policyv1alpha1.SpreadConstraint{
{
SpreadByField: policyv1alpha1.SpreadByFieldRegion,
},
},
}},
expectedErr: "the cluster spread constraint must be enabled in one of the constraints in case of SpreadByField is enabled",
},
{
name: "resourceSelector name is empty when preemption is enabled",
spec: policyv1alpha1.PropagationSpec{
ResourceSelectors: []policyv1alpha1.ResourceSelector{
{
APIVersion: "v1",
Kind: "Pod",
Namespace: "default",
},
},
Preemption: policyv1alpha1.PreemptAlways,
},
expectedErr: "name can not be empty if preemption is Always, the empty name may cause unexpected resources preemption",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
errs := ValidatePropagationSpec(tt.spec)
err := errs.ToAggregate()
if err != nil {
errStr := err.Error()
if tt.expectedErr == "" {
t.Errorf("expected no error:\n but got:\n %s", errStr)
} else if !strings.Contains(errStr, tt.expectedErr) {
t.Errorf("expected to contain:\n %s\ngot:\n %s", tt.expectedErr, errStr)
}
} else {
if tt.expectedErr != "" {
t.Errorf("unexpected no error, expected to contain:\n %s", tt.expectedErr)
}
}
})
}
}
func TestValidateApplicationFailover(t *testing.T) {
tests := []struct {
name string
applicationFailoverBehavior *policyv1alpha1.ApplicationFailoverBehavior
expectedErr string
}{
{
name: "application failover is nil",
applicationFailoverBehavior: nil,
expectedErr: "",
},
{
name: "the tolerationSeconds is less than zero",
applicationFailoverBehavior: &policyv1alpha1.ApplicationFailoverBehavior{
DecisionConditions: policyv1alpha1.DecisionConditions{
TolerationSeconds: pointer.Int32(-100),
},
},
expectedErr: "spec.failover.application.decisionConditions.tolerationSeconds: Invalid value: -100: must be greater than or equal to 0",
},
{
name: "the gracePeriodSeconds is declared when purgeMode is not graciously",
applicationFailoverBehavior: &policyv1alpha1.ApplicationFailoverBehavior{
DecisionConditions: policyv1alpha1.DecisionConditions{
TolerationSeconds: pointer.Int32(100),
},
PurgeMode: policyv1alpha1.Immediately,
GracePeriodSeconds: pointer.Int32(100),
},
expectedErr: "spec.failover.application.gracePeriodSeconds: Invalid value: 100: only takes effect when purgeMode is graciously",
},
{
name: "the gracePeriodSeconds is less than 0 when purgeMode is graciously",
applicationFailoverBehavior: &policyv1alpha1.ApplicationFailoverBehavior{
DecisionConditions: policyv1alpha1.DecisionConditions{
TolerationSeconds: pointer.Int32(100),
},
PurgeMode: policyv1alpha1.Graciously,
GracePeriodSeconds: pointer.Int32(-100),
},
expectedErr: "spec.failover.application.gracePeriodSeconds: Invalid value: -100: must be greater than 0",
},
{
name: "the gracePeriodSeconds is empty when purgeMode is graciously",
applicationFailoverBehavior: &policyv1alpha1.ApplicationFailoverBehavior{
DecisionConditions: policyv1alpha1.DecisionConditions{
TolerationSeconds: pointer.Int32(100),
},
PurgeMode: policyv1alpha1.Graciously,
},
expectedErr: "spec.failover.application.gracePeriodSeconds: Invalid value: \"null\": should not be empty when purgeMode is graciously",
},
{
name: "application behavior is correctly declared",
applicationFailoverBehavior: &policyv1alpha1.ApplicationFailoverBehavior{
DecisionConditions: policyv1alpha1.DecisionConditions{
TolerationSeconds: pointer.Int32(100),
},
},
expectedErr: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
errs := ValidateApplicationFailover(tt.applicationFailoverBehavior, field.NewPath("spec").Child("failover").Child("application"))
err := errs.ToAggregate()
if err != nil && err.Error() != tt.expectedErr {
t.Errorf("expected error:\n %s, but got:\n %s", tt.expectedErr, err.Error())
} else if err == nil && tt.expectedErr != "" {
t.Errorf("expected error:\n %s, but got no error\n", tt.expectedErr)
}
})
}
}