a lot of cleanup and restructuring

This commit is contained in:
Rob Scott
2019-03-27 22:57:01 -04:00
parent 4c6d546395
commit f5cde2db38
15 changed files with 338 additions and 289 deletions
+10 -10
View File
@@ -32,13 +32,13 @@ resources:
below: 100M
above: 4G
images:
tagNotSpecified: 'error'
pullPolicyNotAlways: 'warning'
error:
whitelist:
tagNotSpecified: error
pullPolicyNotAlways: warning
whitelist:
error:
- gcr.io/*
warning:
blacklist:
blacklist:
warning:
- docker.io/*
healthChecks:
readinessProbeMissing: warning
@@ -54,13 +54,13 @@ security:
notReadOnlyRootFileSystem: warning
runAsNonRoot: warning
capabilities:
warning:
blacklist:
blacklist:
error:
- CHOWN
- SYS_CHROOT
- AUDIT_WRITE
error:
whitelist:
whitelist:
warning:
- CHOWN
- DAC_OVERRIDE
- FSETID
+10 -10
View File
@@ -109,13 +109,13 @@ data:
below: 100M
above: 4G
images:
tagNotSpecified: 'error'
pullPolicyNotAlways: 'warning'
error:
whitelist:
tagNotSpecified: error
pullPolicyNotAlways: warning
whitelist:
error:
- gcr.io/*
warning:
blacklist:
blacklist:
warning:
- docker.io/*
healthChecks:
readinessProbeMissing: warning
@@ -131,13 +131,13 @@ data:
notReadOnlyRootFileSystem: warning
runAsNonRoot: warning
capabilities:
warning:
blacklist:
blacklist:
error:
- CHOWN
- SYS_CHROOT
- AUDIT_WRITE
error:
whitelist:
whitelist:
warning:
- CHOWN
- DAC_OVERRIDE
- FSETID
+10 -15
View File
@@ -65,21 +65,16 @@ type HealthChecks struct {
// Images contains the config for images.
type Images struct {
TagNotSpecified Severity `json:"tagNotSpecified"`
PullPolicyNotAlways Severity `json:"pullPolicyNotAlways"`
Repositories Repositories `json:"repositories"`
TagNotSpecified Severity `json:"tagNotSpecified"`
PullPolicyNotAlways Severity `json:"pullPolicyNotAlways"`
Whitelist ErrorWarningLists `json:"whitelist"`
Blacklist ErrorWarningLists `json:"blacklist"`
}
// Repositories provides lists of patterns to match or avoid in image tags.
type Repositories struct {
Error WhitelistBlacklist `json:"error"`
Warning WhitelistBlacklist `json:"warning"`
}
// WhitelistBlacklist can contain a whitelist or blacklist.
type WhitelistBlacklist struct {
Whitelist []string `json:"whitelist"`
Blacklist []string `json:"blacklist"`
// ErrorWarningLists provides lists of patterns to match or avoid in image tags.
type ErrorWarningLists struct {
Error []string `json:"error"`
Warning []string `json:"warning"`
}
// Networking contains the config for networking validations.
@@ -101,8 +96,8 @@ type Security struct {
// SecurityCapabilities contains the config for security capabilities validations.
type SecurityCapabilities struct {
Error WhitelistBlacklist `json:"error"`
Warning WhitelistBlacklist `json:"warning"`
Whitelist ErrorWarningLists `json:"whitelist"`
Blacklist ErrorWarningLists `json:"blacklist"`
}
// ParseFile parses config from a file.
+3
View File
@@ -18,6 +18,9 @@ package config
type Severity string
const (
// SeveritySuccess indicates validation success
SeveritySuccess Severity = "success"
// SeverityIgnore ignores validation failures
SeverityIgnore Severity = "ignore"
+3 -3
View File
@@ -83,12 +83,12 @@ func getTemplateData(config conf.Configuration, kubeAPI *kube.API) (TemplateData
return TemplateData{}, err
}
var clusterSuccesses, clusterFailures, clusterWarnings uint
var clusterSuccesses, clusterErrors, clusterWarnings uint
// Aggregate all summary counts to get a clusterwide count.
for _, nsRes := range nsResults {
for _, rr := range nsRes.Results {
clusterFailures += rr.Summary.Failures
clusterErrors += rr.Summary.Errors
clusterWarnings += rr.Summary.Warnings
clusterSuccesses += rr.Summary.Successes
}
@@ -96,7 +96,7 @@ func getTemplateData(config conf.Configuration, kubeAPI *kube.API) (TemplateData
templateData := TemplateData{
ClusterSummary: &validator.ResultSummary{
Failures: clusterFailures,
Errors: clusterErrors,
Warnings: clusterWarnings,
Successes: clusterSuccesses,
},
+1 -1
View File
@@ -23,7 +23,7 @@ func TestGetTemplateData(t *testing.T) {
sum := &validator.ResultSummary{
Successes: uint(4),
Warnings: uint(1),
Failures: uint(1),
Errors: uint(1),
}
actualTmplData, _ := getTemplateData(c, k8s)
+3 -3
View File
@@ -54,7 +54,7 @@
<tr>
<td class="resource-info">
<div class="name"><span class="caret-expander"></span>{{ .Type }}: <strong>{{ .Name }}</strong></div>
{{ range .PodResults}}
<div class="extra">
<h4>Pod:</h4>
@@ -126,7 +126,7 @@
label: 'Failing',
data: [
{{ range $namespace, $results := .NamespacedResults }}
"{{ $results.Summary.Failures }}",
"{{ $results.Summary.Errors }}",
{{ end }}
],
backgroundColor: '#a11f4c',
@@ -155,7 +155,7 @@
data: {
labels: ["Passing", "Warning", "Failing"],
datasets: [{
data: [{{ .ClusterSummary.Successes }}, {{ .ClusterSummary.Warnings }}, {{ .ClusterSummary.Failures }}],
data: [{{ .ClusterSummary.Successes }}, {{ .ClusterSummary.Warnings }}, {{ .ClusterSummary.Errors }}],
backgroundColor: ['#8BD2DC','#f26c21','#a11f4c'],
}]
},
+22 -62
View File
@@ -25,31 +25,23 @@ import (
// ContainerValidation tracks validation failures associated with a Container.
type ContainerValidation struct {
Container corev1.Container
Summary ResultSummary
Failures []ResultMessage
Warnings []ResultMessage
Successes []ResultMessage
*ResourceValidation
Container *corev1.Container
}
func (cv *ContainerValidation) messages() []ResultMessage {
mssgs := []ResultMessage{}
mssgs = append(mssgs, cv.Failures...)
mssgs = append(mssgs, cv.Warnings...)
mssgs = append(mssgs, cv.Successes...)
return mssgs
}
func validateContainer(conf conf.Configuration, container corev1.Container) ResourceResult {
// ValidateContainer validates that each pod conforms to the Fairwinds config, returns a ResourceResult.
func ValidateContainer(cnConf *conf.Configuration, container *corev1.Container) ResourceResult {
cv := ContainerValidation{
Container: container,
Summary: ResultSummary{},
ResourceValidation: &ResourceValidation{
Summary: &ResultSummary{},
},
}
cv.validateResources(&conf.Resources)
cv.validateHealthChecks(&conf.HealthChecks)
cv.validateImage(&conf.Images)
cv.validateNetworking(&conf.Networking)
cv.validateResources(&cnConf.Resources)
cv.validateHealthChecks(&cnConf.HealthChecks)
cv.validateImage(&cnConf.Images)
cv.validateNetworking(&cnConf.Networking)
cRes := ContainerResult{
Name: container.Name,
@@ -59,68 +51,36 @@ func validateContainer(conf conf.Configuration, container corev1.Container) Reso
rr := ResourceResult{
Name: container.Name,
Type: "Container",
Summary: &cv.Summary,
Summary: cv.Summary,
ContainerResults: []ContainerResult{cRes},
}
return rr
}
func (cv *ContainerValidation) addMessage(message string, severity conf.Severity) {
if severity == conf.SeverityError {
cv.addFailure(message)
} else if severity == conf.SeverityWarning {
cv.addWarning(message)
}
}
func (cv *ContainerValidation) addFailure(message string) {
cv.Summary.Failures++
cv.Failures = append(cv.Failures, ResultMessage{
Message: message,
Type: "failure",
})
}
func (cv *ContainerValidation) addWarning(message string) {
cv.Summary.Warnings++
cv.Warnings = append(cv.Warnings, ResultMessage{
Message: message,
Type: "warning",
})
}
func (cv *ContainerValidation) addSuccess(message string) {
cv.Summary.Successes++
cv.Successes = append(cv.Successes, ResultMessage{
Message: message,
Type: "success",
})
}
func (cv *ContainerValidation) validateResources(resConf *conf.Resources) {
res := cv.Container.Resources
if resConf.CPURequestsMissing.IsActionable() && res.Requests.Cpu().MilliValue() == 0 {
cv.addMessage("CPU Requests are not set", resConf.CPURequestsMissing)
cv.addFailure("CPU Requests are not set", resConf.CPURequestsMissing)
} else {
cv.validateResourceRange("CPU Requests", &resConf.CPURequestRanges, res.Requests.Cpu())
}
if resConf.CPULimitsMissing.IsActionable() && res.Limits.Cpu().MilliValue() == 0 {
cv.addMessage("CPU Limits are not set", resConf.CPULimitsMissing)
cv.addFailure("CPU Limits are not set", resConf.CPULimitsMissing)
} else {
cv.validateResourceRange("CPU Limits", &resConf.CPULimitRanges, res.Requests.Cpu())
}
if resConf.MemoryRequestsMissing.IsActionable() && res.Requests.Memory().MilliValue() == 0 {
cv.addMessage("Memory Requests are not set", resConf.MemoryRequestsMissing)
cv.addFailure("Memory Requests are not set", resConf.MemoryRequestsMissing)
} else {
cv.validateResourceRange("Memory Requests", &resConf.MemoryRequestRanges, res.Requests.Memory())
}
if resConf.MemoryLimitsMissing.IsActionable() && res.Limits.Memory().MilliValue() == 0 {
cv.addMessage("Memory Limits are not set", resConf.MemoryLimitsMissing)
cv.addFailure("Memory Limits are not set", resConf.MemoryLimitsMissing)
} else {
cv.validateResourceRange("Memory Limits", &resConf.MemoryLimitRanges, res.Limits.Memory())
}
@@ -133,11 +93,11 @@ func (cv *ContainerValidation) validateResourceRange(resourceName string, rangeC
errorBelow := rangeConf.Error.Below
if errorAbove != nil && errorAbove.MilliValue() < res.MilliValue() {
cv.addFailure(fmt.Sprintf("%s are too high", resourceName))
cv.addError(fmt.Sprintf("%s are too high", resourceName))
} else if warnAbove != nil && warnAbove.MilliValue() < res.MilliValue() {
cv.addWarning(fmt.Sprintf("%s are too high", resourceName))
} else if errorBelow != nil && errorBelow.MilliValue() > res.MilliValue() {
cv.addFailure(fmt.Sprintf("%s are too low", resourceName))
cv.addError(fmt.Sprintf("%s are too low", resourceName))
} else if warnBelow != nil && warnBelow.MilliValue() > res.MilliValue() {
cv.addWarning(fmt.Sprintf("%s are too low", resourceName))
} else {
@@ -148,7 +108,7 @@ func (cv *ContainerValidation) validateResourceRange(resourceName string, rangeC
func (cv *ContainerValidation) validateHealthChecks(conf *conf.HealthChecks) {
if conf.ReadinessProbeMissing.IsActionable() {
if cv.Container.ReadinessProbe == nil {
cv.addMessage("Readiness probe needs to be configured", conf.ReadinessProbeMissing)
cv.addFailure("Readiness probe needs to be configured", conf.ReadinessProbeMissing)
} else {
cv.addSuccess("Readiness probe configured")
}
@@ -156,7 +116,7 @@ func (cv *ContainerValidation) validateHealthChecks(conf *conf.HealthChecks) {
if conf.LivenessProbeMissing.IsActionable() {
if cv.Container.LivenessProbe == nil {
cv.addMessage("Liveness probe needs to be configured", conf.LivenessProbeMissing)
cv.addFailure("Liveness probe needs to be configured", conf.LivenessProbeMissing)
} else {
cv.addSuccess("Liveness probe configured")
}
@@ -167,7 +127,7 @@ func (cv *ContainerValidation) validateImage(imageConf *conf.Images) {
if imageConf.TagNotSpecified.IsActionable() {
img := strings.Split(cv.Container.Image, ":")
if len(img) == 1 || img[1] == "latest" {
cv.addMessage("Image tag should be specified", imageConf.TagNotSpecified)
cv.addFailure("Image tag should be specified", imageConf.TagNotSpecified)
} else {
cv.addSuccess("Image tag specified")
}
@@ -185,7 +145,7 @@ func (cv *ContainerValidation) validateNetworking(networkConf *conf.Networking)
}
if hostPortSet {
cv.addMessage("Host port is configured, but it shouldn't be", networkConf.HostAliasSet)
cv.addFailure("Host port is configured, but it shouldn't be", networkConf.HostAliasSet)
} else {
cv.addSuccess("Host port is not configured")
}
+86 -43
View File
@@ -69,13 +69,16 @@ func TestValidateResourcesEmptyConfig(t *testing.T) {
}
cv := ContainerValidation{
Container: container,
Container: &container,
ResourceValidation: &ResourceValidation{
Summary: &ResultSummary{},
},
}
expected := conf.Resources{}
cv.validateResources(&expected)
assert.Len(t, cv.Failures, 0)
assert.Len(t, cv.Errors, 0)
}
func TestValidateResourcesEmptyContainer(t *testing.T) {
@@ -83,7 +86,7 @@ func TestValidateResourcesEmptyContainer(t *testing.T) {
Name: "Empty",
}
expectedWarnings := []ResultMessage{
expectedWarnings := []*ResultMessage{
{
Type: "warning",
Message: "CPU Requests are not set",
@@ -94,18 +97,18 @@ func TestValidateResourcesEmptyContainer(t *testing.T) {
},
}
expectedFailures := []ResultMessage{
expectedErrors := []*ResultMessage{
{
Type: "failure",
Type: "error",
Message: "CPU Limits are not set",
},
{
Type: "failure",
Type: "error",
Message: "Memory Limits are not set",
},
}
testValidateResources(t, &container, &resourceConf2, &expectedFailures, &expectedWarnings)
testValidateResources(t, &container, &resourceConf2, &expectedErrors, &expectedWarnings)
}
func TestValidateResourcesPartiallyValid(t *testing.T) {
@@ -127,7 +130,7 @@ func TestValidateResourcesPartiallyValid(t *testing.T) {
},
}
expectedWarnings := []ResultMessage{
expectedWarnings := []*ResultMessage{
{
Type: "warning",
Message: "CPU Requests are too low",
@@ -138,18 +141,18 @@ func TestValidateResourcesPartiallyValid(t *testing.T) {
},
}
expectedFailures := []ResultMessage{
expectedErrors := []*ResultMessage{
{
Type: "failure",
Type: "error",
Message: "Memory Requests are too low",
},
{
Type: "failure",
Type: "error",
Message: "Memory Limits are too low",
},
}
testValidateResources(t, &container, &resourceConf1, &expectedFailures, &expectedWarnings)
testValidateResources(t, &container, &resourceConf1, &expectedErrors, &expectedWarnings)
}
func TestValidateResourcesFullyValid(t *testing.T) {
@@ -179,12 +182,15 @@ func TestValidateResourcesFullyValid(t *testing.T) {
},
}
testValidateResources(t, &container, &resourceConf1, &[]ResultMessage{}, &[]ResultMessage{})
testValidateResources(t, &container, &resourceConf1, &[]*ResultMessage{}, &[]*ResultMessage{})
}
func testValidateResources(t *testing.T, container *corev1.Container, resourceConf *string, expectedFailures *[]ResultMessage, expectedWarnings *[]ResultMessage) {
func testValidateResources(t *testing.T, container *corev1.Container, resourceConf *string, expectedErrors *[]*ResultMessage, expectedWarnings *[]*ResultMessage) {
cv := ContainerValidation{
Container: *container,
Container: container,
ResourceValidation: &ResourceValidation{
Summary: &ResultSummary{},
},
}
parsedConf, err := conf.Parse([]byte(*resourceConf))
@@ -194,8 +200,8 @@ func testValidateResources(t *testing.T, container *corev1.Container, resourceCo
assert.Len(t, cv.Warnings, len(*expectedWarnings))
assert.ElementsMatch(t, cv.Warnings, *expectedWarnings)
assert.Len(t, cv.Failures, len(*expectedFailures))
assert.ElementsMatch(t, cv.Failures, *expectedFailures)
assert.Len(t, cv.Errors, len(*expectedErrors))
assert.ElementsMatch(t, cv.Errors, *expectedErrors)
}
func TestValidateHealthChecks(t *testing.T) {
@@ -212,27 +218,41 @@ func TestValidateHealthChecks(t *testing.T) {
}
probe := corev1.Probe{}
cv1 := ContainerValidation{Container: corev1.Container{Name: ""}}
cv2 := ContainerValidation{Container: corev1.Container{Name: "", LivenessProbe: &probe, ReadinessProbe: &probe}}
cv1 := ContainerValidation{
Container: &corev1.Container{Name: ""},
ResourceValidation: &ResourceValidation{
Summary: &ResultSummary{},
},
}
cv2 := ContainerValidation{
Container: &corev1.Container{
Name: "",
LivenessProbe: &probe,
ReadinessProbe: &probe,
},
ResourceValidation: &ResourceValidation{
Summary: &ResultSummary{},
},
}
l := ResultMessage{Type: "warning", Message: "Liveness probe needs to be configured"}
r := ResultMessage{Type: "failure", Message: "Readiness probe needs to be configured"}
f1 := []ResultMessage{}
f2 := []ResultMessage{r}
w1 := []ResultMessage{l}
l := &ResultMessage{Type: "warning", Message: "Liveness probe needs to be configured"}
r := &ResultMessage{Type: "error", Message: "Readiness probe needs to be configured"}
f1 := []*ResultMessage{}
f2 := []*ResultMessage{r}
w1 := []*ResultMessage{l}
var testCases = []struct {
name string
probes conf.HealthChecks
cv ContainerValidation
failures *[]ResultMessage
warnings *[]ResultMessage
errors *[]*ResultMessage
warnings *[]*ResultMessage
}{
{name: "probes not configured", probes: p1, cv: cv1, failures: &f1},
{name: "probes not required", probes: p2, cv: cv1, failures: &f1},
{name: "probes required & configured", probes: p3, cv: cv2, failures: &f1},
{name: "probes required & not configured", probes: p3, cv: cv1, failures: &f2, warnings: &w1},
{name: "probes configured, but not required", probes: p2, cv: cv2, failures: &f1},
{name: "probes not configured", probes: p1, cv: cv1, errors: &f1},
{name: "probes not required", probes: p2, cv: cv1, errors: &f1},
{name: "probes required & configured", probes: p3, cv: cv2, errors: &f1},
{name: "probes required & not configured", probes: p3, cv: cv1, errors: &f2, warnings: &w1},
{name: "probes configured, but not required", probes: p2, cv: cv2, errors: &f1},
}
for _, tt := range testCases {
@@ -244,8 +264,8 @@ func TestValidateHealthChecks(t *testing.T) {
assert.ElementsMatch(t, tt.cv.Warnings, *tt.warnings)
}
assert.Len(t, tt.cv.Failures, len(*tt.failures))
assert.ElementsMatch(t, tt.cv.Failures, *tt.failures)
assert.Len(t, tt.cv.Errors, len(*tt.errors))
assert.ElementsMatch(t, tt.cv.Errors, *tt.errors)
})
}
}
@@ -257,20 +277,43 @@ func TestValidateImage(t *testing.T) {
i2 := conf.Images{TagNotSpecified: conf.SeverityIgnore}
i3 := conf.Images{TagNotSpecified: conf.SeverityError}
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"}}
cv1 := ContainerValidation{
Container: &corev1.Container{Name: ""},
ResourceValidation: &ResourceValidation{
Summary: &ResultSummary{},
},
}
f := ResultMessage{Message: "Image tag should be specified", Type: "failure"}
f1 := []ResultMessage{}
f2 := []ResultMessage{f}
cv2 := ContainerValidation{
Container: &corev1.Container{Name: "", Image: "test:tag"},
ResourceValidation: &ResourceValidation{
Summary: &ResultSummary{},
},
}
cv3 := ContainerValidation{
Container: &corev1.Container{Name: "", Image: "test:latest"},
ResourceValidation: &ResourceValidation{
Summary: &ResultSummary{},
},
}
cv4 := ContainerValidation{
Container: &corev1.Container{Name: "", Image: "test"},
ResourceValidation: &ResourceValidation{
Summary: &ResultSummary{},
},
}
f := &ResultMessage{Message: "Image tag should be specified", Type: "error"}
f1 := []*ResultMessage{}
f2 := []*ResultMessage{f}
var testCases = []struct {
name string
image conf.Images
cv ContainerValidation
expected []ResultMessage
expected []*ResultMessage
}{
{name: "image not configured", image: i1, cv: cv1, expected: f1},
{name: "image not required", image: i2, cv: cv1, expected: f1},
@@ -283,8 +326,8 @@ func TestValidateImage(t *testing.T) {
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)
assert.Len(t, tt.cv.Errors, len(tt.expected))
assert.ElementsMatch(t, tt.cv.Errors, tt.expected)
})
}
}
+1 -62
View File
@@ -15,72 +15,11 @@
package validator
import (
"context"
"net/http"
conf "github.com/reactiveops/fairwinds/pkg/config"
"github.com/reactiveops/fairwinds/pkg/kube"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/runtime/inject"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission/types"
)
// Validator validates k8s resources.
type Validator struct {
client client.Client
decoder types.Decoder
Config conf.Configuration
}
var _ inject.Client = &Validator{}
// InjectClient injects the client.
func (v *Validator) InjectClient(c client.Client) error {
v.client = c
return nil
}
var _ inject.Decoder = &Validator{}
// InjectDecoder injects the decoder.
func (v *Validator) InjectDecoder(d types.Decoder) error {
v.decoder = d
return nil
}
var _ admission.Handler = &Validator{}
// Handle for Validator to run validation checks.
func (v *Validator) Handle(ctx context.Context, req types.Request) types.Response {
var err error
var allowed bool
var reason string
var results ResourceResult
switch req.AdmissionRequest.Kind.Kind {
case "Deployment":
deploy := appsv1.Deployment{}
err = v.decoder.Decode(req, &deploy)
results = ValidateDeploy(v.Config, &deploy)
case "Pod":
pod := corev1.Pod{}
err = v.decoder.Decode(req, &pod)
results = ValidatePod(v.Config, &pod.Spec)
}
if err != nil {
return admission.ErrorResponse(http.StatusBadRequest, err)
}
if results.Summary.Failures > 0 {
// TODO: Decide what message we want to return here.
allowed, reason = false, "failed validation checks, view details on dashbaord."
}
return admission.ValidationResponse(allowed, reason)
}
// ValidateDeploy validates a single deployment, returns a ResourceResult.
func ValidateDeploy(conf conf.Configuration, deploy *appsv1.Deployment) ResourceResult {
pod := deploy.Spec.Template.Spec
@@ -128,6 +67,6 @@ func addResult(resResult ResourceResult, nsResults NamespacedResults, nsName str
// Aggregate all resource results summary counts to get a namespace wide count.
nsResult.Summary.Successes += resResult.Summary.Successes
nsResult.Summary.Warnings += resResult.Summary.Warnings
nsResult.Summary.Failures += resResult.Summary.Successes
nsResult.Summary.Errors += resResult.Summary.Successes
return nsResults
}
+23 -70
View File
@@ -24,26 +24,17 @@ var log = logf.Log.WithName("Fairwinds Validator")
// PodValidation tracks validation failures associated with a Pod.
type PodValidation struct {
Pod corev1.PodSpec
Summary ResultSummary
Failures []ResultMessage
Warnings []ResultMessage
Successes []ResultMessage
}
func (pv *PodValidation) messages() []ResultMessage {
messages := []ResultMessage{}
messages = append(messages, pv.Failures...)
messages = append(messages, pv.Warnings...)
messages = append(messages, pv.Successes...)
return messages
*ResourceValidation
Pod *corev1.PodSpec
}
// ValidatePod validates that each pod conforms to the Fairwinds config, returns a ResourceResult.
func ValidatePod(podConf conf.Configuration, pod *corev1.PodSpec) ResourceResult {
pv := PodValidation{
Pod: *pod,
Summary: ResultSummary{},
Pod: pod,
ResourceValidation: &ResourceValidation{
Summary: &ResultSummary{},
},
}
pv.validateNetworking(&podConf.Networking)
@@ -53,69 +44,31 @@ func ValidatePod(podConf conf.Configuration, pod *corev1.PodSpec) ResourceResult
ContainerResults: []ContainerResult{},
}
// Add container resource results to the pod resource results.
for _, container := range pod.InitContainers {
ctrRR := validateContainer(podConf, container)
pv.Summary.Successes += ctrRR.Summary.Successes
pv.Summary.Warnings += ctrRR.Summary.Warnings
pv.Summary.Failures += ctrRR.Summary.Failures
pRes.ContainerResults = append(
pRes.ContainerResults,
ctrRR.ContainerResults[0],
)
}
pv.validateContainers(pod.InitContainers, &pRes, &podConf)
pv.validateContainers(pod.Containers, &pRes, &podConf)
for _, container := range pod.Containers {
ctrRR := validateContainer(podConf, container)
pv.Summary.Successes += ctrRR.Summary.Successes
pv.Summary.Warnings += ctrRR.Summary.Warnings
pv.Summary.Failures += ctrRR.Summary.Failures
pRes.ContainerResults = append(
pRes.ContainerResults,
ctrRR.ContainerResults[0],
)
}
rr := ResourceResult{
Type: "Pod",
Summary: &pv.Summary,
Summary: pv.Summary,
PodResults: []PodResult{pRes},
}
return rr
}
func (pv *PodValidation) addMessage(message string, severity conf.Severity) {
if severity == conf.SeverityError {
pv.addFailure(message)
} else if severity == conf.SeverityWarning {
pv.addWarning(message)
func (pv *PodValidation) validateContainers(containers []corev1.Container, pRes *PodResult, podConf *conf.Configuration) {
for _, container := range containers {
ctrRR := ValidateContainer(podConf, &container)
pv.Summary.Successes += ctrRR.Summary.Successes
pv.Summary.Warnings += ctrRR.Summary.Warnings
pv.Summary.Errors += ctrRR.Summary.Errors
pRes.ContainerResults = append(
pRes.ContainerResults,
ctrRR.ContainerResults[0],
)
}
}
func (pv *PodValidation) addFailure(message string) {
pv.Summary.Failures++
pv.Failures = append(pv.Failures, ResultMessage{
Message: message,
Type: "failure",
})
}
func (pv *PodValidation) addWarning(message string) {
pv.Summary.Warnings++
pv.Warnings = append(pv.Warnings, ResultMessage{
Message: message,
Type: "warning",
})
}
func (pv *PodValidation) addSuccess(message string) {
pv.Summary.Successes++
pv.Successes = append(pv.Successes, ResultMessage{
Message: message,
Type: "success",
})
}
func (pv *PodValidation) validateNetworking(networkConf *conf.Networking) {
pv.validateHostAlias(networkConf)
pv.validateHostIPC(networkConf)
@@ -134,7 +87,7 @@ func (pv *PodValidation) validateHostAlias(networkConf *conf.Networking) {
}
if hostAliasSet {
pv.addMessage("Host alias should is configured, but it shouldn't be", networkConf.HostAliasSet)
pv.addFailure("Host alias should is configured, but it shouldn't be", networkConf.HostAliasSet)
} else {
pv.addSuccess("Host alias is not configured")
}
@@ -144,7 +97,7 @@ func (pv *PodValidation) validateHostAlias(networkConf *conf.Networking) {
func (pv *PodValidation) validateHostIPC(networkConf *conf.Networking) {
if networkConf.HostIPCSet.IsActionable() {
if pv.Pod.HostIPC {
pv.addMessage("Host IPC is configured, but it shouldn't be", networkConf.HostIPCSet)
pv.addFailure("Host IPC is configured, but it shouldn't be", networkConf.HostIPCSet)
} else {
pv.addSuccess("Host IPC is not configured")
}
@@ -154,7 +107,7 @@ func (pv *PodValidation) validateHostIPC(networkConf *conf.Networking) {
func (pv *PodValidation) validateHostPID(networkConf *conf.Networking) {
if networkConf.HostPIDSet.IsActionable() {
if pv.Pod.HostPID {
pv.addMessage("Host PID is configured, but it shouldn't be", networkConf.HostPIDSet)
pv.addFailure("Host PID is configured, but it shouldn't be", networkConf.HostPIDSet)
} else {
pv.addSuccess("Host PID is not configured")
}
@@ -164,7 +117,7 @@ func (pv *PodValidation) validateHostPID(networkConf *conf.Networking) {
func (pv *PodValidation) validateHostNetwork(networkConf *conf.Networking) {
if networkConf.HostNetworkSet.IsActionable() {
if pv.Pod.HostNetwork {
pv.addMessage("Host network is configured, but it shouldn't be", networkConf.HostNetworkSet)
pv.addFailure("Host network is configured, but it shouldn't be", networkConf.HostNetworkSet)
} else {
pv.addSuccess("Host network is not configured")
}
+2 -2
View File
@@ -40,10 +40,10 @@ func TestValidatePod(t *testing.T) {
expectedSum := ResultSummary{
Successes: uint(9),
Warnings: uint(0),
Failures: uint(0),
Errors: uint(0),
}
expectedMessages := []ResultMessage{
expectedMessages := []*ResultMessage{
{Message: "Host alias is not configured", Type: "success"},
{Message: "Host IPC is not configured", Type: "success"},
{Message: "Host PID is not configured", Type: "success"},
+72
View File
@@ -0,0 +1,72 @@
// Copyright 2019 ReactiveOps
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package validator
import (
"errors"
"fmt"
conf "github.com/reactiveops/fairwinds/pkg/config"
)
// ResourceValidation contains methods shared by PodValidation and ContainerValidation
type ResourceValidation struct {
Summary *ResultSummary
Errors []*ResultMessage
Warnings []*ResultMessage
Successes []*ResultMessage
}
func (rv *ResourceValidation) messages() []*ResultMessage {
messages := []*ResultMessage{}
messages = append(messages, rv.Errors...)
messages = append(messages, rv.Warnings...)
messages = append(messages, rv.Successes...)
return messages
}
func (rv *ResourceValidation) addFailure(message string, severity conf.Severity) {
if severity == conf.SeverityError {
rv.addError(message)
} else if severity == conf.SeverityWarning {
rv.addWarning(message)
} else {
errMsg := fmt.Sprintf("Invalid severity: %s", severity)
log.Error(errors.New(errMsg), errMsg)
}
}
func (rv *ResourceValidation) addError(message string) {
rv.Summary.Errors++
rv.Errors = append(rv.Errors, &ResultMessage{
Message: message,
Type: conf.SeverityError,
})
}
func (rv *ResourceValidation) addWarning(message string) {
rv.Summary.Warnings++
rv.Warnings = append(rv.Warnings, &ResultMessage{
Message: message,
Type: conf.SeverityWarning,
})
}
func (rv *ResourceValidation) addSuccess(message string) {
rv.Summary.Successes++
rv.Successes = append(rv.Successes, &ResultMessage{
Message: message,
Type: conf.SeveritySuccess,
})
}
+11 -8
View File
@@ -14,6 +14,10 @@
package validator
import (
conf "github.com/reactiveops/fairwinds/pkg/config"
)
// NamespacedResult groups resource results by namespace.
type NamespacedResult struct {
Summary *ResultSummary
@@ -32,34 +36,33 @@ type ResourceResult struct {
PodResults []PodResult
}
// ResultSummary provides a high level overview of success, warnings, and failures.
// ResultSummary provides a high level overview of success, warnings, and errors.
type ResultSummary struct {
Successes uint
Warnings uint
Failures uint
Errors uint
}
// ContainerResult provides a list of validation messages for each container.
type ContainerResult struct {
Name string
Messages []ResultMessage
Messages []*ResultMessage
}
// PodResult provides a list of validation messages for each pod.
type PodResult struct {
Name string
Messages []ResultMessage
Messages []*ResultMessage
ContainerResults []ContainerResult
}
// ResultMessage contains a message and a type indicator (success, warning, or failure).
// ResultMessage contains a message and a type indicator (success, warning, or error).
type ResultMessage struct {
Message string
Type string
Type conf.Severity
}
// Score represents a percentage of validations that were successful.
func (rs *ResultSummary) Score() uint {
return uint(float64(rs.Successes) / float64(rs.Successes+rs.Warnings+rs.Failures) * 100)
return uint(float64(rs.Successes) / float64(rs.Successes+rs.Warnings+rs.Errors) * 100)
}
+81
View File
@@ -0,0 +1,81 @@
// Copyright 2019 ReactiveOps
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package validator
import (
"context"
"net/http"
conf "github.com/reactiveops/fairwinds/pkg/config"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/runtime/inject"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission/types"
)
// Validator validates k8s resources.
type Validator struct {
client client.Client
decoder types.Decoder
Config conf.Configuration
}
var _ inject.Client = &Validator{}
// InjectClient injects the client.
func (v *Validator) InjectClient(c client.Client) error {
v.client = c
return nil
}
var _ inject.Decoder = &Validator{}
// InjectDecoder injects the decoder.
func (v *Validator) InjectDecoder(d types.Decoder) error {
v.decoder = d
return nil
}
var _ admission.Handler = &Validator{}
// Handle for Validator to run validation checks.
func (v *Validator) Handle(ctx context.Context, req types.Request) types.Response {
var err error
var allowed bool
var reason string
var results ResourceResult
switch req.AdmissionRequest.Kind.Kind {
case "Deployment":
deploy := appsv1.Deployment{}
err = v.decoder.Decode(req, &deploy)
results = ValidateDeploy(v.Config, &deploy)
case "Pod":
pod := corev1.Pod{}
err = v.decoder.Decode(req, &pod)
results = ValidatePod(v.Config, &pod.Spec)
}
if err != nil {
return admission.ErrorResponse(http.StatusBadRequest, err)
}
if results.Summary.Errors > 0 {
// TODO: Decide what message we want to return here.
allowed, reason = false, "failed validation checks, view details on dashbaord."
}
return admission.ValidationResponse(allowed, reason)
}