Merge pull request #65 from reactiveops/rb/output-structure

Add categories to dashboard
This commit is contained in:
Bobby Brennan
2019-04-24 13:08:27 -04:00
committed by GitHub
14 changed files with 206 additions and 135 deletions

View File

@@ -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 {

View File

@@ -47,9 +47,9 @@
</div>
<div class="result-messages">
<ul class="message-list">
<li class="success"><i class="fas fa-check"></i> {{ .AuditData.ClusterSummary.Results.Successes }} checks passed</li>
<li class="warning"><i class="fas fa-exclamation"></i> {{ .AuditData.ClusterSummary.Results.Warnings }} checks had warnings</li>
<li class="error"><i class="fas fa-times"></i> {{ .AuditData.ClusterSummary.Results.Errors }} checks had errors</li>
<li class="success"><i class="fas fa-check"></i> {{ .AuditData.ClusterSummary.Results.Totals.Successes }} checks passed</li>
<li class="warning"><i class="fas fa-exclamation"></i> {{ .AuditData.ClusterSummary.Results.Totals.Warnings }} checks had warnings</li>
<li class="error"><i class="fas fa-times"></i> {{ .AuditData.ClusterSummary.Results.Totals.Errors }} checks had errors</li>
</ul>
</div>
<canvas id="clusterScoreChart"></canvas>
@@ -80,7 +80,32 @@
</div>
</td>
</tr>
<tr>
<td class="resource-info">
<div class="name"><span class="caret-expander"></span>Health summary</div>
<div class="expandable-content">
<ul class="message-list">
{{ range $category, $summary := .AuditData.ClusterSummary.Results.ByCategory }}
<li>
<span class="detail-label">{{ $category }}</span>
<span class="detail-value">{{ $summary.Errors }} errors, {{ $summary.Warnings }} warnings</span>
<div class="status-bar">
<div class="status">
<div class="failing">
<div class="warning" style="width: {{ getWarningWidth $summary 280 }}px;">
<div class="passing" style="width: {{ getSuccessWidth $summary 280 }}px;"></div>
</div>
</div>
</div>
</div>
</li>
{{ end }}
</ul>
</div>
</td>
</tr>
</table>
</div>
{{ range $namespace, $results := .AuditData.NamespacedResults }}
@@ -116,8 +141,8 @@
<td class="status-bar">
<div class="status">
<div class="failing">
<div class="warning" style="width: {{ getWarningWidth .Summary 200 }}px;">
<div class="passing" style="width: {{ getSuccessWidth .Summary 200 }}px;"></div>
<div class="warning" style="width: {{ getWarningWidth .Summary.Totals 200 }}px;">
<div class="passing" style="width: {{ getSuccessWidth .Summary.Totals 200 }}px;"></div>
</div>
</div>
</div>

View File

@@ -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) {

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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,
}

View File

@@ -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")
}

View File

@@ -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)
}
}

View File

@@ -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),
}

View File

@@ -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,

View File

@@ -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)
}

View File

@@ -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."
}

View File

@@ -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 {
.namespace .status-bar .status {
float: right;
animation: fadeIn 2s;
}
.cluster .status {
width: 280px;
}
.namespace .status {
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 {

View File

@@ -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'],
}]