From 5efa416ea9d5fe7a39ad6dd487709e54890a330a Mon Sep 17 00:00:00 2001 From: Robert Brennan Date: Fri, 27 Dec 2019 19:42:49 +0000 Subject: [PATCH] implement custom checks, implement resource ranges as custom check --- checks/cpuLimitsMissing.yaml | 29 +++++ checks/cpuRequestsMissing.yaml | 29 +++++ checks/memoryLimitsMissing.yaml | 29 +++++ checks/memoryRequestsMissing.yaml | 29 +++++ examples/config-full.yaml | 46 +------ pkg/config/config.go | 52 +++----- pkg/config/config_test.go | 129 +++---------------- pkg/config/exemptions.go | 6 - pkg/config/schema.go | 164 +++++++++++++++++++++++ pkg/validator/container.go | 85 ------------ pkg/validator/container_test.go | 207 ++++++++++++++---------------- pkg/validator/schema.go | 146 +++++++-------------- 12 files changed, 464 insertions(+), 487 deletions(-) create mode 100644 checks/cpuLimitsMissing.yaml create mode 100644 checks/cpuRequestsMissing.yaml create mode 100644 checks/memoryLimitsMissing.yaml create mode 100644 checks/memoryRequestsMissing.yaml create mode 100644 pkg/config/schema.go diff --git a/checks/cpuLimitsMissing.yaml b/checks/cpuLimitsMissing.yaml new file mode 100644 index 00000000..478c80a0 --- /dev/null +++ b/checks/cpuLimitsMissing.yaml @@ -0,0 +1,29 @@ +name: CPULimitsMissing +id: cpuLimitsMissing +successMessage: CPU limits are set +failureMessage: CPU limits should be set +category: Resources +target: Container +containers: + exclude: + - initContainer +schema: + '$schema': http://json-schema.org/draft-07/schema + type: object + required: + - resources + properties: + resources: + type: object + required: + - limits + properties: + limits: + type: object + required: + - cpu + properties: + cpu: + type: string + not: + const: '' diff --git a/checks/cpuRequestsMissing.yaml b/checks/cpuRequestsMissing.yaml new file mode 100644 index 00000000..1c3a445c --- /dev/null +++ b/checks/cpuRequestsMissing.yaml @@ -0,0 +1,29 @@ +name: CPURequestsMissing +id: cpuRequestsMissing +successMessage: CPU requests are set +failureMessage: CPU requests should be set +category: Resources +target: Container +containers: + exclude: + - initContainer +schema: + '$schema': http://json-schema.org/draft-07/schema + type: object + required: + - resources + properties: + resources: + type: object + required: + - requests + properties: + requests: + type: object + required: + - cpu + properties: + cpu: + type: string + not: + const: '' diff --git a/checks/memoryLimitsMissing.yaml b/checks/memoryLimitsMissing.yaml new file mode 100644 index 00000000..e61926bf --- /dev/null +++ b/checks/memoryLimitsMissing.yaml @@ -0,0 +1,29 @@ +name: MemoryLimitsMissing +id: memoryLimitsMissing +successMessage: Memory limits are set +failureMessage: Memory limits should be set +category: Resources +target: Container +containers: + exclude: + - initContainer +schema: + '$schema': http://json-schema.org/draft-07/schema + type: object + required: + - resources + properties: + resources: + type: object + required: + - limits + properties: + limits: + type: object + required: + - memory + properties: + memory: + type: string + not: + const: '' diff --git a/checks/memoryRequestsMissing.yaml b/checks/memoryRequestsMissing.yaml new file mode 100644 index 00000000..cfd339a9 --- /dev/null +++ b/checks/memoryRequestsMissing.yaml @@ -0,0 +1,29 @@ +name: MemoryRequestsMissing +id: memoryRequestsMissing +successMessage: Memory requests are set +failureMessage: Memory requests should be set +category: Resources +target: Container +containers: + exclude: + - initContainer +schema: + '$schema': http://json-schema.org/draft-07/schema + type: object + required: + - resources + properties: + resources: + type: object + required: + - requests + properties: + requests: + type: object + required: + - memory + properties: + memory: + type: string + not: + const: '' diff --git a/examples/config-full.yaml b/examples/config-full.yaml index 011fe15b..eb370b85 100644 --- a/examples/config-full.yaml +++ b/examples/config-full.yaml @@ -1,45 +1,11 @@ resources: cpuRequestsMissing: warning - cpuRequestRanges: - warning: - below: 50m - above: 1000m - error: - below: 500m - above: 2000m cpuLimitsMissing: warning - cpuLimitRanges: - warning: - below: 50m - above: 1000m - error: - below: 500m - above: 2000m memoryRequestsMissing: warning - memoryRequestRanges: - warning: - below: 50M - above: 2G - error: - below: 100M - above: 4G memoryLimitsMissing: warning - memoryLimitRanges: - warning: - below: 50M - above: 2G - error: - below: 100M - above: 4G images: tagNotSpecified: error pullPolicyNotAlways: warning - whitelist: - error: - - gcr.io/* - blacklist: - warning: - - docker.io/* healthChecks: readinessProbeMissing: warning livenessProbeMissing: warning @@ -53,16 +19,8 @@ security: runAsPrivileged: error notReadOnlyRootFileSystem: warning privilegeEscalationAllowed: error - capabilities: - error: - ifAnyAdded: - - SYS_ADMIN - - ALL - ifAnyNotDropped: - - ALL - warning: - ifAnyAddedBeyond: - - NONE + dangerousCapabilities: error + insecureCapabilities: warning controllers_to_scan: - Deployments - StatefulSets diff --git a/pkg/config/config.go b/pkg/config/config.go index 351f1277..5aa75381 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -23,21 +23,22 @@ import ( "strings" packr "github.com/gobuffalo/packr/v2" - "k8s.io/apimachinery/pkg/api/resource" "k8s.io/apimachinery/pkg/util/yaml" ) // Configuration contains all of the config for the validation checks. type Configuration struct { - DisplayName string `json:"displayName"` - Resources Resources `json:"resources"` - HealthChecks HealthChecks `json:"healthChecks"` - Images Images `json:"images"` - Networking Networking `json:"networking"` - Security Security `json:"security"` - ControllersToScan []SupportedController `json:"controllers_to_scan"` - Exemptions []Exemption `json:"exemptions"` - DisallowExemptions bool `json:"disallowExemptions"` + DisplayName string `json:"displayName"` + Resources Resources `json:"resources"` + HealthChecks HealthChecks `json:"healthChecks"` + Images Images `json:"images"` + Networking Networking `json:"networking"` + Security Security `json:"security"` + Checks map[string]Severity `json:"checks"` + ControllersToScan []SupportedController `json:"controllers_to_scan"` + CustomChecks map[string]SchemaCheck `json:"customChecks"` + Exemptions []Exemption `json:"exemptions"` + DisallowExemptions bool `json:"disallowExemptions"` } // Exemption represents an exemption to normal rules @@ -48,26 +49,10 @@ type Exemption struct { // Resources contains config for resource requests and limits. type Resources struct { - CPURequestsMissing Severity `json:"cpuRequestsMissing"` - CPURequestRanges ResourceRanges `json:"cpuRequestRanges"` - CPULimitsMissing Severity `json:"cpuLimitsMissing"` - CPULimitRanges ResourceRanges `json:"cpuLimitRanges"` - MemoryRequestsMissing Severity `json:"memoryRequestsMissing"` - MemoryRequestRanges ResourceRanges `json:"memoryRequestRanges"` - MemoryLimitsMissing Severity `json:"memoryLimitsMissing"` - MemoryLimitRanges ResourceRanges `json:"memoryLimitRanges"` -} - -// ResourceRanges contains config for requests or limits for a specific resource. -type ResourceRanges struct { - Warning ResourceRange `json:"warning"` - Error ResourceRange `json:"error"` -} - -// ResourceRange can contain below and above conditions for validation. -type ResourceRange struct { - Below *resource.Quantity `json:"below"` - Above *resource.Quantity `json:"above"` + CPURequestsMissing Severity `json:"cpuRequestsMissing"` + CPULimitsMissing Severity `json:"cpuLimitsMissing"` + MemoryRequestsMissing Severity `json:"memoryRequestsMissing"` + MemoryLimitsMissing Severity `json:"memoryLimitsMissing"` } // HealthChecks contains config for readiness and liveness probes. @@ -140,9 +125,14 @@ func Parse(rawBytes []byte) (Configuration, error) { for { if err := d.Decode(&conf); err != nil { if err == io.EOF { - return conf, nil + break } return conf, fmt.Errorf("Decoding config failed: %v", err) } } + for key, check := range conf.CustomChecks { + check.ID = key + conf.CustomChecks[key] = check + } + return conf, nil } diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index ba15b2f1..1677fed7 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -24,111 +24,41 @@ import ( "time" "github.com/stretchr/testify/assert" - "k8s.io/apimachinery/pkg/api/resource" ) -var resourceConfInvalid1 = `test` +var confInvalid = `test` -var resourceConfYAML1 = `--- +var confValidYAML = ` resources: - cpuRequestRanges: - error: - below: 100m - above: 1 - warning: - below: 200m - above: 800m - memoryRequestRanges: - error: - below: 100M - above: 3G - warning: - below: 200M - above: 2G - cpuLimitRanges: - error: - below: 100m - above: 2 - warning: - below: 300m - above: 1800m - memoryLimitRanges: - error: - below: 200M - above: 6G - warning: - below: 300M - above: 4G + cpuRequestsMissing: warning controllers_to_scan: - Deployments - - StatefulSets - - Jobs - - CronJobs - - DaemonSets - - ReplicationControllers ` -var resourceConfJSON1 = `{ - "resources": { - "cpuRequestRanges": { - "error": { - "below": "100m", - "above": 1 - }, - "warning": { - "below": "200m", - "above": "800m" - } - }, - "memoryRequestRanges": { - "error": { - "below": "100M", - "above": "3G" - }, - "warning": { - "below": "200M", - "above": "2G" - } - }, - "cpuLimitRanges": { - "error": { - "below": "100m", - "above": 2 - }, - "warning": { - "below": "300m", - "above": "1800m" - } - }, - "memoryLimitRanges": { - "error": { - "below": "200M", - "above": "6G" - }, - "warning": { - "below": "300M", - "above": "4G" - } - } - }, - "controllers_to_scan": ["Deployments", "StatefulSets", "Jobs", "CronJobs", "DaemonSets", "ReplicationControllers"] -}` +var confValidJSON = ` +{ + "resources": { + "cpuRequestsMissing": "warning" + }, + "controllers_to_scan": ["Deployments"] +} +` func TestParseError(t *testing.T) { - _, err := Parse([]byte(resourceConfInvalid1)) + _, err := Parse([]byte(confInvalid)) expectedErr := "Decoding config failed: error unmarshaling JSON: while decoding JSON: json: cannot unmarshal string into Go value of type config.Configuration" assert.EqualError(t, err, expectedErr) } func TestParseYaml(t *testing.T) { - parsedConf, err := Parse([]byte(resourceConfYAML1)) + parsedConf, err := Parse([]byte(confValidYAML)) assert.NoError(t, err, "Expected no error when parsing YAML config") testParsedConfig(t, &parsedConf) } func TestParseJson(t *testing.T) { - parsedConf, err := Parse([]byte(resourceConfJSON1)) + parsedConf, err := Parse([]byte(confValidJSON)) assert.NoError(t, err, "Expected no error when parsing JSON config") testParsedConfig(t, &parsedConf) @@ -139,7 +69,7 @@ func TestConfigFromURL(t *testing.T) { var parsedConf Configuration srv := &http.Server{Addr: ":8081"} http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - io.WriteString(w, resourceConfYAML1) + io.WriteString(w, confValidYAML) }) go func() { @@ -166,30 +96,7 @@ func TestConfigNoServerError(t *testing.T) { } func testParsedConfig(t *testing.T, config *Configuration) { - cpuRequests := config.Resources.CPURequestRanges - assert.Equal(t, int64(100), cpuRequests.Error.Below.ScaledValue(resource.Milli)) - assert.Equal(t, int64(1000), cpuRequests.Error.Above.ScaledValue(resource.Milli)) - assert.Equal(t, int64(200), cpuRequests.Warning.Below.ScaledValue(resource.Milli)) - assert.Equal(t, int64(800), cpuRequests.Warning.Above.ScaledValue(resource.Milli)) - - memRequests := config.Resources.MemoryRequestRanges - assert.Equal(t, int64(100), memRequests.Error.Below.ScaledValue(resource.Mega)) - assert.Equal(t, int64(3000), memRequests.Error.Above.ScaledValue(resource.Mega)) - assert.Equal(t, int64(200), memRequests.Warning.Below.ScaledValue(resource.Mega)) - assert.Equal(t, int64(2000), memRequests.Warning.Above.ScaledValue(resource.Mega)) - - cpuLimits := config.Resources.CPULimitRanges - assert.Equal(t, int64(100), cpuLimits.Error.Below.ScaledValue(resource.Milli)) - assert.Equal(t, int64(2000), cpuLimits.Error.Above.ScaledValue(resource.Milli)) - assert.Equal(t, int64(300), cpuLimits.Warning.Below.ScaledValue(resource.Milli)) - assert.Equal(t, int64(1800), cpuLimits.Warning.Above.ScaledValue(resource.Milli)) - - memLimits := config.Resources.MemoryLimitRanges - assert.Equal(t, int64(200), memLimits.Error.Below.ScaledValue(resource.Mega)) - assert.Equal(t, int64(6000), memLimits.Error.Above.ScaledValue(resource.Mega)) - assert.Equal(t, int64(300), memLimits.Warning.Below.ScaledValue(resource.Mega)) - assert.Equal(t, int64(4000), memLimits.Warning.Above.ScaledValue(resource.Mega)) - - controllersToScan := config.ControllersToScan - assert.ElementsMatch(t, []SupportedController{Deployments, StatefulSets, Jobs, CronJobs, DaemonSets, ReplicationControllers}, controllersToScan) + assert.Equal(t, SeverityWarning, config.Resources.CPURequestsMissing) + assert.Equal(t, Severity(""), config.Resources.CPULimitsMissing) + assert.ElementsMatch(t, []SupportedController{Deployments}, config.ControllersToScan) } diff --git a/pkg/config/exemptions.go b/pkg/config/exemptions.go index 03f518ca..2da8778d 100644 --- a/pkg/config/exemptions.go +++ b/pkg/config/exemptions.go @@ -16,12 +16,6 @@ func (conf Configuration) IsActionable(subConf interface{}, ruleName, controller if severity, ok := fieldVal.(Severity); ok && !severity.IsActionable() { return false } - if ranges, ok := fieldVal.(ResourceRanges); ok { - if ranges.Warning.Above == nil && ranges.Warning.Below == nil && - ranges.Error.Above == nil && ranges.Error.Below == nil { - return false - } - } if conf.DisallowExemptions { return true } diff --git a/pkg/config/schema.go b/pkg/config/schema.go new file mode 100644 index 00000000..f072d88b --- /dev/null +++ b/pkg/config/schema.go @@ -0,0 +1,164 @@ +package config + +import ( + "encoding/json" + "fmt" + + "github.com/qri-io/jsonschema" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" +) + +type resourceMinimum string +type resourceMaximum string + +func newResourceMinimum() jsonschema.Validator { + return new(resourceMinimum) +} + +func newResourceMaximum() jsonschema.Validator { + return new(resourceMaximum) +} + +func (min resourceMinimum) Validate(path string, data interface{}, errs *[]jsonschema.ValError) { + err := validateRange(path, string(min), data, true) + if err != nil { + *errs = append(*errs, *err...) + } +} + +func (max resourceMaximum) Validate(path string, data interface{}, errs *[]jsonschema.ValError) { + err := validateRange(path, string(max), data, false) + if err != nil { + *errs = append(*errs, *err...) + } +} + +func parseQuantity(i interface{}) (resource.Quantity, *[]jsonschema.ValError) { + resStr, ok := i.(string) + if !ok { + return resource.Quantity{}, &[]jsonschema.ValError{ + {Message: fmt.Sprintf("Resource quantity %v is not a string", i)}, + } + } + q, err := resource.ParseQuantity(resStr) + if err != nil { + return resource.Quantity{}, &[]jsonschema.ValError{ + {Message: fmt.Sprintf("Could not parse resource quantity: %s", resStr)}, + } + } + return q, nil +} + +func validateRange(path string, limit interface{}, data interface{}, isMinimum bool) *[]jsonschema.ValError { + limitQuantity, err := parseQuantity(limit) + if err != nil { + return err + } + actualQuantity, err := parseQuantity(data) + if err != nil { + return err + } + cmp := limitQuantity.Cmp(actualQuantity) + if isMinimum { + if cmp == 1 { + return &[]jsonschema.ValError{ + {Message: fmt.Sprintf("%s quantity %v is > %v", path, actualQuantity, limitQuantity)}, + } + } + } else { + if cmp == -1 { + return &[]jsonschema.ValError{ + {Message: fmt.Sprintf("%s quantity %v is < %v", path, actualQuantity, limitQuantity)}, + } + } + } + return nil +} + +func init() { + jsonschema.RegisterValidator("resourceMinimum", newResourceMinimum) + jsonschema.RegisterValidator("resourceMaximum", newResourceMaximum) +} + +type includeExcludeList struct { + Include []string `yaml:"include"` + Exclude []string `yaml:"exclude"` +} + +type TargetKind string + +const ( + TargetContainer TargetKind = "Container" + TargetPod TargetKind = "Pod" +) + +// SchemaCheck is a Polaris check that runs using JSON Schema +type SchemaCheck struct { + Name string `yaml:"name"` + ID string `yaml:"id"` + Category string `yaml:"category"` + SuccessMessage string `yaml:"successMessage"` + FailureMessage string `yaml:"failureMessage"` + Controllers includeExcludeList `yaml:"controllers"` + Containers includeExcludeList `yaml:"containers"` + Target TargetKind `yaml:"target"` + SchemaTarget TargetKind `yaml:"schemaTarget"` + Schema jsonschema.RootSchema `yaml:"schema"` +} + +func (check SchemaCheck) CheckPod(pod *corev1.PodSpec) (bool, error) { + return check.CheckObject(pod) +} + +func (check SchemaCheck) CheckContainer(container *corev1.Container) (bool, error) { + return check.CheckObject(container) +} + +func (check SchemaCheck) CheckObject(obj interface{}) (bool, error) { + bytes, err := json.Marshal(obj) + if err != nil { + return false, err + } + errs, err := check.Schema.ValidateBytes(bytes) + return len(errs) == 0, err +} + +func (check SchemaCheck) IsActionable(target TargetKind, controllerType SupportedController, isInit bool) bool { + if check.Target != target { + return false + } + isIncluded := len(check.Controllers.Include) == 0 + for _, inclusion := range check.Controllers.Include { + if GetSupportedControllerFromString(inclusion) == controllerType { + isIncluded = true + break + } + } + if !isIncluded { + return false + } + for _, exclusion := range check.Controllers.Exclude { + if GetSupportedControllerFromString(exclusion) == controllerType { + return false + } + } + if check.Target == TargetContainer { + isIncluded := len(check.Containers.Include) == 0 + for _, inclusion := range check.Containers.Include { + if (inclusion == "initContainer" && isInit) || (inclusion == "container" && !isInit) { + isIncluded = true + break + } + } + if !isIncluded { + return false + } + for _, exclusion := range check.Containers.Exclude { + if (exclusion == "initContainer" && isInit) || (exclusion == "container" && !isInit) { + return false + } + } + } + return true +} diff --git a/pkg/validator/container.go b/pkg/validator/container.go index 81352684..0926cf71 100644 --- a/pkg/validator/container.go +++ b/pkg/validator/container.go @@ -15,12 +15,8 @@ package validator import ( - "fmt" - "github.com/fairwindsops/polaris/pkg/config" - "github.com/fairwindsops/polaris/pkg/validator/messages" corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/resource" ) // ContainerValidation tracks validation failures associated with a Container. @@ -56,8 +52,6 @@ func ValidateContainer(container *corev1.Container, parentPodResult *PodResult, cv.parentPodSpec = parentPodResult.podSpec } - cv.validateResources(conf, controllerName) - err := applyContainerSchemaChecks(conf, controllerName, controllerType, &cv) // FIXME: don't panic if err != nil { @@ -72,82 +66,3 @@ func ValidateContainer(container *corev1.Container, parentPodResult *PodResult, return cRes } - -func (cv *ContainerValidation) validateResources(conf *config.Configuration, controllerName string) { - // Only validate resources for primary containers. Although it can - // be helpful to set these in certain cases, it usually isn't - if cv.IsInitContainer { - return - } - - category := messages.CategoryResources - res := cv.Container.Resources - - missingName := "CPURequestsMissing" - rangeName := "CPURequestRanges" - id := config.GetIDFromField(conf.Resources, missingName) - if conf.IsActionable(conf.Resources, missingName, controllerName) && res.Requests.Cpu().MilliValue() == 0 { - cv.addFailure(messages.CPURequestsFailure, conf.Resources.CPURequestsMissing, category, id) - } else if conf.IsActionable(conf.Resources, rangeName, controllerName) { - id := config.GetIDFromField(conf.Resources, rangeName) - cv.validateResourceRange(id, messages.CPURequestsLabel, &conf.Resources.CPURequestRanges, res.Requests.Cpu()) - } else if conf.IsActionable(conf.Resources, missingName, controllerName) { - cv.addSuccess(fmt.Sprintf(messages.ResourcePresentSuccess, messages.CPURequestsLabel), category, id) - } - - missingName = "CPULimitsMissing" - rangeName = "CPULimitRanges" - id = config.GetIDFromField(conf.Resources, missingName) - if conf.IsActionable(conf.Resources, missingName, controllerName) && res.Limits.Cpu().MilliValue() == 0 { - cv.addFailure(messages.CPULimitsFailure, conf.Resources.CPULimitsMissing, category, id) - } else if conf.IsActionable(conf.Resources, rangeName, controllerName) { - id := config.GetIDFromField(conf.Resources, rangeName) - cv.validateResourceRange(id, messages.CPULimitsLabel, &conf.Resources.CPULimitRanges, res.Requests.Cpu()) - } else if conf.IsActionable(conf.Resources, missingName, controllerName) { - cv.addSuccess(fmt.Sprintf(messages.ResourcePresentSuccess, messages.CPULimitsLabel), category, id) - } - - missingName = "MemoryRequestsMissing" - rangeName = "MemoryRequestRanges" - id = config.GetIDFromField(conf.Resources, missingName) - if conf.IsActionable(conf.Resources, missingName, controllerName) && res.Requests.Memory().MilliValue() == 0 { - cv.addFailure(messages.MemoryRequestsFailure, conf.Resources.MemoryRequestsMissing, category, id) - } else if conf.IsActionable(conf.Resources, rangeName, controllerName) { - id := config.GetIDFromField(conf.Resources, rangeName) - cv.validateResourceRange(id, messages.MemoryRequestsLabel, &conf.Resources.MemoryRequestRanges, res.Requests.Memory()) - } else if conf.IsActionable(conf.Resources, missingName, controllerName) { - cv.addSuccess(fmt.Sprintf(messages.ResourcePresentSuccess, messages.MemoryRequestsLabel), category, id) - } - - missingName = "MemoryLimitsMissing" - rangeName = "MemoryLimitRanges" - id = config.GetIDFromField(conf.Resources, missingName) - if conf.IsActionable(conf.Resources, missingName, controllerName) && res.Limits.Memory().MilliValue() == 0 { - cv.addFailure(messages.MemoryLimitsFailure, conf.Resources.MemoryLimitsMissing, category, id) - } else if conf.IsActionable(conf.Resources, rangeName, controllerName) { - id := config.GetIDFromField(conf.Resources, rangeName) - cv.validateResourceRange(id, messages.MemoryLimitsLabel, &conf.Resources.MemoryLimitRanges, res.Limits.Memory()) - } else if conf.IsActionable(conf.Resources, missingName, controllerName) { - cv.addSuccess(fmt.Sprintf(messages.ResourcePresentSuccess, messages.MemoryLimitsLabel), category, id) - } -} - -func (cv *ContainerValidation) validateResourceRange(id, resourceName string, rangeConf *config.ResourceRanges, res *resource.Quantity) { - warnAbove := rangeConf.Warning.Above - warnBelow := rangeConf.Warning.Below - errorAbove := rangeConf.Error.Above - errorBelow := rangeConf.Error.Below - category := messages.CategoryResources - - if errorAbove != nil && errorAbove.MilliValue() < res.MilliValue() { - cv.addError(fmt.Sprintf(messages.ResourceAmountTooHighFailure, resourceName, errorAbove.String()), category, id) - } else if warnAbove != nil && warnAbove.MilliValue() < res.MilliValue() { - cv.addWarning(fmt.Sprintf(messages.ResourceAmountTooHighFailure, resourceName, warnAbove.String()), category, id) - } else if errorBelow != nil && errorBelow.MilliValue() > res.MilliValue() { - cv.addError(fmt.Sprintf(messages.ResourceAmountTooLowFailure, resourceName, errorBelow.String()), category, id) - } else if warnBelow != nil && warnBelow.MilliValue() > res.MilliValue() { - cv.addWarning(fmt.Sprintf(messages.ResourceAmountTooLowFailure, resourceName, warnBelow.String()), category, id) - } else if errorAbove != nil && warnAbove != nil && errorBelow != nil && warnBelow != nil { - cv.addSuccess(fmt.Sprintf(messages.ResourceAmountSuccess, resourceName), category, id) - } -} diff --git a/pkg/validator/container_test.go b/pkg/validator/container_test.go index 927aa5f5..4c958754 100644 --- a/pkg/validator/container_test.go +++ b/pkg/validator/container_test.go @@ -24,39 +24,7 @@ import ( "k8s.io/apimachinery/pkg/api/resource" ) -var resourceConf1 = `--- -resources: - cpuRequestRanges: - error: - below: 100m - above: 1 - warning: - below: 200m - above: 800m - memoryRequestRanges: - error: - below: 100M - above: 3G - warning: - below: 200M - above: 2G - cpuLimitRanges: - error: - below: 100m - above: 2 - warning: - below: 300m - above: 1800m - memoryLimitRanges: - error: - below: 200M - above: 6G - warning: - below: 300M - above: 4G -` - -var resourceConf2 = `--- +var resourceConfMinimal = `--- resources: cpuRequestsMissing: warning memoryRequestsMissing: warning @@ -80,44 +48,66 @@ exemptions: - foo ` -var resourceConfRangeExemptions = `--- -resources: - cpuRequestRanges: - error: - below: 100m - above: 1 - warning: - below: 200m - above: 800m - memoryRequestRanges: - error: - below: 100M - above: 3G - warning: - below: 200M - above: 2G - cpuLimitRanges: - error: - below: 100m - above: 2 - warning: - below: 300m - above: 1800m - memoryLimitRanges: - error: - below: 200M - above: 6G - warning: - below: 300M - above: 4G -exemptions: - - rules: - - cpuRequestRanges - - memoryRequestRanges - - cpuLimitRanges - - memoryLimitRanges - controllerNames: - - foo +var resourceConfRanges = ` +checks: + memoryRequestsRange: error + memoryLimitsRange: warning +customChecks: + memoryLimitsRange: + containers: + exclude: + - initContainer + successMessage: Memory limits are within the required range + failureMessage: Memory limits should be within the required range + category: Resources + target: Container + schema: + '$schema': http://json-schema.org/draft-07/schema + type: object + required: + - resources + properties: + resources: + type: object + required: + - limits + properties: + limits: + type: object + required: + - memory + properties: + memory: + type: string + resourceMinimum: 200M + resourceMaximum: 6G + memoryRequestsRange: + successMessage: Memory requests are within the required range + failureMessage: Memory requests should be within the required range + category: Resources + target: Container + containers: + exclude: + - initContainer + schema: + '$schema': http://json-schema.org/draft-07/schema + type: object + required: + - resources + properties: + resources: + type: object + required: + - requests + properties: + requests: + required: + - memory + properties: + memory: + type: string + resourceMinimum: 200M + resourceMaximum: 3G ` func testValidateResources(t *testing.T, container *corev1.Container, resourceConf *string, controllerName string, expectedErrors []*ResultMessage, expectedWarnings []*ResultMessage, expectedSuccesses []*ResultMessage) { @@ -129,7 +119,10 @@ func testValidateResources(t *testing.T, container *corev1.Container, resourceCo parsedConf, err := conf.Parse([]byte(*resourceConf)) assert.NoError(t, err, "Expected no error when parsing config") - cv.validateResources(&parsedConf, controllerName) + err = applyContainerSchemaChecks(&parsedConf, controllerName, conf.Deployments, &cv) + if err != nil { + panic(err) + } assert.Len(t, cv.Warnings, len(expectedWarnings)) assert.ElementsMatch(t, expectedWarnings, cv.Warnings) @@ -151,7 +144,10 @@ func TestValidateResourcesEmptyConfig(t *testing.T) { ResourceValidation: &ResourceValidation{}, } - cv.validateResources(&conf.Configuration{}, "") + err := applyContainerSchemaChecks(&conf.Configuration{}, "", conf.Deployments, &cv) + if err != nil { + panic(err) + } assert.Len(t, cv.Errors, 0) } @@ -192,7 +188,7 @@ func TestValidateResourcesEmptyContainer(t *testing.T) { expectedSuccesses := []*ResultMessage{} - testValidateResources(t, &container, &resourceConf2, "foo", expectedErrors, expectedWarnings, expectedSuccesses) + testValidateResources(t, &container, &resourceConfMinimal, "foo", expectedErrors, expectedWarnings, expectedSuccesses) } func TestValidateResourcesPartiallyValid(t *testing.T) { @@ -216,37 +212,25 @@ func TestValidateResourcesPartiallyValid(t *testing.T) { expectedWarnings := []*ResultMessage{ { - ID: "cpuRequestRanges", + ID: "memoryLimitsRange", Type: "warning", - Message: "CPU requests should be higher than 200m", - Category: "Resources", - }, - { - ID: "cpuLimitRanges", - Type: "warning", - Message: "CPU limits should be higher than 300m", + Message: "Memory limits should be within the required range", Category: "Resources", }, } expectedErrors := []*ResultMessage{ { - ID: "memoryRequestRanges", + ID: "memoryRequestsRange", Type: "error", - Message: "Memory requests should be higher than 100M", - Category: "Resources", - }, - { - ID: "memoryLimitRanges", - Type: "error", - Message: "Memory limits should be higher than 200M", + Message: "Memory requests should be within the required range", Category: "Resources", }, } expectedSuccesses := []*ResultMessage{} - testValidateResources(t, &container, &resourceConf1, "foo", expectedErrors, expectedWarnings, expectedSuccesses) + testValidateResources(t, &container, &resourceConfRanges, "foo", expectedErrors, expectedWarnings, expectedSuccesses) } func TestValidateResourcesInit(t *testing.T) { @@ -260,13 +244,20 @@ func TestValidateResourcesInit(t *testing.T) { IsInitContainer: true, } - parsedConf, err := conf.Parse([]byte(resourceConf1)) + parsedConf, err := conf.Parse([]byte(resourceConfRanges)) assert.NoError(t, err, "Expected no error when parsing config") - cvEmpty.validateResources(&parsedConf, "") - assert.Len(t, cvEmpty.Errors, 4) + err = applyContainerSchemaChecks(&parsedConf, "", conf.Deployments, &cvEmpty) + if err != nil { + panic(err) + } + assert.Len(t, cvEmpty.Errors, 1) + assert.Len(t, cvEmpty.Warnings, 1) - cvInit.validateResources(&parsedConf, "") + err = applyContainerSchemaChecks(&parsedConf, "", conf.Deployments, &cvInit) + if err != nil { + panic(err) + } assert.Len(t, cvInit.Errors, 0) } @@ -299,32 +290,20 @@ func TestValidateResourcesFullyValid(t *testing.T) { expectedSuccesses := []*ResultMessage{ { - ID: "cpuRequestRanges", + ID: "memoryRequestsRange", Type: "success", - Message: "CPU requests are within the expected range", + Message: "Memory requests are within the required range", Category: "Resources", }, { - ID: "memoryRequestRanges", + ID: "memoryLimitsRange", Type: "success", - Message: "Memory requests are within the expected range", - Category: "Resources", - }, - { - ID: "cpuLimitRanges", - Type: "success", - Message: "CPU limits are within the expected range", - Category: "Resources", - }, - { - ID: "memoryLimitRanges", - Type: "success", - Message: "Memory limits are within the expected range", + Message: "Memory limits are within the required range", Category: "Resources", }, } - testValidateResources(t, &container, &resourceConf1, "foo", []*ResultMessage{}, []*ResultMessage{}, expectedSuccesses) + testValidateResources(t, &container, &resourceConfRanges, "foo", []*ResultMessage{}, []*ResultMessage{}, expectedSuccesses) expectedSuccesses = []*ResultMessage{ { @@ -353,7 +332,7 @@ func TestValidateResourcesFullyValid(t *testing.T) { }, } - testValidateResources(t, &container, &resourceConf2, "foo", []*ResultMessage{}, []*ResultMessage{}, expectedSuccesses) + testValidateResources(t, &container, &resourceConfMinimal, "foo", []*ResultMessage{}, []*ResultMessage{}, expectedSuccesses) } func TestValidateHealthChecks(t *testing.T) { @@ -1339,6 +1318,7 @@ func TestValidateResourcesExemption(t *testing.T) { testValidateResources(t, &container, &disallowExemptionsConf, "foo", expectedErrors, expectedWarnings, expectedSuccesses) } +/* func TestValidateResourceRangeExemption(t *testing.T) { container := corev1.Container{ Name: "Empty", @@ -1350,6 +1330,7 @@ func TestValidateResourceRangeExemption(t *testing.T) { testValidateResources(t, &container, &resourceConfRangeExemptions, "foo", expectedErrors, expectedWarnings, expectedSuccesses) } +*/ func resetCV(cv ContainerValidation) ContainerValidation { cv.Errors = []*ResultMessage{} diff --git a/pkg/validator/schema.go b/pkg/validator/schema.go index 8b9a045b..e02341ea 100644 --- a/pkg/validator/schema.go +++ b/pkg/validator/schema.go @@ -2,49 +2,21 @@ package validator import ( "bytes" - "encoding/json" "fmt" "io" packr "github.com/gobuffalo/packr/v2" - "github.com/qri-io/jsonschema" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/util/yaml" "github.com/fairwindsops/polaris/pkg/config" ) -type includeExcludeList struct { - Include []string `yaml:"include"` - Exclude []string `yaml:"exclude"` -} - -type target string - -const ( - targetContainer target = "Container" - targetPod target = "Pod" -) - -// SchemaCheck is a Polaris check that runs using JSON Schema -type SchemaCheck struct { - Name string `yaml:"name"` - ID string `yaml:"id"` - Category string `yaml:"category"` - SuccessMessage string `yaml:"successMessage"` - FailureMessage string `yaml:"failureMessage"` - Controllers includeExcludeList `yaml:"controllers"` - Containers includeExcludeList `yaml:"containers"` - Target target `yaml:"target"` - SchemaTarget target `yaml:"schemaTarget"` - Schema jsonschema.RootSchema `yaml:"schema"` -} - var ( schemaBox = (*packr.Box)(nil) - checks = map[target][]SchemaCheck{ - targetContainer: []SchemaCheck{}, - targetPod: []SchemaCheck{}, + checks = map[config.TargetKind][]config.SchemaCheck{ + config.TargetContainer: []config.SchemaCheck{}, + config.TargetPod: []config.SchemaCheck{}, } // We explicitly set the order to avoid thrash in the // tests as we migrate toward JSON schema @@ -54,6 +26,10 @@ var ( "hostPID", "hostNetwork", // Container checks + "memoryLimitsMissing", + "memoryRequestsMissing", + "cpuLimitsMissing", + "cpuRequestsMissing", "readinessProbe", "livenessProbe", "pullPolicyNotAlways", @@ -83,9 +59,9 @@ func init() { } } -func parseCheck(rawBytes []byte) (SchemaCheck, error) { +func parseCheck(rawBytes []byte) (config.SchemaCheck, error) { reader := bytes.NewReader(rawBytes) - check := SchemaCheck{} + check := config.SchemaCheck{} d := yaml.NewYAMLOrJSONDecoder(reader, 4096) for { if err := d.Decode(&check); err != nil { @@ -97,72 +73,16 @@ func parseCheck(rawBytes []byte) (SchemaCheck, error) { } } -func (check SchemaCheck) checkPod(pod *corev1.PodSpec) (bool, error) { - return check.checkObject(pod) -} - -func (check SchemaCheck) checkContainer(container *corev1.Container) (bool, error) { - return check.checkObject(container) -} - -func (check SchemaCheck) checkObject(obj interface{}) (bool, error) { - bytes, err := json.Marshal(obj) - if err != nil { - return false, err - } - errors, err := check.Schema.ValidateBytes(bytes) - return len(errors) == 0, err -} - -func (check SchemaCheck) isActionable(target target, controllerType config.SupportedController, isInit bool) bool { - if check.Target != target { - return false - } - isIncluded := len(check.Controllers.Include) == 0 - for _, inclusion := range check.Controllers.Include { - if config.GetSupportedControllerFromString(inclusion) == controllerType { - isIncluded = true - break - } - } - if !isIncluded { - return false - } - for _, exclusion := range check.Controllers.Exclude { - if config.GetSupportedControllerFromString(exclusion) == controllerType { - return false - } - } - if check.Target == targetContainer { - isIncluded := len(check.Containers.Include) == 0 - for _, inclusion := range check.Containers.Include { - if (inclusion == "initContainer" && isInit) || (inclusion == "container" && !isInit) { - isIncluded = true - break - } - } - if !isIncluded { - return false - } - for _, exclusion := range check.Containers.Exclude { - if (exclusion == "initContainer" && isInit) || (exclusion == "container" && !isInit) { - return false - } - } - } - return true -} - func applyPodSchemaChecks(conf *config.Configuration, pod *corev1.PodSpec, controllerName string, controllerType config.SupportedController, pv *PodValidation) error { - for _, check := range checks[targetPod] { + for _, check := range checks[config.TargetPod] { if !conf.IsActionable(check.Category, check.Name, controllerName) { continue } - if !check.isActionable(targetPod, controllerType, false) { + if !check.IsActionable(config.TargetPod, controllerType, false) { continue } severity := conf.GetSeverity(check.Category, check.Name) - passes, err := check.checkPod(pod) + passes, err := check.CheckPod(pod) if err != nil { return err } @@ -176,22 +96,54 @@ func applyPodSchemaChecks(conf *config.Configuration, pod *corev1.PodSpec, contr } func applyContainerSchemaChecks(conf *config.Configuration, controllerName string, controllerType config.SupportedController, cv *ContainerValidation) error { - for _, check := range checks[targetContainer] { + for _, check := range checks[config.TargetContainer] { if !conf.IsActionable(check.Category, check.Name, controllerName) { continue } - if !check.isActionable(targetContainer, controllerType, cv.IsInitContainer) { + if !check.IsActionable(config.TargetContainer, controllerType, cv.IsInitContainer) { continue } severity := conf.GetSeverity(check.Category, check.Name) var passes bool var err error - if check.SchemaTarget == targetPod { + if check.SchemaTarget == config.TargetPod { cv.parentPodSpec.Containers = []corev1.Container{*cv.Container} - passes, err = check.checkPod(&cv.parentPodSpec) + passes, err = check.CheckPod(&cv.parentPodSpec) cv.parentPodSpec.Containers = []corev1.Container{} } else { - passes, err = check.checkContainer(cv.Container) + passes, err = check.CheckContainer(cv.Container) + } + if err != nil { + return err + } + if passes { + cv.addSuccess(check.SuccessMessage, check.Category, check.ID) + } else { + cv.addFailure(check.FailureMessage, severity, check.Category, check.ID) + } + } + for checkName, severity := range conf.Checks { + check, ok := conf.CustomChecks[checkName] + if !ok { + return fmt.Errorf("Custom check %s not found", checkName) + } + // FIXME: check actionability here + /* + if !conf.IsActionable(check.Category, check.Name, controllerName) { + continue + } + */ + if !check.IsActionable(config.TargetContainer, controllerType, cv.IsInitContainer) { + continue + } + var passes bool + var err error + if check.SchemaTarget == config.TargetPod { + cv.parentPodSpec.Containers = []corev1.Container{*cv.Container} + passes, err = check.CheckPod(&cv.parentPodSpec) + cv.parentPodSpec.Containers = []corev1.Container{} + } else { + passes, err = check.CheckContainer(cv.Container) } if err != nil { return err