Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 16 additions & 10 deletions pkg/util/interpreter/validation/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,30 @@ limitations under the License.
package validation

import (
"errors"

utilerrors "k8s.io/apimachinery/pkg/util/errors"
metav1validation "k8s.io/apimachinery/pkg/apis/meta/v1/validation"
"k8s.io/apimachinery/pkg/util/validation/field"

configv1alpha1 "github.com/karmada-io/karmada/pkg/apis/config/v1alpha1"
)

// VerifyDependencies verifies dependencies.
func VerifyDependencies(dependencies []configv1alpha1.DependentObjectReference) error {
var errs []error
for _, dependency := range dependencies {
if len(dependency.APIVersion) == 0 || len(dependency.Kind) == 0 {
errs = append(errs, errors.New("dependency missing required apiVersion or kind"))
continue
allErrs := field.ErrorList{}
fldPath := field.NewPath("dependencies")
for i, dependency := range dependencies {
fldPath := fldPath.Index(i)
if len(dependency.APIVersion) == 0 {
allErrs = append(allErrs, field.Invalid(fldPath.Child("apiVersion"), dependency.APIVersion, "missing required apiVersion"))
}
if len(dependency.Kind) == 0 {
allErrs = append(allErrs, field.Invalid(fldPath.Child("kind"), dependency.Kind, "missing required kind"))
}
if len(dependency.Name) == 0 && dependency.LabelSelector == nil {
errs = append(errs, errors.New("dependency can not leave name and labelSelector all empty"))
allErrs = append(allErrs, field.Invalid(fldPath, dependencies[i], "dependency can not leave name and labelSelector all empty"))
}
allErrs = append(allErrs, metav1validation.ValidateLabelSelector(dependency.LabelSelector, metav1validation.LabelSelectorValidationOptions{
AllowInvalidLabelValueInSelector: false,
}, fldPath.Child("labelSelector"))...)
}
return utilerrors.NewAggregate(errs)
return allErrs.ToAggregate()
}
191 changes: 182 additions & 9 deletions pkg/util/interpreter/validation/validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ limitations under the License.
package validation

import (
"strings"
"testing"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand All @@ -29,15 +30,44 @@ func TestVerifyDependencies(t *testing.T) {
dependencies []configv1alpha1.DependentObjectReference
}
tests := []struct {
name string
args args
wantErr bool
name string
args args
wantErr bool
wantErrContains string
}{
{
name: "normal case",
name: "normal case with name only",
args: args{dependencies: []configv1alpha1.DependentObjectReference{
{APIVersion: "v1", Kind: "Foo", Name: "test"},
{APIVersion: "v2", Kind: "Hu", Namespace: "default", LabelSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"bar": "foo"}}},
}},
wantErr: false,
},
{
name: "normal case with labelSelector only",
args: args{dependencies: []configv1alpha1.DependentObjectReference{
{APIVersion: "v1", Kind: "Foo", LabelSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"app": "nginx"}}},
}},
wantErr: false,
},
{
name: "normal case with both name and labelSelector",
args: args{dependencies: []configv1alpha1.DependentObjectReference{
{APIVersion: "v2", Kind: "Hu", Namespace: "default", Name: "test", LabelSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"bar": "foo"}}},
}},
wantErr: false,
},
{
name: "normal case with matchExpressions",
args: args{dependencies: []configv1alpha1.DependentObjectReference{
{
APIVersion: "v1",
Kind: "ConfigMap",
LabelSelector: &metav1.LabelSelector{
MatchExpressions: []metav1.LabelSelectorRequirement{
{Key: "env", Operator: metav1.LabelSelectorOpIn, Values: []string{"prod", "staging"}},
},
},
},
}},
wantErr: false,
},
Expand All @@ -46,27 +76,170 @@ func TestVerifyDependencies(t *testing.T) {
args: args{dependencies: []configv1alpha1.DependentObjectReference{
{Kind: "Foo", Name: "test"},
}},
wantErr: true,
wantErr: true,
wantErrContains: "missing required apiVersion",
},
{
name: "empty kind",
args: args{dependencies: []configv1alpha1.DependentObjectReference{
{APIVersion: "v1", Name: "test"},
}},
wantErr: true,
wantErr: true,
wantErrContains: "missing required kind",
},
{
name: "empty Name and LabelSelector at the same time",
args: args{dependencies: []configv1alpha1.DependentObjectReference{
{APIVersion: "v1", Kind: "Foo"},
}},
wantErr: true,
wantErr: true,
wantErrContains: "dependency can not leave name and labelSelector all empty",
},
{
name: "invalid label key in matchLabels",
args: args{dependencies: []configv1alpha1.DependentObjectReference{
{
APIVersion: "v1",
Kind: "Pod",
LabelSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"-invalid-key": "value",
},
},
},
}},
wantErr: true,
wantErrContains: "dependencies[0].labelSelector.matchLabels",
},
{
name: "invalid label value in matchLabels",
args: args{dependencies: []configv1alpha1.DependentObjectReference{
{
APIVersion: "v1",
Kind: "Pod",
LabelSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"app": "invalid@value",
},
},
},
}},
wantErr: true,
wantErrContains: "dependencies[0].labelSelector.matchLabels",
},
{
name: "invalid operator in matchExpressions",
args: args{dependencies: []configv1alpha1.DependentObjectReference{
{
APIVersion: "v1",
Kind: "Pod",
LabelSelector: &metav1.LabelSelector{
MatchExpressions: []metav1.LabelSelectorRequirement{
{Key: "env", Operator: "InvalidOp", Values: []string{"prod"}},
},
},
},
}},
wantErr: true,
wantErrContains: "not a valid selector operator",
},
{
name: "matchExpressions with In operator but no values",
args: args{dependencies: []configv1alpha1.DependentObjectReference{
{
APIVersion: "v1",
Kind: "Pod",
LabelSelector: &metav1.LabelSelector{
MatchExpressions: []metav1.LabelSelectorRequirement{
{Key: "env", Operator: metav1.LabelSelectorOpIn, Values: []string{}},
},
},
},
}},
wantErr: true,
wantErrContains: "must be specified when `operator` is 'In' or 'NotIn'",
},
{
name: "matchExpressions with Exists operator and values",
args: args{dependencies: []configv1alpha1.DependentObjectReference{
{
APIVersion: "v1",
Kind: "Pod",
LabelSelector: &metav1.LabelSelector{
MatchExpressions: []metav1.LabelSelectorRequirement{
{Key: "env", Operator: metav1.LabelSelectorOpExists, Values: []string{"prod"}},
},
},
},
}},
wantErr: true,
wantErrContains: "may not be specified when `operator` is 'Exists' or 'DoesNotExist'",
},
{
name: "invalid label key in matchExpressions",
args: args{dependencies: []configv1alpha1.DependentObjectReference{
{
APIVersion: "v1",
Kind: "Pod",
LabelSelector: &metav1.LabelSelector{
MatchExpressions: []metav1.LabelSelectorRequirement{
{Key: "@invalid", Operator: metav1.LabelSelectorOpIn, Values: []string{"value"}},
},
},
},
}},
wantErr: true,
wantErrContains: "dependencies[0].labelSelector.matchExpressions[0].key",
},
{
name: "multiple dependencies with mixed errors",
args: args{dependencies: []configv1alpha1.DependentObjectReference{
{APIVersion: "v1", Kind: "Foo", Name: "valid"},
{Kind: "Bar", Name: "missing-apiversion"},
}},
wantErr: true,
wantErrContains: "dependencies[1].apiVersion",
},
{
name: "multiple errors in single dependency",
args: args{dependencies: []configv1alpha1.DependentObjectReference{
{
// Missing both apiVersion and kind
Name: "test",
},
}},
wantErr: true,
wantErrContains: "missing required",
},
{
name: "label value too long",
args: args{dependencies: []configv1alpha1.DependentObjectReference{
{
APIVersion: "v1",
Kind: "Pod",
LabelSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"app": "this-is-a-very-long-label-value-that-exceeds-the-maximum-allowed-length-of-63-characters-for-kubernetes-labels",
},
},
},
}},
wantErr: true,
wantErrContains: "must be no more than 63 characters",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := VerifyDependencies(tt.args.dependencies); (err != nil) != tt.wantErr {
err := VerifyDependencies(tt.args.dependencies)
if (err != nil) != tt.wantErr {
t.Errorf("VerifyDependencies() error = %v, wantErr %v", err, tt.wantErr)
return
}

if tt.wantErr && tt.wantErrContains != "" {
if !strings.Contains(err.Error(), tt.wantErrContains) {
t.Errorf("VerifyDependencies() error = %v, want error containing %q", err, tt.wantErrContains)
}
}
})
}
Expand Down