Files
capsule/internal/webhook/rules/generic/mutation/workload_resources.go
T
Oliver BählerandGitHub 65a4e19e02 feat: requests and limit policies (#2095)
* 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>
2026-08-21 14:31:06 +02:00

387 lines
9.6 KiB
Go

// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package mutation
import (
"fmt"
"slices"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
resourcehelper "k8s.io/component-helpers/resource"
apirules "github.com/projectcapsule/capsule/pkg/api/rules"
"github.com/projectcapsule/capsule/pkg/runtime/workloads"
)
type workloadResourcePolicies struct {
requests map[corev1.ResourceName]apirules.WorkloadResourceRequestPolicy
limits map[corev1.ResourceName]apirules.WorkloadResourceLimitPolicy
}
func MutateWorkloadResources(
obj *unstructured.Unstructured,
gvk schema.GroupVersionKind,
bodies []*apirules.NamespaceRuleBodyNamespace,
) (bool, error) {
if obj == nil || gvk != corev1.SchemeGroupVersion.WithKind("Pod") {
return false, nil
}
pod := &corev1.Pod{}
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, pod); err != nil {
return false, fmt.Errorf("decode Pod resource policies: %w", err)
}
changed, err := MutatePodResources(pod, bodies)
if err != nil || !changed {
return changed, err
}
mutated, err := runtime.DefaultUnstructuredConverter.ToUnstructured(pod)
if err != nil {
return false, fmt.Errorf("encode Pod resource policies: %w", err)
}
obj.Object = mutated
return true, nil
}
func MutatePodResources(
pod *corev1.Pod,
bodies []*apirules.NamespaceRuleBodyNamespace,
) (bool, error) {
if pod == nil {
return false, nil
}
mutated := false
containerPolicies := collectWorkloadResourcePolicies(bodies, apirules.ValidateContainers)
for i := range pod.Spec.Containers {
changed, err := mutateResourceRequirements(&pod.Spec.Containers[i].Resources, containerPolicies)
if err != nil {
return false, fmt.Errorf("spec.containers[%d].resources: %w", i, err)
}
mutated = mutated || changed
}
initContainerPolicies := collectWorkloadResourcePolicies(bodies, apirules.ValidateInitContainers)
for i := range pod.Spec.InitContainers {
changed, err := mutateResourceRequirements(&pod.Spec.InitContainers[i].Resources, initContainerPolicies)
if err != nil {
return false, fmt.Errorf("spec.initContainers[%d].resources: %w", i, err)
}
mutated = mutated || changed
}
podPolicies := collectWorkloadResourcePolicies(bodies, apirules.ValidatePod)
if len(podPolicies.requests) > 0 || len(podPolicies.limits) > 0 {
resources := pod.Spec.Resources
if resources == nil {
resources = &corev1.ResourceRequirements{}
}
changed, err := mutateResourceRequirements(resources, podPolicies)
if err != nil {
return false, fmt.Errorf("spec.resources: %w", err)
}
if changed {
pod.Spec.Resources = resources
mutated = true
}
}
if mutated {
backfillMissingPodRequests(pod)
}
return mutated, nil
}
// backfillMissingPodRequests keeps Pods mutated by Capsule valid on Kubernetes
// 1.33. That release validates every aggregate container request as soon as
// spec.resources is present, treating a missing Pod-level request as zero. We
// can backfill only resources Kubernetes permits at Pod level. Kubernetes 1.34
// and later skip missing keys; explicitly storing the effective aggregate keeps
// the same semantics on those releases.
func backfillMissingPodRequests(pod *corev1.Pod) {
if pod.Spec.Resources == nil {
return
}
requests := resourcehelper.AggregateContainerRequests(pod, resourcehelper.PodResourcesOptions{})
for name, request := range requests {
if !workloads.PodLevelResourceSupported(name) {
continue
}
if _, found := pod.Spec.Resources.Requests[name]; found {
continue
}
if pod.Spec.Resources.Requests == nil {
pod.Spec.Resources.Requests = corev1.ResourceList{}
}
pod.Spec.Resources.Requests[name] = request.DeepCopy()
}
}
func collectWorkloadResourcePolicies(
bodies []*apirules.NamespaceRuleBodyNamespace,
target apirules.WorkloadValidationTarget,
) workloadResourcePolicies {
out := workloadResourcePolicies{}
for _, body := range bodies {
if body == nil || body.Enforce == nil || body.Enforce.Workloads.Resources == nil {
continue
}
if !resourcePoliciesTarget(body.Enforce.Workloads, target) {
continue
}
resources := body.Enforce.Workloads.Resources
for name, policy := range resources.Requests {
if !resourcePolicySupportsTarget(name, target) {
continue
}
if out.requests == nil {
out.requests = make(map[corev1.ResourceName]apirules.WorkloadResourceRequestPolicy)
}
out.requests[name] = policy
}
for name, policy := range resources.Limits {
if !resourcePolicySupportsTarget(name, target) {
continue
}
if out.limits == nil {
out.limits = make(map[corev1.ResourceName]apirules.WorkloadResourceLimitPolicy)
}
out.limits[name] = policy
}
}
return out
}
func resourcePoliciesTarget(
workload apirules.NamespaceRuleEnforceWorkloadsBody,
target apirules.WorkloadValidationTarget,
) bool {
if len(workload.Targets) == 0 {
return target == apirules.ValidatePod ||
target == apirules.ValidateContainers ||
target == apirules.ValidateInitContainers
}
return slices.Contains(workload.Targets, target)
}
func resourcePolicySupportsTarget(
name corev1.ResourceName,
target apirules.WorkloadValidationTarget,
) bool {
return target != apirules.ValidatePod || workloads.PodLevelResourceSupported(name)
}
func mutateResourceRequirements(
resources *corev1.ResourceRequirements,
policies workloadResourcePolicies,
) (bool, error) {
if resources == nil {
return false, nil
}
requestsChanged, err := mutateRequestPolicies(resources, policies.requests)
if err != nil {
return false, err
}
limitsChanged, err := mutateLimitPolicies(resources, policies.limits)
if err != nil {
return false, err
}
return requestsChanged || limitsChanged, nil
}
func mutateRequestPolicies(
resources *corev1.ResourceRequirements,
policies map[corev1.ResourceName]apirules.WorkloadResourceRequestPolicy,
) (bool, error) {
changed := false
for name, policy := range policies {
policyChanged, err := mutateRequestPolicy(resources, name, policy)
if err != nil {
return false, err
}
changed = policyChanged || changed
}
return changed, nil
}
func mutateRequestPolicy(
resources *corev1.ResourceRequirements,
name corev1.ResourceName,
policy apirules.WorkloadResourceRequestPolicy,
) (bool, error) {
switch policy.Policy {
case apirules.WorkloadResourceRequestPolicyPreserve:
return false, nil
case apirules.WorkloadResourceRequestPolicyDefault:
if policy.Value == nil {
return false, fmt.Errorf("request %q Default policy has no value", name)
}
if _, found := resources.Requests[name]; found {
return false, nil
}
if resources.Requests == nil {
resources.Requests = corev1.ResourceList{}
}
resources.Requests[name] = policy.Value.DeepCopy()
return true, nil
case apirules.WorkloadResourceRequestPolicyRemove:
return removeResource(resources.Requests, name), nil
default:
return false, fmt.Errorf("request %q has unsupported policy %q", name, policy.Policy)
}
}
func mutateLimitPolicies(
resources *corev1.ResourceRequirements,
policies map[corev1.ResourceName]apirules.WorkloadResourceLimitPolicy,
) (bool, error) {
changed := false
for name, policy := range policies {
policyChanged, err := mutateLimitPolicy(resources, name, policy)
if err != nil {
return false, err
}
changed = policyChanged || changed
}
return changed, nil
}
func mutateLimitPolicy(
resources *corev1.ResourceRequirements,
name corev1.ResourceName,
policy apirules.WorkloadResourceLimitPolicy,
) (bool, error) {
switch policy.Policy {
case apirules.WorkloadResourceLimitPolicyPreserve:
return false, nil
case apirules.WorkloadResourceLimitPolicyDefault:
if policy.Value == nil {
return false, fmt.Errorf("limit %q Default policy has no value", name)
}
if _, found := resources.Limits[name]; found {
return false, nil
}
if resources.Limits == nil {
resources.Limits = corev1.ResourceList{}
}
resources.Limits[name] = policy.Value.DeepCopy()
return true, nil
case apirules.WorkloadResourceLimitPolicyRemove:
return removeResource(resources.Limits, name), nil
case apirules.WorkloadResourceLimitPolicyMatchRequest:
return matchResourceRequest(resources, name), nil
case apirules.WorkloadResourceLimitPolicyRatio:
return defaultResourceRatio(resources, name, policy.Value)
default:
return false, fmt.Errorf("limit %q has unsupported policy %q", name, policy.Policy)
}
}
func removeResource(resources corev1.ResourceList, name corev1.ResourceName) bool {
if _, found := resources[name]; !found {
return false
}
delete(resources, name)
return true
}
func matchResourceRequest(resources *corev1.ResourceRequirements, name corev1.ResourceName) bool {
request, found := resources.Requests[name]
if !found {
return false
}
if limit, found := resources.Limits[name]; found && limit.Cmp(request) == 0 {
return false
}
if resources.Limits == nil {
resources.Limits = corev1.ResourceList{}
}
resources.Limits[name] = request.DeepCopy()
return true
}
func defaultResourceRatio(
resources *corev1.ResourceRequirements,
name corev1.ResourceName,
ratio *resource.Quantity,
) (bool, error) {
if ratio == nil {
return false, fmt.Errorf("limit %q Ratio policy has no value", name)
}
if _, found := resources.Limits[name]; found {
return false, nil
}
request, found := resources.Requests[name]
if !found || request.Sign() <= 0 {
return false, nil
}
limit, err := workloads.LimitForRatio(name, request, *ratio)
if err != nil {
return false, err
}
if resources.Limits == nil {
resources.Limits = corev1.ResourceList{}
}
resources.Limits[name] = limit
return true, nil
}