From 290c27e9aa0e95b63a8b117cb6690438633aa1ae Mon Sep 17 00:00:00 2001 From: Mattias Andersson Date: Thu, 5 Dec 2024 12:12:44 +0100 Subject: [PATCH] Add possibility to add resource request/limit to initContainer This commit enables user to configure resource request and limit for cpu and memory on the "setup-ca-certs" initContainer. By not being able to set resources it blocks creation of the Pod if a namespace has a ResourceQuota in place. Signed-off-by: Mattias Andersson --- .../admission_controller.go | 58 +++++++++- .../admission_controller_test.go | 105 +++++++++++++++++- 2 files changed, 160 insertions(+), 3 deletions(-) diff --git a/pkg/certinjectionwebhook/admission_controller.go b/pkg/certinjectionwebhook/admission_controller.go index 12ccdc3..00d7d58 100644 --- a/pkg/certinjectionwebhook/admission_controller.go +++ b/pkg/certinjectionwebhook/admission_controller.go @@ -8,11 +8,11 @@ import ( "context" "encoding/json" "fmt" - "github.com/pkg/errors" admissionv1 "k8s.io/api/admission/v1" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/serializer" @@ -20,6 +20,7 @@ import ( "knative.dev/pkg/apis/duck" "knative.dev/pkg/logging" "knative.dev/pkg/webhook" + "os" "github.com/vmware-tanzu/cert-injection-webhook/pkg/certs" ) @@ -187,6 +188,18 @@ func (ac *admissionController) SetEnvVars(ctx context.Context, obj *corev1.Pod) } } +func ParseResource(envVar string) (resource.Quantity, error) { + value, found := os.LookupEnv(envVar) + if !found { + return resource.Quantity{}, nil // Return an empty Quantity if env var is missing + } + qty, err := resource.ParseQuantity(value) + if err != nil { + return resource.Quantity{}, fmt.Errorf("failed to parse %s: %w", envVar, err) + } + return qty, nil +} + func (ac *admissionController) SetCaCerts(ctx context.Context, obj *corev1.Pod) { if ac.caCertsData == "" { return @@ -224,6 +237,44 @@ func (ac *admissionController) SetCaCerts(ctx context.Context, obj *corev1.Pod) }) } + var resources corev1.ResourceRequirements + + if cpuRequest, err := ParseResource("INIT_CONTAINER_CPU_REQUEST"); err == nil { + if resources.Requests == nil { + resources.Requests = corev1.ResourceList{} + } + resources.Requests[corev1.ResourceCPU] = cpuRequest + } else { + fmt.Printf("Warning: %v\n", err) + } + + if memoryRequest, err := ParseResource("INIT_CONTAINER_MEMORY_REQUEST"); err == nil { + if resources.Requests == nil { + resources.Requests = corev1.ResourceList{} + } + resources.Requests[corev1.ResourceMemory] = memoryRequest + } else { + fmt.Printf("Warning: %v\n", err) + } + + if cpuLimit, err := ParseResource("INIT_CONTAINER_CPU_LIMIT"); err == nil { + if resources.Limits == nil { + resources.Limits = corev1.ResourceList{} + } + resources.Limits[corev1.ResourceCPU] = cpuLimit + } else { + fmt.Printf("Warning: %v\n", err) + } + + if memoryLimit, err := ParseResource("INIT_CONTAINER_MEMORY_LIMIT"); err == nil { + if resources.Limits == nil { + resources.Limits = corev1.ResourceList{} + } + resources.Limits[corev1.ResourceMemory] = memoryLimit + } else { + fmt.Printf("Warning: %v\n", err) + } + container := corev1.Container{ Name: "setup-ca-certs", Image: ac.setupCACertsImage, @@ -244,6 +295,11 @@ func (ac *admissionController) SetCaCerts(ctx context.Context, obj *corev1.Pod) Capabilities: &corev1.Capabilities{Drop: []corev1.Capability{"ALL"}}, }, } + + if len(resources.Requests) > 0 || len(resources.Limits) > 0 { + container.Resources = resources + } + obj.Spec.InitContainers = append([]corev1.Container{container}, obj.Spec.InitContainers...) } diff --git a/pkg/certinjectionwebhook/admission_controller_test.go b/pkg/certinjectionwebhook/admission_controller_test.go index 967b9ec..9d6b627 100644 --- a/pkg/certinjectionwebhook/admission_controller_test.go +++ b/pkg/certinjectionwebhook/admission_controller_test.go @@ -6,20 +6,20 @@ package certinjectionwebhook_test import ( "context" "encoding/json" + "os" "testing" jp "github.com/evanphx/json-patch/v5" "github.com/sclevine/spec" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/vmware-tanzu/cert-injection-webhook/pkg/certinjectionwebhook" "gomodules.xyz/jsonpatch/v3" admissionv1 "k8s.io/api/admission/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" wtesting "knative.dev/pkg/webhook/testing" - - "github.com/vmware-tanzu/cert-injection-webhook/pkg/certinjectionwebhook" ) func TestPodAdmissionController(t *testing.T) { @@ -1396,3 +1396,104 @@ func testPodAdmissionController(t *testing.T, when spec.G, it spec.S) { }) } + +func TestParseResource(t *testing.T) { + tests := []struct { + name string + envVar string + envValue string + expectErr bool + expectVal string + }{ + {"Valid CPU Request", "TEST_CPU_REQUEST", "100m", false, "100m"}, + {"Valid Memory Request", "TEST_MEMORY_REQUEST", "128Mi", false, "128Mi"}, + {"Invalid Format", "TEST_INVALID", "invalid", true, ""}, + {"Missing Env Var", "TEST_MISSING", "", false, ""}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Set or unset the environment variable + if tt.envValue != "" { + os.Setenv(tt.envVar, tt.envValue) + defer os.Unsetenv(tt.envVar) + } else { + os.Unsetenv(tt.envVar) + } + + qty, err := certinjectionwebhook.ParseResource(tt.envVar) + if (err != nil) != tt.expectErr { + t.Errorf("Expected error: %v, got: %v", tt.expectErr, err) + } + + if err == nil && tt.expectVal != "" && qty.String() != tt.expectVal { + t.Errorf("Expected value: %s, got: %s", tt.expectVal, qty.String()) + } + }) + } +} + +func TestResourceRequirementsParsing(t *testing.T) { + // Set environment variables + os.Setenv("INIT_CONTAINER_CPU_REQUEST", "200m") + os.Setenv("INIT_CONTAINER_MEMORY_REQUEST", "256Mi") + os.Setenv("INIT_CONTAINER_CPU_LIMIT", "1") + os.Setenv("INIT_CONTAINER_MEMORY_LIMIT", "512Mi") + defer func() { + os.Unsetenv("INIT_CONTAINER_CPU_REQUEST") + os.Unsetenv("INIT_CONTAINER_MEMORY_REQUEST") + os.Unsetenv("INIT_CONTAINER_CPU_LIMIT") + os.Unsetenv("INIT_CONTAINER_MEMORY_LIMIT") + }() + + var resources corev1.ResourceRequirements + + // Apply the logic under test + if cpuRequest, err := certinjectionwebhook.ParseResource("INIT_CONTAINER_CPU_REQUEST"); err == nil { + if resources.Requests == nil { + resources.Requests = corev1.ResourceList{} + } + resources.Requests[corev1.ResourceCPU] = cpuRequest + } + if memoryRequest, err := certinjectionwebhook.ParseResource("INIT_CONTAINER_MEMORY_REQUEST"); err == nil { + if resources.Requests == nil { + resources.Requests = corev1.ResourceList{} + } + resources.Requests[corev1.ResourceMemory] = memoryRequest + } + if cpuLimit, err := certinjectionwebhook.ParseResource("INIT_CONTAINER_CPU_LIMIT"); err == nil { + if resources.Limits == nil { + resources.Limits = corev1.ResourceList{} + } + resources.Limits[corev1.ResourceCPU] = cpuLimit + } + if memoryLimit, err := certinjectionwebhook.ParseResource("INIT_CONTAINER_MEMORY_LIMIT"); err == nil { + if resources.Limits == nil { + resources.Limits = corev1.ResourceList{} + } + resources.Limits[corev1.ResourceMemory] = memoryLimit + } + + expectedRequests := map[corev1.ResourceName]string{ + corev1.ResourceCPU: "200m", + corev1.ResourceMemory: "256Mi", + } + expectedLimits := map[corev1.ResourceName]string{ + corev1.ResourceCPU: "1", + corev1.ResourceMemory: "512Mi", + } + + for k, v := range expectedRequests { + qty := resources.Requests[k] // Copy value from map + if qty.String() != v { + t.Errorf("Expected request %s for %s, got %s", v, k, qty.String()) + } + } + + for k, v := range expectedLimits { + qty := resources.Limits[k] // Copy value from map + if qty.String() != v { + t.Errorf("Expected limit %s for %s, got %s", v, k, qty.String()) + } + } +}