migrate health checks to schemas

This commit is contained in:
Robert Brennan
2019-12-20 20:15:45 +00:00
parent d4e3258d53
commit 8b20fd9dcf
6 changed files with 148 additions and 35 deletions

23
checks/livenessProbe.yaml Normal file
View File

@@ -0,0 +1,23 @@
name: LivenessProbeMissing
id: livenessProbeMissing
successMessage: Liveness probe is configured
failureMessage: Liveness probe should be configured
category: Health Checks
controllers:
exclude:
- Job
- CronJob
containers:
exclude:
- initContainer
target: Container
schema:
'$schema': http://json-schema.org/draft-07/schema
type: object
required:
- livenessProbe
properties:
livenessProbe:
type: object
not:
const: null

View File

@@ -0,0 +1,23 @@
name: ReadinessProbeMissing
id: readinessProbeMissing
successMessage: Readiness probe is configured
failureMessage: Readiness probe should be configured
category: Health Checks
controllers:
exclude:
- Job
- CronJob
containers:
exclude:
- initContainer
target: Container
schema:
'$schema': http://json-schema.org/draft-07/schema
type: object
required:
- readinessProbe
properties:
readinessProbe:
type: object
not:
const: null

View File

@@ -58,9 +58,13 @@ func ValidateContainer(container *corev1.Container, parentPodResult *PodResult,
}
cv.validateResources(conf, controllerName)
if !isInit && controllerType != config.Jobs && controllerType != config.CronJobs {
cv.validateHealthChecks(conf, controllerName)
err := applyContainerSchemaChecks(conf, container, controllerName, controllerType, isInit, &cv)
// FIXME: don't panic
if err != nil {
panic(err)
}
cv.validateImage(conf, controllerName)
cv.validateNetworking(conf, controllerName)
cv.validateSecurity(conf, controllerName)
@@ -153,31 +157,6 @@ func (cv *ContainerValidation) validateResourceRange(id, resourceName string, ra
}
}
func (cv *ContainerValidation) validateHealthChecks(conf *config.Configuration, controllerName string) {
category := messages.CategoryHealthChecks
name := "ReadinessProbeMissing"
// Don't validate readiness probes on init containers
if !cv.IsInitContainer && conf.IsActionable(conf.HealthChecks, name, controllerName) {
id := config.GetIDFromField(conf.HealthChecks, name)
if cv.Container.ReadinessProbe == nil {
cv.addFailure(messages.ReadinessProbeFailure, conf.HealthChecks.ReadinessProbeMissing, category, id)
} else {
cv.addSuccess(messages.ReadinessProbeSuccess, category, id)
}
}
name = "LivenessProbeMissing"
if conf.IsActionable(conf.HealthChecks, name, controllerName) {
id := config.GetIDFromField(conf.HealthChecks, "LivenessProbeMissing")
if cv.Container.LivenessProbe == nil {
cv.addFailure(messages.LivenessProbeFailure, conf.HealthChecks.LivenessProbeMissing, category, id)
} else {
cv.addSuccess(messages.LivenessProbeSuccess, category, id)
}
}
}
func (cv *ContainerValidation) validateImage(conf *config.Configuration, controllerName string) {
category := messages.CategoryImages

View File

@@ -409,17 +409,21 @@ func TestValidateHealthChecks(t *testing.T) {
{name: "probes configured, but not required", probes: p2, cv: goodCV, errors: &f1},
}
for _, tt := range testCases {
for idx, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
tt.cv.validateHealthChecks(&conf.Configuration{HealthChecks: tt.probes}, "")
err := applyContainerSchemaChecks(&conf.Configuration{HealthChecks: tt.probes}, tt.cv.Container, "", conf.Deployments, tt.cv.IsInitContainer, &tt.cv)
if err != nil {
panic(err)
}
message := fmt.Sprintf("test case %d", idx)
if tt.warnings != nil {
assert.Len(t, tt.cv.Warnings, len(*tt.warnings))
assert.ElementsMatch(t, tt.cv.Warnings, *tt.warnings)
assert.Len(t, tt.cv.Warnings, len(*tt.warnings), message)
assert.ElementsMatch(t, tt.cv.Warnings, *tt.warnings, message)
}
assert.Len(t, tt.cv.Errors, len(*tt.errors))
assert.ElementsMatch(t, tt.cv.Errors, *tt.errors)
assert.Len(t, tt.cv.Errors, len(*tt.errors), message)
assert.ElementsMatch(t, tt.cv.Errors, *tt.errors, message)
})
}
}

View File

@@ -32,7 +32,11 @@ func ValidatePod(conf config.Configuration, pod *corev1.PodSpec, controllerName
ResourceValidation: &ResourceValidation{},
}
applyPodSchemaChecks(&conf, pod, controllerName, &pv)
err := applyPodSchemaChecks(&conf, pod, controllerName, controllerType, &pv)
// FIXME: don't panic
if err != nil {
panic(err)
}
pRes := PodResult{
Messages: pv.messages(),

View File

@@ -34,6 +34,7 @@ type SchemaCheck struct {
SuccessMessage string `yaml:"success_message"`
FailureMessage string `yaml:"failure_message"`
Controllers IncludeExcludeList `yaml:"controllers"`
Containers IncludeExcludeList `yaml:"containers"`
Target Target `yaml:"target"`
Schema jsonschema.RootSchema `yaml:"schema"`
}
@@ -44,10 +45,16 @@ var (
TargetContainer: []SchemaCheck{},
TargetPod: []SchemaCheck{},
}
// We explicitly set the order to avoid thrash in the
// tests as we migrate toward JSON schema
checkOrder = []string{
// Pod checks
"hostIPC",
"hostPID",
"hostNetwork",
// Container checks
"readinessProbe",
"livenessProbe",
}
)
@@ -109,11 +116,62 @@ func (check SchemaCheck) checkPod(pod *corev1.PodSpec) (bool, error) {
return len(errors) == 0, err
}
func applyPodSchemaChecks(conf *config.Configuration, pod *corev1.PodSpec, controllerName string, pv *PodValidation) error {
func (check SchemaCheck) checkContainer(container *corev1.Container) (bool, error) {
bytes, err := json.Marshal(container)
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] {
if !conf.IsActionable(check.Category, check.Name, controllerName) {
continue
}
if !check.isActionable(TargetPod, controllerType, false) {
continue
}
severity := conf.GetSeverity(check.Category, check.Name)
passes, err := check.checkPod(pod)
if err != nil {
@@ -127,3 +185,25 @@ func applyPodSchemaChecks(conf *config.Configuration, pod *corev1.PodSpec, contr
}
return nil
}
func applyContainerSchemaChecks(conf *config.Configuration, container *corev1.Container, controllerName string, controllerType config.SupportedController, isInit bool, cv *ContainerValidation) error {
for _, check := range checks[TargetContainer] {
if !conf.IsActionable(check.Category, check.Name, controllerName) {
continue
}
if !check.isActionable(TargetContainer, controllerType, isInit) {
continue
}
severity := conf.GetSeverity(check.Category, check.Name)
passes, err := check.checkContainer(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)
}
}
return nil
}