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 }