Refactor validation

This commit is contained in:
Robert Brennan
2020-01-03 17:17:05 +00:00
parent 5cd52242c4
commit 2770be643f
17 changed files with 835 additions and 1134 deletions

11
main.go
View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

183
pkg/validator/output.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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