mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-08-19 04:26:45 +00:00
feat(globalcustomquota): add support for not-equals field selector (#1997)
Signed-off-by: sandert-k8s <sandert98@gmail.com>
This commit is contained in:
@@ -19,6 +19,7 @@ import (
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
"k8s.io/utils/ptr"
|
||||
|
||||
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
||||
"github.com/projectcapsule/capsule/pkg/api/meta"
|
||||
@@ -3365,4 +3366,280 @@ var _ = Describe("when GlobalCustomQuota uses ledger-backed reconciliation", Ord
|
||||
expectGlobalQuotaUsedAndClaims(ctx, cpuQuota.GetName(), "200m", 2)
|
||||
expectGlobalQuotaUsedAndClaims(ctx, emptyDirQuota.GetName(), "2Gi", 2)
|
||||
})
|
||||
|
||||
It("excludes succeeded pods from cpu limit quota using not-equals field selector", func() {
|
||||
q := &capsulev1beta2.GlobalCustomQuota{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "gq-not-equals-succeeded",
|
||||
Labels: map[string]string{
|
||||
"e2e.capsule.dev/test-suite": "globalcustomquota-ledger",
|
||||
},
|
||||
},
|
||||
Spec: capsulev1beta2.GlobalCustomQuotaSpec{
|
||||
CustomQuotaSpec: capsulev1beta2.CustomQuotaSpec{
|
||||
Limit: resource.MustParse("2"),
|
||||
Sources: []capsulev1beta2.CustomQuotaSpecSource{
|
||||
{
|
||||
VersionKind: runtime.VersionKind{
|
||||
APIVersion: "v1",
|
||||
Kind: "Pod",
|
||||
},
|
||||
CustomQuotaSpecSourceConfig: capsulev1beta2.CustomQuotaSpecSourceConfig{
|
||||
Operation: quota.OpAdd,
|
||||
Path: ".spec.containers[*].resources.limits.cpu",
|
||||
Selectors: []selectors.SelectorWithFields{
|
||||
{
|
||||
FieldSelectors: []string{
|
||||
".status.phase!=Succeeded",
|
||||
".status.phase!=Failed",
|
||||
".status.phase!=Unknown",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
NamespaceSelectors: []selectors.NamespaceSelector{
|
||||
{
|
||||
LabelSelector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{
|
||||
tenantLabel: tenantValue,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
EventuallyCreation(func() error {
|
||||
return k8sClient.Create(ctx, q)
|
||||
}).Should(Succeed())
|
||||
awaitGlobalQuotaReady(ctx, q.GetName())
|
||||
|
||||
// Running pod with 1 CPU limit — should be counted.
|
||||
runningPod := &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "ne-running",
|
||||
Namespace: testNamespace,
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
SecurityContext: nobodyPodSecurityContext(),
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "main",
|
||||
Image: "nginx:1.27.0",
|
||||
SecurityContext: restrictedContainerSecurityContext(),
|
||||
Resources: corev1.ResourceRequirements{
|
||||
Limits: corev1.ResourceList{
|
||||
corev1.ResourceCPU: resource.MustParse("1"),
|
||||
corev1.ResourceMemory: resource.MustParse("64Mi"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
RestartPolicy: corev1.RestartPolicyAlways,
|
||||
},
|
||||
}
|
||||
EventuallyCreation(func() error {
|
||||
runningPod.ResourceVersion = ""
|
||||
return k8sClient.Create(ctx, runningPod)
|
||||
}).Should(Succeed())
|
||||
expectGlobalQuotaUsedAndClaims(ctx, q.GetName(), "1", 1)
|
||||
|
||||
// Succeeded pod with 1 CPU limit — must be excluded after completion.
|
||||
succeededPod := &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "ne-succeeded",
|
||||
Namespace: testNamespace,
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
SecurityContext: nobodyPodSecurityContext(),
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "main",
|
||||
Image: "busybox:1.36",
|
||||
Command: []string{"sh", "-c", "exit 0"},
|
||||
SecurityContext: restrictedContainerSecurityContext(),
|
||||
Resources: corev1.ResourceRequirements{
|
||||
Limits: corev1.ResourceList{
|
||||
corev1.ResourceCPU: resource.MustParse("1"),
|
||||
corev1.ResourceMemory: resource.MustParse("64Mi"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
RestartPolicy: corev1.RestartPolicyNever,
|
||||
TerminationGracePeriodSeconds: ptr.To(int64(0)),
|
||||
},
|
||||
}
|
||||
EventuallyCreation(func() error {
|
||||
succeededPod.ResourceVersion = ""
|
||||
return k8sClient.Create(ctx, succeededPod)
|
||||
}).Should(Succeed())
|
||||
|
||||
// Wait for the pod to complete.
|
||||
Eventually(func(g Gomega) {
|
||||
obj := &corev1.Pod{}
|
||||
g.Expect(k8sClient.Get(ctx, types.NamespacedName{
|
||||
Name: succeededPod.Name,
|
||||
Namespace: testNamespace,
|
||||
}, obj)).To(Succeed())
|
||||
g.Expect(obj.Status.Phase).To(Equal(corev1.PodSucceeded))
|
||||
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
|
||||
|
||||
// Succeeded pod must be excluded; only the running pod should count.
|
||||
expectGlobalQuotaUsedAndClaims(ctx, q.GetName(), "1", 1)
|
||||
expectLedgerSettled(ctx, ControllerNamespace, q.GetName())
|
||||
|
||||
// Admission: 1 existing CPU + 2 requested > limit 2 — must be denied.
|
||||
expectPodCreationDeniedContaining(func(name string) *corev1.Pod {
|
||||
return &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: testNamespace},
|
||||
Spec: corev1.PodSpec{
|
||||
SecurityContext: nobodyPodSecurityContext(),
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "main",
|
||||
Image: "nginx:1.27.0",
|
||||
SecurityContext: restrictedContainerSecurityContext(),
|
||||
Resources: corev1.ResourceRequirements{
|
||||
Limits: corev1.ResourceList{
|
||||
corev1.ResourceCPU: resource.MustParse("2"),
|
||||
corev1.ResourceMemory: resource.MustParse("64Mi"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
RestartPolicy: corev1.RestartPolicyAlways,
|
||||
},
|
||||
}
|
||||
}, q.GetName())
|
||||
})
|
||||
|
||||
It("excludes failed pods from cpu limit quota using not-equals field selector", func() {
|
||||
q := &capsulev1beta2.GlobalCustomQuota{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "gq-not-equals-failed",
|
||||
Labels: map[string]string{
|
||||
"e2e.capsule.dev/test-suite": "globalcustomquota-ledger",
|
||||
},
|
||||
},
|
||||
Spec: capsulev1beta2.GlobalCustomQuotaSpec{
|
||||
CustomQuotaSpec: capsulev1beta2.CustomQuotaSpec{
|
||||
Limit: resource.MustParse("2"),
|
||||
Sources: []capsulev1beta2.CustomQuotaSpecSource{
|
||||
{
|
||||
VersionKind: runtime.VersionKind{
|
||||
APIVersion: "v1",
|
||||
Kind: "Pod",
|
||||
},
|
||||
CustomQuotaSpecSourceConfig: capsulev1beta2.CustomQuotaSpecSourceConfig{
|
||||
Operation: quota.OpAdd,
|
||||
Path: ".spec.containers[*].resources.limits.cpu",
|
||||
Selectors: []selectors.SelectorWithFields{
|
||||
{
|
||||
FieldSelectors: []string{
|
||||
".status.phase!=Succeeded",
|
||||
".status.phase!=Failed",
|
||||
".status.phase!=Unknown",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
NamespaceSelectors: []selectors.NamespaceSelector{
|
||||
{
|
||||
LabelSelector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{
|
||||
tenantLabel: tenantValue,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
EventuallyCreation(func() error {
|
||||
return k8sClient.Create(ctx, q)
|
||||
}).Should(Succeed())
|
||||
awaitGlobalQuotaReady(ctx, q.GetName())
|
||||
|
||||
// Running pod with 1 CPU limit — should be counted.
|
||||
runningPod := &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "ne-failed-running",
|
||||
Namespace: testNamespace,
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
SecurityContext: nobodyPodSecurityContext(),
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "main",
|
||||
Image: "nginx:1.27.0",
|
||||
SecurityContext: restrictedContainerSecurityContext(),
|
||||
Resources: corev1.ResourceRequirements{
|
||||
Limits: corev1.ResourceList{
|
||||
corev1.ResourceCPU: resource.MustParse("1"),
|
||||
corev1.ResourceMemory: resource.MustParse("64Mi"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
RestartPolicy: corev1.RestartPolicyAlways,
|
||||
},
|
||||
}
|
||||
EventuallyCreation(func() error {
|
||||
runningPod.ResourceVersion = ""
|
||||
return k8sClient.Create(ctx, runningPod)
|
||||
}).Should(Succeed())
|
||||
expectGlobalQuotaUsedAndClaims(ctx, q.GetName(), "1", 1)
|
||||
|
||||
// Failed pod with 1 CPU limit — must be excluded after failure.
|
||||
failedPod := &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "ne-failed-terminal",
|
||||
Namespace: testNamespace,
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
SecurityContext: nobodyPodSecurityContext(),
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "main",
|
||||
Image: "busybox:1.36",
|
||||
Command: []string{"sh", "-c", "exit 1"},
|
||||
SecurityContext: restrictedContainerSecurityContext(),
|
||||
Resources: corev1.ResourceRequirements{
|
||||
Limits: corev1.ResourceList{
|
||||
corev1.ResourceCPU: resource.MustParse("1"),
|
||||
corev1.ResourceMemory: resource.MustParse("64Mi"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
RestartPolicy: corev1.RestartPolicyNever,
|
||||
TerminationGracePeriodSeconds: ptr.To(int64(0)),
|
||||
},
|
||||
}
|
||||
EventuallyCreation(func() error {
|
||||
failedPod.ResourceVersion = ""
|
||||
return k8sClient.Create(ctx, failedPod)
|
||||
}).Should(Succeed())
|
||||
|
||||
// Wait for the pod to fail.
|
||||
Eventually(func(g Gomega) {
|
||||
obj := &corev1.Pod{}
|
||||
g.Expect(k8sClient.Get(ctx, types.NamespacedName{
|
||||
Name: failedPod.Name,
|
||||
Namespace: testNamespace,
|
||||
}, obj)).To(Succeed())
|
||||
g.Expect(obj.Status.Phase).To(Equal(corev1.PodFailed))
|
||||
}, defaultTimeoutInterval, defaultPollInterval).Should(Succeed())
|
||||
|
||||
// Failed pod must be excluded; only the running pod should count.
|
||||
expectGlobalQuotaUsedAndClaims(ctx, q.GetName(), "1", 1)
|
||||
expectLedgerSettled(ctx, ControllerNamespace, q.GetName())
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user