Files
Oliver BählerandGitHub 34262c5536 fix: improve rules api (#1961)
* fix(controller): decode old object for delete requests

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: modernize golang

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: modernize golang

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* chore: modernize golang

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>

* fix: preserve ca-bundles injected from external providers

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* fix: improve rules api

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* fix: improve rules api

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* fix: improve rules api

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* fix: improve rules api

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* fix: improve rules api

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* fix: improve rules api

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* fix: improve rules api

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* fix: improve rules api

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

---------

Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>
Signed-off-by: Oliver Baehler <oliver@sudo-i.net>
2026-06-10 00:18:40 +02:00

836 lines
20 KiB
Go

package workloads
import (
"testing"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
)
func TestGetPodQoSClass(t *testing.T) {
t.Parallel()
tests := []struct {
name string
pod *corev1.Pod
want corev1.PodQOSClass
}{
{
name: "nil pod returns BestEffort",
pod: nil,
want: corev1.PodQOSBestEffort,
},
{
name: "status QoS class takes precedence over computed value",
pod: &corev1.Pod{
Status: corev1.PodStatus{
QOSClass: corev1.PodQOSGuaranteed,
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
bestEffortContainer("app"),
},
},
},
want: corev1.PodQOSGuaranteed,
},
{
name: "empty status computes QoS class",
pod: &corev1.Pod{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
bestEffortContainer("app"),
},
},
},
want: corev1.PodQOSBestEffort,
},
{
name: "pod-level Guaranteed takes precedence over BestEffort containers",
pod: &corev1.Pod{
Spec: corev1.PodSpec{
Resources: guaranteedPodResourcesPtr("100m", "128Mi"),
Containers: []corev1.Container{
bestEffortContainer("app"),
},
},
},
want: corev1.PodQOSGuaranteed,
},
{
name: "pod-level Burstable takes precedence over Guaranteed containers",
pod: &corev1.Pod{
Spec: corev1.PodSpec{
Resources: requestOnlyPodResourcesPtr("100m", "128Mi"),
Containers: []corev1.Container{
guaranteedContainer("app", "100m", "128Mi"),
},
},
},
want: corev1.PodQOSBurstable,
},
{
name: "BestEffort without pod-level or container-level resources",
pod: &corev1.Pod{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
bestEffortContainer("app"),
},
},
},
want: corev1.PodQOSBestEffort,
},
{
name: "Burstable with container requests only",
pod: &corev1.Pod{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
requestOnlyContainer("app", "100m", "128Mi"),
},
},
},
want: corev1.PodQOSBurstable,
},
{
name: "Burstable with container limits only",
pod: &corev1.Pod{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
limitOnlyContainer("app", "100m", "128Mi"),
},
},
},
want: corev1.PodQOSBurstable,
},
{
name: "Guaranteed with equal CPU and memory requests and limits",
pod: &corev1.Pod{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
guaranteedContainer("app", "100m", "128Mi"),
},
},
},
want: corev1.PodQOSGuaranteed,
},
{
name: "Burstable when CPU request and limit differ",
pod: &corev1.Pod{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
containerWithResources(
"app",
corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("100m"),
corev1.ResourceMemory: resource.MustParse("128Mi"),
},
corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("200m"),
corev1.ResourceMemory: resource.MustParse("128Mi"),
},
),
},
},
},
want: corev1.PodQOSBurstable,
},
{
name: "Burstable when memory request and limit differ",
pod: &corev1.Pod{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
containerWithResources(
"app",
corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("100m"),
corev1.ResourceMemory: resource.MustParse("128Mi"),
},
corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("100m"),
corev1.ResourceMemory: resource.MustParse("256Mi"),
},
),
},
},
},
want: corev1.PodQOSBurstable,
},
{
name: "Burstable when one container is BestEffort",
pod: &corev1.Pod{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
guaranteedContainer("app", "100m", "128Mi"),
bestEffortContainer("sidecar"),
},
},
},
want: corev1.PodQOSBurstable,
},
{
name: "Guaranteed with multiple guaranteed containers",
pod: &corev1.Pod{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
guaranteedContainer("app", "100m", "128Mi"),
guaranteedContainer("sidecar", "50m", "64Mi"),
},
},
},
want: corev1.PodQOSGuaranteed,
},
{
name: "Burstable when init container has requests only",
pod: &corev1.Pod{
Spec: corev1.PodSpec{
InitContainers: []corev1.Container{
requestOnlyContainer("init", "100m", "128Mi"),
},
Containers: []corev1.Container{
guaranteedContainer("app", "100m", "128Mi"),
},
},
},
want: corev1.PodQOSBurstable,
},
{
name: "Guaranteed when init and regular containers are guaranteed",
pod: &corev1.Pod{
Spec: corev1.PodSpec{
InitContainers: []corev1.Container{
guaranteedContainer("init", "100m", "128Mi"),
},
Containers: []corev1.Container{
guaranteedContainer("app", "100m", "128Mi"),
},
},
},
want: corev1.PodQOSGuaranteed,
},
{
name: "Burstable when ephemeral container has requests only",
pod: &corev1.Pod{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
guaranteedContainer("app", "100m", "128Mi"),
},
EphemeralContainers: []corev1.EphemeralContainer{
{
EphemeralContainerCommon: corev1.EphemeralContainerCommon{
Name: "debug",
Resources: requestOnlyRequirements("100m", "128Mi"),
},
},
},
},
},
want: corev1.PodQOSBurstable,
},
{
name: "Guaranteed when ephemeral container is guaranteed",
pod: &corev1.Pod{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
guaranteedContainer("app", "100m", "128Mi"),
},
EphemeralContainers: []corev1.EphemeralContainer{
{
EphemeralContainerCommon: corev1.EphemeralContainerCommon{
Name: "debug",
Resources: guaranteedRequirements("100m", "128Mi"),
},
},
},
},
},
want: corev1.PodQOSGuaranteed,
},
{
name: "zero CPU and memory requests and limits are ignored",
pod: &corev1.Pod{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
containerWithResources(
"app",
corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("0"),
corev1.ResourceMemory: resource.MustParse("0"),
},
corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("0"),
corev1.ResourceMemory: resource.MustParse("0"),
},
),
},
},
},
want: corev1.PodQOSBestEffort,
},
{
name: "unsupported resources do not influence QoS",
pod: &corev1.Pod{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
containerWithResources(
"app",
corev1.ResourceList{
corev1.ResourceName("example.com/gpu"): resource.MustParse("1"),
},
corev1.ResourceList{
corev1.ResourceName("example.com/gpu"): resource.MustParse("1"),
},
),
},
},
},
want: corev1.PodQOSBestEffort,
},
{
name: "hugepages do not influence QoS",
pod: &corev1.Pod{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
containerWithResources(
"app",
corev1.ResourceList{
corev1.ResourceName("hugepages-2Mi"): resource.MustParse("2Mi"),
},
corev1.ResourceList{
corev1.ResourceName("hugepages-2Mi"): resource.MustParse("2Mi"),
},
),
},
},
},
want: corev1.PodQOSBestEffort,
},
{
name: "pod-level unsupported resources do not influence QoS",
pod: &corev1.Pod{
Spec: corev1.PodSpec{
Resources: &corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceName("example.com/gpu"): resource.MustParse("1"),
},
Limits: corev1.ResourceList{
corev1.ResourceName("example.com/gpu"): resource.MustParse("1"),
},
},
Containers: []corev1.Container{
bestEffortContainer("app"),
},
},
},
want: corev1.PodQOSBestEffort,
},
{
name: "pod-level zero resources do not mask container-level Guaranteed",
pod: &corev1.Pod{
Spec: corev1.PodSpec{
Resources: &corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("0"),
corev1.ResourceMemory: resource.MustParse("0"),
},
Limits: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("0"),
corev1.ResourceMemory: resource.MustParse("0"),
},
},
Containers: []corev1.Container{
guaranteedContainer("app", "100m", "128Mi"),
},
},
},
want: corev1.PodQOSGuaranteed,
},
{
name: "pod-level CPU only request is Burstable",
pod: &corev1.Pod{
Spec: corev1.PodSpec{
Resources: &corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("100m"),
},
},
Containers: []corev1.Container{
bestEffortContainer("app"),
},
},
},
want: corev1.PodQOSBurstable,
},
{
name: "pod-level memory only limit is Burstable",
pod: &corev1.Pod{
Spec: corev1.PodSpec{
Resources: &corev1.ResourceRequirements{
Limits: corev1.ResourceList{
corev1.ResourceMemory: resource.MustParse("128Mi"),
},
},
Containers: []corev1.Container{
bestEffortContainer("app"),
},
},
},
want: corev1.PodQOSBurstable,
},
{
name: "pod-level CPU and memory unequal request and limit is Burstable",
pod: &corev1.Pod{
Spec: corev1.PodSpec{
Resources: &corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("100m"),
corev1.ResourceMemory: resource.MustParse("128Mi"),
},
Limits: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("200m"),
corev1.ResourceMemory: resource.MustParse("128Mi"),
},
},
Containers: []corev1.Container{
bestEffortContainer("app"),
},
},
},
want: corev1.PodQOSBurstable,
},
{
name: "no containers and no resources is BestEffort",
pod: &corev1.Pod{
Spec: corev1.PodSpec{},
},
want: corev1.PodQOSBestEffort,
},
{
name: "aggregate container requests and limits equal returns Guaranteed",
pod: &corev1.Pod{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
guaranteedContainer("app", "100m", "128Mi"),
guaranteedContainer("sidecar", "200m", "256Mi"),
},
},
},
want: corev1.PodQOSGuaranteed,
},
{
name: "mismatched container requests/limits are Burstable even if totals match",
pod: &corev1.Pod{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
containerWithResources(
"a",
corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("100m"),
corev1.ResourceMemory: resource.MustParse("128Mi"),
},
corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("200m"),
corev1.ResourceMemory: resource.MustParse("128Mi"),
},
),
containerWithResources(
"b",
corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("200m"),
corev1.ResourceMemory: resource.MustParse("128Mi"),
},
corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("100m"),
corev1.ResourceMemory: resource.MustParse("128Mi"),
},
),
},
},
},
want: corev1.PodQOSBurstable,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got := GetPodQoSClass(tt.pod)
if got != tt.want {
t.Fatalf("expected QoS class %q, got %q", tt.want, got)
}
})
}
}
func TestComputePodLevelQoSClassNilPodResources(t *testing.T) {
t.Parallel()
got, ok := computePodLevelQoSClass(&corev1.Pod{
Spec: corev1.PodSpec{
Resources: nil,
},
})
if got != corev1.PodQOSBestEffort {
t.Fatalf("expected QoS class %q, got %q", corev1.PodQOSBestEffort, got)
}
if ok {
t.Fatalf("expected ok=false, got true")
}
}
func TestComputePodLevelQoSClass(t *testing.T) {
t.Parallel()
tests := []struct {
name string
pod *corev1.Pod
want corev1.PodQOSClass
ok bool
}{
{
name: "nil pod returns false",
pod: nil,
want: corev1.PodQOSBestEffort,
ok: false,
},
{
name: "no pod-level resources returns false",
pod: &corev1.Pod{
Spec: corev1.PodSpec{},
},
want: corev1.PodQOSBestEffort,
ok: false,
},
{
name: "pod-level Guaranteed",
pod: &corev1.Pod{
Spec: corev1.PodSpec{
Resources: guaranteedPodResourcesPtr("100m", "128Mi"),
},
},
want: corev1.PodQOSGuaranteed,
ok: true,
},
{
name: "pod-level request only is Burstable",
pod: &corev1.Pod{
Spec: corev1.PodSpec{
Resources: requestOnlyPodResourcesPtr("100m", "128Mi"),
},
},
want: corev1.PodQOSBurstable,
ok: true,
},
{
name: "pod-level limit only is Burstable",
pod: &corev1.Pod{
Spec: corev1.PodSpec{
Resources: limitOnlyPodResourcesPtr("100m", "128Mi"),
},
},
want: corev1.PodQOSBurstable,
ok: true,
},
{
name: "pod-level CPU only equal request and limit is Burstable",
pod: &corev1.Pod{
Spec: corev1.PodSpec{
Resources: &corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("100m"),
},
Limits: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("100m"),
},
},
},
},
want: corev1.PodQOSBurstable,
ok: true,
},
{
name: "pod-level memory only equal request and limit is Burstable",
pod: &corev1.Pod{
Spec: corev1.PodSpec{
Resources: &corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceMemory: resource.MustParse("128Mi"),
},
Limits: corev1.ResourceList{
corev1.ResourceMemory: resource.MustParse("128Mi"),
},
},
},
},
want: corev1.PodQOSBurstable,
ok: true,
},
{
name: "pod-level zero resources return false",
pod: &corev1.Pod{
Spec: corev1.PodSpec{
Resources: &corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("0"),
corev1.ResourceMemory: resource.MustParse("0"),
},
Limits: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("0"),
corev1.ResourceMemory: resource.MustParse("0"),
},
},
},
},
want: corev1.PodQOSBestEffort,
ok: false,
},
{
name: "pod-level unsupported resources return false",
pod: &corev1.Pod{
Spec: corev1.PodSpec{
Resources: &corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceName("example.com/gpu"): resource.MustParse("1"),
},
Limits: corev1.ResourceList{
corev1.ResourceName("example.com/gpu"): resource.MustParse("1"),
},
},
},
},
want: corev1.PodQOSBestEffort,
ok: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got, ok := computePodLevelQoSClass(tt.pod)
if got != tt.want {
t.Fatalf("expected QoS class %q, got %q", tt.want, got)
}
if ok != tt.ok {
t.Fatalf("expected ok=%t, got %t", tt.ok, ok)
}
})
}
}
func TestQoSHelpers(t *testing.T) {
t.Parallel()
t.Run("hasSupportedQoSResource", func(t *testing.T) {
t.Parallel()
tests := []struct {
name string
resources corev1.ResourceList
want bool
}{
{
name: "nil resources",
resources: nil,
want: false,
},
{
name: "empty resources",
resources: corev1.ResourceList{},
want: false,
},
{
name: "CPU positive",
resources: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("1"),
},
want: true,
},
{
name: "memory positive",
resources: corev1.ResourceList{
corev1.ResourceMemory: resource.MustParse("1Mi"),
},
want: true,
},
{
name: "CPU zero",
resources: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("0"),
},
want: false,
},
{
name: "unsupported positive",
resources: corev1.ResourceList{
corev1.ResourceName("example.com/gpu"): resource.MustParse("1"),
},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got := hasSupportedQoSResource(tt.resources)
if got != tt.want {
t.Fatalf("expected %t, got %t", tt.want, got)
}
})
}
})
t.Run("positiveResource", func(t *testing.T) {
t.Parallel()
resources := corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("100m"),
corev1.ResourceMemory: resource.MustParse("0"),
}
if got, ok := positiveResource(resources, corev1.ResourceCPU); !ok || got.Cmp(resource.MustParse("100m")) != 0 {
t.Fatalf("expected positive CPU resource, got %q ok=%t", got.String(), ok)
}
if got, ok := positiveResource(resources, corev1.ResourceMemory); ok {
t.Fatalf("expected zero memory to be ignored, got %q ok=%t", got.String(), ok)
}
if got, ok := positiveResource(resources, corev1.ResourceStorage); ok {
t.Fatalf("expected missing storage to be ignored, got %q ok=%t", got.String(), ok)
}
})
t.Run("isSupportedQoSComputeResource", func(t *testing.T) {
t.Parallel()
tests := []struct {
name string
in corev1.ResourceName
want bool
}{
{name: "CPU", in: corev1.ResourceCPU, want: true},
{name: "memory", in: corev1.ResourceMemory, want: true},
{name: "storage", in: corev1.ResourceStorage, want: false},
{name: "ephemeral storage", in: corev1.ResourceEphemeralStorage, want: false},
{name: "extended resource", in: corev1.ResourceName("example.com/gpu"), want: false},
{name: "hugepage", in: corev1.ResourceName("hugepages-2Mi"), want: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got := isSupportedQoSComputeResource(tt.in)
if got != tt.want {
t.Fatalf("expected %t, got %t", tt.want, got)
}
})
}
})
}
func bestEffortContainer(name string) corev1.Container {
return corev1.Container{Name: name}
}
func requestOnlyContainer(name, cpu, memory string) corev1.Container {
return corev1.Container{
Name: name,
Resources: requestOnlyRequirements(cpu, memory),
}
}
func limitOnlyContainer(name, cpu, memory string) corev1.Container {
return corev1.Container{
Name: name,
Resources: limitOnlyRequirements(cpu, memory),
}
}
func guaranteedContainer(name, cpu, memory string) corev1.Container {
return corev1.Container{
Name: name,
Resources: guaranteedRequirements(cpu, memory),
}
}
func containerWithResources(
name string,
requests corev1.ResourceList,
limits corev1.ResourceList,
) corev1.Container {
return corev1.Container{
Name: name,
Resources: corev1.ResourceRequirements{
Requests: requests,
Limits: limits,
},
}
}
func requestOnlyRequirements(cpu, memory string) corev1.ResourceRequirements {
return corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse(cpu),
corev1.ResourceMemory: resource.MustParse(memory),
},
}
}
func limitOnlyRequirements(cpu, memory string) corev1.ResourceRequirements {
return corev1.ResourceRequirements{
Limits: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse(cpu),
corev1.ResourceMemory: resource.MustParse(memory),
},
}
}
func guaranteedRequirements(cpu, memory string) corev1.ResourceRequirements {
return corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse(cpu),
corev1.ResourceMemory: resource.MustParse(memory),
},
Limits: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse(cpu),
corev1.ResourceMemory: resource.MustParse(memory),
},
}
}
func requestOnlyPodResources(cpu, memory string) corev1.ResourceRequirements {
return requestOnlyRequirements(cpu, memory)
}
func limitOnlyPodResources(cpu, memory string) corev1.ResourceRequirements {
return limitOnlyRequirements(cpu, memory)
}
func guaranteedPodResources(cpu, memory string) corev1.ResourceRequirements {
return guaranteedRequirements(cpu, memory)
}
func requestOnlyPodResourcesPtr(cpu, memory string) *corev1.ResourceRequirements {
resources := requestOnlyRequirements(cpu, memory)
return &resources
}
func limitOnlyPodResourcesPtr(cpu, memory string) *corev1.ResourceRequirements {
resources := limitOnlyRequirements(cpu, memory)
return &resources
}
func guaranteedPodResourcesPtr(cpu, memory string) *corev1.ResourceRequirements {
resources := guaranteedRequirements(cpu, memory)
return &resources
}