Merge pull request #10 from reactiveops/jg/add-config-probes-tags

add image and healthcheck config and checks
This commit is contained in:
Jess G
2019-01-03 09:22:05 -08:00
committed by GitHub
6 changed files with 141 additions and 43 deletions

View File

@@ -17,6 +17,9 @@ jobs:
steps:
- checkout
- run: dep ensure
- run: go get -u github.com/golang/lint/golint
- run: go list ./... | grep -v vendor | xargs golint
- run: go list ./... | grep -v vendor | xargs go vet
- run: go test ./pkg/... -coverprofile cover.out
build:

View File

@@ -77,28 +77,15 @@ data:
memory:
min: 10m
max: 2000M
ingresses:
whitelist:
- '*.example.com'
prevent_overlaps: true
health_checks:
healthChecks:
readiness:
require: true
liveness:
require: true
images:
require_tag: true
repos:
whitelist:
- gcr.io
namespaces:
require_labels: true
security_context:
capabilities:
whitelist:
- 'CAP_SYS_ADMIN'
prevent_privileged: true
read_only_file_system: true
tagRequired: true
whitelistRepos:
- gcr.io
---
apiVersion: extensions/v1beta1
kind: Deployment

View File

@@ -5,28 +5,35 @@ import (
"fmt"
"io"
"io/ioutil"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
"k8s.io/apimachinery/pkg/util/yaml"
)
// ResourceMinMax sets a range for a min and max setting for a resource.
type ResourceMinMax struct {
Min *resource.Quantity
Max *resource.Quantity
}
// ResourceList maps the resource name to a range on min and max values.
type ResourceList map[corev1.ResourceName]ResourceMinMax
// RequestsAndLimits contains config for resource requests and limits.
type RequestsAndLimits struct {
Requests ResourceList
Limits ResourceList
}
// Configuration contains all of the config for the validation checks.
type Configuration struct {
Resources RequestsAndLimits
Resources RequestsAndLimits
HealthChecks Probes
Images Images
}
// ParseFile parses config from a file
// ParseFile parses config from a file.
func ParseFile(path string) (Configuration, error) {
rawBytes, err := ioutil.ReadFile(path)
if err != nil {
@@ -35,7 +42,7 @@ func ParseFile(path string) (Configuration, error) {
return Parse(rawBytes)
}
// Parse parses config from a byte array
// Parse parses config from a byte array.
func Parse(rawBytes []byte) (Configuration, error) {
reader := bytes.NewReader(rawBytes)
conf := Configuration{}
@@ -49,3 +56,20 @@ func Parse(rawBytes []byte) (Configuration, error) {
}
}
}
// Probes contains config for the readiness and liveness probes.
type Probes struct {
Readiness ResourceRequire
Liveness ResourceRequire
}
// ResourceRequire indicates if this resource should be validated.
type ResourceRequire struct {
Require bool
}
// Images contains the config for images.
type Images struct {
TagRequired bool
WhitelistRepos []string
}

View File

@@ -23,7 +23,7 @@ import (
var resourceConfInvalid1 = `test`
var resourceConfYaml1 = `---
var resourceConfYAML1 = `---
resources:
requests:
cpu:
@@ -41,7 +41,7 @@ resources:
max: 4G
`
var resourceConfJson1 = `{
var resourceConfJSON1 = `{
"resources": {
"requests": {
"cpu": {
@@ -72,7 +72,7 @@ func TestParseError(t *testing.T) {
}
func TestParseYaml(t *testing.T) {
parsedConf, err := Parse([]byte(resourceConfYaml1))
parsedConf, err := Parse([]byte(resourceConfYAML1))
assert.NoError(t, err, "Expected no error when parsing config")
requests := parsedConf.Resources.Requests
@@ -89,7 +89,7 @@ func TestParseYaml(t *testing.T) {
}
func TestParseJson(t *testing.T) {
parsedConf, err := Parse([]byte(resourceConfJson1))
parsedConf, err := Parse([]byte(resourceConfJSON1))
assert.NoError(t, err, "Expected no error when parsing config")
requests := parsedConf.Resources.Requests

View File

@@ -15,6 +15,8 @@
package validator
import (
"strings"
conf "github.com/reactiveops/fairwinds/pkg/config"
"github.com/reactiveops/fairwinds/pkg/types"
corev1 "k8s.io/api/core/v1"
@@ -33,8 +35,8 @@ func validateContainer(conf conf.Configuration, container corev1.Container) Cont
}
cv.validateResources(conf.Resources)
// cv.validateHealthChecks(conf.HealthChecks)
// cv.validateTags(conf.Image)
cv.validateHealthChecks(conf.HealthChecks)
cv.validateImage(conf.Images)
return cv
}
@@ -65,24 +67,23 @@ func (cv *ContainerValidation) withinRange(resourceName string, expectedRange co
}
}
// func probes(conf conf.ResourceRequestsAndLimits, c corev1.Container, results types.ContainerResults) types.ContainerResults {
// if c.ReadinessProbe == nil {
// results.AddFailure("Readiness Probe", "placeholder", "placeholder")
// }
func (cv *ContainerValidation) validateHealthChecks(conf conf.Probes) {
if conf.Readiness.Require && cv.Container.ReadinessProbe == nil {
cv.addFailure("readiness", "probe needs to be configured", "nil")
}
if conf.Liveness.Require && cv.Container.LivenessProbe == nil {
cv.addFailure("liveness", "probe needs to be configured", "nil")
}
}
// if c.LivenessProbe == nil {
// results.AddFailure("Liveness Probe", "placeholder", "placeholder")
// }
// return results
// }
// func tag(conf conf.ResourceRequestsAndLimits, c corev1.Container, results types.ContainerResults) types.ContainerResults {
// img := strings.Split(c.Image, ":")
// if len(img) == 1 || img[1] == "latest" {
// results.AddFailure("Image Tag", "not latest", "latest")
// }
// return results
// }
func (cv *ContainerValidation) validateImage(conf conf.Images) {
if conf.TagRequired {
img := strings.Split(cv.Container.Image, ":")
if len(img) == 1 || img[1] == "latest" {
cv.addFailure("Image Tag", "not latest", "latest")
}
}
}
// func hostPort(conf conf.ResourceRequestsAndLimits, c corev1.Container, results types.ContainerResults) types.ContainerResults {
// for _, port := range c.Ports {

View File

@@ -165,3 +165,86 @@ func testValidateResources(t *testing.T, container *corev1.Container, resourceCo
assert.Len(t, cv.Failures, len(*expectedFailures))
assert.ElementsMatch(t, cv.Failures, *expectedFailures)
}
func TestValidateHealthChecks(t *testing.T) {
// Test setup.
p1 := conf.Probes{}
p2 := conf.Probes{
Readiness: conf.ResourceRequire{Require: false},
Liveness: conf.ResourceRequire{Require: false},
}
p3 := conf.Probes{
Readiness: conf.ResourceRequire{Require: true},
Liveness: conf.ResourceRequire{Require: true},
}
probe := corev1.Probe{}
cv1 := ContainerValidation{Container: corev1.Container{Name: ""}}
cv2 := ContainerValidation{Container: corev1.Container{Name: "", LivenessProbe: &probe, ReadinessProbe: &probe}}
l := types.Failure{Name: "liveness", Expected: "probe needs to be configured", Actual: "nil"}
r := types.Failure{Name: "readiness", Expected: "probe needs to be configured", Actual: "nil"}
f1 := []types.Failure{}
f2 := []types.Failure{r, l}
var testCases = []struct {
name string
probes conf.Probes
cv ContainerValidation
expected []types.Failure
}{
{name: "probes not configured", probes: p1, cv: cv1, expected: f1},
{name: "probes not required", probes: p2, cv: cv1, expected: f1},
{name: "probes required & configured", probes: p3, cv: cv2, expected: f1},
{name: "probes required & not configured", probes: p3, cv: cv1, expected: f2},
{name: "probes configured, but not required", probes: p2, cv: cv2, expected: f1},
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
tt.cv.validateHealthChecks(tt.probes)
assert.Len(t, tt.cv.Failures, len(tt.expected))
assert.ElementsMatch(t, tt.cv.Failures, tt.expected)
})
}
}
func TestValidateImage(t *testing.T) {
// Test setup.
i1 := conf.Images{}
i2 := conf.Images{TagRequired: false}
i3 := conf.Images{TagRequired: true}
cv1 := ContainerValidation{Container: corev1.Container{Name: ""}}
cv2 := ContainerValidation{Container: corev1.Container{Name: "", Image: "test:tag"}}
cv3 := ContainerValidation{Container: corev1.Container{Name: "", Image: "test:latest"}}
cv4 := ContainerValidation{Container: corev1.Container{Name: "", Image: "test"}}
f := types.Failure{Name: "Image Tag", Expected: "not latest", Actual: "latest"}
f1 := []types.Failure{}
f2 := []types.Failure{f}
var testCases = []struct {
name string
image conf.Images
cv ContainerValidation
expected []types.Failure
}{
{name: "image not configured", image: i1, cv: cv1, expected: f1},
{name: "image not required", image: i2, cv: cv1, expected: f1},
{name: "image tag required and configured", image: i3, cv: cv2, expected: f1},
{name: "image tag required, but not configured", image: i3, cv: cv1, expected: f2},
{name: "image tag required, but is latest", image: i3, cv: cv3, expected: f2},
{name: "image tag required, but is empty", image: i3, cv: cv4, expected: f2},
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
tt.cv.validateImage(tt.image)
assert.Len(t, tt.cv.Failures, len(tt.expected))
assert.ElementsMatch(t, tt.cv.Failures, tt.expected)
})
}
}