From 2770be643ff8a9cee4c96ce168475ab490aa3733 Mon Sep 17 00:00:00 2001 From: Robert Brennan Date: Fri, 3 Jan 2020 17:17:05 +0000 Subject: [PATCH] Refactor validation --- main.go | 11 +- pkg/dashboard/dashboard.go | 17 +- pkg/dashboard/helpers.go | 4 - pkg/validator/container.go | 48 +- pkg/validator/container_test.go | 757 +++++++++++++++---------------- pkg/validator/controller.go | 14 +- pkg/validator/controller_test.go | 100 ++-- pkg/validator/fullaudit.go | 57 ++- pkg/validator/fullaudit_test.go | 45 +- pkg/validator/output.go | 183 ++++++++ pkg/validator/pod.go | 38 +- pkg/validator/pod_test.go | 144 ++---- pkg/validator/resource.go | 109 ----- pkg/validator/schema.go | 101 +++-- pkg/validator/schema_test.go | 58 +-- pkg/validator/types.go | 270 ----------- pkg/webhook/validator.go | 13 +- 17 files changed, 835 insertions(+), 1134 deletions(-) create mode 100644 pkg/validator/output.go delete mode 100644 pkg/validator/resource.go delete mode 100644 pkg/validator/types.go diff --git a/main.go b/main.go index a2d9ada4..be2c1e0c 100644 --- a/main.go +++ b/main.go @@ -107,11 +107,12 @@ func main() { } else if *audit { auditData := runAndReportAudit(c, *auditPath, *auditOutputFile, *auditOutputURL, *auditOutputFormat) - if *setExitCode && auditData.ClusterSummary.Results.Totals.Errors > 0 { - logrus.Infof("%d errors found in audit", auditData.ClusterSummary.Results.Totals.Errors) + numErrors := auditData.GetSummary().Errors + if *setExitCode && numErrors > 0 { + logrus.Infof("%d errors found in audit", numErrors) os.Exit(3) - } else if *minScore != 0 && auditData.ClusterSummary.Score < uint(*minScore) { - logrus.Infof("Audit score of %d is less than the provided minimum of %d", auditData.ClusterSummary.Score, *minScore) + } else if *minScore != 0 && auditData.GetSummary().GetScore() < uint(*minScore) { + logrus.Infof("Audit score of %d is less than the provided minimum of %d", auditData.GetSummary().GetScore(), *minScore) os.Exit(4) } } @@ -228,7 +229,7 @@ func runAndReportAudit(c conf.Configuration, auditPath string, outputFile string var outputBytes []byte if outputFormat == "score" { - outputBytes = []byte(fmt.Sprintf("%d\n", auditData.ClusterSummary.Score)) + outputBytes = []byte(fmt.Sprintf("%d\n", auditData.GetSummary().GetScore())) } else if outputFormat == "yaml" { jsonBytes, err := json.Marshal(auditData) if err == nil { diff --git a/pkg/dashboard/dashboard.go b/pkg/dashboard/dashboard.go index 54b574f8..dc2a0bb0 100644 --- a/pkg/dashboard/dashboard.go +++ b/pkg/dashboard/dashboard.go @@ -90,15 +90,14 @@ type templateData struct { // GetBaseTemplate puts together the dashboard template. Individual pieces can be overridden before rendering. func GetBaseTemplate(name string) (*template.Template, error) { tmpl := template.New(name).Funcs(template.FuncMap{ - "getWarningWidth": getWarningWidth, - "getSuccessWidth": getSuccessWidth, - "getWeatherIcon": getWeatherIcon, - "getWeatherText": getWeatherText, - "getGrade": getGrade, - "getIcon": getIcon, - "getCategoryLink": getCategoryLink, - "getCategoryInfo": getCategoryInfo, - "getAllControllerResults": getAllControllerResults, + "getWarningWidth": getWarningWidth, + "getSuccessWidth": getSuccessWidth, + "getWeatherIcon": getWeatherIcon, + "getWeatherText": getWeatherText, + "getGrade": getGrade, + "getIcon": getIcon, + "getCategoryLink": getCategoryLink, + "getCategoryInfo": getCategoryInfo, }) templateFileNames := []string{ diff --git a/pkg/dashboard/helpers.go b/pkg/dashboard/helpers.go index 961bf2c1..be82c684 100644 --- a/pkg/dashboard/helpers.go +++ b/pkg/dashboard/helpers.go @@ -21,10 +21,6 @@ import ( "github.com/fairwindsops/polaris/pkg/validator" ) -func getAllControllerResults(nr validator.NamespaceResult) []validator.ControllerResult { - return nr.GetAllControllerResults() -} - func getWarningWidth(counts validator.CountSummary, fullWidth int) uint { return uint(float64(counts.Successes+counts.Warnings) / float64(counts.Successes+counts.Warnings+counts.Errors) * float64(fullWidth)) } diff --git a/pkg/validator/container.go b/pkg/validator/container.go index 0926cf71..0d6d3a56 100644 --- a/pkg/validator/container.go +++ b/pkg/validator/container.go @@ -19,40 +19,8 @@ import ( corev1 "k8s.io/api/core/v1" ) -// ContainerValidation tracks validation failures associated with a Container. -type ContainerValidation struct { - *ResourceValidation - Container *corev1.Container - IsInitContainer bool - parentPodSpec corev1.PodSpec -} - -// ValidateContainer validates that each pod conforms to the Polaris config, returns a ResourceResult. -// FIXME When validating a container, there are some things in a container spec -// that can be affected by the podSpec. This means we need a copy of the -// relevant podSpec in order to check certain aspects of a containerSpec. -// Perhaps there is a more ideal solution instead of attaching a parent -// podSpec to every container Validation struct... -func ValidateContainer(container *corev1.Container, parentPodResult *PodResult, conf *config.Configuration, controllerName string, controllerType config.SupportedController, isInit bool) ContainerResult { - cv := ContainerValidation{ - Container: container, - ResourceValidation: &ResourceValidation{}, - IsInitContainer: isInit, - } - - // Support initializing - // FIXME This is a product of pulling in the podSpec, ideally we'd never - // expect this be nil but our tests have conditions in which the - // parent podResult isn't initialized in this ContainerValidation - // struct. - if parentPodResult == nil { - // initialize a blank pod spec - cv.parentPodSpec = corev1.PodSpec{} - } else { - cv.parentPodSpec = parentPodResult.podSpec - } - - err := applyContainerSchemaChecks(conf, controllerName, controllerType, &cv) +func ValidateContainer(conf *config.Configuration, basePod *corev1.PodSpec, container *corev1.Container, controllerName string, controllerType config.SupportedController, isInit bool) ContainerResult { + results, err := applyContainerSchemaChecks(conf, basePod, container, controllerName, controllerType, isInit) // FIXME: don't panic if err != nil { panic(err) @@ -60,9 +28,17 @@ func ValidateContainer(container *corev1.Container, parentPodResult *PodResult, cRes := ContainerResult{ Name: container.Name, - Messages: cv.messages(), - Summary: cv.summary(), + Messages: results, } return cRes } + +func ValidateContainers(conf *config.Configuration, basePod *corev1.PodSpec, containers []corev1.Container, controllerName string, controllerType config.SupportedController, isInit bool) []ContainerResult { + results := []ContainerResult{} + for _, container := range containers { + cRes := ValidateContainer(conf, basePod, &container, controllerName, controllerType, isInit) + results = append(results, cRes) + } + return results +} diff --git a/pkg/validator/container_test.go b/pkg/validator/container_test.go index 66d20865..333f66a6 100644 --- a/pkg/validator/container_test.go +++ b/pkg/validator/container_test.go @@ -47,45 +47,36 @@ exemptions: - foo ` -func testValidate(t *testing.T, container *corev1.Container, resourceConf *string, controllerName string, expectedErrors []*ResultMessage, expectedWarnings []*ResultMessage, expectedSuccesses []*ResultMessage) { - cv := ContainerValidation{ - Container: container, - ResourceValidation: &ResourceValidation{}, - } - +func testValidate(t *testing.T, container *corev1.Container, resourceConf *string, controllerName string, expectedErrors []ResultMessage, expectedWarnings []ResultMessage, expectedSuccesses []ResultMessage) { parsedConf, err := conf.Parse([]byte(*resourceConf)) assert.NoError(t, err, "Expected no error when parsing config") - err = applyContainerSchemaChecks(&parsedConf, controllerName, conf.Deployments, &cv) + results, err := applyContainerSchemaChecks(&parsedConf, &corev1.PodSpec{}, container, controllerName, conf.Deployments, false) if err != nil { panic(err) } + summary := results.GetSummary() - assert.Len(t, cv.Warnings, len(expectedWarnings)) - assert.ElementsMatch(t, expectedWarnings, cv.Warnings) + assert.Equal(t, uint(len(expectedWarnings)), summary.Warnings) + assert.ElementsMatch(t, expectedWarnings, results.GetWarnings()) - assert.Len(t, cv.Errors, len(expectedErrors)) - assert.ElementsMatch(t, expectedErrors, cv.Errors) + assert.Equal(t, uint(len(expectedErrors)), summary.Errors) + assert.ElementsMatch(t, expectedErrors, results.GetErrors()) - assert.Len(t, cv.Successes, len(expectedSuccesses)) - assert.ElementsMatch(t, expectedSuccesses, cv.Successes) + assert.Equal(t, uint(len(expectedSuccesses)), summary.Successes) + assert.ElementsMatch(t, expectedSuccesses, results.GetSuccesses()) } func TestValidateResourcesEmptyConfig(t *testing.T) { - container := corev1.Container{ + container := &corev1.Container{ Name: "Empty", } - cv := ContainerValidation{ - Container: &container, - ResourceValidation: &ResourceValidation{}, - } - - err := applyContainerSchemaChecks(&conf.Configuration{}, "", conf.Deployments, &cv) + results, err := applyContainerSchemaChecks(&conf.Configuration{}, &corev1.PodSpec{}, container, "", conf.Deployments, false) if err != nil { panic(err) } - assert.Len(t, cv.Errors, 0) + assert.Equal(t, uint(0), results.GetSummary().Errors) } func TestValidateResourcesEmptyContainer(t *testing.T) { @@ -93,37 +84,41 @@ func TestValidateResourcesEmptyContainer(t *testing.T) { Name: "Empty", } - expectedWarnings := []*ResultMessage{ + expectedWarnings := []ResultMessage{ { ID: "cpuRequestsMissing", - Type: "warning", + Type: "failure", + Severity: "warning", Message: "CPU requests should be set", Category: "Resources", }, { ID: "memoryRequestsMissing", - Type: "warning", + Type: "failure", + Severity: "warning", Message: "Memory requests should be set", Category: "Resources", }, } - expectedErrors := []*ResultMessage{ + expectedErrors := []ResultMessage{ { ID: "cpuLimitsMissing", - Type: "error", + Type: "failure", + Severity: "error", Message: "CPU limits should be set", Category: "Resources", }, { ID: "memoryLimitsMissing", - Type: "error", + Type: "failure", + Severity: "error", Message: "Memory limits should be set", Category: "Resources", }, } - expectedSuccesses := []*ResultMessage{} + expectedSuccesses := []ResultMessage{} testValidate(t, &container, &resourceConfMinimal, "foo", expectedErrors, expectedWarnings, expectedSuccesses) } @@ -142,60 +137,56 @@ func TestValidateHealthChecks(t *testing.T) { } probe := corev1.Probe{} - emptyCV := ContainerValidation{ - Container: &corev1.Container{Name: ""}, - ResourceValidation: &ResourceValidation{}, - } - emptyCVInit := ContainerValidation{ - Container: &corev1.Container{Name: ""}, - ResourceValidation: &ResourceValidation{}, - IsInitContainer: true, - } - goodCV := ContainerValidation{ - Container: &corev1.Container{ - Name: "", - LivenessProbe: &probe, - ReadinessProbe: &probe, - }, - ResourceValidation: &ResourceValidation{}, + emptyContainer := &corev1.Container{Name: ""} + goodContainer := &corev1.Container{ + Name: "", + LivenessProbe: &probe, + ReadinessProbe: &probe, } - l := &ResultMessage{ID: "livenessProbeMissing", Type: "warning", Message: "Liveness probe should be configured", Category: "Health Checks"} - r := &ResultMessage{ID: "readinessProbeMissing", Type: "error", Message: "Readiness probe should be configured", Category: "Health Checks"} - f1 := []*ResultMessage{} - f2 := []*ResultMessage{r} - w1 := []*ResultMessage{l} + l := ResultMessage{ID: "livenessProbeMissing", Type: "failure", + Severity: "warning", Message: "Liveness probe should be configured", Category: "Health Checks"} + r := ResultMessage{ID: "readinessProbeMissing", Type: "failure", + Severity: "error", Message: "Readiness probe should be configured", Category: "Health Checks"} + f1 := []ResultMessage{} + f2 := []ResultMessage{r} + w1 := []ResultMessage{l} var testCases = []struct { - name string - probes map[string]conf.Severity - cv ContainerValidation - errors *[]*ResultMessage - warnings *[]*ResultMessage + name string + probes map[string]conf.Severity + container *corev1.Container + isInit bool + errors *[]ResultMessage + warnings *[]ResultMessage }{ - {name: "probes not configured", probes: p1, cv: emptyCV, errors: &f1}, - {name: "probes not required", probes: p2, cv: emptyCV, errors: &f1}, - {name: "probes required & configured", probes: p3, cv: goodCV, errors: &f1}, - {name: "probes required, not configured, but init", probes: p3, cv: emptyCVInit, errors: &f1}, - {name: "probes required & not configured", probes: p3, cv: emptyCV, errors: &f2, warnings: &w1}, - {name: "probes configured, but not required", probes: p2, cv: goodCV, errors: &f1}, + {name: "probes not configured", probes: p1, container: emptyContainer, errors: &f1}, + {name: "probes not required", probes: p2, container: emptyContainer, errors: &f1}, + {name: "probes required & configured", probes: p3, container: goodContainer, errors: &f1}, + {name: "probes required, not configured, but init", probes: p3, container: emptyContainer, isInit: true, errors: &f1}, + {name: "probes required & not configured", probes: p3, container: emptyContainer, errors: &f2, warnings: &w1}, + {name: "probes configured, but not required", probes: p2, container: goodContainer, errors: &f1}, } for idx, tt := range testCases { t.Run(tt.name, func(t *testing.T) { - err := applyContainerSchemaChecks(&conf.Configuration{Checks: tt.probes}, "", conf.Deployments, &tt.cv) + results, err := applyContainerSchemaChecks(&conf.Configuration{Checks: tt.probes}, &corev1.PodSpec{}, tt.container, "", conf.Deployments, tt.isInit) 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), message) - assert.ElementsMatch(t, tt.cv.Warnings, *tt.warnings, message) + warnings := results.GetWarnings() + assert.Len(t, warnings, len(*tt.warnings), message) + assert.ElementsMatch(t, warnings, *tt.warnings, message) } - assert.Len(t, tt.cv.Errors, len(*tt.errors), message) - assert.ElementsMatch(t, tt.cv.Errors, *tt.errors, message) + if tt.errors != nil { + errors := results.GetErrors() + assert.Len(t, errors, len(*tt.errors), message) + assert.ElementsMatch(t, errors, *tt.errors, message) + } }) } } @@ -211,101 +202,94 @@ func TestValidateImage(t *testing.T) { "pullPolicyNotAlways": conf.SeverityError, } - emptyCV := ContainerValidation{ - Container: &corev1.Container{}, - ResourceValidation: &ResourceValidation{}, - } - badCV := ContainerValidation{ - Container: &corev1.Container{Image: "test"}, - ResourceValidation: &ResourceValidation{}, - } - lessBadCV := ContainerValidation{ - Container: &corev1.Container{Image: "test:latest", ImagePullPolicy: ""}, - ResourceValidation: &ResourceValidation{}, - } - goodCV := ContainerValidation{ - Container: &corev1.Container{Image: "test:0.1.0", ImagePullPolicy: "Always"}, - ResourceValidation: &ResourceValidation{}, - } + emptyContainer := &corev1.Container{} + badContainer := &corev1.Container{Image: "test"} + lessBadContainer := &corev1.Container{Image: "test:latest", ImagePullPolicy: ""} + goodContainer := &corev1.Container{Image: "test:0.1.0", ImagePullPolicy: "Always"} var testCases = []struct { - name string - image map[string]conf.Severity - cv ContainerValidation - expected []*ResultMessage + name string + image map[string]conf.Severity + container *corev1.Container + expected []ResultMessage }{ { - name: "emptyConf + emptyCV", - image: emptyConf, - cv: emptyCV, - expected: []*ResultMessage{}, + name: "emptyConf + emptyCV", + image: emptyConf, + container: emptyContainer, + expected: []ResultMessage{}, }, { - name: "standardConf + emptyCV", - image: standardConf, - cv: emptyCV, - expected: []*ResultMessage{{ + name: "standardConf + emptyCV", + image: standardConf, + container: emptyContainer, + expected: []ResultMessage{{ ID: "tagNotSpecified", Message: "Image tag should be specified", - Type: "error", + Type: "failure", + Severity: "error", Category: "Images", }}, }, { - name: "standardConf + badCV", - image: standardConf, - cv: badCV, - expected: []*ResultMessage{{ + name: "standardConf + badCV", + image: standardConf, + container: badContainer, + expected: []ResultMessage{{ ID: "tagNotSpecified", Message: "Image tag should be specified", - Type: "error", + Type: "failure", + Severity: "error", Category: "Images", }}, }, { - name: "standardConf + lessBadCV", - image: standardConf, - cv: lessBadCV, - expected: []*ResultMessage{{ + name: "standardConf + lessBadCV", + image: standardConf, + container: lessBadContainer, + expected: []ResultMessage{{ ID: "tagNotSpecified", Message: "Image tag should be specified", - Type: "error", + Type: "failure", + Severity: "error", Category: "Images", }}, }, { - name: "strongConf + badCV", - image: strongConf, - cv: badCV, - expected: []*ResultMessage{{ + name: "strongConf + badCV", + image: strongConf, + container: badContainer, + expected: []ResultMessage{{ ID: "pullPolicyNotAlways", Message: "Image pull policy should be \"Always\"", - Type: "error", + Type: "failure", + Severity: "error", Category: "Images", }, { ID: "tagNotSpecified", Message: "Image tag should be specified", - Type: "error", + Type: "failure", + Severity: "error", Category: "Images", }}, }, { - name: "strongConf + goodCV", - image: strongConf, - cv: goodCV, - expected: []*ResultMessage{}, + name: "strongConf + goodCV", + image: strongConf, + container: goodContainer, + expected: []ResultMessage{}, }, } for _, tt := range testCases { t.Run(tt.name, func(t *testing.T) { - tt.cv = resetCV(tt.cv) - err := applyContainerSchemaChecks(&conf.Configuration{Checks: tt.image}, "", conf.Deployments, &tt.cv) + results, err := applyContainerSchemaChecks(&conf.Configuration{Checks: tt.image}, &corev1.PodSpec{}, tt.container, "", conf.Deployments, false) if err != nil { panic(err) } - assert.Len(t, tt.cv.Errors, len(tt.expected)) - assert.ElementsMatch(t, tt.cv.Errors, tt.expected) + errors := results.GetErrors() + assert.Len(t, errors, len(tt.expected)) + assert.ElementsMatch(t, errors, tt.expected) }) } } @@ -320,100 +304,94 @@ func TestValidateNetworking(t *testing.T) { "hostPortSet": conf.SeverityError, } - emptyCV := ContainerValidation{ - Container: &corev1.Container{Name: ""}, - ResourceValidation: &ResourceValidation{}, + emptyContainer := &corev1.Container{Name: ""} + badContainer := &corev1.Container{ + Ports: []corev1.ContainerPort{{ + ContainerPort: 3000, + HostPort: 443, + }}, } - - badCV := ContainerValidation{ - Container: &corev1.Container{ - Ports: []corev1.ContainerPort{{ - ContainerPort: 3000, - HostPort: 443, - }}, - }, - ResourceValidation: &ResourceValidation{}, - } - - goodCV := ContainerValidation{ - Container: &corev1.Container{ - Ports: []corev1.ContainerPort{{ - ContainerPort: 3000, - }}, - }, - ResourceValidation: &ResourceValidation{}, + goodContainer := &corev1.Container{ + Ports: []corev1.ContainerPort{{ + ContainerPort: 3000, + }}, } var testCases = []struct { name string networkConf map[string]conf.Severity - cv ContainerValidation - expectedMessages []*ResultMessage + container *corev1.Container + expectedMessages []ResultMessage }{ { name: "empty ports + empty validation config", networkConf: emptyConf, - cv: emptyCV, - expectedMessages: []*ResultMessage{}, + container: emptyContainer, + expectedMessages: []ResultMessage{}, }, { name: "empty ports + standard validation config", networkConf: standardConf, - cv: emptyCV, - expectedMessages: []*ResultMessage{{ + container: emptyContainer, + expectedMessages: []ResultMessage{{ ID: "hostPortSet", Message: "Host port is not configured", Type: "success", + Severity: "warning", Category: "Networking", }}, }, { name: "empty ports + strong validation config", networkConf: standardConf, - cv: emptyCV, - expectedMessages: []*ResultMessage{{ + container: emptyContainer, + expectedMessages: []ResultMessage{{ ID: "hostPortSet", Message: "Host port is not configured", Type: "success", + Severity: "warning", Category: "Networking", }}, }, { name: "host ports + empty validation config", networkConf: emptyConf, - cv: badCV, - expectedMessages: []*ResultMessage{}, + container: badContainer, + expectedMessages: []ResultMessage{}, }, { name: "host ports + standard validation config", networkConf: standardConf, - cv: badCV, - expectedMessages: []*ResultMessage{{ + container: badContainer, + expectedMessages: []ResultMessage{{ ID: "hostPortSet", Message: "Host port should not be configured", - Type: "warning", + Type: "failure", + Severity: "warning", Category: "Networking", }}, }, { name: "no host ports + standard validation config", networkConf: standardConf, - cv: goodCV, - expectedMessages: []*ResultMessage{{ + container: goodContainer, + expectedMessages: []ResultMessage{{ ID: "hostPortSet", Message: "Host port is not configured", Type: "success", + Severity: "warning", Category: "Networking", }}, }, { name: "host ports + strong validation config", networkConf: strongConf, - cv: badCV, - expectedMessages: []*ResultMessage{{ + container: badContainer, + expectedMessages: []ResultMessage{{ ID: "hostPortSet", Message: "Host port should not be configured", - Type: "error", + Type: "failure", + Severity: "error", Category: "Networking", }}, }, @@ -421,13 +399,16 @@ func TestValidateNetworking(t *testing.T) { for _, tt := range testCases { t.Run(tt.name, func(t *testing.T) { - tt.cv = resetCV(tt.cv) - err := applyContainerSchemaChecks(&conf.Configuration{Checks: tt.networkConf}, "", conf.Deployments, &tt.cv) + results, err := applyContainerSchemaChecks(&conf.Configuration{Checks: tt.networkConf}, &corev1.PodSpec{}, tt.container, "", conf.Deployments, false) if err != nil { panic(err) } - assert.Len(t, tt.cv.messages(), len(tt.expectedMessages)) - assert.ElementsMatch(t, tt.cv.messages(), tt.expectedMessages) + messages := []ResultMessage{} + for _, msg := range results { + messages = append(messages, msg) + } + assert.Len(t, messages, len(tt.expectedMessages)) + assert.ElementsMatch(t, messages, tt.expectedMessages) }) } } @@ -455,13 +436,10 @@ func TestValidateSecurity(t *testing.T) { "insecureCapabilities": conf.SeverityError, } - emptyCV := ContainerValidation{ - Container: &corev1.Container{Name: ""}, - ResourceValidation: &ResourceValidation{}, - } - - badCV := ContainerValidation{ - Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{ + emptyContainer := &corev1.Container{Name: ""} + badContainer := &corev1.Container{ + Name: "", + SecurityContext: &corev1.SecurityContext{ RunAsNonRoot: &falseVar, ReadOnlyRootFilesystem: &falseVar, Privileged: &trueVar, @@ -469,48 +447,34 @@ func TestValidateSecurity(t *testing.T) { Capabilities: &corev1.Capabilities{ Add: []corev1.Capability{"AUDIT_WRITE", "SYS_ADMIN", "NET_ADMIN"}, }, - }}, - ResourceValidation: &ResourceValidation{}, - } - - badCVWithGoodPodSpec := ContainerValidation{ - Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{ - RunAsNonRoot: &falseVar, - ReadOnlyRootFilesystem: &falseVar, - Privileged: &trueVar, - AllowPrivilegeEscalation: &trueVar, - Capabilities: &corev1.Capabilities{ - Add: []corev1.Capability{"AUDIT_WRITE", "SYS_ADMIN", "NET_ADMIN"}, - }, - }}, - ResourceValidation: &ResourceValidation{}, - parentPodSpec: corev1.PodSpec{ - SecurityContext: &corev1.PodSecurityContext{ - RunAsNonRoot: &trueVar, - }, }, } - - badCVWithBadPodSpec := ContainerValidation{ - Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{ + emptyPodSpec := &corev1.PodSpec{} + goodPodSpec := &corev1.PodSpec{ + SecurityContext: &corev1.PodSecurityContext{ + RunAsNonRoot: &trueVar, + }, + } + badPodSpec := &corev1.PodSpec{ + SecurityContext: &corev1.PodSecurityContext{ + RunAsNonRoot: &falseVar, + }, + } + inheritContainer := &corev1.Container{ + Name: "", + SecurityContext: &corev1.SecurityContext{ RunAsNonRoot: nil, // this will use the default from the podspec - ReadOnlyRootFilesystem: &falseVar, - Privileged: &trueVar, - AllowPrivilegeEscalation: &trueVar, + ReadOnlyRootFilesystem: &trueVar, + Privileged: &falseVar, + AllowPrivilegeEscalation: &falseVar, Capabilities: &corev1.Capabilities{ - Add: []corev1.Capability{"AUDIT_WRITE", "SYS_ADMIN", "NET_ADMIN"}, - }, - }}, - ResourceValidation: &ResourceValidation{}, - parentPodSpec: corev1.PodSpec{ - SecurityContext: &corev1.PodSecurityContext{ - RunAsNonRoot: &falseVar, + Drop: []corev1.Capability{"ALL"}, }, }, } - - goodCV := ContainerValidation{ - Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{ + goodContainer := &corev1.Container{ + Name: "", + SecurityContext: &corev1.SecurityContext{ RunAsNonRoot: &trueVar, ReadOnlyRootFilesystem: &trueVar, Privileged: &falseVar, @@ -518,12 +482,11 @@ func TestValidateSecurity(t *testing.T) { Capabilities: &corev1.Capabilities{ Drop: []corev1.Capability{"NET_BIND_SERVICE", "FOWNER"}, }, - }}, - ResourceValidation: &ResourceValidation{}, + }, } - - strongCV := ContainerValidation{ - Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{ + strongContainer := &corev1.Container{ + Name: "", + SecurityContext: &corev1.SecurityContext{ RunAsNonRoot: &trueVar, ReadOnlyRootFilesystem: &trueVar, Privileged: &falseVar, @@ -531,379 +494,407 @@ func TestValidateSecurity(t *testing.T) { Capabilities: &corev1.Capabilities{ Drop: []corev1.Capability{"ALL"}, }, - }}, - ResourceValidation: &ResourceValidation{}, - } - - strongCVWithPodSpecSecurityContext := ContainerValidation{ - Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{ - RunAsNonRoot: nil, // not set but overridden via podSpec - ReadOnlyRootFilesystem: &trueVar, - Privileged: &falseVar, - AllowPrivilegeEscalation: &falseVar, - Capabilities: &corev1.Capabilities{ - Drop: []corev1.Capability{"ALL"}, - }, - }}, - ResourceValidation: &ResourceValidation{}, - parentPodSpec: corev1.PodSpec{ - SecurityContext: &corev1.PodSecurityContext{ - RunAsNonRoot: &trueVar, - }, - }, - } - - strongCVWithBadPodSpecSecurityContext := ContainerValidation{ - Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{ - RunAsNonRoot: &trueVar, // will override the bad setting in PodSpec - ReadOnlyRootFilesystem: &trueVar, - Privileged: &falseVar, - AllowPrivilegeEscalation: &falseVar, - Capabilities: &corev1.Capabilities{ - Drop: []corev1.Capability{"ALL"}, - }, - }}, - ResourceValidation: &ResourceValidation{}, - parentPodSpec: corev1.PodSpec{ - SecurityContext: &corev1.PodSecurityContext{ - RunAsNonRoot: &falseVar, // is overridden at container level with RunAsNonRoot:true - }, }, } var testCases = []struct { name string securityConf map[string]conf.Severity - cv ContainerValidation - expectedMessages []*ResultMessage + container *corev1.Container + pod *corev1.PodSpec + expectedMessages []ResultMessage }{ { name: "empty security context + empty validation config", securityConf: emptyConf, - cv: emptyCV, - expectedMessages: []*ResultMessage{}, + container: emptyContainer, + pod: emptyPodSpec, + expectedMessages: []ResultMessage{}, }, { name: "empty security context + standard validation config", securityConf: standardConf, - cv: emptyCV, - expectedMessages: []*ResultMessage{{ + container: emptyContainer, + pod: emptyPodSpec, + expectedMessages: []ResultMessage{{ ID: "runAsRootAllowed", Message: "Should not be allowed to run as root", - Type: "warning", + Type: "failure", + Severity: "warning", Category: "Security", }, { ID: "notReadOnlyRootFileSystem", Message: "Filesystem should be read only", - Type: "warning", + Type: "failure", + Severity: "warning", Category: "Security", }, { ID: "runAsPrivileged", Message: "Not running as privileged", Type: "success", + Severity: "error", Category: "Security", }, { ID: "privilegeEscalationAllowed", Message: "Privilege escalation not allowed", Type: "success", + Severity: "error", Category: "Security", }, { ID: "insecureCapabilities", Message: "Container does not have any insecure capabilities", Type: "success", + Severity: "warning", Category: "Security", }, { ID: "dangerousCapabilities", Message: "Container does not have any dangerous capabilities", Type: "success", + Severity: "error", Category: "Security", }}, }, { name: "bad security context + standard validation config", securityConf: standardConf, - cv: badCV, - expectedMessages: []*ResultMessage{{ + container: badContainer, + pod: emptyPodSpec, + expectedMessages: []ResultMessage{{ ID: "dangerousCapabilities", Message: "Container should not have dangerous capabilities", - Type: "error", + Type: "failure", + Severity: "error", Category: "Security", }, { ID: "privilegeEscalationAllowed", Message: "Privilege escalation should not be allowed", - Type: "error", + Type: "failure", + Severity: "error", Category: "Security", }, { ID: "runAsPrivileged", Message: "Should not be running as privileged", - Type: "error", + Type: "failure", + Severity: "error", Category: "Security", }, { ID: "insecureCapabilities", Message: "Container should not have insecure capabilities", - Type: "warning", + Type: "failure", + Severity: "warning", Category: "Security", }, { ID: "runAsRootAllowed", Message: "Should not be allowed to run as root", - Type: "warning", + Type: "failure", + Severity: "warning", Category: "Security", }, { ID: "notReadOnlyRootFileSystem", Message: "Filesystem should be read only", - Type: "warning", + Type: "failure", + Severity: "warning", Category: "Security", }}, }, { name: "bad security context + standard validation config with good settings in podspec", securityConf: standardConf, - cv: badCVWithGoodPodSpec, - expectedMessages: []*ResultMessage{{ + container: badContainer, + pod: goodPodSpec, + expectedMessages: []ResultMessage{{ ID: "dangerousCapabilities", Message: "Container should not have dangerous capabilities", - Type: "error", + Type: "failure", + Severity: "error", Category: "Security", }, { ID: "privilegeEscalationAllowed", Message: "Privilege escalation should not be allowed", - Type: "error", + Type: "failure", + Severity: "error", Category: "Security", }, { ID: "runAsPrivileged", Message: "Should not be running as privileged", - Type: "error", + Type: "failure", + Severity: "error", Category: "Security", }, { ID: "insecureCapabilities", Message: "Container should not have insecure capabilities", - Type: "warning", + Type: "failure", + Severity: "warning", Category: "Security", }, { ID: "runAsRootAllowed", Message: "Should not be allowed to run as root", - Type: "warning", + Type: "failure", + Severity: "warning", Category: "Security", }, { ID: "notReadOnlyRootFileSystem", Message: "Filesystem should be read only", - Type: "warning", + Type: "failure", + Severity: "warning", Category: "Security", }}, }, { name: "bad security context + standard validation config from default set in podspec", securityConf: standardConf, - cv: badCVWithBadPodSpec, - expectedMessages: []*ResultMessage{{ + container: badContainer, + pod: badPodSpec, + expectedMessages: []ResultMessage{{ ID: "dangerousCapabilities", Message: "Container should not have dangerous capabilities", - Type: "error", + Type: "failure", + Severity: "error", Category: "Security", }, { ID: "insecureCapabilities", Message: "Container should not have insecure capabilities", - Type: "warning", + Type: "failure", + Severity: "warning", Category: "Security", }, { ID: "privilegeEscalationAllowed", Message: "Privilege escalation should not be allowed", - Type: "error", + Type: "failure", + Severity: "error", Category: "Security", }, { ID: "runAsPrivileged", Message: "Should not be running as privileged", - Type: "error", + Type: "failure", + Severity: "error", Category: "Security", }, { ID: "runAsRootAllowed", Message: "Should not be allowed to run as root", - Type: "warning", + Type: "failure", + Severity: "warning", Category: "Security", }, { ID: "notReadOnlyRootFileSystem", Message: "Filesystem should be read only", - Type: "warning", + Type: "failure", + Severity: "warning", Category: "Security", }}, }, { name: "good security context + standard validation config", securityConf: standardConf, - cv: goodCV, - expectedMessages: []*ResultMessage{{ + container: goodContainer, + pod: emptyPodSpec, + expectedMessages: []ResultMessage{{ ID: "runAsRootAllowed", Message: "Is not allowed to run as root", Type: "success", + Severity: "warning", Category: "Security", }, { ID: "notReadOnlyRootFileSystem", Message: "Filesystem is read only", Type: "success", + Severity: "warning", Category: "Security", }, { ID: "runAsPrivileged", Message: "Not running as privileged", Type: "success", + Severity: "error", Category: "Security", }, { ID: "privilegeEscalationAllowed", Message: "Privilege escalation not allowed", Type: "success", + Severity: "error", Category: "Security", }, { ID: "dangerousCapabilities", Message: "Container does not have any dangerous capabilities", Type: "success", + Severity: "error", Category: "Security", }, { ID: "insecureCapabilities", Message: "Container does not have any insecure capabilities", Type: "success", + Severity: "warning", Category: "Security", }}, }, { name: "good security context + strong validation config", securityConf: strongConf, - cv: goodCV, - expectedMessages: []*ResultMessage{{ + container: goodContainer, + pod: emptyPodSpec, + expectedMessages: []ResultMessage{{ ID: "dangerousCapabilities", Message: "Container does not have any dangerous capabilities", Type: "success", + Severity: "error", Category: "Security", }, { ID: "insecureCapabilities", Message: "Container does not have any insecure capabilities", Type: "success", + Severity: "error", Category: "Security", }, { ID: "runAsRootAllowed", Message: "Is not allowed to run as root", Type: "success", + Severity: "error", Category: "Security", }, { ID: "notReadOnlyRootFileSystem", Message: "Filesystem is read only", Type: "success", + Severity: "error", Category: "Security", }, { ID: "runAsPrivileged", Message: "Not running as privileged", Type: "success", + Severity: "error", Category: "Security", }, { ID: "privilegeEscalationAllowed", Message: "Privilege escalation not allowed", Type: "success", + Severity: "error", Category: "Security", }}, }, { name: "strong security context + strong validation config", securityConf: strongConf, - cv: strongCV, - expectedMessages: []*ResultMessage{{ + container: strongContainer, + pod: emptyPodSpec, + expectedMessages: []ResultMessage{{ ID: "runAsRootAllowed", Message: "Is not allowed to run as root", Type: "success", + Severity: "error", Category: "Security", }, { ID: "notReadOnlyRootFileSystem", Message: "Filesystem is read only", Type: "success", + Severity: "error", Category: "Security", }, { ID: "runAsPrivileged", Message: "Not running as privileged", Type: "success", + Severity: "error", Category: "Security", }, { ID: "privilegeEscalationAllowed", Message: "Privilege escalation not allowed", Type: "success", + Severity: "error", Category: "Security", }, { ID: "dangerousCapabilities", Message: "Container does not have any dangerous capabilities", Type: "success", + Severity: "error", Category: "Security", }, { ID: "insecureCapabilities", Message: "Container does not have any insecure capabilities", Type: "success", + Severity: "error", Category: "Security", }}, }, { name: "strong security context + strong validation config via podspec default", securityConf: strongConf, - cv: strongCVWithPodSpecSecurityContext, - expectedMessages: []*ResultMessage{{ + container: inheritContainer, + pod: goodPodSpec, + expectedMessages: []ResultMessage{{ ID: "runAsRootAllowed", Message: "Is not allowed to run as root", Type: "success", + Severity: "error", Category: "Security", }, { ID: "notReadOnlyRootFileSystem", Message: "Filesystem is read only", Type: "success", + Severity: "error", Category: "Security", }, { ID: "runAsPrivileged", Message: "Not running as privileged", Type: "success", + Severity: "error", Category: "Security", }, { ID: "privilegeEscalationAllowed", Message: "Privilege escalation not allowed", Type: "success", + Severity: "error", Category: "Security", }, { ID: "dangerousCapabilities", Message: "Container does not have any dangerous capabilities", Type: "success", + Severity: "error", Category: "Security", }, { ID: "insecureCapabilities", Message: "Container does not have any insecure capabilities", Type: "success", + Severity: "error", Category: "Security", }}, }, { name: "strong security context + strong validation config with bad setting in podspec default", securityConf: strongConf, - cv: strongCVWithBadPodSpecSecurityContext, - expectedMessages: []*ResultMessage{{ + container: strongContainer, + pod: badPodSpec, + expectedMessages: []ResultMessage{{ ID: "runAsRootAllowed", Message: "Is not allowed to run as root", Type: "success", + Severity: "error", Category: "Security", }, { ID: "notReadOnlyRootFileSystem", Message: "Filesystem is read only", Type: "success", + Severity: "error", Category: "Security", }, { ID: "runAsPrivileged", Message: "Not running as privileged", Type: "success", + Severity: "error", Category: "Security", }, { ID: "privilegeEscalationAllowed", Message: "Privilege escalation not allowed", Type: "success", + Severity: "error", Category: "Security", }, { ID: "dangerousCapabilities", Message: "Container does not have any dangerous capabilities", Type: "success", + Severity: "error", Category: "Security", }, { ID: "insecureCapabilities", Message: "Container does not have any insecure capabilities", Type: "success", + Severity: "error", Category: "Security", }}, }, @@ -911,13 +902,16 @@ func TestValidateSecurity(t *testing.T) { for _, tt := range testCases { t.Run(tt.name, func(t *testing.T) { - tt.cv = resetCV(tt.cv) - err := applyContainerSchemaChecks(&conf.Configuration{Checks: tt.securityConf}, "", conf.Deployments, &tt.cv) + results, err := applyContainerSchemaChecks(&conf.Configuration{Checks: tt.securityConf}, tt.pod, tt.container, "", conf.Deployments, false) if err != nil { panic(err) } - assert.Len(t, tt.cv.messages(), len(tt.expectedMessages)) - assert.ElementsMatch(t, tt.expectedMessages, tt.cv.messages()) + messages := []ResultMessage{} + for _, msg := range results { + messages = append(messages, msg) + } + assert.Len(t, messages, len(tt.expectedMessages)) + assert.ElementsMatch(t, tt.expectedMessages, messages) }) } } @@ -932,134 +926,136 @@ func TestValidateRunAsRoot(t *testing.T) { "runAsRootAllowed": conf.SeverityWarning, }, } + + goodContainer := &corev1.Container{ + SecurityContext: &corev1.SecurityContext{ + RunAsNonRoot: &trueVar, + }, + } + badContainer := &corev1.Container{ + SecurityContext: &corev1.SecurityContext{ + RunAsNonRoot: &falseVar, + }, + } + inheritContainer := &corev1.Container{ + SecurityContext: &corev1.SecurityContext{ + RunAsNonRoot: nil, + }, + } + runAsUserContainer := &corev1.Container{ + SecurityContext: &corev1.SecurityContext{ + RunAsUser: &nonRootUser, + }, + } + runAsUser0Container := &corev1.Container{ + SecurityContext: &corev1.SecurityContext{ + RunAsUser: &rootUser, + }, + } + badPod := &corev1.PodSpec{ + SecurityContext: &corev1.PodSecurityContext{ + RunAsNonRoot: &falseVar, + }, + } + runAsUserPod := &corev1.PodSpec{ + SecurityContext: &corev1.PodSecurityContext{ + RunAsUser: &nonRootUser, + }, + } + emptyPod := &corev1.PodSpec{} + testCases := []struct { - name string - cv ContainerValidation - message ResultMessage + name string + container *corev1.Container + pod *corev1.PodSpec + message ResultMessage }{ { - name: "pod=false,container=nil", - cv: ContainerValidation{ - ResourceValidation: &ResourceValidation{}, - Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{ - RunAsNonRoot: nil, - }}, - parentPodSpec: corev1.PodSpec{ - SecurityContext: &corev1.PodSecurityContext{ - RunAsNonRoot: &falseVar, - }, - }, - }, + name: "pod=false,container=nil", + container: inheritContainer, + pod: badPod, message: ResultMessage{ ID: "runAsRootAllowed", Message: "Should not be allowed to run as root", - Type: "warning", + Type: "failure", + Severity: "warning", Category: "Security", }, }, { - name: "pod=false,container=true", - cv: ContainerValidation{ - ResourceValidation: &ResourceValidation{}, - Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{ - RunAsNonRoot: &trueVar, - }}, - parentPodSpec: corev1.PodSpec{ - SecurityContext: &corev1.PodSecurityContext{ - RunAsNonRoot: &falseVar, - }, - }, - }, + name: "pod=false,container=true", + container: goodContainer, + pod: badPod, message: ResultMessage{ ID: "runAsRootAllowed", Message: "Is not allowed to run as root", Type: "success", + Severity: "warning", Category: "Security", }, }, { - name: "pod=nil,container=runAsUser", - cv: ContainerValidation{ - ResourceValidation: &ResourceValidation{}, - Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{ - RunAsUser: &nonRootUser, - }}, - }, + name: "pod=nil,container=runAsUser", + container: runAsUserContainer, + pod: emptyPod, message: ResultMessage{ ID: "runAsRootAllowed", Message: "Is not allowed to run as root", Type: "success", + Severity: "warning", Category: "Security", }, }, { - name: "pod=runAsUser,container=nil", - cv: ContainerValidation{ - ResourceValidation: &ResourceValidation{}, - Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{}}, - parentPodSpec: corev1.PodSpec{ - SecurityContext: &corev1.PodSecurityContext{ - RunAsUser: &nonRootUser, - }, - }, - }, + name: "pod=runAsUser,container=nil", + container: inheritContainer, + pod: runAsUserPod, message: ResultMessage{ ID: "runAsRootAllowed", Message: "Is not allowed to run as root", Type: "success", + Severity: "warning", Category: "Security", }, }, { - name: "pod=runAsUser,container=runAsUser0", - cv: ContainerValidation{ - ResourceValidation: &ResourceValidation{}, - Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{ - RunAsUser: &rootUser, - }}, - parentPodSpec: corev1.PodSpec{ - SecurityContext: &corev1.PodSecurityContext{ - RunAsUser: &nonRootUser, - }, - }, - }, + name: "pod=runAsUser,container=runAsUser0", + container: runAsUser0Container, + pod: runAsUserPod, message: ResultMessage{ ID: "runAsRootAllowed", Message: "Should not be allowed to run as root", - Type: "warning", + Type: "failure", + Severity: "warning", Category: "Security", }, }, { - name: "pod=false,container=runAsUser", - cv: ContainerValidation{ - ResourceValidation: &ResourceValidation{}, - Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{ - RunAsNonRoot: &falseVar, - }}, - parentPodSpec: corev1.PodSpec{ - SecurityContext: &corev1.PodSecurityContext{ - RunAsUser: &nonRootUser, - }, - }, - }, + name: "pod=runAsUser,container=false", + pod: runAsUserPod, + container: badContainer, message: ResultMessage{ ID: "runAsRootAllowed", Message: "Should not be allowed to run as root", - Type: "warning", + Type: "failure", + Severity: "warning", Category: "Security", }, }, } for idx, tt := range testCases { t.Run(tt.name, func(t *testing.T) { - err := applyContainerSchemaChecks(&config, "", conf.Deployments, &tt.cv) + results, err := applyContainerSchemaChecks(&config, tt.pod, tt.container, "", conf.Deployments, false) if err != nil { panic(err) } - assert.Len(t, tt.cv.messages(), 1) - if len(tt.cv.messages()) > 0 { - assert.Equal(t, &tt.message, tt.cv.messages()[0], fmt.Sprintf("Test case %d failed", idx)) + messages := []ResultMessage{} + for _, msg := range results { + messages = append(messages, msg) + } + assert.Len(t, messages, 1) + if len(messages) > 0 { + assert.Equal(t, tt.message, messages[0], fmt.Sprintf("Test case %d failed", idx)) } }) } @@ -1070,37 +1066,41 @@ func TestValidateResourcesExemption(t *testing.T) { Name: "Empty", } - expectedWarnings := []*ResultMessage{} - expectedErrors := []*ResultMessage{} - expectedSuccesses := []*ResultMessage{} + expectedWarnings := []ResultMessage{} + expectedErrors := []ResultMessage{} + expectedSuccesses := []ResultMessage{} testValidate(t, &container, &resourceConfExemptions, "foo", expectedErrors, expectedWarnings, expectedSuccesses) - expectedWarnings = []*ResultMessage{ + expectedWarnings = []ResultMessage{ { ID: "cpuRequestsMissing", - Type: "warning", + Type: "failure", + Severity: "warning", Message: "CPU requests should be set", Category: "Resources", }, { ID: "memoryRequestsMissing", - Type: "warning", + Type: "failure", + Severity: "warning", Message: "Memory requests should be set", Category: "Resources", }, } - expectedErrors = []*ResultMessage{ + expectedErrors = []ResultMessage{ { ID: "cpuLimitsMissing", - Type: "error", + Type: "failure", + Severity: "error", Message: "CPU limits should be set", Category: "Resources", }, { ID: "memoryLimitsMissing", - Type: "error", + Type: "failure", + Severity: "error", Message: "Memory limits should be set", Category: "Resources", }, @@ -1110,10 +1110,3 @@ func TestValidateResourcesExemption(t *testing.T) { testValidate(t, &container, &disallowExemptionsConf, "foo", expectedErrors, expectedWarnings, expectedSuccesses) } - -func resetCV(cv ContainerValidation) ContainerValidation { - cv.Errors = []*ResultMessage{} - cv.Successes = []*ResultMessage{} - cv.Warnings = []*ResultMessage{} - return cv -} diff --git a/pkg/validator/controller.go b/pkg/validator/controller.go index d3ac89c1..93558095 100644 --- a/pkg/validator/controller.go +++ b/pkg/validator/controller.go @@ -21,13 +21,12 @@ import ( "github.com/fairwindsops/polaris/pkg/kube" "github.com/fairwindsops/polaris/pkg/validator/controllers" controller "github.com/fairwindsops/polaris/pkg/validator/controllers" - "github.com/sirupsen/logrus" ) const exemptionAnnotationKey = "polaris.fairwinds.com/exempt" // ValidateController validates a single controller, returns a ControllerResult. -func ValidateController(conf conf.Configuration, controller controller.Interface) ControllerResult { +func ValidateController(conf *conf.Configuration, controller controller.Interface) ControllerResult { controllerType := controller.GetType() pod := controller.GetPodSpec() podResult := ValidatePod(conf, pod, controller.GetName(), controllerType) @@ -41,24 +40,21 @@ func ValidateController(conf conf.Configuration, controller controller.Interface // ValidateControllers validates that each deployment conforms to the Polaris config, // builds a list of ResourceResults organized by namespace. -func ValidateControllers(config conf.Configuration, kubeResources *kube.ResourceProvider, nsResults *NamespacedResults) { +func ValidateControllers(config *conf.Configuration, kubeResources *kube.ResourceProvider) []ControllerResult { var controllersToAudit []controller.Interface for _, supportedControllers := range config.ControllersToScan { loadedControllers, _ := controllers.LoadControllersByType(supportedControllers, kubeResources) controllersToAudit = append(controllersToAudit, loadedControllers...) } + results := []ControllerResult{} for _, controller := range controllersToAudit { if !config.DisallowExemptions && hasExemptionAnnotation(controller) { continue } - controllerResult := ValidateController(config, controller) - nsResult := nsResults.getNamespaceResult(controller.GetNamespace()) - nsResult.Summary.appendResults(*controllerResult.PodResult.Summary) - if err := nsResult.AddResult(controller.GetType(), controllerResult); err != nil { - logrus.Errorf("Internal Error: Failed to add a grouped result: %s", err) - } + results = append(results, ValidateController(config, controller)) } + return results } func hasExemptionAnnotation(ctrl controller.Interface) bool { diff --git a/pkg/validator/controller_test.go b/pkg/validator/controller_test.go index 90a67486..298680d6 100644 --- a/pkg/validator/controller_test.go +++ b/pkg/validator/controller_test.go @@ -35,30 +35,22 @@ func TestValidateController(t *testing.T) { }, } deployment := controller.NewDeploymentController(test.MockDeploy()) - expectedSum := ResultSummary{ - Totals: CountSummary{ - Successes: uint(2), - Warnings: uint(0), - Errors: uint(0), - }, - ByCategory: make(map[string]*CountSummary), - } - expectedSum.ByCategory["Security"] = &CountSummary{ + expectedSum := CountSummary{ Successes: uint(2), Warnings: uint(0), Errors: uint(0), } - expectedMessages := []*ResultMessage{ - {ID: "hostIPCSet", Message: "Host IPC is not configured", Type: "success", Category: "Security"}, - {ID: "hostPIDSet", Message: "Host PID is not configured", Type: "success", Category: "Security"}, + expectedMessages := ResultSet{ + "hostIPCSet": {ID: "hostIPCSet", Message: "Host IPC is not configured", Type: "success", Severity: "error", Category: "Security"}, + "hostPIDSet": {ID: "hostPIDSet", Message: "Host PID is not configured", Type: "success", Severity: "error", Category: "Security"}, } - actualResult := ValidateController(c, deployment) + actualResult := ValidateController(&c, deployment) assert.Equal(t, "Deployments", actualResult.Type) assert.Equal(t, 1, len(actualResult.PodResult.ContainerResults), "should be equal") - assert.EqualValues(t, &expectedSum, actualResult.PodResult.Summary) + assert.EqualValues(t, expectedSum, actualResult.GetSummary()) assert.EqualValues(t, expectedMessages, actualResult.PodResult.Messages) } @@ -80,60 +72,46 @@ func TestSkipHealthChecks(t *testing.T) { deploymentBase := test.MockDeploy() deploymentBase.Spec.Template.Spec.InitContainers = []corev1.Container{test.MockContainer("test")} deployment := controller.NewDeploymentController(deploymentBase) - expectedSum := ResultSummary{ - Totals: CountSummary{ - Successes: uint(0), - Warnings: uint(1), - Errors: uint(1), - }, - ByCategory: make(map[string]*CountSummary), - } - expectedSum.ByCategory["Health Checks"] = &CountSummary{ + expectedSum := CountSummary{ Successes: uint(0), Warnings: uint(1), Errors: uint(1), } - expectedMessages := []*ResultMessage{ - {ID: "readinessProbeMissing", Message: "Readiness probe should be configured", Type: "error", Category: "Health Checks"}, - {ID: "livenessProbeMissing", Message: "Liveness probe should be configured", Type: "warning", Category: "Health Checks"}, + expectedMessages := ResultSet{ + "readinessProbeMissing": {ID: "readinessProbeMissing", Message: "Readiness probe should be configured", Type: "failure", Severity: "error", Category: "Health Checks"}, + "livenessProbeMissing": {ID: "livenessProbeMissing", Message: "Liveness probe should be configured", Type: "failure", Severity: "warning", Category: "Health Checks"}, } - actualResult := ValidateController(c, deployment) + actualResult := ValidateController(&c, deployment) assert.Equal(t, "Deployments", actualResult.Type) assert.Equal(t, 2, len(actualResult.PodResult.ContainerResults), "should be equal") - assert.EqualValues(t, &expectedSum, actualResult.PodResult.Summary) - assert.EqualValues(t, []*ResultMessage{}, actualResult.PodResult.ContainerResults[0].Messages) + assert.EqualValues(t, expectedSum, actualResult.GetSummary()) + assert.EqualValues(t, ResultSet{}, actualResult.PodResult.ContainerResults[0].Messages) assert.EqualValues(t, expectedMessages, actualResult.PodResult.ContainerResults[1].Messages) job := controller.NewJobController(test.MockJob()) - expectedSum = ResultSummary{ - Totals: CountSummary{ - Successes: uint(0), - Warnings: uint(0), - Errors: uint(0), - }, - ByCategory: make(map[string]*CountSummary), + expectedSum = CountSummary{ + Successes: uint(0), + Warnings: uint(0), + Errors: uint(0), } - expectedMessages = []*ResultMessage{} - actualResult = ValidateController(c, job) + expectedMessages = ResultSet{} + actualResult = ValidateController(&c, job) assert.Equal(t, "Jobs", actualResult.Type) assert.Equal(t, 1, len(actualResult.PodResult.ContainerResults), "should be equal") - assert.EqualValues(t, &expectedSum, actualResult.PodResult.Summary) + assert.EqualValues(t, expectedSum, actualResult.GetSummary()) assert.EqualValues(t, expectedMessages, actualResult.PodResult.ContainerResults[0].Messages) cronjob := controller.NewCronJobController(test.MockCronJob()) - expectedSum = ResultSummary{ - Totals: CountSummary{ - Successes: uint(0), - Warnings: uint(0), - Errors: uint(0), - }, - ByCategory: make(map[string]*CountSummary), + expectedSum = CountSummary{ + Successes: uint(0), + Warnings: uint(0), + Errors: uint(0), } - expectedMessages = []*ResultMessage{} - actualResult = ValidateController(c, cronjob) + expectedMessages = ResultSet{} + actualResult = ValidateController(&c, cronjob) assert.Equal(t, "CronJobs", actualResult.Type) assert.Equal(t, 1, len(actualResult.PodResult.ContainerResults), "should be equal") - assert.EqualValues(t, &expectedSum, actualResult.PodResult.Summary) + assert.EqualValues(t, expectedSum, actualResult.GetSummary()) assert.EqualValues(t, expectedMessages, actualResult.PodResult.ContainerResults[0].Messages) } @@ -151,29 +129,19 @@ func TestControllerExemptions(t *testing.T) { Deployments: []appsv1.Deployment{test.MockDeploy()}, } - expectedSum := ResultSummary{ - Totals: CountSummary{ - Successes: uint(0), - Warnings: uint(1), - Errors: uint(1), - }, - ByCategory: make(map[string]*CountSummary), - } - expectedSum.ByCategory["Health Checks"] = &CountSummary{ + expectedSum := CountSummary{ Successes: uint(0), Warnings: uint(1), Errors: uint(1), } - nsResults := NamespacedResults{} - ValidateControllers(c, resources, &nsResults) - actualResult := nsResults[""].DeploymentResults[0] - assert.Equal(t, "Deployments", actualResult.Type) - assert.EqualValues(t, &expectedSum, actualResult.PodResult.Summary) + actualResults := ValidateControllers(&c, resources) + assert.Equal(t, 1, len(actualResults)) + assert.Equal(t, "Deployments", actualResults[0].Type) + assert.EqualValues(t, expectedSum, actualResults[0].GetSummary()) resources.Deployments[0].ObjectMeta.Annotations = map[string]string{ exemptionAnnotationKey: "true", } - nsResults = NamespacedResults{} - ValidateControllers(c, resources, &nsResults) - assert.Equal(t, (*NamespaceResult)(nil), nsResults[""]) + actualResults = ValidateControllers(&c, resources) + assert.Equal(t, 0, len(actualResults)) } diff --git a/pkg/validator/fullaudit.go b/pkg/validator/fullaudit.go index 3b07f242..4d07cfd9 100644 --- a/pkg/validator/fullaudit.go +++ b/pkg/validator/fullaudit.go @@ -1,36 +1,36 @@ package validator import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "os" "time" conf "github.com/fairwindsops/polaris/pkg/config" "github.com/fairwindsops/polaris/pkg/kube" + + "github.com/sirupsen/logrus" + apiMachineryYAML "k8s.io/apimachinery/pkg/util/yaml" ) // RunAudit runs a full Polaris audit and returns an AuditData object func RunAudit(config conf.Configuration, kubeResources *kube.ResourceProvider) (AuditData, error) { - nsResults := NamespacedResults{} - ValidateControllers(config, kubeResources, &nsResults) - - clusterResults := ResultSummary{} - - // Aggregate all summary counts to get a clusterwide count. - for _, result := range nsResults.GetAllControllerResults() { - clusterResults.appendResults(*result.PodResult.Summary) - } - displayName := config.DisplayName if displayName == "" { displayName = kubeResources.SourceName } + results := ValidateControllers(&config, kubeResources) + auditData := AuditData{ PolarisOutputVersion: PolarisOutputVersion, AuditTime: kubeResources.CreationTime.Format(time.RFC3339), SourceType: kubeResources.SourceType, SourceName: kubeResources.SourceName, DisplayName: displayName, - ClusterSummary: ClusterSummary{ + ClusterInfo: ClusterInfo{ Version: kubeResources.ServerVersion, Nodes: len(kubeResources.Nodes), Pods: len(kubeResources.Pods), @@ -41,10 +41,39 @@ func RunAudit(config conf.Configuration, kubeResources *kube.ResourceProvider) ( Jobs: len(kubeResources.Jobs), CronJobs: len(kubeResources.CronJobs), ReplicationControllers: len(kubeResources.ReplicationControllers), - Results: clusterResults, - Score: clusterResults.Totals.GetScore(), }, - NamespacedResults: nsResults, + Results: results, } return auditData, nil } + +// ReadAuditFromFile reads the data from a past audit stored in a JSON or YAML file. +func ReadAuditFromFile(fileName string) AuditData { + auditData := AuditData{} + oldFileBytes, err := ioutil.ReadFile(fileName) + if err != nil { + logrus.Errorf("Unable to read contents of loaded file: %v", err) + os.Exit(1) + } + auditData, err = ParseAudit(oldFileBytes) + if err != nil { + logrus.Errorf("Error parsing file contents into auditData: %v", err) + os.Exit(1) + } + return auditData +} + +// ParseAudit decodes either a YAML or JSON file and returns AuditData. +func ParseAudit(oldFileBytes []byte) (AuditData, error) { + reader := bytes.NewReader(oldFileBytes) + conf := AuditData{} + d := apiMachineryYAML.NewYAMLOrJSONDecoder(reader, 4096) + for { + if err := d.Decode(&conf); err != nil { + if err == io.EOF { + return conf, nil + } + return conf, fmt.Errorf("Decoding config failed: %v", err) + } + } +} diff --git a/pkg/validator/fullaudit_test.go b/pkg/validator/fullaudit_test.go index af9411c0..4ca9ab6d 100644 --- a/pkg/validator/fullaudit_test.go +++ b/pkg/validator/fullaudit_test.go @@ -30,16 +30,7 @@ func TestGetTemplateData(t *testing.T) { }, } - // TODO: split out the logic for calculating summaries into another set of tests - sum := ResultSummary{ - Totals: CountSummary{ - Successes: uint(0), - Warnings: uint(4), - Errors: uint(4), - }, - ByCategory: CategorySummary{}, - } - sum.ByCategory["Health Checks"] = &CountSummary{ + sum := CountSummary{ Successes: uint(0), Warnings: uint(4), Errors: uint(4), @@ -48,17 +39,33 @@ func TestGetTemplateData(t *testing.T) { actualAudit, err := RunAudit(c, resources) assert.Equal(t, err, nil, "error should be nil") - assert.EqualValues(t, sum, actualAudit.ClusterSummary.Results) + assert.EqualValues(t, sum, actualAudit.GetSummary()) assert.Equal(t, actualAudit.SourceType, "Cluster", "should be from a cluster") assert.Equal(t, actualAudit.SourceName, "test", "should be from a cluster") - assert.Equal(t, 1, len(actualAudit.NamespacedResults["test"].DeploymentResults), "should be equal") - assert.Equal(t, 1, len(actualAudit.NamespacedResults["test"].DeploymentResults), "should be equal") - assert.Equal(t, 1, len(actualAudit.NamespacedResults["test"].DeploymentResults[0].PodResult.ContainerResults), "should be equal") - assert.Equal(t, 2, len(actualAudit.NamespacedResults["test"].DeploymentResults[0].PodResult.ContainerResults[0].Messages), "should be equal") + assert.Equal(t, 6, len(actualAudit.Results)) - assert.Equal(t, 1, len(actualAudit.NamespacedResults["test"].StatefulSetResults), "should be equal") - assert.Equal(t, 1, len(actualAudit.NamespacedResults["test"].StatefulSetResults), "should be equal") - assert.Equal(t, 1, len(actualAudit.NamespacedResults["test"].StatefulSetResults[0].PodResult.ContainerResults), "should be equal") - assert.Equal(t, 2, len(actualAudit.NamespacedResults["test"].StatefulSetResults[0].PodResult.ContainerResults[0].Messages), "should be equal") + assert.Equal(t, "Deployments", actualAudit.Results[0].Type) + assert.Equal(t, 1, len(actualAudit.Results[0].PodResult.ContainerResults)) + assert.Equal(t, 2, len(actualAudit.Results[0].PodResult.ContainerResults[0].Messages)) + + assert.Equal(t, "StatefulSets", actualAudit.Results[1].Type) + assert.Equal(t, 1, len(actualAudit.Results[1].PodResult.ContainerResults)) + assert.Equal(t, 2, len(actualAudit.Results[1].PodResult.ContainerResults[0].Messages)) + + assert.Equal(t, "DaemonSets", actualAudit.Results[2].Type) + assert.Equal(t, 1, len(actualAudit.Results[2].PodResult.ContainerResults)) + assert.Equal(t, 2, len(actualAudit.Results[2].PodResult.ContainerResults[0].Messages)) + + assert.Equal(t, "Jobs", actualAudit.Results[3].Type) + assert.Equal(t, 1, len(actualAudit.Results[3].PodResult.ContainerResults)) + assert.Equal(t, 0, len(actualAudit.Results[3].PodResult.ContainerResults[0].Messages)) + + assert.Equal(t, "CronJobs", actualAudit.Results[4].Type) + assert.Equal(t, 1, len(actualAudit.Results[4].PodResult.ContainerResults)) + assert.Equal(t, 0, len(actualAudit.Results[4].PodResult.ContainerResults[0].Messages)) + + assert.Equal(t, "ReplicationController", actualAudit.Results[5].Type) + assert.Equal(t, 1, len(actualAudit.Results[5].PodResult.ContainerResults)) + assert.Equal(t, 2, len(actualAudit.Results[5].PodResult.ContainerResults[0].Messages)) } diff --git a/pkg/validator/output.go b/pkg/validator/output.go new file mode 100644 index 00000000..fbb38469 --- /dev/null +++ b/pkg/validator/output.go @@ -0,0 +1,183 @@ +// Copyright 2019 FairwindsOps Inc +// +// 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 ( + "github.com/fairwindsops/polaris/pkg/config" +) + +const ( + // PolarisOutputVersion is the version of the current output structure + PolarisOutputVersion = "1.0" +) + +// AuditData contains all the data from a full Polaris audit +type AuditData struct { + PolarisOutputVersion string + AuditTime string + SourceType string + SourceName string + DisplayName string + ClusterInfo ClusterInfo + Results []ControllerResult +} + +// ClusterInfo contains Polaris results as well as some high-level stats +type ClusterInfo struct { + Version string + Nodes int + Pods int + Namespaces int + Deployments int + StatefulSets int + DaemonSets int + Jobs int + CronJobs int + ReplicationControllers int +} + +// MessageType represents the type of Message +type MessageType string + +const ( + // MessageTypeSuccess indicates a validation success + MessageTypeSuccess MessageType = "success" + + // MessageTypeWarning indicates a validation failure + MessageTypeFailure MessageType = "failure" +) + +// ResultMessage is the result of a given check +type ResultMessage struct { + ID string + Message string + Type MessageType + Severity config.Severity + Category string +} + +// ResultSet contiains the results for a set of checks +type ResultSet map[string]ResultMessage + +// ControllerResult provides results for a controller +type ControllerResult struct { + Name string + Type string + Messages ResultSet + PodResult PodResult +} + +// PodResult provides a list of validation messages for each pod. +type PodResult struct { + Name string + Messages ResultSet + ContainerResults []ContainerResult +} + +// ContainerResult provides a list of validation messages for each container. +type ContainerResult struct { + Name string + Messages ResultSet +} + +// CountSummary provides a high level overview of success, warnings, and errors. +type CountSummary struct { + Successes uint + Warnings uint + Errors uint +} + +// GetScore returns an overall score in [0, 100] for the CountSummary +func (cs CountSummary) GetScore() uint { + total := (cs.Successes * 2) + cs.Warnings + (cs.Errors * 2) + return uint((float64(cs.Successes*2) / float64(total)) * 100) +} + +func (cs *CountSummary) AddSummary(other CountSummary) { + cs.Successes += other.Successes + cs.Warnings += other.Warnings + cs.Errors += other.Errors +} + +// CategorySummary provides a map from category name to a CountSummary +type CategorySummary map[string]*CountSummary + +func (rs ResultSet) GetSummary() CountSummary { + cs := CountSummary{} + for _, result := range rs { + if result.Type == MessageTypeFailure { + if result.Severity == config.SeverityWarning { + cs.Warnings += 1 + } else { + cs.Errors += 1 + } + } else { + cs.Successes += 1 + } + } + return cs +} + +func (p PodResult) GetSummary() CountSummary { + summary := p.Messages.GetSummary() + for _, containerResult := range p.ContainerResults { + summary.AddSummary(containerResult.Messages.GetSummary()) + } + return summary +} + +func (c ControllerResult) GetSummary() CountSummary { + summary := c.Messages.GetSummary() + summary.AddSummary(c.PodResult.GetSummary()) + return summary +} + +func (a AuditData) GetSummary() CountSummary { + summary := CountSummary{} + for _, ctrlResult := range a.Results { + summary.AddSummary(ctrlResult.GetSummary()) + } + return summary +} + +func (rs ResultSet) GetSuccesses() []ResultMessage { + successes := []ResultMessage{} + for _, msg := range rs { + if msg.Type == MessageTypeSuccess { + successes = append(successes, msg) + } + } + return successes +} + +func (rs ResultSet) GetWarnings() []ResultMessage { + warnings := []ResultMessage{} + for _, msg := range rs { + if msg.Type == MessageTypeFailure && msg.Severity == config.SeverityWarning { + warnings = append(warnings, msg) + } + } + return warnings +} + +func (rs ResultSet) GetErrors() []ResultMessage { + errors := []ResultMessage{} + for _, msg := range rs { + if msg.Type == MessageTypeFailure && msg.Severity == config.SeverityError { + errors = append(errors, msg) + } + } + return errors +} diff --git a/pkg/validator/pod.go b/pkg/validator/pod.go index c9130a9c..ef14290c 100644 --- a/pkg/validator/pod.go +++ b/pkg/validator/pod.go @@ -19,45 +19,27 @@ import ( corev1 "k8s.io/api/core/v1" ) -// PodValidation tracks validation failures associated with a Pod. -type PodValidation struct { - *ResourceValidation - Pod *corev1.PodSpec -} - // ValidatePod validates that each pod conforms to the Polaris config, returns a ResourceResult. -func ValidatePod(conf config.Configuration, pod *corev1.PodSpec, controllerName string, controllerType config.SupportedController) PodResult { - pv := PodValidation{ - Pod: pod, - ResourceValidation: &ResourceValidation{}, - } - - err := applyPodSchemaChecks(&conf, pod, controllerName, controllerType, &pv) +func ValidatePod(conf *config.Configuration, pod *corev1.PodSpec, controllerName string, controllerType config.SupportedController) PodResult { + podResults, err := applyPodSchemaChecks(conf, pod, controllerName, controllerType) // FIXME: don't panic if err != nil { panic(err) } pRes := PodResult{ - Messages: pv.messages(), + Messages: podResults, ContainerResults: []ContainerResult{}, - Summary: pv.summary(), - podSpec: *pod, } - pv.validateContainers(pod.InitContainers, &pRes, &conf, controllerName, controllerType, true) - pv.validateContainers(pod.Containers, &pRes, &conf, controllerName, controllerType, false) + podCopy := *pod + podCopy.InitContainers = []corev1.Container{} + podCopy.Containers = []corev1.Container{} - for _, cRes := range pRes.ContainerResults { - pRes.Summary.appendResults(*cRes.Summary) - } + containerResults := ValidateContainers(conf, &podCopy, pod.InitContainers, controllerName, controllerType, true) + pRes.ContainerResults = append(pRes.ContainerResults, containerResults...) + containerResults = ValidateContainers(conf, &podCopy, pod.Containers, controllerName, controllerType, false) + pRes.ContainerResults = append(pRes.ContainerResults, containerResults...) return pRes } - -func (pv *PodValidation) validateContainers(containers []corev1.Container, pRes *PodResult, conf *config.Configuration, controllerName string, controllerType config.SupportedController, isInit bool) { - for _, container := range containers { - cRes := ValidateContainer(&container, pRes, conf, controllerName, controllerType, isInit) - pRes.ContainerResults = append(pRes.ContainerResults, cRes) - } -} diff --git a/pkg/validator/pod_test.go b/pkg/validator/pod_test.go index fd9df744..5c3e050c 100644 --- a/pkg/validator/pod_test.go +++ b/pkg/validator/pod_test.go @@ -36,35 +36,22 @@ func TestValidatePod(t *testing.T) { k8s = test.SetupAddControllers(k8s, "test") pod := test.MockPod() - expectedSum := ResultSummary{ - Totals: CountSummary{ - Successes: uint(4), - Warnings: uint(0), - Errors: uint(0), - }, - ByCategory: make(map[string]*CountSummary), - } - expectedSum.ByCategory["Networking"] = &CountSummary{ - Successes: uint(2), - Warnings: uint(0), - Errors: uint(0), - } - expectedSum.ByCategory["Security"] = &CountSummary{ - Successes: uint(2), + expectedSum := CountSummary{ + Successes: uint(4), Warnings: uint(0), Errors: uint(0), } - expectedMessages := []*ResultMessage{ - {ID: "hostIPCSet", Message: "Host IPC is not configured", Type: "success", Category: "Security"}, - {ID: "hostNetworkSet", Message: "Host network is not configured", Type: "success", Category: "Networking"}, - {ID: "hostPIDSet", Message: "Host PID is not configured", Type: "success", Category: "Security"}, + expectedMessages := ResultSet{ + "hostIPCSet": {ID: "hostIPCSet", Message: "Host IPC is not configured", Type: "success", Severity: "error", Category: "Security"}, + "hostNetworkSet": {ID: "hostNetworkSet", Message: "Host network is not configured", Type: "success", Severity: "warning", Category: "Networking"}, + "hostPIDSet": {ID: "hostPIDSet", Message: "Host PID is not configured", Type: "success", Severity: "error", Category: "Security"}, } - actualPodResult := ValidatePod(c, &pod.Spec, "", conf.Deployments) + actualPodResult := ValidatePod(&c, &pod.Spec, "", conf.Deployments) assert.Equal(t, 1, len(actualPodResult.ContainerResults), "should be equal") - assert.EqualValues(t, &expectedSum, actualPodResult.Summary) + assert.EqualValues(t, expectedSum, actualPodResult.GetSummary()) assert.EqualValues(t, expectedMessages, actualPodResult.Messages) } @@ -83,34 +70,21 @@ func TestInvalidIPCPod(t *testing.T) { pod := test.MockPod() pod.Spec.HostIPC = true - expectedSum := ResultSummary{ - Totals: CountSummary{ - Successes: uint(3), - Warnings: uint(0), - Errors: uint(1), - }, - ByCategory: make(map[string]*CountSummary), - } - expectedSum.ByCategory["Networking"] = &CountSummary{ - Successes: uint(2), - Warnings: uint(0), - Errors: uint(0), - } - expectedSum.ByCategory["Security"] = &CountSummary{ - Successes: uint(1), + expectedSum := CountSummary{ + Successes: uint(3), Warnings: uint(0), Errors: uint(1), } - expectedMessages := []*ResultMessage{ - {ID: "hostIPCSet", Message: "Host IPC should not be configured", Type: "error", Category: "Security"}, - {ID: "hostNetworkSet", Message: "Host network is not configured", Type: "success", Category: "Networking"}, - {ID: "hostPIDSet", Message: "Host PID is not configured", Type: "success", Category: "Security"}, + expectedMessages := ResultSet{ + "hostIPCSet": {ID: "hostIPCSet", Message: "Host IPC should not be configured", Type: "failure", Severity: "error", Category: "Security"}, + "hostNetworkSet": {ID: "hostNetworkSet", Message: "Host network is not configured", Type: "success", Severity: "warning", Category: "Networking"}, + "hostPIDSet": {ID: "hostPIDSet", Message: "Host PID is not configured", Type: "success", Severity: "error", Category: "Security"}, } - actualPodResult := ValidatePod(c, &pod.Spec, "", conf.Deployments) + actualPodResult := ValidatePod(&c, &pod.Spec, "", conf.Deployments) assert.Equal(t, 1, len(actualPodResult.ContainerResults), "should be equal") - assert.EqualValues(t, &expectedSum, actualPodResult.Summary) + assert.EqualValues(t, expectedSum, actualPodResult.GetSummary()) assert.EqualValues(t, expectedMessages, actualPodResult.Messages) } @@ -129,36 +103,22 @@ func TestInvalidNeworkPod(t *testing.T) { pod := test.MockPod() pod.Spec.HostNetwork = true - expectedSum := ResultSummary{ - Totals: CountSummary{ - Successes: uint(3), - Warnings: uint(1), - Errors: uint(0), - }, - ByCategory: make(map[string]*CountSummary), - } - expectedSum.ByCategory["Networking"] = &CountSummary{ - Successes: uint(1), + expectedSum := CountSummary{ + Successes: uint(3), Warnings: uint(1), Errors: uint(0), } - expectedSum.ByCategory["Security"] = &CountSummary{ - Successes: uint(2), - Warnings: uint(0), - Errors: uint(0), + expectedMessages := ResultSet{ + "hostNetworkSet": {ID: "hostNetworkSet", Message: "Host network should not be configured", Type: "failure", Severity: "warning", Category: "Networking"}, + "hostIPCSet": {ID: "hostIPCSet", Message: "Host IPC is not configured", Type: "success", Severity: "error", Category: "Security"}, + "hostPIDSet": {ID: "hostPIDSet", Message: "Host PID is not configured", Type: "success", Severity: "error", Category: "Security"}, } - expectedMessages := []*ResultMessage{ - {ID: "hostNetworkSet", Message: "Host network should not be configured", Type: "warning", Category: "Networking"}, - {ID: "hostIPCSet", Message: "Host IPC is not configured", Type: "success", Category: "Security"}, - {ID: "hostPIDSet", Message: "Host PID is not configured", Type: "success", Category: "Security"}, - } - - actualPodResult := ValidatePod(c, &pod.Spec, "", conf.Deployments) + actualPodResult := ValidatePod(&c, &pod.Spec, "", conf.Deployments) assert.Equal(t, 1, len(actualPodResult.ContainerResults), "should be equal") - assert.EqualValues(t, &expectedSum, actualPodResult.Summary) + assert.EqualValues(t, expectedSum, actualPodResult.GetSummary()) assert.EqualValues(t, expectedMessages, actualPodResult.Messages) } @@ -177,35 +137,22 @@ func TestInvalidPIDPod(t *testing.T) { pod := test.MockPod() pod.Spec.HostPID = true - expectedSum := ResultSummary{ - Totals: CountSummary{ - Successes: uint(3), - Warnings: uint(0), - Errors: uint(1), - }, - ByCategory: make(map[string]*CountSummary), - } - expectedSum.ByCategory["Networking"] = &CountSummary{ - Successes: uint(2), - Warnings: uint(0), - Errors: uint(0), - } - expectedSum.ByCategory["Security"] = &CountSummary{ - Successes: uint(1), + expectedSum := CountSummary{ + Successes: uint(3), Warnings: uint(0), Errors: uint(1), } - expectedMessages := []*ResultMessage{ - {ID: "hostPIDSet", Message: "Host PID should not be configured", Type: "error", Category: "Security"}, - {ID: "hostIPCSet", Message: "Host IPC is not configured", Type: "success", Category: "Security"}, - {ID: "hostNetworkSet", Message: "Host network is not configured", Type: "success", Category: "Networking"}, + expectedMessages := ResultSet{ + "hostPIDSet": {ID: "hostPIDSet", Message: "Host PID should not be configured", Type: "failure", Severity: "error", Category: "Security"}, + "hostIPCSet": {ID: "hostIPCSet", Message: "Host IPC is not configured", Type: "success", Severity: "error", Category: "Security"}, + "hostNetworkSet": {ID: "hostNetworkSet", Message: "Host network is not configured", Type: "success", Severity: "warning", Category: "Networking"}, } - actualPodResult := ValidatePod(c, &pod.Spec, "", conf.Deployments) + actualPodResult := ValidatePod(&c, &pod.Spec, "", conf.Deployments) assert.Equal(t, 1, len(actualPodResult.ContainerResults), "should be equal") - assert.EqualValues(t, &expectedSum, actualPodResult.Summary) + assert.EqualValues(t, expectedSum, actualPodResult.GetSummary()) assert.EqualValues(t, expectedMessages, actualPodResult.Messages) } @@ -230,32 +177,19 @@ func TestExemption(t *testing.T) { pod := test.MockPod() pod.Spec.HostIPC = true - expectedSum := ResultSummary{ - Totals: CountSummary{ - Successes: uint(3), - Warnings: uint(0), - Errors: uint(0), - }, - ByCategory: make(map[string]*CountSummary), - } - expectedSum.ByCategory["Networking"] = &CountSummary{ - Successes: uint(2), + expectedSum := CountSummary{ + Successes: uint(3), Warnings: uint(0), Errors: uint(0), } - expectedSum.ByCategory["Security"] = &CountSummary{ - Successes: uint(1), - Warnings: uint(0), - Errors: uint(0), - } - expectedMessages := []*ResultMessage{ - {ID: "hostNetworkSet", Message: "Host network is not configured", Type: "success", Category: "Networking"}, - {ID: "hostPIDSet", Message: "Host PID is not configured", Type: "success", Category: "Security"}, + expectedMessages := ResultSet{ + "hostNetworkSet": {ID: "hostNetworkSet", Message: "Host network is not configured", Type: "success", Severity: "warning", Category: "Networking"}, + "hostPIDSet": {ID: "hostPIDSet", Message: "Host PID is not configured", Type: "success", Severity: "error", Category: "Security"}, } - actualPodResult := ValidatePod(c, &pod.Spec, "foo", conf.Deployments) + actualPodResult := ValidatePod(&c, &pod.Spec, "foo", conf.Deployments) assert.Equal(t, 1, len(actualPodResult.ContainerResults), "should be equal") - assert.EqualValues(t, &expectedSum, actualPodResult.Summary) + assert.EqualValues(t, expectedSum, actualPodResult.GetSummary()) assert.EqualValues(t, expectedMessages, actualPodResult.Messages) } diff --git a/pkg/validator/resource.go b/pkg/validator/resource.go deleted file mode 100644 index 8e7c38e2..00000000 --- a/pkg/validator/resource.go +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright 2019 FairwindsOps Inc -// -// 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 ( - conf "github.com/fairwindsops/polaris/pkg/config" - "github.com/sirupsen/logrus" -) - -// ResourceValidation contains methods shared by PodValidation and ContainerValidation -type ResourceValidation struct { - 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) summary() *ResultSummary { - counts := CountSummary{ - Errors: uint(len(rv.Errors)), - Warnings: uint(len(rv.Warnings)), - Successes: uint(len(rv.Successes)), - } - byCategory := CategorySummary{} - for _, msg := range rv.messages() { - if _, ok := byCategory[msg.Category]; !ok { - byCategory[msg.Category] = &CountSummary{} - } - if msg.Type == MessageTypeError { - byCategory[msg.Category].Errors++ - } else if msg.Type == MessageTypeWarning { - byCategory[msg.Category].Warnings++ - } else if msg.Type == MessageTypeSuccess { - byCategory[msg.Category].Successes++ - } - } - return &ResultSummary{ - Totals: counts, - ByCategory: byCategory, - } -} - -func (rv *ResourceValidation) addMessage(message ResultMessage) { - if message.Type == MessageTypeError { - rv.Errors = append(rv.Errors, &message) - } else if message.Type == MessageTypeWarning { - rv.Warnings = append(rv.Warnings, &message) - } else if message.Type == MessageTypeSuccess { - rv.Successes = append(rv.Successes, &message) - } else { - panic("Bad message type") - } -} - -func (rv *ResourceValidation) addFailure(message string, severity conf.Severity, category string, id string) { - if severity == conf.SeverityError { - rv.addError(message, category, id) - } else if severity == conf.SeverityWarning { - rv.addWarning(message, category, id) - } else { - logrus.Errorf("Invalid severity: %s", severity) - } -} - -func (rv *ResourceValidation) addError(message string, category string, id string) { - rv.Errors = append(rv.Errors, &ResultMessage{ - ID: id, - Message: message, - Type: MessageTypeError, - Category: category, - }) -} - -func (rv *ResourceValidation) addWarning(message string, category string, id string) { - rv.Warnings = append(rv.Warnings, &ResultMessage{ - ID: id, - Message: message, - Type: MessageTypeWarning, - Category: category, - }) -} - -func (rv *ResourceValidation) addSuccess(message string, category string, id string) { - rv.Successes = append(rv.Successes, &ResultMessage{ - ID: id, - Message: message, - Type: MessageTypeSuccess, - Category: category, - }) -} diff --git a/pkg/validator/schema.go b/pkg/validator/schema.go index 2060991d..ec5a3c2a 100644 --- a/pkg/validator/schema.go +++ b/pkg/validator/schema.go @@ -72,72 +72,85 @@ func parseCheck(rawBytes []byte) (config.SchemaCheck, error) { } } -func applyPodSchemaChecks(conf *config.Configuration, pod *corev1.PodSpec, controllerName string, controllerType config.SupportedController, pv *PodValidation) error { +func resolveCheck(conf *config.Configuration, checkID string, controllerName string, controllerType config.SupportedController, target config.TargetKind, isInitContainer bool) (*config.SchemaCheck, error) { + check, ok := conf.CustomChecks[checkID] + if !ok { + check, ok = builtInChecks[checkID] + } + if !ok { + return nil, fmt.Errorf("Check %s not found", checkID) + } + if !conf.IsActionable(check.ID, controllerName) { + return nil, nil + } + if !check.IsActionable(target, controllerType, isInitContainer) { + return nil, nil + } + return &check, nil +} + +func makeResult(conf *config.Configuration, check *config.SchemaCheck, passes bool) ResultMessage { + result := ResultMessage{ + ID: check.ID, + Severity: conf.Checks[check.ID], + Category: check.Category, + } + if passes { + result.Message = check.SuccessMessage + result.Type = MessageTypeSuccess + } else { + result.Message = check.FailureMessage + result.Type = MessageTypeFailure + } + return result +} + +func applyPodSchemaChecks(conf *config.Configuration, pod *corev1.PodSpec, controllerName string, controllerType config.SupportedController) (ResultSet, error) { + results := ResultSet{} checkIDs := getSortedKeys(conf.Checks) for _, checkID := range checkIDs { - check, ok := conf.CustomChecks[checkID] - if !ok { - check, ok = builtInChecks[checkID] + check, err := resolveCheck(conf, checkID, controllerName, controllerType, config.TargetPod, false) + if err != nil { + return nil, err } - if !ok { - return fmt.Errorf("Check %s not found", checkID) - } - if !conf.IsActionable(check.ID, controllerName) { - continue - } - if !check.IsActionable(config.TargetPod, controllerType, false) { + if err != nil { + return nil, err + } else if check == nil { continue } passes, err := check.CheckPod(pod) if err != nil { - return err - } - if passes { - pv.addSuccess(check.SuccessMessage, check.Category, check.ID) - } else { - severity := conf.Checks[checkID] - pv.addFailure(check.FailureMessage, severity, check.Category, check.ID) + return nil, err } + results[check.ID] = makeResult(conf, check, passes) } - return nil + return results, nil } -func applyContainerSchemaChecks(conf *config.Configuration, controllerName string, controllerType config.SupportedController, cv *ContainerValidation) error { +func applyContainerSchemaChecks(conf *config.Configuration, basePod *corev1.PodSpec, container *corev1.Container, controllerName string, controllerType config.SupportedController, isInit bool) (ResultSet, error) { + results := ResultSet{} checkIDs := getSortedKeys(conf.Checks) for _, checkID := range checkIDs { - check, ok := conf.CustomChecks[checkID] - if !ok { - check, ok = builtInChecks[checkID] - } - if !ok { - return fmt.Errorf("Check %s not found", checkID) - } - if !conf.IsActionable(check.ID, controllerName) { - continue - } - if !check.IsActionable(config.TargetContainer, controllerType, cv.IsInitContainer) { + check, err := resolveCheck(conf, checkID, controllerName, controllerType, config.TargetContainer, isInit) + if err != nil { + return nil, err + } else if check == nil { 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{} + basePod.Containers = []corev1.Container{*container} + passes, err = check.CheckPod(basePod) + basePod.Containers = []corev1.Container{} } else { - passes, err = check.CheckContainer(cv.Container) + passes, err = check.CheckContainer(container) } if err != nil { - return err - } - if passes { - cv.addSuccess(check.SuccessMessage, check.Category, check.ID) - } else { - severity := conf.Checks[checkID] - cv.addFailure(check.FailureMessage, severity, check.Category, check.ID) + return nil, err } + results[check.ID] = makeResult(conf, check, passes) } - return nil + return results, nil } func getSortedKeys(m map[string]config.Severity) []string { diff --git a/pkg/validator/schema_test.go b/pkg/validator/schema_test.go index a6d8737b..2c1825d0 100644 --- a/pkg/validator/schema_test.go +++ b/pkg/validator/schema_test.go @@ -110,55 +110,50 @@ func TestValidateResourcesPartiallyValid(t *testing.T) { }, } - expectedWarnings := []*ResultMessage{ + expectedWarnings := []ResultMessage{ { ID: "memoryLimitsRange", - Type: "warning", + Type: "failure", + Severity: "warning", Message: "Memory limits should be within the required range", Category: "Resources", }, } - expectedErrors := []*ResultMessage{ + expectedErrors := []ResultMessage{ { ID: "memoryRequestsRange", - Type: "error", + Type: "failure", + Severity: "error", Message: "Memory requests should be within the required range", Category: "Resources", }, } - expectedSuccesses := []*ResultMessage{} + expectedSuccesses := []ResultMessage{} testValidate(t, &container, &resourceConfRanges, "foo", expectedErrors, expectedWarnings, expectedSuccesses) } func TestValidateResourcesInit(t *testing.T) { - cvEmpty := ContainerValidation{ - Container: &corev1.Container{}, - ResourceValidation: &ResourceValidation{}, - } - cvInit := ContainerValidation{ - Container: &corev1.Container{}, - ResourceValidation: &ResourceValidation{}, - IsInitContainer: true, - } + emptyContainer := &corev1.Container{} parsedConf, err := conf.Parse([]byte(resourceConfRanges)) assert.NoError(t, err, "Expected no error when parsing config") - err = applyContainerSchemaChecks(&parsedConf, "", conf.Deployments, &cvEmpty) + results, err := applyContainerSchemaChecks(&parsedConf, &corev1.PodSpec{}, emptyContainer, "", conf.Deployments, false) if err != nil { panic(err) } - assert.Len(t, cvEmpty.Errors, 1) - assert.Len(t, cvEmpty.Warnings, 1) + assert.Equal(t, uint(1), results.GetSummary().Errors) + assert.Equal(t, uint(1), results.GetSummary().Warnings) - err = applyContainerSchemaChecks(&parsedConf, "", conf.Deployments, &cvInit) + results, err = applyContainerSchemaChecks(&parsedConf, &corev1.PodSpec{}, emptyContainer, "", conf.Deployments, true) if err != nil { panic(err) } - assert.Len(t, cvInit.Errors, 0) + assert.Equal(t, uint(0), results.GetSummary().Errors) + assert.Equal(t, uint(0), results.GetSummary().Warnings) } func TestValidateResourcesFullyValid(t *testing.T) { @@ -188,51 +183,57 @@ func TestValidateResourcesFullyValid(t *testing.T) { }, } - expectedSuccesses := []*ResultMessage{ + expectedSuccesses := []ResultMessage{ { ID: "memoryRequestsRange", Type: "success", + Severity: "error", Message: "Memory requests are within the required range", Category: "Resources", }, { ID: "memoryLimitsRange", Type: "success", + Severity: "warning", Message: "Memory limits are within the required range", Category: "Resources", }, } - testValidate(t, &container, &resourceConfRanges, "foo", []*ResultMessage{}, []*ResultMessage{}, expectedSuccesses) + testValidate(t, &container, &resourceConfRanges, "foo", []ResultMessage{}, []ResultMessage{}, expectedSuccesses) - expectedSuccesses = []*ResultMessage{ + expectedSuccesses = []ResultMessage{ { ID: "cpuRequestsMissing", Type: "success", + Severity: "warning", Message: "CPU requests are set", Category: "Resources", }, { ID: "memoryRequestsMissing", Type: "success", + Severity: "warning", Message: "Memory requests are set", Category: "Resources", }, { ID: "cpuLimitsMissing", Type: "success", + Severity: "error", Message: "CPU limits are set", Category: "Resources", }, { ID: "memoryLimitsMissing", Type: "success", + Severity: "error", Message: "Memory limits are set", Category: "Resources", }, } - testValidate(t, &container, &resourceConfMinimal, "foo", []*ResultMessage{}, []*ResultMessage{}, expectedSuccesses) + testValidate(t, &container, &resourceConfMinimal, "foo", []ResultMessage{}, []ResultMessage{}, expectedSuccesses) } func TestValidateCustomCheckExemptions(t *testing.T) { @@ -241,15 +242,16 @@ func TestValidateCustomCheckExemptions(t *testing.T) { Image: "hub.docker.com/foo", } - expectedWarnings := []*ResultMessage{} - expectedErrors := []*ResultMessage{} - expectedSuccesses := []*ResultMessage{} + expectedWarnings := []ResultMessage{} + expectedErrors := []ResultMessage{} + expectedSuccesses := []ResultMessage{} testValidate(t, &container, &customCheckExemptions, "exempt", expectedErrors, expectedWarnings, expectedSuccesses) - expectedErrors = []*ResultMessage{ + expectedErrors = []ResultMessage{ { ID: "foo", - Type: "error", + Type: "failure", + Severity: "error", Message: "fail!", Category: "Security", }, diff --git a/pkg/validator/types.go b/pkg/validator/types.go deleted file mode 100644 index ffe86474..00000000 --- a/pkg/validator/types.go +++ /dev/null @@ -1,270 +0,0 @@ -// Copyright 2019 FairwindsOps Inc -// -// 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 ( - "bytes" - "fmt" - "io" - "io/ioutil" - "os" - - "github.com/sirupsen/logrus" - - "github.com/fairwindsops/polaris/pkg/config" - conf "github.com/fairwindsops/polaris/pkg/config" - corev1 "k8s.io/api/core/v1" - apiMachineryYAML "k8s.io/apimachinery/pkg/util/yaml" -) - -const ( - // PolarisOutputVersion is the version of the current output structure - PolarisOutputVersion = "0.3" -) - -// AuditData contains all the data from a full Polaris audit -type AuditData struct { - PolarisOutputVersion string - AuditTime string - SourceType string - SourceName string - DisplayName string - ClusterSummary ClusterSummary - NamespacedResults NamespacedResults -} - -// ClusterSummary contains Polaris results as well as some high-level stats -type ClusterSummary struct { - Results ResultSummary - Version string - Nodes int - Pods int - Namespaces int - Deployments int - StatefulSets int - DaemonSets int - Jobs int - CronJobs int - ReplicationControllers int - Score uint -} - -// MessageType represents the type of Message -type MessageType string - -const ( - // MessageTypeSuccess indicates a validation success - MessageTypeSuccess MessageType = "success" - - // MessageTypeWarning indicates a validation warning - MessageTypeWarning MessageType = "warning" - - // MessageTypeError indicates a validation error - MessageTypeError MessageType = "error" -) - -// NamespaceResult groups container results by parent resource. -type NamespaceResult struct { - Name string - Summary *ResultSummary - - // TODO: This struct could use some love to reorganize it as just having "results" - // and then having methods to return filtered results by type - // (deploy, daemonset, etc) - // The way this is structured right now makes it difficult to add - // additional result types and potentially miss things in the metrics - // summary. - DeploymentResults []ControllerResult - StatefulSetResults []ControllerResult - DaemonSetResults []ControllerResult - JobResults []ControllerResult - CronJobResults []ControllerResult - ReplicationControllerResults []ControllerResult -} - -// AddResult adds a result to the result sets by leveraging the types supported by NamespaceResult -func (n *NamespaceResult) AddResult(resourceType config.SupportedController, result ControllerResult) error { - // Iterate all the resource types supported in this struct - var results *[]ControllerResult - switch resourceType { - case conf.Deployments: - results = &n.DeploymentResults - case conf.StatefulSets: - results = &n.StatefulSetResults - case conf.DaemonSets: - results = &n.DaemonSetResults - case conf.Jobs: - results = &n.JobResults - case conf.CronJobs: - results = &n.CronJobResults - case conf.ReplicationControllers: - results = &n.ReplicationControllerResults - default: - return fmt.Errorf("Unknown Resource Type: (%s) Missing Implementation in NamespacedResult", resourceType) - } - - // Append the new result to the results pointer loaded from the supported values - *results = append(*results, result) - - return nil -} - -// GetAllControllerResults grabs all the different types of controller results from the namespaced result as a single list for easier iteration -func (n NamespaceResult) GetAllControllerResults() []ControllerResult { - all := []ControllerResult{} - all = append(all, n.DeploymentResults...) - all = append(all, n.StatefulSetResults...) - all = append(all, n.DaemonSetResults...) - all = append(all, n.JobResults...) - all = append(all, n.CronJobResults...) - all = append(all, n.ReplicationControllerResults...) - - return all -} - -// NamespacedResults is a mapping of namespace name to the validation results. -type NamespacedResults map[string]*NamespaceResult - -// GetAllControllerResults aggregates all the namespaced results in the set together -func (nsResults NamespacedResults) GetAllControllerResults() []ControllerResult { - all := []ControllerResult{} - for _, nsResult := range nsResults { - all = append(all, nsResult.GetAllControllerResults()...) - } - return all -} - -func (nsResults NamespacedResults) getNamespaceResult(nsName string) *NamespaceResult { - nsResult := &NamespaceResult{} - switch nsResults[nsName] { - case nil: - nsResult = &NamespaceResult{ - Summary: &ResultSummary{}, - DeploymentResults: []ControllerResult{}, - StatefulSetResults: []ControllerResult{}, - DaemonSetResults: []ControllerResult{}, - JobResults: []ControllerResult{}, - CronJobResults: []ControllerResult{}, - ReplicationControllerResults: []ControllerResult{}, - } - nsResults[nsName] = nsResult - default: - nsResult = nsResults[nsName] - } - return nsResult -} - -// CountSummary provides a high level overview of success, warnings, and errors. -type CountSummary struct { - Successes uint - Warnings uint - Errors uint -} - -// GetScore returns an overall score in [0, 100] for the CountSummary -func (cs *CountSummary) GetScore() uint { - total := (cs.Successes * 2) + cs.Warnings + (cs.Errors * 2) - return uint((float64(cs.Successes*2) / float64(total)) * 100) -} - -func (cs *CountSummary) appendCounts(toAppend CountSummary) { - cs.Errors += toAppend.Errors - cs.Warnings += toAppend.Warnings - cs.Successes += toAppend.Successes -} - -// CategorySummary provides a map from category name to a CountSummary -type CategorySummary map[string]*CountSummary - -// ResultSummary provides a high level overview of success, warnings, and errors. -type ResultSummary struct { - Totals CountSummary - ByCategory CategorySummary -} - -func (rs *ResultSummary) appendResults(toAppend ResultSummary) { - rs.Totals.appendCounts(toAppend.Totals) - for category, summary := range toAppend.ByCategory { - if rs.ByCategory == nil { - rs.ByCategory = CategorySummary{} - } - if _, exists := rs.ByCategory[category]; !exists { - rs.ByCategory[category] = &CountSummary{} - } - rs.ByCategory[category].appendCounts(*summary) - } -} - -// ControllerResult provides a wrapper around a PodResult -type ControllerResult struct { - Name string - Type string - PodResult PodResult -} - -// ContainerResult provides a list of validation messages for each container. -type ContainerResult struct { - Name string - Messages []*ResultMessage - Summary *ResultSummary -} - -// PodResult provides a list of validation messages for each pod. -type PodResult struct { - Name string - Summary *ResultSummary - Messages []*ResultMessage - ContainerResults []ContainerResult - podSpec corev1.PodSpec -} - -// ResultMessage contains a message and a type indicator (success, warning, or error). -type ResultMessage struct { - ID string - Message string - Type MessageType - Category string -} - -// ReadAuditFromFile reads the data from a past audit stored in a JSON or YAML file. -func ReadAuditFromFile(fileName string) AuditData { - auditData := AuditData{} - oldFileBytes, err := ioutil.ReadFile(fileName) - if err != nil { - logrus.Errorf("Unable to read contents of loaded file: %v", err) - os.Exit(1) - } - auditData, err = ParseAudit(oldFileBytes) - if err != nil { - logrus.Errorf("Error parsing file contents into auditData: %v", err) - os.Exit(1) - } - return auditData -} - -// ParseAudit decodes either a YAML or JSON file and returns AuditData. -func ParseAudit(oldFileBytes []byte) (AuditData, error) { - reader := bytes.NewReader(oldFileBytes) - conf := AuditData{} - d := apiMachineryYAML.NewYAMLOrJSONDecoder(reader, 4096) - for { - if err := d.Decode(&conf); err != nil { - if err == io.EOF { - return conf, nil - } - return conf, fmt.Errorf("Decoding config failed: %v", err) - } - } -} diff --git a/pkg/webhook/validator.go b/pkg/webhook/validator.go index 60e7f842..56fc233d 100644 --- a/pkg/webhook/validator.go +++ b/pkg/webhook/validator.go @@ -94,7 +94,7 @@ func (v *Validator) Handle(ctx context.Context, req types.Request) types.Respons if req.AdmissionRequest.Kind.Kind == "Pod" { pod := corev1.Pod{} err = v.decoder.Decode(req, &pod) - podResult = validator.ValidatePod(v.Config, &pod.Spec, "", config.Unsupported) + podResult = validator.ValidatePod(&v.Config, &pod.Spec, "", config.Unsupported) } else { var controller controllers.Interface if yes := v.Config.CheckIfKindIsConfiguredForValidation(req.AdmissionRequest.Kind.Kind); !yes { @@ -138,7 +138,7 @@ func (v *Validator) Handle(ctx context.Context, req types.Request) types.Respons err = v.decoder.Decode(req, &replicationController) controller = controllers.NewReplicationControllerController(replicationController) } - controllerResult := validator.ValidateController(v.Config, controller) + controllerResult := validator.ValidateController(&v.Config, controller) podResult = controllerResult.PodResult } @@ -149,11 +149,12 @@ func (v *Validator) Handle(ctx context.Context, req types.Request) types.Respons allowed := true reason := "" - if podResult.Summary.Totals.Errors > 0 { + numErrors := podResult.GetSummary().Errors + if numErrors > 0 { allowed = false reason = getFailureReason(podResult) } - logrus.Infof("%d validation errors found when validating %s", podResult.Summary.Totals.Errors, podResult.Name) + logrus.Infof("%d validation errors found when validating %s", numErrors, podResult.Name) return admission.ValidationResponse(allowed, reason) } @@ -161,14 +162,14 @@ func getFailureReason(podResult validator.PodResult) string { reason := "\nPolaris prevented this deployment due to configuration problems:\n" for _, message := range podResult.Messages { - if message.Type == validator.MessageTypeError { + if message.Type == validator.MessageTypeFailure && message.Severity == config.SeverityError { reason += fmt.Sprintf("- Pod: %s\n", message.Message) } } for _, containerResult := range podResult.ContainerResults { for _, message := range containerResult.Messages { - if message.Type == validator.MessageTypeError { + if message.Type == validator.MessageTypeFailure && message.Severity == config.SeverityError { reason += fmt.Sprintf("- Container %s: %s\n", containerResult.Name, message.Message) } }