Skip to content

Commit

Permalink
Merge pull request #5287 from mohamedawnallah/addFederatedResourceQuo…
Browse files Browse the repository at this point in the history
…taValidationTests

pkg: test `federatedresourcequota` webhook
  • Loading branch information
karmada-bot authored Aug 2, 2024
2 parents 1432eca + 7373ab3 commit 04a4d84
Showing 1 changed file with 371 additions and 0 deletions.
371 changes: 371 additions & 0 deletions pkg/webhook/federatedresourcequota/validating_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,60 @@ limitations under the License.
package federatedresourcequota

import (
"context"
"errors"
"fmt"
"net/http"
"reflect"
"sort"
"strings"
"testing"

corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/validation/field"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"

clustervalidation "github.com/karmada-io/karmada/pkg/apis/cluster/validation"
policyv1alpha1 "github.com/karmada-io/karmada/pkg/apis/policy/v1alpha1"
)

type fakeDecoder struct {
err error
obj runtime.Object
}

// Decode mocks the Decode method of admission.Decoder.
func (f *fakeDecoder) Decode(_ admission.Request, obj runtime.Object) error {
if f.err != nil {
return f.err
}
if f.obj != nil {
reflect.ValueOf(obj).Elem().Set(reflect.ValueOf(f.obj).Elem())
}
return nil
}

// DecodeRaw mocks the DecodeRaw method of admission.Decoder.
func (f *fakeDecoder) DecodeRaw(_ runtime.RawExtension, obj runtime.Object) error {
if f.err != nil {
return f.err
}
if f.obj != nil {
reflect.ValueOf(obj).Elem().Set(reflect.ValueOf(f.obj).Elem())
}
return nil
}

// sortAndJoinMessages sorts and joins error message parts to ensure consistent ordering.
// This prevents test flakiness caused by varying error message order.
func sortAndJoinMessages(message string) string {
parts := strings.Split(strings.Trim(message, "[]"), ", ")
sort.Strings(parts)
return strings.Join(parts, ", ")
}

func Test_validateOverallAndAssignments(t *testing.T) {
specFld := field.NewPath("spec")
cpuParse := resource.MustParse("10")
Expand Down Expand Up @@ -172,3 +216,330 @@ func Test_validateOverallAndAssignments(t *testing.T) {
})
}
}

