From 55363fd7a849b3c16814ef54fed659e8d722356f Mon Sep 17 00:00:00 2001 From: Bobby Brennan Date: Fri, 19 Apr 2019 17:19:13 +0000 Subject: [PATCH] Add categories to dashboard add version, cluster stats to output add comment update UI changes to summary aggregation add category summaries to dash --- pkg/dashboard/helpers.go | 12 ++-- pkg/dashboard/templates/dashboard.gohtml | 35 +++++++++-- pkg/validator/container.go | 18 ++---- pkg/validator/container_test.go | 78 ++++++++---------------- pkg/validator/deploy.go | 5 +- pkg/validator/fullaudit.go | 12 +--- pkg/validator/fullaudit_test.go | 25 ++++++-- pkg/validator/pod.go | 21 +++---- pkg/validator/pod_test.go | 20 +++++- pkg/validator/resource.go | 41 +++++++++++-- pkg/validator/types.go | 39 +++++++++--- pkg/webhook/validator.go | 2 +- public/css/main.css | 25 +++++--- public/js/charts.js | 6 +- 14 files changed, 205 insertions(+), 134 deletions(-) diff --git a/pkg/dashboard/helpers.go b/pkg/dashboard/helpers.go index cc63317d5..fa37adbda 100644 --- a/pkg/dashboard/helpers.go +++ b/pkg/dashboard/helpers.go @@ -18,12 +18,12 @@ import ( "github.com/reactiveops/fairwinds/pkg/validator" ) -func getWarningWidth(rs validator.ResultSummary, fullWidth int) uint { - return uint(float64(rs.Successes+rs.Warnings) / float64(rs.Successes+rs.Warnings+rs.Errors) * float64(fullWidth)) +func getWarningWidth(counts validator.CountSummary, fullWidth int) uint { + return uint(float64(counts.Successes+counts.Warnings) / float64(counts.Successes+counts.Warnings+counts.Errors) * float64(fullWidth)) } -func getSuccessWidth(rs validator.ResultSummary, fullWidth int) uint { - return uint(float64(rs.Successes) / float64(rs.Successes+rs.Warnings+rs.Errors) * float64(fullWidth)) +func getSuccessWidth(counts validator.CountSummary, fullWidth int) uint { + return uint(float64(counts.Successes) / float64(counts.Successes+counts.Warnings+counts.Errors) * float64(fullWidth)) } func getGrade(rs validator.ResultSummary) string { @@ -58,8 +58,8 @@ func getGrade(rs validator.ResultSummary) string { } func getScore(rs validator.ResultSummary) uint { - total := (rs.Successes * 2) + rs.Warnings + (rs.Errors * 2) - return uint((float64(rs.Successes*2) / float64(total)) * 100) + total := (rs.Totals.Successes * 2) + rs.Totals.Warnings + (rs.Totals.Errors * 2) + return uint((float64(rs.Totals.Successes*2) / float64(total)) * 100) } func getWeatherIcon(rs validator.ResultSummary) string { diff --git a/pkg/dashboard/templates/dashboard.gohtml b/pkg/dashboard/templates/dashboard.gohtml index 8be85ccdd..0afd67b81 100644 --- a/pkg/dashboard/templates/dashboard.gohtml +++ b/pkg/dashboard/templates/dashboard.gohtml @@ -47,9 +47,9 @@
@@ -80,7 +80,32 @@ + + +
Health summary
+
+ +
+ + + {{ range $namespace, $results := .AuditData.NamespacedResults }} @@ -116,8 +141,8 @@
-
-
+
+
diff --git a/pkg/validator/container.go b/pkg/validator/container.go index da81e6619..dff92354d 100644 --- a/pkg/validator/container.go +++ b/pkg/validator/container.go @@ -31,12 +31,10 @@ type ContainerValidation struct { } // ValidateContainer validates that each pod conforms to the Fairwinds config, returns a ResourceResult. -func ValidateContainer(cnConf *conf.Configuration, container *corev1.Container) ResourceResult { +func ValidateContainer(cnConf *conf.Configuration, container *corev1.Container) ContainerResult { cv := ContainerValidation{ - Container: container, - ResourceValidation: &ResourceValidation{ - Summary: &ResultSummary{}, - }, + Container: container, + ResourceValidation: &ResourceValidation{}, } cv.validateResources(&cnConf.Resources) @@ -48,16 +46,10 @@ func ValidateContainer(cnConf *conf.Configuration, container *corev1.Container) cRes := ContainerResult{ Name: container.Name, Messages: cv.messages(), + Summary: cv.summary(), } - rr := ResourceResult{ - Name: container.Name, - Type: "Container", - Summary: cv.Summary, - ContainerResults: []ContainerResult{cRes}, - } - - return rr + return cRes } func (cv *ContainerValidation) validateResources(resConf *conf.Resources) { diff --git a/pkg/validator/container_test.go b/pkg/validator/container_test.go index 55ea4be21..05c376514 100644 --- a/pkg/validator/container_test.go +++ b/pkg/validator/container_test.go @@ -69,10 +69,8 @@ func TestValidateResourcesEmptyConfig(t *testing.T) { } cv := ContainerValidation{ - Container: &container, - ResourceValidation: &ResourceValidation{ - Summary: &ResultSummary{}, - }, + Container: &container, + ResourceValidation: &ResourceValidation{}, } expected := conf.Resources{} @@ -195,10 +193,8 @@ func TestValidateResourcesFullyValid(t *testing.T) { func testValidateResources(t *testing.T, container *corev1.Container, resourceConf *string, expectedErrors *[]*ResultMessage, expectedWarnings *[]*ResultMessage) { cv := ContainerValidation{ - Container: container, - ResourceValidation: &ResourceValidation{ - Summary: &ResultSummary{}, - }, + Container: container, + ResourceValidation: &ResourceValidation{}, } parsedConf, err := conf.Parse([]byte(*resourceConf)) @@ -227,10 +223,8 @@ func TestValidateHealthChecks(t *testing.T) { probe := corev1.Probe{} cv1 := ContainerValidation{ - Container: &corev1.Container{Name: ""}, - ResourceValidation: &ResourceValidation{ - Summary: &ResultSummary{}, - }, + Container: &corev1.Container{Name: ""}, + ResourceValidation: &ResourceValidation{}, } cv2 := ContainerValidation{ Container: &corev1.Container{ @@ -238,9 +232,7 @@ func TestValidateHealthChecks(t *testing.T) { LivenessProbe: &probe, ReadinessProbe: &probe, }, - ResourceValidation: &ResourceValidation{ - Summary: &ResultSummary{}, - }, + ResourceValidation: &ResourceValidation{}, } l := &ResultMessage{Type: "warning", Message: "Liveness probe should be configured", Category: "Health Checks"} @@ -286,31 +278,23 @@ func TestValidateImage(t *testing.T) { i3 := conf.Images{TagNotSpecified: conf.SeverityError} cv1 := ContainerValidation{ - Container: &corev1.Container{Name: ""}, - ResourceValidation: &ResourceValidation{ - Summary: &ResultSummary{}, - }, + Container: &corev1.Container{Name: ""}, + ResourceValidation: &ResourceValidation{}, } cv2 := ContainerValidation{ - Container: &corev1.Container{Name: "", Image: "test:tag"}, - ResourceValidation: &ResourceValidation{ - Summary: &ResultSummary{}, - }, + Container: &corev1.Container{Name: "", Image: "test:tag"}, + ResourceValidation: &ResourceValidation{}, } cv3 := ContainerValidation{ - Container: &corev1.Container{Name: "", Image: "test:latest"}, - ResourceValidation: &ResourceValidation{ - Summary: &ResultSummary{}, - }, + Container: &corev1.Container{Name: "", Image: "test:latest"}, + ResourceValidation: &ResourceValidation{}, } cv4 := ContainerValidation{ - Container: &corev1.Container{Name: "", Image: "test"}, - ResourceValidation: &ResourceValidation{ - Summary: &ResultSummary{}, - }, + Container: &corev1.Container{Name: "", Image: "test"}, + ResourceValidation: &ResourceValidation{}, } f := &ResultMessage{Message: "Image tag should be specified", Type: "error", Category: "Images"} @@ -351,10 +335,8 @@ func TestValidateNetworking(t *testing.T) { } emptyCV := ContainerValidation{ - Container: &corev1.Container{Name: ""}, - ResourceValidation: &ResourceValidation{ - Summary: &ResultSummary{}, - }, + Container: &corev1.Container{Name: ""}, + ResourceValidation: &ResourceValidation{}, } badCV := ContainerValidation{ @@ -364,9 +346,7 @@ func TestValidateNetworking(t *testing.T) { HostPort: 443, }}, }, - ResourceValidation: &ResourceValidation{ - Summary: &ResultSummary{}, - }, + ResourceValidation: &ResourceValidation{}, } goodCV := ContainerValidation{ @@ -375,9 +355,7 @@ func TestValidateNetworking(t *testing.T) { ContainerPort: 3000, }}, }, - ResourceValidation: &ResourceValidation{ - Summary: &ResultSummary{}, - }, + ResourceValidation: &ResourceValidation{}, } var testCases = []struct { @@ -497,10 +475,8 @@ func TestValidateSecurity(t *testing.T) { } emptyCV := ContainerValidation{ - Container: &corev1.Container{Name: ""}, - ResourceValidation: &ResourceValidation{ - Summary: &ResultSummary{}, - }, + Container: &corev1.Container{Name: ""}, + ResourceValidation: &ResourceValidation{}, } badCV := ContainerValidation{ @@ -513,9 +489,7 @@ func TestValidateSecurity(t *testing.T) { Add: []corev1.Capability{"AUDIT_CONTROL", "SYS_ADMIN", "NET_ADMIN"}, }, }}, - ResourceValidation: &ResourceValidation{ - Summary: &ResultSummary{}, - }, + ResourceValidation: &ResourceValidation{}, } goodCV := ContainerValidation{ @@ -528,9 +502,7 @@ func TestValidateSecurity(t *testing.T) { Drop: []corev1.Capability{"NET_BIND_SERVICE", "FOWNER"}, }, }}, - ResourceValidation: &ResourceValidation{ - Summary: &ResultSummary{}, - }, + ResourceValidation: &ResourceValidation{}, } strongCV := ContainerValidation{ @@ -543,9 +515,7 @@ func TestValidateSecurity(t *testing.T) { Drop: []corev1.Capability{"ALL"}, }, }}, - ResourceValidation: &ResourceValidation{ - Summary: &ResultSummary{}, - }, + ResourceValidation: &ResourceValidation{}, } var testCases = []struct { diff --git a/pkg/validator/deploy.go b/pkg/validator/deploy.go index 14729f76e..0fc28bd1a 100644 --- a/pkg/validator/deploy.go +++ b/pkg/validator/deploy.go @@ -63,10 +63,7 @@ func addResult(resResult ResourceResult, nsResults NamespacedResults, nsName str } nsResult.Results = append(nsResult.Results, resResult) + nsResult.Summary.appendResults(*resResult.Summary) - // Aggregate all resource results summary counts to get a namespace wide count. - nsResult.Summary.Successes += resResult.Summary.Successes - nsResult.Summary.Warnings += resResult.Summary.Warnings - nsResult.Summary.Errors += resResult.Summary.Errors return nsResults } diff --git a/pkg/validator/fullaudit.go b/pkg/validator/fullaudit.go index 4aa082e6b..853698371 100644 --- a/pkg/validator/fullaudit.go +++ b/pkg/validator/fullaudit.go @@ -40,14 +40,12 @@ func RunAudit(config conf.Configuration, kubeAPI *kube.API) (AuditData, error) { return AuditData{}, err } - var clusterSuccesses, clusterErrors, clusterWarnings uint + clusterResults := ResultSummary{} // Aggregate all summary counts to get a clusterwide count. for _, nsRes := range nsResults { for _, rr := range nsRes.Results { - clusterErrors += rr.Summary.Errors - clusterWarnings += rr.Summary.Warnings - clusterSuccesses += rr.Summary.Successes + clusterResults.appendResults(*rr.Summary) } } @@ -81,11 +79,7 @@ func RunAudit(config conf.Configuration, kubeAPI *kube.API) (AuditData, error) { Nodes: len(nodes.Items), Pods: numPods, Namespaces: len(namespaces.Items), - Results: ResultSummary{ - Errors: clusterErrors, - Warnings: clusterWarnings, - Successes: clusterSuccesses, - }, + Results: clusterResults, }, NamespacedResults: nsResults, } diff --git a/pkg/validator/fullaudit_test.go b/pkg/validator/fullaudit_test.go index 223e76cd2..d3a2d301e 100644 --- a/pkg/validator/fullaudit_test.go +++ b/pkg/validator/fullaudit_test.go @@ -20,17 +20,30 @@ func TestGetTemplateData(t *testing.T) { } sum := ResultSummary{ - Successes: uint(4), + Totals: CountSummary{ + Successes: uint(4), + Warnings: uint(1), + Errors: uint(1), + }, + ByCategory: CategorySummary{}, + } + sum.ByCategory["Health Checks"] = &CountSummary{ + Successes: uint(0), Warnings: uint(1), Errors: uint(1), } + sum.ByCategory["Resources"] = &CountSummary{ + Successes: uint(4), + Warnings: uint(0), + Errors: uint(0), + } actualAudit, err := RunAudit(c, k8s) assert.Equal(t, err, nil, "error should be nil") - assert.EqualValues(t, actualAudit.ClusterSummary.Results, sum) - assert.Equal(t, len(actualAudit.NamespacedResults["test"].Results), 1, "should be equal") - assert.Equal(t, len(actualAudit.NamespacedResults["test"].Results[0].PodResults), 1, "should be equal") - assert.Equal(t, len(actualAudit.NamespacedResults["test"].Results[0].PodResults[0].ContainerResults), 1, "should be equal") - assert.Equal(t, len(actualAudit.NamespacedResults["test"].Results[0].PodResults[0].ContainerResults[0].Messages), 6, "should be equal") + assert.EqualValues(t, sum, actualAudit.ClusterSummary.Results) + assert.Equal(t, 1, len(actualAudit.NamespacedResults["test"].Results), "should be equal") + assert.Equal(t, 1, len(actualAudit.NamespacedResults["test"].Results[0].PodResults), "should be equal") + assert.Equal(t, 1, len(actualAudit.NamespacedResults["test"].Results[0].PodResults[0].ContainerResults), "should be equal") + assert.Equal(t, 6, len(actualAudit.NamespacedResults["test"].Results[0].PodResults[0].ContainerResults[0].Messages), "should be equal") } diff --git a/pkg/validator/pod.go b/pkg/validator/pod.go index 1f8ae56f5..708e243ff 100644 --- a/pkg/validator/pod.go +++ b/pkg/validator/pod.go @@ -32,10 +32,8 @@ type PodValidation struct { // ValidatePod validates that each pod conforms to the Fairwinds config, returns a ResourceResult. func ValidatePod(podConf conf.Configuration, pod *corev1.PodSpec) ResourceResult { pv := PodValidation{ - Pod: pod, - ResourceValidation: &ResourceValidation{ - Summary: &ResultSummary{}, - }, + Pod: pod, + ResourceValidation: &ResourceValidation{}, } pv.validateSecurity(&podConf.Security) @@ -51,23 +49,20 @@ func ValidatePod(podConf conf.Configuration, pod *corev1.PodSpec) ResourceResult rr := ResourceResult{ Type: "Pod", - Summary: pv.Summary, + Summary: pv.summary(), PodResults: []PodResult{pRes}, } + for _, cRes := range pRes.ContainerResults { + rr.Summary.appendResults(*cRes.Summary) + } return rr } func (pv *PodValidation) validateContainers(containers []corev1.Container, pRes *PodResult, podConf *conf.Configuration) { for _, container := range containers { - ctrRR := ValidateContainer(podConf, &container) - pv.Summary.Successes += ctrRR.Summary.Successes - pv.Summary.Warnings += ctrRR.Summary.Warnings - pv.Summary.Errors += ctrRR.Summary.Errors - pRes.ContainerResults = append( - pRes.ContainerResults, - ctrRR.ContainerResults[0], - ) + cRes := ValidateContainer(podConf, &container) + pRes.ContainerResults = append(pRes.ContainerResults, cRes) } } diff --git a/pkg/validator/pod_test.go b/pkg/validator/pod_test.go index 2c853d270..878ebeb42 100644 --- a/pkg/validator/pod_test.go +++ b/pkg/validator/pod_test.go @@ -39,7 +39,25 @@ func TestValidatePod(t *testing.T) { pod := test.MockPod() expectedSum := ResultSummary{ - Successes: uint(8), + Totals: CountSummary{ + Successes: uint(8), + 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["Resources"] = &CountSummary{ + Successes: uint(4), + Warnings: uint(0), + Errors: uint(0), + } + expectedSum.ByCategory["Security"] = &CountSummary{ + Successes: uint(2), Warnings: uint(0), Errors: uint(0), } diff --git a/pkg/validator/resource.go b/pkg/validator/resource.go index 130088707..9db8d0ed3 100644 --- a/pkg/validator/resource.go +++ b/pkg/validator/resource.go @@ -22,7 +22,6 @@ import ( // ResourceValidation contains methods shared by PodValidation and ContainerValidation type ResourceValidation struct { - Summary *ResultSummary Errors []*ResultMessage Warnings []*ResultMessage Successes []*ResultMessage @@ -36,6 +35,43 @@ func (rv *ResourceValidation) messages() []*ResultMessage { 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) { if severity == conf.SeverityError { rv.addError(message, category) @@ -48,7 +84,6 @@ func (rv *ResourceValidation) addFailure(message string, severity conf.Severity, } func (rv *ResourceValidation) addError(message string, category string) { - rv.Summary.Errors++ rv.Errors = append(rv.Errors, &ResultMessage{ Message: message, Type: MessageTypeError, @@ -57,7 +92,6 @@ func (rv *ResourceValidation) addError(message string, category string) { } func (rv *ResourceValidation) addWarning(message string, category string) { - rv.Summary.Warnings++ rv.Warnings = append(rv.Warnings, &ResultMessage{ Message: message, Type: MessageTypeWarning, @@ -66,7 +100,6 @@ func (rv *ResourceValidation) addWarning(message string, category string) { } func (rv *ResourceValidation) addSuccess(message string, category string) { - rv.Summary.Successes++ rv.Successes = append(rv.Successes, &ResultMessage{ Message: message, Type: MessageTypeSuccess, diff --git a/pkg/validator/types.go b/pkg/validator/types.go index 482f2cd64..ef9e10b37 100644 --- a/pkg/validator/types.go +++ b/pkg/validator/types.go @@ -46,22 +46,52 @@ type ResourceResult struct { PodResults []PodResult } -// ResultSummary provides a high level overview of success, warnings, and errors. -type ResultSummary struct { +// CountSummary provides a high level overview of success, warnings, and errors. +type CountSummary struct { Successes uint Warnings uint Errors uint } +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) + } +} + // 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 } @@ -72,8 +102,3 @@ type ResultMessage struct { Type MessageType Category string } - -// Score represents a percentage of validations that were successful. -func (rs *ResultSummary) Score() uint { - return uint(float64(rs.Successes) / float64(rs.Successes+rs.Warnings+rs.Errors) * 100) -} diff --git a/pkg/webhook/validator.go b/pkg/webhook/validator.go index d342e2136..1073d5b6d 100644 --- a/pkg/webhook/validator.go +++ b/pkg/webhook/validator.go @@ -103,7 +103,7 @@ func (v *Validator) Handle(ctx context.Context, req types.Request) types.Respons return admission.ErrorResponse(http.StatusBadRequest, err) } - if results.Summary.Errors > 0 { + if results.Summary.Totals.Errors > 0 { // TODO: Decide what message we want to return here. allowed, reason = false, "failed validation checks, view details on dashbaord." } diff --git a/public/css/main.css b/public/css/main.css index 227394721..35040937c 100644 --- a/public/css/main.css +++ b/public/css/main.css @@ -99,6 +99,10 @@ body { margin-top: 10px; } +.cluster .expandable-table ul.message-list { + margin: 10px 26px; +} + #clusterScoreChart { width: 550px; position: relative; @@ -223,35 +227,40 @@ ul.message-list li i { color: #a11f4c; } -.namespace td.status-bar { +.namespace .status-bar { vertical-align: top; padding-top: 18px; } +.namespace .status-bar .status { + float: right; + animation: fadeIn 2s; +} +.cluster .status { + width: 280px; +} .namespace .status { - float: right; width: 200px; } -.namespace .status div { +.status div { height: 15px; border-radius: 10px; } -.namespace .status .passing { +.status .passing { background-color: #8BD2DC; float: left; } -.namespace .status .warning { +.status .warning { background-color: #f26c21; float: left; } -.namespace .status .failing { +.status .failing { background-color: #a11f4c; - width: 200px; - animation: fadeIn 2s; + width: 100%; } @keyframes fadeIn { diff --git a/public/js/charts.js b/public/js/charts.js index c51e4b81f..aa88f8ae8 100644 --- a/public/js/charts.js +++ b/public/js/charts.js @@ -5,9 +5,9 @@ $(function () { labels: ["Passing", "Warning", "Error"], datasets: [{ data: [ - fairwindsAuditData.ClusterSummary.Results.Successes, - fairwindsAuditData.ClusterSummary.Results.Warnings, - fairwindsAuditData.ClusterSummary.Results.Errors, + fairwindsAuditData.ClusterSummary.Results.Totals.Successes, + fairwindsAuditData.ClusterSummary.Results.Totals.Warnings, + fairwindsAuditData.ClusterSummary.Results.Totals.Errors, ], backgroundColor: ['#8BD2DC', '#f26c21', '#a11f4c'], }]