mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-08-23 06:26:43 +00:00
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>
This commit is contained in:
@@ -9,12 +9,15 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
k8smeta "k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
k8svalidation "k8s.io/apimachinery/pkg/util/validation"
|
||||
|
||||
"github.com/projectcapsule/capsule/pkg/api/rules"
|
||||
"github.com/projectcapsule/capsule/pkg/api/runtime"
|
||||
workloadruntime "github.com/projectcapsule/capsule/pkg/runtime/workloads"
|
||||
)
|
||||
|
||||
func ValidateRuleStatusBody(
|
||||
@@ -172,6 +175,28 @@ func validateWorkloadRules(
|
||||
ruleIndex int,
|
||||
workloads rules.NamespaceRuleEnforceWorkloadsBody,
|
||||
) error {
|
||||
for j, target := range workloads.Targets {
|
||||
switch target {
|
||||
case rules.DeprecatedValidateImages,
|
||||
rules.ValidatePod,
|
||||
rules.ValidateInitContainers,
|
||||
rules.ValidateEphemeralContainers,
|
||||
rules.ValidateContainers,
|
||||
rules.ValidateVolumes:
|
||||
default:
|
||||
return fmt.Errorf(
|
||||
"rules[%d].enforce.workloads.targets[%d] %q is invalid: unsupported workload target",
|
||||
ruleIndex,
|
||||
j,
|
||||
target,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if err := validateWorkloadResourceRules(ruleIndex, workloads); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for j, registry := range workloads.Registries {
|
||||
if err := validateExpression(
|
||||
registry.Expression,
|
||||
@@ -193,6 +218,173 @@ func validateWorkloadRules(
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateWorkloadResourceRules(
|
||||
ruleIndex int,
|
||||
workloads rules.NamespaceRuleEnforceWorkloadsBody,
|
||||
) error {
|
||||
resources := workloads.Resources
|
||||
if resources == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("rules[%d].enforce.workloads.resources", ruleIndex)
|
||||
if len(resources.Requests) == 0 && len(resources.Limits) == 0 {
|
||||
return fmt.Errorf("%s is invalid: at least one request or limit policy is required", path)
|
||||
}
|
||||
|
||||
podTarget, err := validateWorkloadResourceTargets(path, workloads.Targets)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := validateWorkloadRequestPolicies(path, resources.Requests, podTarget); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return validateWorkloadLimitPolicies(path, resources, podTarget)
|
||||
}
|
||||
|
||||
func validateWorkloadResourceTargets(
|
||||
path string,
|
||||
targets []rules.WorkloadValidationTarget,
|
||||
) (bool, error) {
|
||||
podTarget := false
|
||||
|
||||
for _, target := range targets {
|
||||
switch target {
|
||||
case rules.ValidatePod:
|
||||
podTarget = true
|
||||
case rules.ValidateContainers, rules.ValidateInitContainers:
|
||||
case rules.ValidateEphemeralContainers, rules.ValidateVolumes, rules.DeprecatedValidateImages:
|
||||
return false, fmt.Errorf(
|
||||
"%s is invalid: workload target %q does not support resource policies",
|
||||
path,
|
||||
target,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return podTarget, nil
|
||||
}
|
||||
|
||||
func validateWorkloadRequestPolicies(
|
||||
path string,
|
||||
policies map[corev1.ResourceName]rules.WorkloadResourceRequestPolicy,
|
||||
podTarget bool,
|
||||
) error {
|
||||
for name, policy := range policies {
|
||||
policyPath := fmt.Sprintf("%s.requests[%q]", path, name)
|
||||
if err := validateWorkloadResourceName(name, podTarget); err != nil {
|
||||
return fmt.Errorf("%s is invalid: %w", policyPath, err)
|
||||
}
|
||||
|
||||
switch policy.Policy {
|
||||
case rules.WorkloadResourceRequestPolicyPreserve,
|
||||
rules.WorkloadResourceRequestPolicyRemove:
|
||||
if policy.Value != nil {
|
||||
return fmt.Errorf("%s.value is invalid: value is only supported by the Default policy", policyPath)
|
||||
}
|
||||
case rules.WorkloadResourceRequestPolicyDefault:
|
||||
if err := validateDefaultResourceQuantity(policyPath, policy.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("%s.policy %q is invalid: unsupported request policy", policyPath, policy.Policy)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateWorkloadLimitPolicies(
|
||||
path string,
|
||||
resources *rules.WorkloadResourceRules,
|
||||
podTarget bool,
|
||||
) error {
|
||||
one := resource.MustParse("1")
|
||||
|
||||
for name, policy := range resources.Limits {
|
||||
policyPath := fmt.Sprintf("%s.limits[%q]", path, name)
|
||||
if err := validateWorkloadResourceName(name, podTarget); err != nil {
|
||||
return fmt.Errorf("%s is invalid: %w", policyPath, err)
|
||||
}
|
||||
|
||||
switch policy.Policy {
|
||||
case rules.WorkloadResourceLimitPolicyPreserve,
|
||||
rules.WorkloadResourceLimitPolicyRemove,
|
||||
rules.WorkloadResourceLimitPolicyMatchRequest:
|
||||
if policy.Value != nil {
|
||||
return fmt.Errorf(
|
||||
"%s.value is invalid: value is only supported by the Default and Ratio policies",
|
||||
policyPath,
|
||||
)
|
||||
}
|
||||
case rules.WorkloadResourceLimitPolicyDefault:
|
||||
if err := validateDefaultResourceQuantity(policyPath, policy.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
case rules.WorkloadResourceLimitPolicyRatio:
|
||||
if policy.Value == nil {
|
||||
return fmt.Errorf("%s.value is invalid: Ratio requires a value", policyPath)
|
||||
}
|
||||
|
||||
if !workloadruntime.RatioSupportedResource(name) {
|
||||
return fmt.Errorf(
|
||||
"%s.policy is invalid: Ratio is only supported for cpu, memory, and ephemeral-storage",
|
||||
policyPath,
|
||||
)
|
||||
}
|
||||
|
||||
if policy.Value.Cmp(one) < 0 {
|
||||
return fmt.Errorf("%s.value is invalid: Ratio must be greater than or equal to 1", policyPath)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("%s.policy %q is invalid: unsupported limit policy", policyPath, policy.Policy)
|
||||
}
|
||||
|
||||
if requestPolicy, found := resources.Requests[name]; found &&
|
||||
requestPolicy.Policy == rules.WorkloadResourceRequestPolicyRemove &&
|
||||
(policy.Policy == rules.WorkloadResourceLimitPolicyMatchRequest ||
|
||||
policy.Policy == rules.WorkloadResourceLimitPolicyRatio) {
|
||||
return fmt.Errorf(
|
||||
"%s is invalid: %s requires a request which is removed by the request policy",
|
||||
policyPath,
|
||||
policy.Policy,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateDefaultResourceQuantity(path string, value *resource.Quantity) error {
|
||||
if value == nil {
|
||||
return fmt.Errorf("%s.value is invalid: Default requires a value", path)
|
||||
}
|
||||
|
||||
if value.Sign() < 0 {
|
||||
return fmt.Errorf("%s.value is invalid: quantity must not be negative", path)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateWorkloadResourceName(name corev1.ResourceName, podTarget bool) error {
|
||||
if errs := k8svalidation.IsQualifiedName(string(name)); len(errs) > 0 {
|
||||
return fmt.Errorf("resource name is invalid: %s", strings.Join(errs, "; "))
|
||||
}
|
||||
|
||||
if !podTarget {
|
||||
return nil
|
||||
}
|
||||
|
||||
if workloadruntime.PodLevelResourceSupported(name) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("resource %q is not supported by pod-level resources", name)
|
||||
}
|
||||
|
||||
func validateServiceRules(
|
||||
ruleIndex int,
|
||||
services rules.NamespaceRuleEnforceServicesBody,
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
// Copyright 2020-2026 Project Capsule Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package ruleengine
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
|
||||
"github.com/projectcapsule/capsule/pkg/api/rules"
|
||||
)
|
||||
|
||||
func TestValidateRuleStatusBodyWorkloadResources(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
body *rules.NamespaceRuleBodyNamespace
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "valid",
|
||||
body: workloadResourceRuleForValidation(
|
||||
[]rules.WorkloadValidationTarget{rules.ValidateContainers},
|
||||
rules.WorkloadResourceRequestPolicy{Policy: rules.WorkloadResourceRequestPolicyDefault, Value: validationQuantity("1Gi")},
|
||||
rules.WorkloadResourceLimitPolicy{Policy: rules.WorkloadResourceLimitPolicyRatio, Value: validationQuantity("1.5")},
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "omitted target permits container-only resource names",
|
||||
body: workloadResourceRuleForValidationWithName(
|
||||
nil,
|
||||
corev1.ResourceEphemeralStorage,
|
||||
rules.WorkloadResourceRequestPolicy{Policy: rules.WorkloadResourceRequestPolicyPreserve},
|
||||
rules.WorkloadResourceLimitPolicy{Policy: rules.WorkloadResourceLimitPolicyRatio, Value: validationQuantity("1.5")},
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "ratio below one",
|
||||
body: workloadResourceRuleForValidation(
|
||||
nil,
|
||||
rules.WorkloadResourceRequestPolicy{Policy: rules.WorkloadResourceRequestPolicyPreserve},
|
||||
rules.WorkloadResourceLimitPolicy{Policy: rules.WorkloadResourceLimitPolicyRatio, Value: validationQuantity("0.5")},
|
||||
),
|
||||
wantErr: "Ratio must be greater than or equal to 1",
|
||||
},
|
||||
{
|
||||
name: "ratio missing value",
|
||||
body: workloadResourceRuleForValidation(
|
||||
nil,
|
||||
rules.WorkloadResourceRequestPolicy{Policy: rules.WorkloadResourceRequestPolicyPreserve},
|
||||
rules.WorkloadResourceLimitPolicy{Policy: rules.WorkloadResourceLimitPolicyRatio},
|
||||
),
|
||||
wantErr: "Ratio requires a value",
|
||||
},
|
||||
{
|
||||
name: "unsupported target",
|
||||
body: workloadResourceRuleForValidation(
|
||||
[]rules.WorkloadValidationTarget{rules.ValidateEphemeralContainers},
|
||||
rules.WorkloadResourceRequestPolicy{Policy: rules.WorkloadResourceRequestPolicyPreserve},
|
||||
rules.WorkloadResourceLimitPolicy{Policy: rules.WorkloadResourceLimitPolicyRatio, Value: validationQuantity("1.5")},
|
||||
),
|
||||
wantErr: "does not support resource policies",
|
||||
},
|
||||
{
|
||||
name: "pod-level ephemeral storage",
|
||||
body: workloadResourceRuleForValidation(
|
||||
[]rules.WorkloadValidationTarget{rules.ValidatePod},
|
||||
rules.WorkloadResourceRequestPolicy{Policy: rules.WorkloadResourceRequestPolicyPreserve},
|
||||
rules.WorkloadResourceLimitPolicy{Policy: rules.WorkloadResourceLimitPolicyRatio, Value: validationQuantity("1.5")},
|
||||
),
|
||||
wantErr: "is not supported by pod-level resources",
|
||||
},
|
||||
{
|
||||
name: "ratio request removed",
|
||||
body: workloadResourceRuleForValidation(
|
||||
nil,
|
||||
rules.WorkloadResourceRequestPolicy{Policy: rules.WorkloadResourceRequestPolicyRemove},
|
||||
rules.WorkloadResourceLimitPolicy{Policy: rules.WorkloadResourceLimitPolicyRatio, Value: validationQuantity("1.5")},
|
||||
),
|
||||
wantErr: "requires a request which is removed",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
err := ValidateRuleStatusBody(nil, []*rules.NamespaceRuleBodyNamespace{tt.body})
|
||||
if tt.wantErr == "" {
|
||||
if err != nil {
|
||||
t.Fatalf("ValidateRuleStatusBody() error = %v", err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
|
||||
t.Fatalf("ValidateRuleStatusBody() error = %v, want containing %q", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func workloadResourceRuleForValidation(
|
||||
targets []rules.WorkloadValidationTarget,
|
||||
requestPolicy rules.WorkloadResourceRequestPolicy,
|
||||
limitPolicy rules.WorkloadResourceLimitPolicy,
|
||||
) *rules.NamespaceRuleBodyNamespace {
|
||||
name := corev1.ResourceMemory
|
||||
if len(targets) == 1 && targets[0] == rules.ValidatePod {
|
||||
name = corev1.ResourceEphemeralStorage
|
||||
}
|
||||
|
||||
return workloadResourceRuleForValidationWithName(targets, name, requestPolicy, limitPolicy)
|
||||
}
|
||||
|
||||
func workloadResourceRuleForValidationWithName(
|
||||
targets []rules.WorkloadValidationTarget,
|
||||
name corev1.ResourceName,
|
||||
requestPolicy rules.WorkloadResourceRequestPolicy,
|
||||
limitPolicy rules.WorkloadResourceLimitPolicy,
|
||||
) *rules.NamespaceRuleBodyNamespace {
|
||||
|
||||
return &rules.NamespaceRuleBodyNamespace{
|
||||
Enforce: &rules.NamespaceRuleEnforceBody{
|
||||
Workloads: rules.NamespaceRuleEnforceWorkloadsBody{
|
||||
Targets: targets,
|
||||
Resources: &rules.WorkloadResourceRules{
|
||||
Requests: map[corev1.ResourceName]rules.WorkloadResourceRequestPolicy{name: requestPolicy},
|
||||
Limits: map[corev1.ResourceName]rules.WorkloadResourceLimitPolicy{name: limitPolicy},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func validationQuantity(value string) *resource.Quantity {
|
||||
quantity := resource.MustParse(value)
|
||||
|
||||
return &quantity
|
||||
}
|
||||
Reference in New Issue
Block a user