func TestValidatingAdmission_Handle(t *testing.T) {
tests := []struct {
name string
decoder admission.Decoder
req admission.Request
want admission.Response
}{
{
name: "Decode Error Handling",
decoder: &fakeDecoder{
err: errors.New("decode error"),
},
req: admission.Request{},
want: admission.Errored(http.StatusBadRequest, errors.New("decode error")),
},
{
name: "Validation Success - Resource Limits Match",
decoder: &fakeDecoder{
obj: &policyv1alpha1.FederatedResourceQuota{
Spec: policyv1alpha1.FederatedResourceQuotaSpec{
Overall: corev1.ResourceList{
"cpu": resource.MustParse("10"),
"memory": resource.MustParse("10Gi"),
},
StaticAssignments: []policyv1alpha1.StaticClusterAssignment{
{
ClusterName: "m1",
Hard: corev1.ResourceList{
"cpu": resource.MustParse("5"),
"memory": resource.MustParse("5Gi"),
},
},
{
ClusterName: "m2",
Hard: corev1.ResourceList{
"cpu": resource.MustParse("5"),
"memory": resource.MustParse("5Gi"),
},
},
},
},
},
},
req: admission.Request{},
want: admission.Allowed(""),
},
{
name: "Validation Error - Resource Limits Exceeded",
decoder: &fakeDecoder{
obj: &policyv1alpha1.FederatedResourceQuota{
Spec: policyv1alpha1.FederatedResourceQuotaSpec{
Overall: corev1.ResourceList{
"cpu": resource.MustParse("5"),
"memory": resource.MustParse("5Gi"),
},
StaticAssignments: []policyv1alpha1.StaticClusterAssignment{
{
ClusterName: "m1",
Hard: corev1.ResourceList{
"cpu": resource.MustParse("5"),
"memory": resource.MustParse("5Gi"),
},
},
{
ClusterName: "m2",
Hard: corev1.ResourceList{
"cpu": resource.MustParse("5"),
"memory": resource.MustParse("5Gi"),
},
},
},
},
},
},
req: admission.Request{},
want: admission.Denied(fmt.Sprintf("[spec.overall[cpu]: Invalid value: \"%s\": overall is less than assignments, spec.overall[memory]: Invalid value: \"%s\": overall is less than assignments]", "5", "5Gi")),
},
{
name: "Validation Error - CPU Allocation Exceeds Overall Limit",
decoder: &fakeDecoder{
obj: &policyv1alpha1.FederatedResourceQuota{
Spec: policyv1alpha1.FederatedResourceQuotaSpec{
Overall: corev1.ResourceList{
"cpu": resource.MustParse("5"),
"memory": resource.MustParse("10Gi"),
},
StaticAssignments: []policyv1alpha1.StaticClusterAssignment{
{
ClusterName: "m1",
Hard: corev1.ResourceList{
"cpu": resource.MustParse("10"),
"memory": resource.MustParse("5Gi"),
},
},
{
ClusterName: "m2",
Hard: corev1.ResourceList{
"cpu": resource.MustParse("5"),
"memory": resource.MustParse("5Gi"),
},
},
},
},
},
},
req: admission.Request{},
want: admission.Denied(fmt.Sprintf("spec.overall[cpu]: Invalid value: \"%s\": overall is less than assignments", "5")),
},
{
name: "Validation Error - Memory Allocation Exceeds Overall Limit",
decoder: &fakeDecoder{
obj: &policyv1alpha1.FederatedResourceQuota{
Spec: policyv1alpha1.FederatedResourceQuotaSpec{
Overall: corev1.ResourceList{
"cpu": resource.MustParse("10"),
"memory": resource.MustParse("5Gi"),
},
StaticAssignments: []policyv1alpha1.StaticClusterAssignment{
{
ClusterName: "m1",
Hard: corev1.ResourceList{
"cpu": resource.MustParse("5"),
"memory": resource.MustParse("5Gi"),
},
},
{
ClusterName: "m2",
Hard: corev1.ResourceList{
"cpu": resource.MustParse("5"),
"memory": resource.MustParse("10Gi"),
},
},
},
},
},
},
req: admission.Request{},
want: admission.Denied(fmt.Sprintf("spec.overall[memory]: Invalid value: \"%s\": overall is less than assignments", "5Gi")),
},
{
name: "Invalid Cluster Name",
decoder: &fakeDecoder{
obj: &policyv1alpha1.FederatedResourceQuota{
Spec: policyv1alpha1.FederatedResourceQuotaSpec{
Overall: corev1.ResourceList{
"cpu": resource.MustParse("10"),
"memory": resource.MustParse("10Gi"),
},
StaticAssignments: []policyv1alpha1.StaticClusterAssignment{
{
ClusterName: "invalid cluster name",
Hard: corev1.ResourceList{
"cpu": resource.MustParse("5"),
"memory": resource.MustParse("5Gi"),
},
},
},
},
},
},
req: admission.Request{},
want: admission.Denied("[spec.staticAssignments[0].clusterName: Invalid value: \"invalid cluster name\": a lowercase RFC 1123 label must consist of lower case alphanumeric characters or '-', and must start and end with an alphanumeric character (e.g. 'my-name', or '123-abc', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?')]"),
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
v := &ValidatingAdmission{
Decoder: tt.decoder,
}
got := v.Handle(context.Background(), tt.req)
got.Result.Message = sortAndJoinMessages(got.Result.Message)
tt.want.Result.Message = sortAndJoinMessages(tt.want.Result.Message)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Handle() = %v, want %v", got, tt.want)
}
})
}
}

