diff --git a/pkg/webhook/pod/validating/cluster_colocation_profile.go b/pkg/webhook/pod/validating/cluster_colocation_profile.go index 8e8b42f37..53b6d0a09 100644 --- a/pkg/webhook/pod/validating/cluster_colocation_profile.go +++ b/pkg/webhook/pod/validating/cluster_colocation_profile.go @@ -121,35 +121,16 @@ func forbidSpecialQoSClassAndPriorityClass(pod *corev1.Pod, qoSClass extension.Q } func validateResources(pod *corev1.Pod) field.ErrorList { + allErrs := field.ErrorList{} qos := extension.GetPodQoSClassRaw(pod) - resourceValidator := NewRequestLimitValidator(pod) - switch qos { - case extension.QoSLSR: - // FIXME - // 1. CPU should be integer - // 2. Consider whether to cannel the restriction that memory must be equal - resourceValidator = resourceValidator. - ExpectRequestLimitMustEqual(corev1.ResourceCPU). - ExpectRequestLimitMustEqual(corev1.ResourceMemory). - ExpectPositive() - case extension.QoSLS: - switch extension.GetPodPriorityClassRaw(pod) { - case extension.PriorityProd: - resourceValidator = resourceValidator. - ExpectRequestNoMoreThanLimit(corev1.ResourceCPU). - ExpectRequestNoMoreThanLimit(corev1.ResourceMemory). - ExpectPositive() - case extension.PriorityBatch: - resourceValidator = resourceValidator. - ExpectRequestNoMoreThanLimit(extension.BatchCPU). - ExpectRequestNoMoreThanLimit(extension.BatchMemory). - ExpectPositive() + if qos == extension.QoSLSR || qos == extension.QoSLSE { + requests := util.GetPodRequest(pod) + cpu := requests[corev1.ResourceCPU] + if cpu.IsZero() { + allErrs = append(allErrs, field.Required(field.NewPath("pod.spec.containers[*].resources.requests"), "LSR Pod must declare the requested CPUs")) + } else if cpu.Value()*1000 != cpu.MilliValue() { + allErrs = append(allErrs, field.Invalid(field.NewPath("pod.spec.containers[*].resources.requests"), cpu.String(), "the requested CPUs of LSR Pod must be integer")) } - case extension.QoSBE: - resourceValidator = resourceValidator. - ExpectRequestNoMoreThanLimit(extension.BatchCPU). - ExpectRequestNoMoreThanLimit(extension.BatchMemory). - ExpectPositive() } - return resourceValidator.Validate() + return allErrs } diff --git a/pkg/webhook/pod/validating/cluster_colocation_profile_test.go b/pkg/webhook/pod/validating/cluster_colocation_profile_test.go index e8a1834dd..843dbdf6f 100644 --- a/pkg/webhook/pod/validating/cluster_colocation_profile_test.go +++ b/pkg/webhook/pod/validating/cluster_colocation_profile_test.go @@ -58,7 +58,6 @@ func TestClusterColocationProfileValidatingPod(t *testing.T) { newPod *corev1.Pod wantAllowed bool wantReason string - filterFunc ContainerFilterFunc wantErr bool }{ { @@ -249,97 +248,13 @@ func TestClusterColocationProfileValidatingPod(t *testing.T) { extension.LabelPodQoS: string(extension.QoSLSR), }, }, - Spec: corev1.PodSpec{ - Priority: pointer.Int32(extension.PriorityProdValueMax), - }, - }, - wantAllowed: true, - }, - { - name: "forbidden QoS and priorityClass combination: BE And Prod", - operation: admissionv1.Create, - newPod: &corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Labels: map[string]string{ - extension.LabelPodQoS: string(extension.QoSBE), - }, - }, - Spec: corev1.PodSpec{ - Priority: pointer.Int32(extension.PriorityProdValueMax), - }, - }, - wantAllowed: false, - wantReason: `Pod: Forbidden: koordinator.sh/qosClass=BE and priorityClass=koord-prod cannot be used in combination`, - }, - { - name: "forbidden QoS and priorityClass combination: LSR And Batch", - operation: admissionv1.Create, - newPod: &corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Labels: map[string]string{ - extension.LabelPodQoS: string(extension.QoSLSR), - }, - }, - Spec: corev1.PodSpec{ - Priority: pointer.Int32(extension.PriorityBatchValueMax), - }, - }, - wantAllowed: false, - wantReason: `Pod: Forbidden: koordinator.sh/qosClass=LSR and priorityClass=koord-batch cannot be used in combination`, - }, - { - name: "forbidden QoS and priorityClass combination: LSR And Mid", - operation: admissionv1.Create, - newPod: &corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Labels: map[string]string{ - extension.LabelPodQoS: string(extension.QoSLSR), - }, - }, - Spec: corev1.PodSpec{ - Priority: pointer.Int32(extension.PriorityMidValueMax), - }, - }, - wantAllowed: false, - wantReason: `Pod: Forbidden: koordinator.sh/qosClass=LSR and priorityClass=koord-mid cannot be used in combination`, - }, - { - name: "forbidden QoS and priorityClass combination: LSR And Free", - operation: admissionv1.Create, - newPod: &corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Labels: map[string]string{ - extension.LabelPodQoS: string(extension.QoSLSR), - }, - }, - Spec: corev1.PodSpec{ - Priority: pointer.Int32(extension.PriorityFreeValueMax), - }, - }, - wantAllowed: false, - wantReason: `Pod: Forbidden: koordinator.sh/qosClass=LSR and priorityClass=koord-free cannot be used in combination`, - }, - { - name: "validate resources - LSR And Prod", - operation: admissionv1.Create, - newPod: &corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Labels: map[string]string{ - extension.LabelPodQoS: string(extension.QoSLS), - }, - }, Spec: corev1.PodSpec{ Priority: pointer.Int32(extension.PriorityProdValueMax), Containers: []corev1.Container{ { Resources: corev1.ResourceRequirements{ - Limits: corev1.ResourceList{ - corev1.ResourceCPU: resource.MustParse("1"), - corev1.ResourceMemory: resource.MustParse("4Gi"), - }, Requests: corev1.ResourceList{ - corev1.ResourceCPU: resource.MustParse("1"), - corev1.ResourceMemory: resource.MustParse("4Gi"), + corev1.ResourceCPU: resource.MustParse("1"), }, }, }, @@ -349,38 +264,23 @@ func TestClusterColocationProfileValidatingPod(t *testing.T) { wantAllowed: true, }, { - name: "forbidden resources - LSR And Prod: negative resource requirements", + name: "forbidden QoS and priorityClass combination: BE And Prod", operation: admissionv1.Create, newPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{ - extension.LabelPodQoS: string(extension.QoSLSR), + extension.LabelPodQoS: string(extension.QoSBE), }, }, Spec: corev1.PodSpec{ Priority: pointer.Int32(extension.PriorityProdValueMax), - Containers: []corev1.Container{ - { - Name: "test-container-a", - Resources: corev1.ResourceRequirements{ - Limits: corev1.ResourceList{ - corev1.ResourceCPU: resource.MustParse("-1"), - corev1.ResourceMemory: resource.MustParse("-4Gi"), - }, - Requests: corev1.ResourceList{ - corev1.ResourceCPU: resource.MustParse("-1"), - corev1.ResourceMemory: resource.MustParse("-4Gi"), - }, - }, - }, - }, }, }, wantAllowed: false, - wantReason: `[pod.spec.containers.test-container-a.resources.requests.cpu: Invalid value: "-1": quantity must be positive, pod.spec.containers.test-container-a.resources.requests.memory: Invalid value: "-4Gi": quantity must be positive, pod.spec.containers.test-container-a.resources.limits.cpu: Invalid value: "-1": quantity must be positive, pod.spec.containers.test-container-a.resources.limits.memory: Invalid value: "-4Gi": quantity must be positive]`, + wantReason: `Pod: Forbidden: koordinator.sh/qosClass=BE and priorityClass=koord-prod cannot be used in combination`, }, { - name: "forbidden resources - LSR And Prod: requests less than limits", + name: "forbidden QoS and priorityClass combination: LSR And Batch", operation: admissionv1.Create, newPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ @@ -389,18 +289,12 @@ func TestClusterColocationProfileValidatingPod(t *testing.T) { }, }, Spec: corev1.PodSpec{ - Priority: pointer.Int32(extension.PriorityProdValueMax), + Priority: pointer.Int32(extension.PriorityBatchValueMax), Containers: []corev1.Container{ { - Name: "test-container-a", Resources: corev1.ResourceRequirements{ - Limits: corev1.ResourceList{ - corev1.ResourceCPU: resource.MustParse("2"), - corev1.ResourceMemory: resource.MustParse("8Gi"), - }, Requests: corev1.ResourceList{ - corev1.ResourceCPU: resource.MustParse("2"), - corev1.ResourceMemory: resource.MustParse("4Gi"), + corev1.ResourceCPU: resource.MustParse("1"), }, }, }, @@ -408,10 +302,10 @@ func TestClusterColocationProfileValidatingPod(t *testing.T) { }, }, wantAllowed: false, - wantReason: `pod.spec.containers.test-container-a.resources: Forbidden: resource memory of container test-container-a: quantity of request and limit must be equal`, + wantReason: `Pod: Forbidden: koordinator.sh/qosClass=LSR and priorityClass=koord-batch cannot be used in combination`, }, { - name: "forbidden resources - LSR And Prod: missing CPU/Memory", + name: "forbidden QoS and priorityClass combination: LSR And Mid", operation: admissionv1.Create, newPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ @@ -420,55 +314,48 @@ func TestClusterColocationProfileValidatingPod(t *testing.T) { }, }, Spec: corev1.PodSpec{ - Priority: pointer.Int32(extension.PriorityProdValueMax), + Priority: pointer.Int32(extension.PriorityMidValueMax), Containers: []corev1.Container{ { - Name: "test-container-a", Resources: corev1.ResourceRequirements{ - Limits: corev1.ResourceList{ - corev1.ResourceCPU: resource.MustParse("2"), - corev1.ResourceMemory: resource.MustParse("8Gi"), + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("1"), }, - Requests: corev1.ResourceList{}, }, }, }, }, }, wantAllowed: false, - wantReason: `[pod.spec.containers.test-container-a.resources.requests.cpu: Not found: "null", pod.spec.containers.test-container-a.resources: Forbidden: resource cpu of container test-container-a: quantity of request and limit must be equal, pod.spec.containers.test-container-a.resources.requests.memory: Not found: "null", pod.spec.containers.test-container-a.resources: Forbidden: resource memory of container test-container-a: quantity of request and limit must be equal]`, + wantReason: `Pod: Forbidden: koordinator.sh/qosClass=LSR and priorityClass=koord-mid cannot be used in combination`, }, { - name: "validate resources - LS And Prod: requests equals limits", + name: "forbidden QoS and priorityClass combination: LSR And Free", operation: admissionv1.Create, newPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{ - extension.LabelPodQoS: string(extension.QoSLS), + extension.LabelPodQoS: string(extension.QoSLSR), }, }, Spec: corev1.PodSpec{ - Priority: pointer.Int32(extension.PriorityProdValueMax), + Priority: pointer.Int32(extension.PriorityFreeValueMax), Containers: []corev1.Container{ { Resources: corev1.ResourceRequirements{ - Limits: corev1.ResourceList{ - corev1.ResourceCPU: resource.MustParse("1"), - corev1.ResourceMemory: resource.MustParse("4Gi"), - }, Requests: corev1.ResourceList{ - corev1.ResourceCPU: resource.MustParse("1"), - corev1.ResourceMemory: resource.MustParse("4Gi"), + corev1.ResourceCPU: resource.MustParse("1"), }, }, }, }, }, }, - wantAllowed: true, + wantAllowed: false, + wantReason: `Pod: Forbidden: koordinator.sh/qosClass=LSR and priorityClass=koord-free cannot be used in combination`, }, { - name: "validate resources - LS And Prod: request cpu less than limits", + name: "validate resources - LSR And Prod", operation: admissionv1.Create, newPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ @@ -482,99 +369,9 @@ func TestClusterColocationProfileValidatingPod(t *testing.T) { { Resources: corev1.ResourceRequirements{ Limits: corev1.ResourceList{ - corev1.ResourceCPU: resource.MustParse("2"), - corev1.ResourceMemory: resource.MustParse("4Gi"), - }, - Requests: corev1.ResourceList{ corev1.ResourceCPU: resource.MustParse("1"), corev1.ResourceMemory: resource.MustParse("4Gi"), }, - }, - }, - }, - }, - }, - wantAllowed: true, - }, - { - name: "forbidden resources - LS And Prod: negative resource requirements", - operation: admissionv1.Create, - newPod: &corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Labels: map[string]string{ - extension.LabelPodQoS: string(extension.QoSLS), - }, - }, - Spec: corev1.PodSpec{ - Priority: pointer.Int32(extension.PriorityProdValueMax), - Containers: []corev1.Container{ - { - Name: "test-container-a", - Resources: corev1.ResourceRequirements{ - Limits: corev1.ResourceList{ - corev1.ResourceCPU: resource.MustParse("-1"), - corev1.ResourceMemory: resource.MustParse("-4Gi"), - }, - Requests: corev1.ResourceList{ - corev1.ResourceCPU: resource.MustParse("-1"), - corev1.ResourceMemory: resource.MustParse("-4Gi"), - }, - }, - }, - }, - }, - }, - wantAllowed: false, - wantReason: `[pod.spec.containers.test-container-a.resources.requests.cpu: Invalid value: "-1": quantity must be positive, pod.spec.containers.test-container-a.resources.requests.memory: Invalid value: "-4Gi": quantity must be positive, pod.spec.containers.test-container-a.resources.limits.cpu: Invalid value: "-1": quantity must be positive, pod.spec.containers.test-container-a.resources.limits.memory: Invalid value: "-4Gi": quantity must be positive]`, - }, - { - name: "forbidden resources - LS And Batch: negative resource requirements", - operation: admissionv1.Create, - newPod: &corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Labels: map[string]string{ - extension.LabelPodQoS: string(extension.QoSBE), - }, - }, - Spec: corev1.PodSpec{ - Priority: pointer.Int32(extension.PriorityBatchValueMin), - Containers: []corev1.Container{ - { - Name: "test-container-a", - Resources: corev1.ResourceRequirements{ - Limits: corev1.ResourceList{ - extension.BatchCPU: resource.MustParse("-1"), - extension.BatchMemory: resource.MustParse("-4Gi"), - }, - Requests: corev1.ResourceList{ - extension.BatchCPU: resource.MustParse("-1"), - extension.BatchMemory: resource.MustParse("-4Gi"), - }, - }, - }, - }, - }, - }, - wantAllowed: false, - wantReason: `[pod.spec.containers.test-container-a.resources.requests.kubernetes.io/batch-cpu: Invalid value: "-1": quantity must be positive, pod.spec.containers.test-container-a.resources.requests.kubernetes.io/batch-memory: Invalid value: "-4Gi": quantity must be positive, pod.spec.containers.test-container-a.resources.limits.kubernetes.io/batch-cpu: Invalid value: "-1": quantity must be positive, pod.spec.containers.test-container-a.resources.limits.kubernetes.io/batch-memory: Invalid value: "-4Gi": quantity must be positive]`, - }, - { - //name: "allow resources - LS And Prod: requests has cpu/memory and missing limits", - name: "mark", - operation: admissionv1.Create, - newPod: &corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Labels: map[string]string{ - extension.LabelPodQoS: string(extension.QoSLS), - }, - }, - Spec: corev1.PodSpec{ - Priority: pointer.Int32(extension.PriorityProdValueMax), - Containers: []corev1.Container{ - { - Name: "test-container-a", - Resources: corev1.ResourceRequirements{ - Limits: corev1.ResourceList{}, Requests: corev1.ResourceList{ corev1.ResourceCPU: resource.MustParse("1"), corev1.ResourceMemory: resource.MustParse("4Gi"), @@ -587,81 +384,25 @@ func TestClusterColocationProfileValidatingPod(t *testing.T) { wantAllowed: true, }, { - name: "allow resources - LS And Prod: missing requests but has limits", + name: "forbidden resources - LSR And Prod: unset CPUs", operation: admissionv1.Create, newPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{ - extension.LabelPodQoS: string(extension.QoSLS), + extension.LabelPodQoS: string(extension.QoSLSR), }, }, Spec: corev1.PodSpec{ Priority: pointer.Int32(extension.PriorityProdValueMax), Containers: []corev1.Container{ { - Name: "test-container-a", + Name: "test-container-skip", Resources: corev1.ResourceRequirements{ Limits: corev1.ResourceList{ - corev1.ResourceCPU: resource.MustParse("1"), corev1.ResourceMemory: resource.MustParse("4Gi"), }, - Requests: corev1.ResourceList{}, - }, - }, - }, - }, - }, - wantAllowed: true, - }, - { - name: "allow resources - LS And Batch: missing requests but has limits", - operation: admissionv1.Create, - newPod: &corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Labels: map[string]string{ - extension.LabelPodQoS: string(extension.QoSLS), - }, - }, - Spec: corev1.PodSpec{ - Priority: pointer.Int32(extension.PriorityBatchValueMax), - Containers: []corev1.Container{ - { - Name: "test-container-a", - Resources: corev1.ResourceRequirements{ - Limits: corev1.ResourceList{ - extension.BatchCPU: resource.MustParse("1"), - extension.BatchMemory: resource.MustParse("4Gi"), - }, - Requests: corev1.ResourceList{}, - }, - }, - }, - }, - }, - wantAllowed: true, - }, - { - name: "forbidden resources - BE And Batch: negative resource requirements", - operation: admissionv1.Create, - newPod: &corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Labels: map[string]string{ - extension.LabelPodQoS: string(extension.QoSBE), - }, - }, - Spec: corev1.PodSpec{ - Priority: pointer.Int32(extension.PriorityBatchValueMin), - Containers: []corev1.Container{ - { - Name: "test-container-a", - Resources: corev1.ResourceRequirements{ - Limits: corev1.ResourceList{ - extension.BatchCPU: resource.MustParse("-1"), - extension.BatchMemory: resource.MustParse("-4Gi"), - }, Requests: corev1.ResourceList{ - extension.BatchCPU: resource.MustParse("-1"), - extension.BatchMemory: resource.MustParse("-4Gi"), + corev1.ResourceMemory: resource.MustParse("0Gi"), }, }, }, @@ -669,94 +410,10 @@ func TestClusterColocationProfileValidatingPod(t *testing.T) { }, }, wantAllowed: false, - wantReason: `[pod.spec.containers.test-container-a.resources.requests.kubernetes.io/batch-cpu: Invalid value: "-1": quantity must be positive, pod.spec.containers.test-container-a.resources.requests.kubernetes.io/batch-memory: Invalid value: "-4Gi": quantity must be positive, pod.spec.containers.test-container-a.resources.limits.kubernetes.io/batch-cpu: Invalid value: "-1": quantity must be positive, pod.spec.containers.test-container-a.resources.limits.kubernetes.io/batch-memory: Invalid value: "-4Gi": quantity must be positive]`, + wantReason: `pod.spec.containers[*].resources.requests: Required value: LSR Pod must declare the requested CPUs`, }, { - name: "allow resources - BE And Batch: requests has cpu/memory and missing limits", - operation: admissionv1.Create, - newPod: &corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Labels: map[string]string{ - extension.LabelPodQoS: string(extension.QoSBE), - }, - }, - Spec: corev1.PodSpec{ - Priority: pointer.Int32(extension.PriorityBatchValueMin), - Containers: []corev1.Container{ - { - Name: "test-container-a", - Resources: corev1.ResourceRequirements{ - Limits: corev1.ResourceList{}, - Requests: corev1.ResourceList{ - extension.BatchCPU: resource.MustParse("1"), - extension.BatchMemory: resource.MustParse("4Gi"), - }, - }, - }, - }, - }, - }, - wantAllowed: true, - }, - { - name: "allow resources - BE And Batch: limits has cpu/memory and missing requests", - operation: admissionv1.Create, - newPod: &corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Labels: map[string]string{ - extension.LabelPodQoS: string(extension.QoSBE), - }, - }, - Spec: corev1.PodSpec{ - Priority: pointer.Int32(extension.PriorityBatchValueMin), - Containers: []corev1.Container{ - { - Name: "test-container-a", - Resources: corev1.ResourceRequirements{ - Limits: corev1.ResourceList{ - extension.BatchCPU: resource.MustParse("1"), - extension.BatchMemory: resource.MustParse("4Gi"), - }, - Requests: corev1.ResourceList{}, - }, - }, - }, - }, - }, - wantAllowed: true, - }, - { - name: "validate resources - BE And Batch: request memory must equal limits and cpu less than limits", - operation: admissionv1.Create, - newPod: &corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Labels: map[string]string{ - extension.LabelPodQoS: string(extension.QoSBE), - }, - }, - Spec: corev1.PodSpec{ - Priority: pointer.Int32(extension.PriorityBatchValueMin), - Containers: []corev1.Container{ - { - Name: "test-container-a", - Resources: corev1.ResourceRequirements{ - Limits: corev1.ResourceList{ - extension.BatchCPU: resource.MustParse("2"), - extension.BatchMemory: resource.MustParse("4Gi"), - }, - Requests: corev1.ResourceList{ - extension.BatchCPU: resource.MustParse("1"), - extension.BatchMemory: resource.MustParse("4Gi"), - }, - }, - }, - }, - }, - }, - wantAllowed: true, - }, - { - name: "forbidden resources - LSR And Prod: zeroed resource requests but filtered", + name: "forbidden resources - LSR And Prod: non-integer CPUs", operation: admissionv1.Create, newPod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ @@ -771,11 +428,11 @@ func TestClusterColocationProfileValidatingPod(t *testing.T) { Name: "test-container-skip", Resources: corev1.ResourceRequirements{ Limits: corev1.ResourceList{ - corev1.ResourceCPU: resource.MustParse("1"), + corev1.ResourceCPU: resource.MustParse("100m"), corev1.ResourceMemory: resource.MustParse("4Gi"), }, Requests: corev1.ResourceList{ - corev1.ResourceCPU: resource.MustParse("0"), + corev1.ResourceCPU: resource.MustParse("100m"), corev1.ResourceMemory: resource.MustParse("0Gi"), }, }, @@ -783,22 +440,13 @@ func TestClusterColocationProfileValidatingPod(t *testing.T) { }, }, }, - filterFunc: func(container *corev1.Container) bool { - if container.Name == "test-container-skip" { - return false - } - return true - }, - wantAllowed: true, + wantAllowed: false, + wantReason: `pod.spec.containers[*].resources.requests: Invalid value: "100m": the requested CPUs of LSR Pod must be integer`, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if tt.filterFunc != nil { - RegisterContainerFilterFunc(tt.filterFunc) - defer func() { containerFilterFns = nil }() - } client := fake.NewClientBuilder().Build() decoder, _ := admission.NewDecoder(scheme.Scheme) h := &PodValidatingHandler{ diff --git a/pkg/webhook/pod/validating/request_limit_validator.go b/pkg/webhook/pod/validating/request_limit_validator.go deleted file mode 100644 index a7adda119..000000000 --- a/pkg/webhook/pod/validating/request_limit_validator.go +++ /dev/null @@ -1,176 +0,0 @@ -/* -Copyright 2022 The Koordinator 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 validating - -import ( - "fmt" - "sort" - - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/resource" - "k8s.io/apimachinery/pkg/util/validation/field" -) - -type resourceValidateFunc func(container *corev1.Container) field.ErrorList - -type requestLimitValidator struct { - containers []*corev1.Container - predicates []resourceValidateFunc - allErrs field.ErrorList -} - -// NewRequestLimitValidator return a requestLimitValidator object, used to validate the resource -// request of pod with different QoSClass and PriorityClass (including request/limits). Check README -// for more details. -func NewRequestLimitValidator(pod *corev1.Pod) *requestLimitValidator { - containers := make([]*corev1.Container, 0, len(pod.Spec.Containers)) - for i := range pod.Spec.Containers { - containers = append(containers, &pod.Spec.Containers[i]) - } - return &requestLimitValidator{containers: containers} -} - -func (v *requestLimitValidator) ExpectRequestLimitShouldEqual(resourceName corev1.ResourceName) *requestLimitValidator { - v.predicates = append(v.predicates, func(container *corev1.Container) field.ErrorList { - requestQuantity, ok := container.Resources.Requests[resourceName] - if !ok { - return nil - } - limitQuantity, ok := container.Resources.Limits[resourceName] - if !ok { - return nil - } - if requestQuantity.Cmp(limitQuantity) != 0 { - allErrs := field.ErrorList{} - errDetail := fmt.Sprintf("resource %s of container %s: quantity of request and limit should be equal", - resourceName, container.Name) - fieldPath := field.NewPath("pod.spec.containers", container.Name, "resources") - allErrs = append(allErrs, field.Forbidden(fieldPath, errDetail)) - return allErrs - } - return nil - }) - return v -} - -func (v *requestLimitValidator) ExpectRequestLimitMustEqual(resourceName corev1.ResourceName) *requestLimitValidator { - v.predicates = append(v.predicates, func(container *corev1.Container) field.ErrorList { - allErrs := field.ErrorList{} - fieldPath := field.NewPath("pod.spec.containers", container.Name, "resources") - requestQuantity, ok := container.Resources.Requests[resourceName] - if !ok { - allErrs = append(allErrs, field.NotFound(fieldPath.Child("requests", string(resourceName)), nil)) - } - limitQuantity, ok := container.Resources.Limits[resourceName] - if !ok { - allErrs = append(allErrs, field.NotFound(fieldPath.Child("limits", string(resourceName)), nil)) - } - if requestQuantity.Cmp(limitQuantity) != 0 { - errDetail := fmt.Sprintf("resource %s of container %s: quantity of request and limit must be equal", - resourceName, container.Name) - allErrs = append(allErrs, field.Forbidden(fieldPath, errDetail)) - } - return allErrs - }) - return v -} - -func (v *requestLimitValidator) ExpectRequestNoMoreThanLimit(resourceName corev1.ResourceName) *requestLimitValidator { - v.predicates = append(v.predicates, func(container *corev1.Container) field.ErrorList { - allErrs := field.ErrorList{} - fieldPath := field.NewPath("pod.spec.containers", container.Name, "resources") - requestQuantity, _ := container.Resources.Requests[resourceName] - limitQuantity, ok := container.Resources.Limits[resourceName] - if cmp := requestQuantity.Cmp(limitQuantity); ok && cmp > 0 { - op := "<=" - errDetail := fmt.Sprintf("container %s: resource %s quantity should satisify request %s limit", - container.Name, resourceName, op) - allErrs = append(allErrs, field.Forbidden(fieldPath, errDetail)) - } - return allErrs - }) - return v -} - -type resourceNameAndQuantity struct { - name corev1.ResourceName - quantity resource.Quantity -} - -func expectPositive(fieldPath *field.Path, resourceList corev1.ResourceList) field.ErrorList { - var rqs []resourceNameAndQuantity - for name, quantity := range resourceList { - rqs = append(rqs, resourceNameAndQuantity{ - name: name, - quantity: quantity, - }) - } - sort.Slice(rqs, func(i, j int) bool { - return rqs[i].name < rqs[j].name - }) - - var allErrs field.ErrorList - for _, v := range rqs { - name, quantity := v.name, v.quantity - if quantity.Value() <= 0 { - allErrs = append(allErrs, field.Invalid(fieldPath.Child(string(name)), quantity.String(), "quantity must be positive")) - } - } - return allErrs -} - -func (v *requestLimitValidator) ExpectPositive() *requestLimitValidator { - v.predicates = append(v.predicates, func(container *corev1.Container) field.ErrorList { - allErrs := field.ErrorList{} - fieldPath := field.NewPath("pod.spec.containers", container.Name, "resources") - - allErrs = append(allErrs, expectPositive(fieldPath.Child("requests"), container.Resources.Requests)...) - allErrs = append(allErrs, expectPositive(fieldPath.Child("limits"), container.Resources.Limits)...) - return allErrs - }) - return v -} - -type ContainerFilterFunc func(container *corev1.Container) bool - -var containerFilterFns []ContainerFilterFunc - -func RegisterContainerFilterFunc(fn ContainerFilterFunc) { - containerFilterFns = append(containerFilterFns, fn) -} - -func (v *requestLimitValidator) Validate() field.ErrorList { - for _, container := range v.containers { - filtered := false - for _, filterFn := range containerFilterFns { - if !filterFn(container) { - filtered = true - break - } - } - if filtered { - continue - } - - for _, predicate := range v.predicates { - if err := predicate(container); err != nil { - v.allErrs = append(v.allErrs, err...) - } - } - } - return v.allErrs -}