implement custom checks, implement resource ranges as custom check

This commit is contained in:
Robert Brennan
2019-12-27 19:42:49 +00:00
parent 7b0fe81d01
commit 5efa416ea9
12 changed files with 464 additions and 487 deletions

View File

@@ -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: ''

View File

@@ -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: ''

View File

@@ -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: ''

View File

@@ -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: ''

View File

@@ -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

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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
}

164
pkg/config/schema.go Normal file
View File

@@ -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
}

View File

@@ -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)
}
}

View File

@@ -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{}

View File

@@ -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