diff --git a/README.md b/README.md index cd93277..54e3f05 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/e2e.bats b/e2e.bats index ba27820..4bc64f0 100644 --- a/e2e.bats +++ b/e2e.bats @@ -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"}}' @@ -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"}}' @@ -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 ] +} diff --git a/settings.go b/settings.go index 85758d5..4534f77 100644 --- a/settings.go +++ b/settings.go @@ -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 { @@ -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") @@ -38,15 +48,15 @@ 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) } @@ -54,7 +64,7 @@ func (s *Settings) Valid() error { if cpuError != nil || memoryError != nil { return errors.Join(cpuError, memoryError) } - return nil + return nil } func NewSettingsFromValidationReq(validationReq *kubewarden_protocol.ValidationRequest) (Settings, error) { @@ -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() } diff --git a/settings_test.go b/settings_test.go index ad0dd32..8399e3e 100644 --- a/settings_test.go +++ b/settings_test.go @@ -9,6 +9,24 @@ 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 @@ -16,6 +34,9 @@ func TestParsingResourceConfiguration(t *testing.T) { 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"}, @@ -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) { @@ -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{ @@ -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) }) } diff --git a/test_data/pod_without_resources.json b/test_data/pod_without_resources.json new file mode 100644 index 0000000..0244ec2 --- /dev/null +++ b/test_data/pod_without_resources.json @@ -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" + + } + ] + } + } +} diff --git a/validate.go b/validate.go index f90c9d1..3e537ee 100644 --- a/validate.go +++ b/validate.go @@ -12,9 +12,13 @@ import ( kubewarden_protocol "github.com/kubewarden/policy-sdk-go/protocol" ) +func missingResourceQuantity(resources map[string]*api_resource.Quantity, resourceName string) bool { + resourceStr, found := resources[resourceName] + return !found || resourceStr == nil || len(strings.TrimSpace(string(*resourceStr))) == 0 +} + func adjustResourceRequest(container *corev1.Container, resourceName string, resourceConfig *ResourceConfiguration) bool { - resourceStr, found := container.Resources.Requests[resourceName] - if !found || resourceStr == nil || len(strings.TrimSpace(string(*resourceStr))) == 0 { + if missingResourceQuantity(container.Resources.Requests, resourceName) { if !resourceConfig.DefaultRequest.IsZero() { newRequest := api_resource.Quantity(resourceConfig.DefaultRequest.String()) container.Resources.Requests[resourceName] = &newRequest @@ -24,6 +28,57 @@ func adjustResourceRequest(container *corev1.Container, resourceName string, res return false } +func validateContainerResourceLimits(container *corev1.Container, settings *Settings) error { + if container.Resources.Limits == nil && settings.Cpu.IgnoreValues && settings.Memory.IgnoreValues { + return fmt.Errorf("container does not have any resource limits") + } + + if settings.Cpu.IgnoreValues && missingResourceQuantity(container.Resources.Limits, "cpu") { + return fmt.Errorf("container does not have a cpu limit") + } + + if settings.Memory.IgnoreValues && missingResourceQuantity(container.Resources.Limits, "memory") { + return fmt.Errorf("container does not have a memory limit") + } + + return nil +} + +func validateContainerResourceRequests(container *corev1.Container, settings *Settings) error { + if container.Resources.Requests == nil && settings.Cpu.IgnoreValues && settings.Memory.IgnoreValues { + return fmt.Errorf("container does not have any resource requests") + } + + _, found := container.Resources.Requests["cpu"] + if !found && settings.Cpu.IgnoreValues { + return fmt.Errorf("container does not have a cpu request") + } + + _, found = container.Resources.Requests["memory"] + if !found && settings.Memory.IgnoreValues { + return fmt.Errorf("container does not have a memory request") + } + + return nil +} + +// If IgnoreValues is set to true, confirm that the respective limits/requests are set. +// We only check for the presence of the limits/requests, not their values. +// Returns an error if the limits/requests are not set and IgnoreValues is set to true. +func validateContainerResources(container *corev1.Container, settings *Settings) error { + if container.Resources == nil && (settings.Cpu.IgnoreValues || settings.Memory.IgnoreValues) { + missing := fmt.Sprintf("required Cpu:%t, Memory:%t", settings.Cpu.IgnoreValues, settings.Memory.IgnoreValues) + return fmt.Errorf("container does not have any resource limits or requests: %s", missing) + } + if err := validateContainerResourceLimits(container, settings); err != nil { + return err + } + if err := validateContainerResourceRequests(container, settings); err != nil { + return err + } + return nil +} + // When the CPU/Memory request is specified: no action or check is done against it. // When the CPU/Memory request is not specified: the policy mutates the container definition, the `defaultRequest` value is used. The policy does not check the consistency of the applied value. // Return `true` when the container has been mutated @@ -38,15 +93,17 @@ func validateAndAdjustContainerResourceRequests(container *corev1.Container, set return mutated } +// validateAndAdjustContainerResourceLimit validates the container against the passed resourceConfig // and mutates it if the validation didn't pass. +// Returns true when it mutates the container. func validateAndAdjustContainerResourceLimit(container *corev1.Container, resourceName string, resourceConfig *ResourceConfiguration) (bool, error) { - resourceStr, found := container.Resources.Limits[resourceName] - if !found || resourceStr == nil || len(strings.TrimSpace(string(*resourceStr))) == 0 { + if missingResourceQuantity(container.Resources.Limits, resourceName) { if !resourceConfig.DefaultLimit.IsZero() { newLimit := api_resource.Quantity(resourceConfig.DefaultLimit.String()) container.Resources.Limits[resourceName] = &newLimit return true, nil } } else { + resourceStr := container.Resources.Limits[resourceName] resourceLimit, err := resource.ParseQuantity(string(*resourceStr)) if err != nil { return false, fmt.Errorf("invalid %s limit", resourceName) @@ -58,11 +115,22 @@ func validateAndAdjustContainerResourceLimit(container *corev1.Container, resour return false, nil } -// When the CPU/Memory limit is specified: the request is accepted if the limit defined by the container is less than or equal to the `maxLimit`. Otherwise the request is rejected. -// When the CPU/Memory limit is not specified: the container is mutated to use the `defaultLimit`. +// validateAndAdjustContainerResourceLimits validates the container and mutates +// it when possible, when it doesn't pass validation. +// +// When the CPU/Memory limit is specified: the request is accepted if the limit +// defined by the container is less than or equal to the `maxLimit`, or +// IgnoreValues is true. Otherwise the request is rejected. +// +// When the CPU/Memory limit is not specified: the container is mutated to use +// the `defaultLimit`. +// // Return `true` when the container has been mutated. func validateAndAdjustContainerResourceLimits(container *corev1.Container, settings *Settings) (bool, error) { mutated := false + if settings.Memory.IgnoreValues { + return false, nil + } if settings.Memory != nil { var err error mutated, err = validateAndAdjustContainerResourceLimit(container, "memory", settings.Memory) @@ -71,6 +139,9 @@ func validateAndAdjustContainerResourceLimits(container *corev1.Container, setti } } + if settings.Cpu.IgnoreValues { + return false, nil + } if settings.Cpu != nil { cpuMutation, err := validateAndAdjustContainerResourceLimit(container, "cpu", settings.Cpu) if err != nil { @@ -100,7 +171,6 @@ func validateAndAdjustContainer(container *corev1.Container, settings *Settings) } requestsMutation := validateAndAdjustContainerResourceRequests(container, settings) return limitsMutation || requestsMutation, nil - } func shouldSkipContainer(image string, ignoreImages []string) bool { @@ -125,6 +195,10 @@ func validatePodSpec(pod *corev1.PodSpec, settings *Settings) (bool, error) { if shouldSkipContainer(container.Image, settings.IgnoreImages) { continue } + if err := validateContainerResources(container, settings); err != nil { + return false, err + } + containerMutated, err := validateAndAdjustContainer(container, settings) if err != nil { return false, err @@ -132,7 +206,6 @@ func validatePodSpec(pod *corev1.PodSpec, settings *Settings) (bool, error) { mutated = mutated || containerMutated } return mutated, nil - } func validate(payload []byte) ([]byte, error) { diff --git a/validate_test.go b/validate_test.go index 2ba05b8..a4d3760 100644 --- a/validate_test.go +++ b/validate_test.go @@ -363,9 +363,200 @@ func TestContainerIsRequiredToHaveLimits(t *testing.T) { t.Logf("%+v", test.container.Resources) t.Error(diff) } + }) + } +} + +func TestIgroreValues(t *testing.T) { + oneCore := resource.MustParse("1") + oneGi := resource.MustParse("1Gi") + oneCoreCpuQuantity := apimachinery_pkg_api_resource.Quantity("1") + oneGiMemoryQuantity := apimachinery_pkg_api_resource.Quantity("1Gi") + twoCoreCpuQuantity := apimachinery_pkg_api_resource.Quantity("2") + var tests = []struct { + name string + container corev1.Container + settings Settings + expectedResouceLimits *corev1.ResourceRequirements + expectedErrorMsg string + }{ + {"memory resources requests and limits defined and ignore cpu", corev1.Container{ + Image: "image1:latest", + Resources: &corev1.ResourceRequirements{ + Limits: map[string]*apimachinery_pkg_api_resource.Quantity{ + "cpu": &oneCoreCpuQuantity, + "memory": &oneGiMemoryQuantity, + }, + Requests: map[string]*apimachinery_pkg_api_resource.Quantity{ + "cpu": &oneCoreCpuQuantity, + "memory": &oneGiMemoryQuantity, + }, + }, + }, + Settings{ + Cpu: &ResourceConfiguration{ + IgnoreValues: true, + }, + Memory: &ResourceConfiguration{ + DefaultLimit: oneGi, + DefaultRequest: oneGi, + MaxLimit: oneGi, + }, + IgnoreImages: []string{"image1:latest"}, + }, &corev1.ResourceRequirements{ + Limits: map[string]*apimachinery_pkg_api_resource.Quantity{ + "cpu": &oneCoreCpuQuantity, + "memory": &oneGiMemoryQuantity, + }, + Requests: map[string]*apimachinery_pkg_api_resource.Quantity{ + "cpu": &oneCoreCpuQuantity, + "memory": &oneGiMemoryQuantity, + }, + }, ""}, + {"cpu resources requests and limits defined and ignore memory", corev1.Container{ + Image: "image1:latest", + Resources: &corev1.ResourceRequirements{ + Limits: map[string]*apimachinery_pkg_api_resource.Quantity{ + "cpu": &oneCoreCpuQuantity, + "memory": &oneGiMemoryQuantity, + }, + Requests: map[string]*apimachinery_pkg_api_resource.Quantity{ + "cpu": &oneCoreCpuQuantity, + "memory": &oneGiMemoryQuantity, + }, + }, + }, Settings{ + Cpu: &ResourceConfiguration{ + DefaultLimit: oneCore, + DefaultRequest: oneCore, + MaxLimit: oneCore, + IgnoreValues: true, + }, + Memory: &ResourceConfiguration{ + IgnoreValues: true, + }, + }, &corev1.ResourceRequirements{ + Limits: map[string]*apimachinery_pkg_api_resource.Quantity{ + "cpu": &oneCoreCpuQuantity, + "memory": &oneGiMemoryQuantity, + }, + Requests: map[string]*apimachinery_pkg_api_resource.Quantity{ + "cpu": &oneCoreCpuQuantity, + "memory": &oneGiMemoryQuantity, + }, + }, ""}, + {"container with no resources defined and ignore values", corev1.Container{}, + Settings{ + Cpu: &ResourceConfiguration{ + IgnoreValues: true, + }, + Memory: &ResourceConfiguration{ + IgnoreValues: true, + }, + }, &corev1.ResourceRequirements{}, + "container does not have any resource limits"}, + {"container with missing cpu values and ignore cpu values", corev1.Container{ + Image: "image1:latest", + Resources: &corev1.ResourceRequirements{ + Limits: map[string]*apimachinery_pkg_api_resource.Quantity{ + "memory": &oneGiMemoryQuantity, + }, + Requests: map[string]*apimachinery_pkg_api_resource.Quantity{ + "memory": &oneGiMemoryQuantity, + }, + }, + }, Settings{ + Cpu: &ResourceConfiguration{ + IgnoreValues: true, + }, + Memory: &ResourceConfiguration{ + DefaultLimit: oneGi, + DefaultRequest: oneGi, + MaxLimit: oneGi, + }, + }, &corev1.ResourceRequirements{ + Limits: map[string]*apimachinery_pkg_api_resource.Quantity{ + "memory": &oneGiMemoryQuantity, + }, + Requests: map[string]*apimachinery_pkg_api_resource.Quantity{ + "memory": &oneGiMemoryQuantity, + }, + }, "container does not have a cpu limit"}, + {"container with missing memory values and ignore memory values", corev1.Container{ + Image: "image1:latest", + Resources: &corev1.ResourceRequirements{ + Limits: map[string]*apimachinery_pkg_api_resource.Quantity{ + "cpu": &twoCoreCpuQuantity, + }, + Requests: map[string]*apimachinery_pkg_api_resource.Quantity{ + "cpu": &oneCoreCpuQuantity, + }, + }, + }, Settings{ + Cpu: &ResourceConfiguration{ + DefaultLimit: oneCore, + DefaultRequest: oneCore, + MaxLimit: oneCore, + }, + Memory: &ResourceConfiguration{ + IgnoreValues: true, + }, + }, &corev1.ResourceRequirements{ + Limits: map[string]*apimachinery_pkg_api_resource.Quantity{ + "cpu": &twoCoreCpuQuantity, + }, + Requests: map[string]*apimachinery_pkg_api_resource.Quantity{ + "cpu": &oneCoreCpuQuantity, + }, + }, "container does not have a memory limit"}, + {"container missing memory requests values and ignore memory values", corev1.Container{ + Image: "image1:latest", + Resources: &corev1.ResourceRequirements{ + Limits: map[string]*apimachinery_pkg_api_resource.Quantity{ + "cpu": &oneCoreCpuQuantity, + "memory": &oneGiMemoryQuantity, + }, + Requests: map[string]*apimachinery_pkg_api_resource.Quantity{ + "cpu": &oneCoreCpuQuantity, + }, + }, + }, Settings{ + Cpu: &ResourceConfiguration{ + DefaultLimit: oneCore, + DefaultRequest: oneCore, + MaxLimit: oneCore, + }, + Memory: &ResourceConfiguration{ + IgnoreValues: true, + }, + }, &corev1.ResourceRequirements{ + Limits: map[string]*apimachinery_pkg_api_resource.Quantity{ + "cpu": &oneCoreCpuQuantity, + "memory": &oneGiMemoryQuantity, + }, + Requests: map[string]*apimachinery_pkg_api_resource.Quantity{ + "cpu": &oneCoreCpuQuantity, + }, + }, "container does not have a memory request"}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := validateContainerResources(&test.container, &test.settings) + if err != nil && len(test.expectedErrorMsg) == 0 { + t.Fatalf("unexpected error: %q", err) + } + if len(test.expectedErrorMsg) > 0 { + if err == nil { + t.Fatalf("expected error message with string '%s'. But no error has been returned", test.expectedErrorMsg) + } + if !strings.Contains(err.Error(), test.expectedErrorMsg) { + t.Errorf("invalid error message. Expected the string '%s' in the error. Got '%s'", test.expectedErrorMsg, err.Error()) + } + } }) } + } func TestIgnoreImageSettings(t *testing.T) {