mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-08-23 06:26:43 +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>
387 lines
9.6 KiB
Go
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
|
|
}
|