diff --git a/config.yml b/config.yml index dc1bcc40..823c0088 100644 --- a/config.yml +++ b/config.yml @@ -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 diff --git a/deploy/all.yaml b/deploy/all.yaml index 34f51e70..3e02ad3e 100644 --- a/deploy/all.yaml +++ b/deploy/all.yaml @@ -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 diff --git a/pkg/config/config.go b/pkg/config/config.go index 11ab1109..8103cceb 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -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. diff --git a/pkg/config/severity.go b/pkg/config/severity.go index ca61f253..2cb26c28 100644 --- a/pkg/config/severity.go +++ b/pkg/config/severity.go @@ -18,6 +18,9 @@ package config type Severity string const ( + // SeveritySuccess indicates validation success + SeveritySuccess Severity = "success" + // SeverityIgnore ignores validation failures SeverityIgnore Severity = "ignore" diff --git a/pkg/dashboard/dashboard.go b/pkg/dashboard/dashboard.go index 2a0c3627..bbad69ed 100644 --- a/pkg/dashboard/dashboard.go +++ b/pkg/dashboard/dashboard.go @@ -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, }, diff --git a/pkg/dashboard/dashboard_test.go b/pkg/dashboard/dashboard_test.go index 59d4a2b3..294db6a1 100644 --- a/pkg/dashboard/dashboard_test.go +++ b/pkg/dashboard/dashboard_test.go @@ -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) diff --git a/pkg/dashboard/templates/dashboard.gohtml b/pkg/dashboard/templates/dashboard.gohtml index 59703d89..f5d8c038 100644 --- a/pkg/dashboard/templates/dashboard.gohtml +++ b/pkg/dashboard/templates/dashboard.gohtml @@ -54,7 +54,7 @@
{{ .Type }}: {{ .Name }}
- + {{ range .PodResults}}

Pod:

@@ -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'], }] }, diff --git a/pkg/validator/container.go b/pkg/validator/container.go index 75d15511..b76442d6 100644 --- a/pkg/validator/container.go +++ b/pkg/validator/container.go @@ -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") } diff --git a/pkg/validator/container_test.go b/pkg/validator/container_test.go index ec4a41ed..c643a8af 100644 --- a/pkg/validator/container_test.go +++ b/pkg/validator/container_test.go @@ -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) }) } } diff --git a/pkg/validator/deploy.go b/pkg/validator/deploy.go index 86ee853f..3aded4ee 100644 --- a/pkg/validator/deploy.go +++ b/pkg/validator/deploy.go @@ -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 } diff --git a/pkg/validator/pod.go b/pkg/validator/pod.go index 97fcb277..3595fb9b 100644 --- a/pkg/validator/pod.go +++ b/pkg/validator/pod.go @@ -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") } diff --git a/pkg/validator/pod_test.go b/pkg/validator/pod_test.go index d44a91e2..ac4468bc 100644 --- a/pkg/validator/pod_test.go +++ b/pkg/validator/pod_test.go @@ -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"}, diff --git a/pkg/validator/resource.go b/pkg/validator/resource.go new file mode 100644 index 00000000..42c1a2be --- /dev/null +++ b/pkg/validator/resource.go @@ -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, + }) +} diff --git a/pkg/validator/types.go b/pkg/validator/types.go index 51e53e5f..b0a015c2 100644 --- a/pkg/validator/types.go +++ b/pkg/validator/types.go @@ -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) } - diff --git a/pkg/validator/validator.go b/pkg/validator/validator.go new file mode 100644 index 00000000..99cd8680 --- /dev/null +++ b/pkg/validator/validator.go @@ -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) +}