mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-08-22 22:16:46 +00:00
* chore: save progress Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat: requests and limit policies Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat: requests and limit policies Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat: requests and limit policies Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat: requests and limit policies Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat: requests and limit policies Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat: requests and limit policies Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat: requests and limit policies Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat: requests and limit policies Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat: requests and limit policies Signed-off-by: Oliver Baehler <oliver@sudo-i.net> --------- Signed-off-by: Oliver Baehler <oliver@sudo-i.net>
304 lines
9.5 KiB
Go
304 lines
9.5 KiB
Go
// Copyright 2020-2026 Project Capsule Authors
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package mutation
|
|
|
|
import (
|
|
"testing"
|
|
|
|
corev1 "k8s.io/api/core/v1"
|
|
"k8s.io/apimachinery/pkg/api/resource"
|
|
|
|
"github.com/projectcapsule/capsule/pkg/api/rules"
|
|
)
|
|
|
|
func TestMutatePodResourcesUsesAllDefaultTargets(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
pod := &corev1.Pod{Spec: corev1.PodSpec{
|
|
Containers: []corev1.Container{{
|
|
Name: "app",
|
|
Resources: corev1.ResourceRequirements{
|
|
Requests: corev1.ResourceList{corev1.ResourceMemory: resource.MustParse("1Gi")},
|
|
Limits: corev1.ResourceList{corev1.ResourceCPU: resource.MustParse("1")},
|
|
},
|
|
}},
|
|
InitContainers: []corev1.Container{{
|
|
Name: "init",
|
|
Resources: corev1.ResourceRequirements{
|
|
Requests: corev1.ResourceList{corev1.ResourceMemory: resource.MustParse("512Mi")},
|
|
},
|
|
}},
|
|
Resources: &corev1.ResourceRequirements{
|
|
Requests: corev1.ResourceList{corev1.ResourceCPU: resource.MustParse("500m")},
|
|
Limits: corev1.ResourceList{corev1.ResourceCPU: resource.MustParse("1")},
|
|
},
|
|
}}
|
|
|
|
bodies := []*rules.NamespaceRuleBodyNamespace{{Enforce: &rules.NamespaceRuleEnforceBody{
|
|
Workloads: rules.NamespaceRuleEnforceWorkloadsBody{
|
|
Resources: &rules.WorkloadResourceRules{
|
|
Requests: map[corev1.ResourceName]rules.WorkloadResourceRequestPolicy{
|
|
corev1.ResourceCPU: {
|
|
Policy: rules.WorkloadResourceRequestPolicyDefault,
|
|
Value: quantityPointer("100m"),
|
|
},
|
|
},
|
|
Limits: map[corev1.ResourceName]rules.WorkloadResourceLimitPolicy{
|
|
corev1.ResourceCPU: {Policy: rules.WorkloadResourceLimitPolicyRemove},
|
|
corev1.ResourceMemory: {
|
|
Policy: rules.WorkloadResourceLimitPolicyMatchRequest,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}}}
|
|
|
|
changed, err := MutatePodResources(pod, bodies)
|
|
if err != nil {
|
|
t.Fatalf("MutatePodResources() error = %v", err)
|
|
}
|
|
if !changed {
|
|
t.Fatal("MutatePodResources() changed = false, want true")
|
|
}
|
|
|
|
assertResourceQuantity(t, pod.Spec.Containers[0].Resources.Requests, corev1.ResourceCPU, "100m")
|
|
assertResourceMissing(t, pod.Spec.Containers[0].Resources.Limits, corev1.ResourceCPU)
|
|
assertResourceQuantity(t, pod.Spec.Containers[0].Resources.Limits, corev1.ResourceMemory, "1Gi")
|
|
assertResourceQuantity(t, pod.Spec.InitContainers[0].Resources.Requests, corev1.ResourceCPU, "100m")
|
|
assertResourceQuantity(t, pod.Spec.InitContainers[0].Resources.Limits, corev1.ResourceMemory, "512Mi")
|
|
|
|
assertResourceQuantity(t, pod.Spec.Resources.Requests, corev1.ResourceCPU, "500m")
|
|
assertResourceQuantity(t, pod.Spec.Resources.Requests, corev1.ResourceMemory, "1Gi")
|
|
assertResourceMissing(t, pod.Spec.Resources.Limits, corev1.ResourceCPU)
|
|
}
|
|
|
|
func TestMutatePodResourcesBackfillsOnlySupportedMissingPodRequests(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
pod := &corev1.Pod{Spec: corev1.PodSpec{
|
|
Containers: []corev1.Container{{
|
|
Name: "app",
|
|
Resources: corev1.ResourceRequirements{Requests: corev1.ResourceList{
|
|
corev1.ResourceMemory: resource.MustParse("1Gi"),
|
|
corev1.ResourceEphemeralStorage: resource.MustParse("2Gi"),
|
|
}},
|
|
}, {
|
|
Name: "sidecar",
|
|
Resources: corev1.ResourceRequirements{Requests: corev1.ResourceList{
|
|
corev1.ResourceMemory: resource.MustParse("512Mi"),
|
|
}},
|
|
}},
|
|
Resources: &corev1.ResourceRequirements{
|
|
Requests: corev1.ResourceList{corev1.ResourceCPU: resource.MustParse("500m")},
|
|
},
|
|
}}
|
|
|
|
bodies := []*rules.NamespaceRuleBodyNamespace{{Enforce: &rules.NamespaceRuleEnforceBody{
|
|
Workloads: rules.NamespaceRuleEnforceWorkloadsBody{
|
|
Targets: []rules.WorkloadValidationTarget{rules.ValidateContainers},
|
|
Resources: &rules.WorkloadResourceRules{Requests: map[corev1.ResourceName]rules.WorkloadResourceRequestPolicy{
|
|
corev1.ResourceCPU: {
|
|
Policy: rules.WorkloadResourceRequestPolicyDefault,
|
|
Value: quantityPointer("100m"),
|
|
},
|
|
}},
|
|
},
|
|
}}}
|
|
|
|
changed, err := MutatePodResources(pod, bodies)
|
|
if err != nil {
|
|
t.Fatalf("MutatePodResources() error = %v", err)
|
|
}
|
|
if !changed {
|
|
t.Fatal("MutatePodResources() changed = false, want true")
|
|
}
|
|
|
|
assertResourceQuantity(t, pod.Spec.Resources.Requests, corev1.ResourceCPU, "500m")
|
|
assertResourceQuantity(t, pod.Spec.Resources.Requests, corev1.ResourceMemory, "1536Mi")
|
|
assertResourceMissing(t, pod.Spec.Resources.Requests, corev1.ResourceEphemeralStorage)
|
|
}
|
|
|
|
func TestMutatePodResourcesDefaultTargetsSkipPodIncompatibleResources(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
pod := &corev1.Pod{Spec: corev1.PodSpec{
|
|
Containers: []corev1.Container{{Name: "app"}},
|
|
}}
|
|
|
|
bodies := []*rules.NamespaceRuleBodyNamespace{{Enforce: &rules.NamespaceRuleEnforceBody{
|
|
Workloads: rules.NamespaceRuleEnforceWorkloadsBody{
|
|
Resources: &rules.WorkloadResourceRules{
|
|
Requests: map[corev1.ResourceName]rules.WorkloadResourceRequestPolicy{
|
|
corev1.ResourceEphemeralStorage: {
|
|
Policy: rules.WorkloadResourceRequestPolicyDefault,
|
|
Value: quantityPointer("1Gi"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}}}
|
|
|
|
changed, err := MutatePodResources(pod, bodies)
|
|
if err != nil {
|
|
t.Fatalf("MutatePodResources() error = %v", err)
|
|
}
|
|
if !changed {
|
|
t.Fatal("MutatePodResources() changed = false, want true")
|
|
}
|
|
|
|
assertResourceQuantity(
|
|
t,
|
|
pod.Spec.Containers[0].Resources.Requests,
|
|
corev1.ResourceEphemeralStorage,
|
|
"1Gi",
|
|
)
|
|
if pod.Spec.Resources != nil {
|
|
t.Fatalf("pod.Spec.Resources = %#v, want nil", pod.Spec.Resources)
|
|
}
|
|
}
|
|
|
|
func TestMutatePodResourcesPodRatio(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
pod := &corev1.Pod{Spec: corev1.PodSpec{
|
|
Resources: &corev1.ResourceRequirements{
|
|
Requests: corev1.ResourceList{corev1.ResourceCPU: resource.MustParse("500m")},
|
|
},
|
|
}}
|
|
|
|
bodies := []*rules.NamespaceRuleBodyNamespace{{Enforce: &rules.NamespaceRuleEnforceBody{
|
|
Workloads: rules.NamespaceRuleEnforceWorkloadsBody{
|
|
Targets: []rules.WorkloadValidationTarget{rules.ValidatePod},
|
|
Resources: &rules.WorkloadResourceRules{
|
|
Limits: map[corev1.ResourceName]rules.WorkloadResourceLimitPolicy{
|
|
corev1.ResourceCPU: {
|
|
Policy: rules.WorkloadResourceLimitPolicyRatio,
|
|
Value: quantityPointer("1.5"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}}}
|
|
|
|
changed, err := MutatePodResources(pod, bodies)
|
|
if err != nil {
|
|
t.Fatalf("MutatePodResources() error = %v", err)
|
|
}
|
|
if !changed {
|
|
t.Fatal("MutatePodResources() changed = false, want true")
|
|
}
|
|
|
|
assertResourceQuantity(t, pod.Spec.Resources.Limits, corev1.ResourceCPU, "750m")
|
|
}
|
|
|
|
func TestMutatePodResourcesPreservesExplicitRatioLimit(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
pod := &corev1.Pod{Spec: corev1.PodSpec{Containers: []corev1.Container{{
|
|
Name: "app",
|
|
Resources: corev1.ResourceRequirements{
|
|
Requests: corev1.ResourceList{corev1.ResourceMemory: resource.MustParse("1Gi")},
|
|
Limits: corev1.ResourceList{corev1.ResourceMemory: resource.MustParse("2Gi")},
|
|
},
|
|
}}}}
|
|
|
|
bodies := resourceMutationBodies(
|
|
rules.WorkloadResourceLimitPolicyRatio,
|
|
quantityPointer("1.5"),
|
|
)
|
|
|
|
changed, err := MutatePodResources(pod, bodies)
|
|
if err != nil {
|
|
t.Fatalf("MutatePodResources() error = %v", err)
|
|
}
|
|
if changed {
|
|
t.Fatal("MutatePodResources() changed an explicitly supplied ratio limit")
|
|
}
|
|
|
|
assertResourceQuantity(t, pod.Spec.Containers[0].Resources.Limits, corev1.ResourceMemory, "2Gi")
|
|
}
|
|
|
|
func TestMutatePodResourcesLastPolicyWins(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
pod := &corev1.Pod{Spec: corev1.PodSpec{Containers: []corev1.Container{{
|
|
Name: "app",
|
|
Resources: corev1.ResourceRequirements{
|
|
Limits: corev1.ResourceList{corev1.ResourceCPU: resource.MustParse("1")},
|
|
},
|
|
}}}}
|
|
|
|
bodies := []*rules.NamespaceRuleBodyNamespace{
|
|
{Enforce: &rules.NamespaceRuleEnforceBody{Workloads: rules.NamespaceRuleEnforceWorkloadsBody{
|
|
Resources: &rules.WorkloadResourceRules{Limits: map[corev1.ResourceName]rules.WorkloadResourceLimitPolicy{
|
|
corev1.ResourceCPU: {Policy: rules.WorkloadResourceLimitPolicyRemove},
|
|
}},
|
|
}}},
|
|
{Enforce: &rules.NamespaceRuleEnforceBody{Workloads: rules.NamespaceRuleEnforceWorkloadsBody{
|
|
Targets: []rules.WorkloadValidationTarget{rules.ValidateContainers},
|
|
Resources: &rules.WorkloadResourceRules{Limits: map[corev1.ResourceName]rules.WorkloadResourceLimitPolicy{
|
|
corev1.ResourceCPU: {Policy: rules.WorkloadResourceLimitPolicyPreserve},
|
|
}},
|
|
}}},
|
|
}
|
|
|
|
changed, err := MutatePodResources(pod, bodies)
|
|
if err != nil {
|
|
t.Fatalf("MutatePodResources() error = %v", err)
|
|
}
|
|
if changed {
|
|
t.Fatal("MutatePodResources() ignored the later Preserve policy")
|
|
}
|
|
|
|
assertResourceQuantity(t, pod.Spec.Containers[0].Resources.Limits, corev1.ResourceCPU, "1")
|
|
}
|
|
|
|
func resourceMutationBodies(
|
|
policy rules.WorkloadResourceLimitPolicyType,
|
|
value *resource.Quantity,
|
|
) []*rules.NamespaceRuleBodyNamespace {
|
|
return []*rules.NamespaceRuleBodyNamespace{{Enforce: &rules.NamespaceRuleEnforceBody{
|
|
Workloads: rules.NamespaceRuleEnforceWorkloadsBody{
|
|
Resources: &rules.WorkloadResourceRules{
|
|
Limits: map[corev1.ResourceName]rules.WorkloadResourceLimitPolicy{
|
|
corev1.ResourceMemory: {Policy: policy, Value: value},
|
|
},
|
|
},
|
|
},
|
|
}}}
|
|
}
|
|
|
|
func quantityPointer(value string) *resource.Quantity {
|
|
quantity := resource.MustParse(value)
|
|
|
|
return &quantity
|
|
}
|
|
|
|
func assertResourceQuantity(
|
|
t *testing.T,
|
|
resources corev1.ResourceList,
|
|
name corev1.ResourceName,
|
|
want string,
|
|
) {
|
|
t.Helper()
|
|
|
|
got, found := resources[name]
|
|
if !found {
|
|
t.Fatalf("resource %q is missing", name)
|
|
}
|
|
|
|
wantQuantity := resource.MustParse(want)
|
|
if got.Cmp(wantQuantity) != 0 {
|
|
t.Fatalf("resource %q = %s, want %s", name, got.String(), wantQuantity.String())
|
|
}
|
|
}
|
|
|
|
func assertResourceMissing(t *testing.T, resources corev1.ResourceList, name corev1.ResourceName) {
|
|
t.Helper()
|
|
|
|
if value, found := resources[name]; found {
|
|
t.Fatalf("resource %q = %s, want missing", name, value.String())
|
|
}
|
|
}
|