func Test_validateFederatedResourceQuotaStatus(t *testing.T) {
fld := field.NewPath("status")

tests := []struct {
name string
status *policyv1alpha1.FederatedResourceQuotaStatus
expected field.ErrorList
}{
{
name: "Valid FederatedResourceQuotaStatus",
status: &policyv1alpha1.FederatedResourceQuotaStatus{
Overall: corev1.ResourceList{"cpu": resource.MustParse("10")},
OverallUsed: corev1.ResourceList{"cpu": resource.MustParse("5")},
AggregatedStatus: []policyv1alpha1.ClusterQuotaStatus{
{
ClusterName: "valid-cluster",
ResourceQuotaStatus: corev1.ResourceQuotaStatus{
Hard: corev1.ResourceList{"cpu": resource.MustParse("10")},
Used: corev1.ResourceList{"cpu": resource.MustParse("5")},
},
},
},
},
expected: field.ErrorList{},
},
{
name: "Invalid Overall Resource List",
status: &policyv1alpha1.FederatedResourceQuotaStatus{
Overall: corev1.ResourceList{"invalid-resource": resource.MustParse("10")},
OverallUsed: corev1.ResourceList{"cpu": resource.MustParse("5")},
AggregatedStatus: []policyv1alpha1.ClusterQuotaStatus{
{
ClusterName: "valid-cluster",
ResourceQuotaStatus: corev1.ResourceQuotaStatus{
Hard: corev1.ResourceList{"cpu": resource.MustParse("10")},
Used: corev1.ResourceList{"cpu": resource.MustParse("5")},
},
},
},
},
expected: field.ErrorList{
field.Invalid(fld.Child("overall").Key("invalid-resource"), "invalid-resource", "must be a standard resource type or fully qualified"),
field.Invalid(fld.Child("overall").Key("invalid-resource"), "invalid-resource", "must be a standard resource for quota"),
},
},
{
name: "Invalid AggregatedStatus Resource List",
status: &policyv1alpha1.FederatedResourceQuotaStatus{
Overall: corev1.ResourceList{"cpu": resource.MustParse("10")},
OverallUsed: corev1.ResourceList{"cpu": resource.MustParse("5")},
AggregatedStatus: []policyv1alpha1.ClusterQuotaStatus{
{
ClusterName: "valid-cluster",
ResourceQuotaStatus: corev1.ResourceQuotaStatus{
Hard: corev1.ResourceList{"invalid-resource": resource.MustParse("10")},
Used: corev1.ResourceList{"cpu": resource.MustParse("5")},
},
},
},
},
expected: field.ErrorList{
field.Invalid(fld.Child("aggregatedStatus").Index(0).Child("hard").Key("invalid-resource"), "invalid-resource", "must be a standard resource type or fully qualified"),
field.Invalid(fld.Child("aggregatedStatus").Index(0).Child("hard").Key("invalid-resource"), "invalid-resource", "must be a standard resource for quota"),
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := validateFederatedResourceQuotaStatus(tt.status, fld); !reflect.DeepEqual(got, tt.expected) {
t.Errorf("validateFederatedResourceQuotaStatus() = %v, want %v", got, tt.expected)
}
})
}
}

func Test_validateClusterQuotaStatus(t *testing.T) {
fld := field.NewPath("status").Child("aggregatedStatus")

tests := []struct {
name string
status *policyv1alpha1.ClusterQuotaStatus
expected field.ErrorList
}{
{
name: "Valid ClusterQuotaStatus",
status: &policyv1alpha1.ClusterQuotaStatus{
ClusterName: "valid-cluster",
ResourceQuotaStatus: corev1.ResourceQuotaStatus{
Hard: corev1.ResourceList{"cpu": resource.MustParse("10")},
Used: corev1.ResourceList{"cpu": resource.MustParse("5")},
},
},
expected: field.ErrorList{},
},
{
name: "Invalid Cluster Name",
status: &policyv1alpha1.ClusterQuotaStatus{
ClusterName: "invalid cluster name",
ResourceQuotaStatus: corev1.ResourceQuotaStatus{
Hard: corev1.ResourceList{"cpu": resource.MustParse("10")},
Used: corev1.ResourceList{"cpu": resource.MustParse("5")},
},
},
expected: field.ErrorList{
field.Invalid(fld.Child("clusterName"), "invalid cluster name", strings.Join(clustervalidation.ValidateClusterName("invalid cluster name"), ",")),
},
},
{
name: "Invalid Resource List - Hard",
status: &policyv1alpha1.ClusterQuotaStatus{
ClusterName: "valid-cluster",
ResourceQuotaStatus: corev1.ResourceQuotaStatus{
Hard: corev1.ResourceList{"invalid-resource": resource.MustParse("10")},
Used: corev1.ResourceList{"cpu": resource.MustParse("5")},
},
},
expected: field.ErrorList{
field.Invalid(fld.Child("hard").Key("invalid-resource"), "invalid-resource", "must be a standard resource type or fully qualified"),
field.Invalid(fld.Child("hard").Key("invalid-resource"), "invalid-resource", "must be a standard resource for quota"),
},
},
{
name: "Invalid Resource List - Used",
status: &policyv1alpha1.ClusterQuotaStatus{
ClusterName: "valid-cluster",
ResourceQuotaStatus: corev1.ResourceQuotaStatus{
Hard: corev1.ResourceList{"cpu": resource.MustParse("10")},
Used: corev1.ResourceList{"invalid-resource": resource.MustParse("5")},
},
},
expected: field.ErrorList{
field.Invalid(fld.Child("used").Key("invalid-resource"), "invalid-resource", "must be a standard resource type or fully qualified"),
field.Invalid(fld.Child("used").Key("invalid-resource"), "invalid-resource", "must be a standard resource for quota"),
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := validateClusterQuotaStatus(tt.status, fld); !reflect.DeepEqual(got, tt.expected) {
t.Errorf("validateClusterQuotaStatus() = %v, want %v", got, tt.expected)
}
})
}
}

0 comments on commit 04a4d84

Please sign in to comment.