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