Skip to content

Commit

Permalink
feat:(ignoreValues): Adding flag for single-stage validation. (#18)
Browse files Browse the repository at this point in the history
* feat:(ignoreValues): Adding flag for single-stage validation.

The goal of this addition is to be able to enforce that cpu and/or
memory requests are set, with no care as to what they are set to.

Signed-off-by: José Guilherme Vanz <[email protected]>
Signed-off-by: Víctor Cuadrado Juan <[email protected]>
  • Loading branch information
TechDufus authored Mar 13, 2024
1 parent 2d712d9 commit 17d3a41
Show file tree
Hide file tree
Showing 7 changed files with 459 additions and 58 deletions.
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,29 @@ should be expressed using the [quantity
definitions](https://kubernetes.io/docs/reference/kubernetes-api/common-definitions/quantity/)
of Kubernetes.

If you would only like to enforce that requests and limits are set (ie, you do not care
what they are set to, just that they have been set), you can set `ignoreValues` to `true`.
This will skip the enforcement of specific values and only enforce that requests and
limits are set. Here is an example of how to configure this:

```yaml
# optional
memory:
ignoreValues: true
# optional
cpu:
defaultRequest: 100m
defaultLimit: 200m
maxLimit: 500m
# optional
ignoreImages: ["ghcr.io/foo/bar:1.23", "myimage", "otherimages:v1"]
```

Please note from the above example, that when `ignoreValues` is set to `true`, the
`defaultRequest`, `defaultLimit`, and `maxLimit` fields must not be set. Additionally,
`ignoreValues` default value is `false`, so it's recommended to only provide it when
you want to set it to `true`.

Any container that uses an image that matches an entry in this list will be excluded
from enforcement.

Expand Down
32 changes: 32 additions & 0 deletions e2e.bats
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@
[ $(expr "$output" : '.*no settings provided. At least one resource limit or request must be verified.*') -ne 0 ]
}

@test "no quantities are allowed when ignoreValues is true" {
run kwctl run annotated-policy.wasm -r test_data/pod_exceeding_range.json \
--settings-json '{"cpu": {"maxLimit": "1m", "defaultRequest" : "1m", "defaultLimit" : "1m", "ignoreValues": true}, "memory" : {"maxLimit": "1G", "defaultRequest" : "1G", "defaultLimit" : "1G", "ignoreValues": true}, "ignoreImages": ["image:latest"]}'

[ "$status" -ne 0 ]
[ $(expr "$output" : '.*ignoreValues cannot be true when any quantities are defined.*') -ne 0 ]
}

@test "accept containers within the expected range" {
run kwctl run annotated-policy.wasm -r test_data/pod_within_range.json \
--settings-json '{"cpu": {"maxLimit": "3m", "defaultRequest" : "2m", "defaultLimit" : "2m"}, "memory" : {"maxLimit": "3G", "defaultRequest" : "2G", "defaultLimit" : "2G"}}'
Expand Down Expand Up @@ -52,6 +60,15 @@
[ $(expr "$output" : '.*patch.*') -ne 0 ]
}

@test "reject deployment with no resources when ignoreValues is true" {
run kwctl run annotated-policy.wasm -r test_data/deployment_without_resources_admission_request.json \
--settings-json '{"cpu": {"ignoreValues": true}, "memory" : {"ignoreValues": true}}'

[ "$status" -eq 0 ]
[ $(expr "$output" : '.*allowed.*false') -ne 0 ]
[ $(expr "$output" : '.*patch.*') -eq 0 ]
}

@test "mutate deployment with limits but no request resources" {
run kwctl run annotated-policy.wasm -r test_data/deployment_with_limits_admission_request.json \
--settings-json '{"cpu": {"maxLimit": "4", "defaultRequest" : "2", "defaultLimit" : "2"}, "memory" : {"maxLimit": "4G", "defaultRequest" : "2G", "defaultLimit" : "2G"}}'
Expand All @@ -60,4 +77,19 @@
[ $(expr "$output" : '.*allowed.*true') -ne 0 ]
[ $(expr "$output" : '.*patch.*') -ne 0 ]
}
@test "reject containers with no resources when ignoreValues is true" {
run kwctl run annotated-policy.wasm -r test_data/pod_without_resources.json \
--settings-json '{"cpu": {"ignoreValues": true}, "memory" : {"ignoreValues": true}}'

[ "$status" -eq 0 ]
[ $(expr "$output" : '.*allowed.*false') -ne 0 ]
[ $(expr "$output" : '.*patch.*') -eq 0 ]
}
@test "allow containers while ignoring resources" {
run kwctl run annotated-policy.wasm -r test_data/pod_within_range.json \
--settings-json '{"cpu": {"ignoreValues": true}, "memory" : {"ignoreValues": true}}'

[ "$status" -eq 0 ]
[ $(expr "$output" : '.*allowed.*true') -ne 0 ]
[ $(expr "$output" : '.*patch.*') -eq 0 ]
}
20 changes: 15 additions & 5 deletions settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type ResourceConfiguration struct {
MaxLimit resource.Quantity `json:"maxLimit"`
DefaultRequest resource.Quantity `json:"defaultRequest"`
DefaultLimit resource.Quantity `json:"defaultLimit"`
IgnoreValues bool `json:"ignoreValues,omitempty"`
}

type Settings struct {
Expand All @@ -23,9 +24,18 @@ type Settings struct {
}

func (r *ResourceConfiguration) valid() error {
if (!r.MaxLimit.IsZero() || !r.DefaultLimit.IsZero() || !r.DefaultRequest.IsZero()) && r.IgnoreValues {
return fmt.Errorf("ignoreValues cannot be true when any quantities are defined")
}

if r.IgnoreValues {
return nil
}

if r.MaxLimit.IsZero() && r.DefaultLimit.IsZero() && r.DefaultRequest.IsZero() {
return fmt.Errorf("all the quantities must be defined")
}

if r.MaxLimit.Cmp(r.DefaultLimit) < 0 ||
r.MaxLimit.Cmp(r.DefaultRequest) < 0 {
return fmt.Errorf("default values cannot be greater than the max limit")
Expand All @@ -38,23 +48,23 @@ func (s *Settings) Valid() error {
if s.Cpu == nil && s.Memory == nil {
return fmt.Errorf("no settings provided. At least one resource limit or request must be verified")
}
var cpuError, memoryError error;
var cpuError, memoryError error
if s.Cpu != nil {
cpuError = s.Cpu.valid();
cpuError = s.Cpu.valid()
if cpuError != nil {
cpuError = errors.Join(fmt.Errorf("invalid cpu settings"), cpuError)
}
}
if s.Memory != nil {
memoryError = s.Memory.valid();
memoryError = s.Memory.valid()
if memoryError != nil {
memoryError = errors.Join(fmt.Errorf("invalid memory settings"), memoryError)
}
}
if cpuError != nil || memoryError != nil {
return errors.Join(cpuError, memoryError)
}
return nil
return nil
}

func NewSettingsFromValidationReq(validationReq *kubewarden_protocol.ValidationRequest) (Settings, error) {
Expand All @@ -74,6 +84,6 @@ func validateSettings(payload []byte) ([]byte, error) {
err = settings.Valid()
if err != nil {
return kubewarden.RejectSettings(kubewarden.Message(fmt.Sprintf("Provided settings are not valid: %v", err)))
}
}
return kubewarden.AcceptSettings()
}
104 changes: 59 additions & 45 deletions settings_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,34 @@ import (
kubewarden_protocol "github.com/kubewarden/policy-sdk-go/protocol"
)

func checkSettingsValues(t *testing.T, settings *ResourceConfiguration, expectedMaxLimit, expectedDefaultRequest, expectedDefaultLimit string, expectedIgnoreValues bool) {
actualMaxLimit := resource.MustParse(expectedMaxLimit)
if !settings.MaxLimit.Equal(actualMaxLimit) {
t.Errorf("invalid max limit quantity parsed. Expected %+v, got %+v", actualMaxLimit, settings.MaxLimit)
}
actualDefaultRequest := resource.MustParse(expectedDefaultRequest)
if !settings.DefaultRequest.Equal(actualDefaultRequest) {
t.Errorf("invalid default request quantity parsed. Expected %+v, got %+v", actualDefaultRequest, settings.DefaultRequest)
}
actualDefaultLimit := resource.MustParse(expectedDefaultLimit)
if !settings.DefaultLimit.Equal(actualDefaultLimit) {
t.Errorf("invalid default limit quantity parsed. Expected %+v, got %+v", actualDefaultLimit, settings.DefaultLimit)
}
if settings.IgnoreValues != expectedIgnoreValues {
t.Errorf("invalid ignoreValues value. Expected %t, got %t", expectedIgnoreValues, settings.IgnoreValues)
}
}

func TestParsingResourceConfiguration(t *testing.T) {
var tests = []struct {
name string
rawSettings []byte
errorMessage string
}{
{"no suffix", []byte(`{"maxLimit": "3", "defaultLimit": "2", "defaultRequest": "1"}`), ""},
{"invalid ignoreValues with valid resource configuration", []byte(`{"maxLimit": "3", "defaultLimit": "2", "defaultRequest": "1", "ignoreValues": true}`), "ignoreValues cannot be true when any quantities are defined"},
{"valid ignoreValues", []byte(`{"maxLimit": "3", "defaultLimit": "2", "defaultRequest": "1", "ignoreValues": false}`), ""},
{"valid ignoreValues", []byte(`{"ignoreValues": true}`), ""},
{"invalid limit suffix", []byte(`{"maxLimit": "1x", "defaultLimit": "1m", "defaultRequest": "1m"}`), "quantities must match the regular expression"},
{"invalid request suffix", []byte(`{"maxLimit": "3m", "defaultLimit": "2m", "defaultRequest": "1x"}`), "quantities must match the regular expression"},
{"defaults greater than max limit", []byte(`{"maxLimit": "2m", "defaultRequest": "3m", "defaultLimit": "4m"}`), "default values cannot be greater than the max limit"},
Expand Down Expand Up @@ -99,31 +120,8 @@ func TestNewSettingsFromValidationReq(t *testing.T) {
if err != nil {
t.Fatalf("Unexpected error %+v", err)
}
expectedCpuValue := resource.MustParse("3m")
if !settings.Cpu.MaxLimit.Equal(expectedCpuValue) {
t.Errorf("invalid cpu max limit quantity parsed. Expected %+v, go %+v", expectedCpuValue, settings.Cpu.MaxLimit)
}
expectedCpuValue = resource.MustParse("1m")
if !settings.Cpu.DefaultRequest.Equal(expectedCpuValue) {
t.Errorf("invalid cpu default request quantity parsed. Expected %+v, go %+v", expectedCpuValue, settings.Cpu.DefaultRequest)
}
expectedCpuValue = resource.MustParse("2m")
if !settings.Cpu.DefaultLimit.Equal(expectedCpuValue) {
t.Errorf("invalid cpu default limit quantity parsed. Expected %+v, go %+v", expectedCpuValue, settings.Cpu.DefaultLimit)
}

expectedMemoryValue := resource.MustParse("3G")
if !settings.Memory.MaxLimit.Equal(expectedMemoryValue) {
t.Errorf("invalid memory max limit quantity parsed. Expected %+v, go %+v", expectedMemoryValue, settings.Memory.MaxLimit)
}
expectedMemoryValue = resource.MustParse("2G")
if !settings.Memory.DefaultLimit.Equal(expectedMemoryValue) {
t.Errorf("invalid memory default limit quantity parsed. Expected %+v, go %+v", expectedMemoryValue, settings.Memory.DefaultLimit)
}
expectedMemoryValue = resource.MustParse("1G")
if !settings.Memory.DefaultRequest.Equal(expectedMemoryValue) {
t.Errorf("invalid memory default request quantity parsed. Expected %+v, go %+v", expectedMemoryValue, settings.Memory.DefaultRequest)
}
checkSettingsValues(t, settings.Cpu, "3m", "1m", "2m", false)
checkSettingsValues(t, settings.Memory, "3G", "1G", "2G", false)
}

func TestNewSettingsPartialFieldsOnlyFromValidationReq(t *testing.T) {
Expand All @@ -139,19 +137,7 @@ func TestNewSettingsPartialFieldsOnlyFromValidationReq(t *testing.T) {
if settings.Cpu != nil {
t.Fatal("cpu settings should be null")
}

expectedMemoryValue := resource.MustParse("3G")
if !settings.Memory.MaxLimit.Equal(expectedMemoryValue) {
t.Errorf("invalid memory max limit quantity parsed. Expected %+v, go %+v", expectedMemoryValue, settings.Memory.MaxLimit)
}
expectedMemoryValue = resource.MustParse("2G")
if !settings.Memory.DefaultLimit.Equal(expectedMemoryValue) {
t.Errorf("invalid memory default limit quantity parsed. Expected %+v, go %+v", expectedMemoryValue, settings.Memory.DefaultLimit)
}
expectedMemoryValue = resource.MustParse("1G")
if !settings.Memory.DefaultRequest.Equal(expectedMemoryValue) {
t.Errorf("invalid memory default request quantity parsed. Expected %+v, go %+v", expectedMemoryValue, settings.Memory.DefaultRequest)
}
checkSettingsValues(t, settings.Memory, "3G", "1G", "2G", false)
})
t.Run("only cpu fields", func(t *testing.T) {
validationReq := &kubewarden_protocol.ValidationRequest{
Expand All @@ -166,15 +152,43 @@ func TestNewSettingsPartialFieldsOnlyFromValidationReq(t *testing.T) {
t.Fatal("memory settings should be null")
}

expectedCpuValue := resource.MustParse("1")
if !settings.Cpu.MaxLimit.Equal(expectedCpuValue) {
t.Errorf("invalid memory max limit quantity parsed. Expected %+v, go %+v", expectedCpuValue, settings.Cpu.MaxLimit)
checkSettingsValues(t, settings.Cpu, "1", "1", "1", false)
})
t.Run("only memory fields with ignoreValues", func(t *testing.T) {
validationReq := &kubewarden_protocol.ValidationRequest{
Settings: []byte(`{"memory":{"maxLimit": "3G","defaultRequest": "1G", "defaultLimit": "2G", "ignoreValues": true}}`),
}
if !settings.Cpu.DefaultLimit.Equal(expectedCpuValue) {
t.Errorf("invalid memory default limit quantity parsed. Expected %+v, go %+v", expectedCpuValue, settings.Cpu.DefaultLimit)
settings, err := NewSettingsFromValidationReq(validationReq)
if err != nil {
t.Fatalf("Unexpected error %+v", err)
}
if settings.Cpu != nil {
t.Fatal("cpu settings should be null")
}
checkSettingsValues(t, settings.Memory, "3G", "1G", "2G", true)
})
t.Run("only cpu fields with ignoreValues", func(t *testing.T) {
validationReq := &kubewarden_protocol.ValidationRequest{
Settings: []byte(`{"cpu":{"maxLimit": "1","defaultRequest": "1", "defaultLimit": "1", "ignoreValues": true}}`),
}
settings, err := NewSettingsFromValidationReq(validationReq)
if err != nil {
t.Fatalf("Unexpected error %+v", err)
}
if settings.Memory != nil {
t.Fatal("memory settings should be null")
}
checkSettingsValues(t, settings.Cpu, "1", "1", "1", true)
})
t.Run("both cpu and memory fields with ignoreValues", func(t *testing.T) {
validationReq := &kubewarden_protocol.ValidationRequest{
Settings: []byte(`{"cpu":{"ignoreValues": true}, "memory":{"ignoreValues": true}}`),
}
if !settings.Cpu.DefaultRequest.Equal(expectedCpuValue) {
t.Errorf("invalid memory default request quantity parsed. Expected %+v, go %+v", expectedCpuValue, settings.Cpu.DefaultRequest)
settings, err := NewSettingsFromValidationReq(validationReq)
if err != nil {
t.Fatalf("Unexpected error %+v", err)
}
checkSettingsValues(t, settings.Cpu, "0", "0", "0", true)
checkSettingsValues(t, settings.Memory, "0", "0", "0", true)
})
}
58 changes: 58 additions & 0 deletions test_data/pod_without_resources.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
{
"uid": "1299d386-525b-4032-98ae-1949f69f9cfc",
"kind": {
"group": "",
"kind": "Pod",
"version": "v1"
},
"resource": {
"group": "",
"version": "v1",
"resource": "pods"
},
"requestKind": {
"group": "",
"version": "v1",
"kind": "Pod"
},
"requestResource": {
"group": "",
"version": "v1",
"resource": "pods"
},
"name": "nginx",
"namespace": "default",
"operation": "CREATE",
"userInfo": {
"username": "kubernetes-admin",
"groups": [
"system:masters",
"system:authenticated"
]
},
"object": {
"apiVersion": "v1",
"kind": "Pod",
"metadata": {
"name": "test-pod",
"namespace": "default",
"labels": {
"cc-center": "123",
"owner": "team-alpha"
}
},
"spec": {
"containers": [
{
"name": "pause",
"image": "registry.k8s.io/pause"
},
{
"name": "mycontainer",
"image": "image:latest"

}
]
}
}
}
Loading

0 comments on commit 17d3a41

Please sign in to comment.