Files
polaris/pkg/validator/schema.go
Robert Brennan 9120949f62 tests passing
2021-03-17 21:06:17 +00:00

304 lines
8.1 KiB
Go

package validator
import (
"bytes"
"fmt"
"io"
"sort"
"strings"
"github.com/gobuffalo/packr/v2"
corev1 "k8s.io/api/core/v1"
metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/yaml"
"github.com/fairwindsops/polaris/pkg/config"
"github.com/fairwindsops/polaris/pkg/kube"
)
var (
schemaBox = (*packr.Box)(nil)
builtInChecks = map[string]config.SchemaCheck{}
// We explicitly set the order to avoid thrash in the
// tests as we migrate toward JSON schema
checkOrder = []string{
// Controller Checks
"multipleReplicasForDeployment",
// Pod checks
"hostIPCSet",
"hostPIDSet",
"hostNetworkSet",
// Container checks
"memoryLimitsMissing",
"memoryRequestsMissing",
"cpuLimitsMissing",
"cpuRequestsMissing",
"readinessProbeMissing",
"livenessProbeMissing",
"pullPolicyNotAlways",
"tagNotSpecified",
"hostPortSet",
"runAsRootAllowed",
"runAsPrivileged",
"notReadOnlyRootFilesystem",
"privilegeEscalationAllowed",
"dangerousCapabilities",
"insecureCapabilities",
"priorityClassNotSet",
// Other checks
"tlsSettingsMissing",
"pdbDisruptionsAllowedGreaterThanZero",
}
)
type schemaTestCase struct {
Target config.TargetKind
Resource kube.GenericResource
IsInitContianer bool
Container *corev1.Container
}
func (test schemaTestCase) getName() string {
name := fmt.Sprintf("%s/%s/%s", test.Resource.Kind, test.Resource.ObjectMeta.GetNamespace(), test.Resource.ObjectMeta.GetName())
if test.Container != nil {
name += "/" + test.Container.Name
}
return name + ": " + string(test.Target)
}
func init() {
schemaBox = packr.New("Schemas", "../../checks")
for _, checkID := range checkOrder {
contents, err := schemaBox.Find(checkID + ".yaml")
if err != nil {
panic(err)
}
check, err := parseCheck(contents)
if err != nil {
panic(err)
}
check.ID = checkID
builtInChecks[checkID] = check
}
}
func parseCheck(rawBytes []byte) (config.SchemaCheck, error) {
reader := bytes.NewReader(rawBytes)
check := config.SchemaCheck{}
d := yaml.NewYAMLOrJSONDecoder(reader, 4096)
for {
if err := d.Decode(&check); err != nil {
if err == io.EOF {
return check, nil
}
return check, fmt.Errorf("Decoding schema check failed: %v", err)
}
}
}
func resolveCheck(conf *config.Configuration, checkID string, test schemaTestCase) (*config.SchemaCheck, error) {
if !conf.DisallowExemptions && hasExemptionAnnotation(test.Resource.ObjectMeta, checkID) {
return nil, nil
}
check, ok := conf.CustomChecks[checkID]
if !ok {
check, ok = builtInChecks[checkID]
}
if !ok {
return nil, fmt.Errorf("Check %s not found", checkID)
}
containerName := ""
if test.Container != nil {
containerName = test.Container.Name
}
if !conf.IsActionable(check.ID, test.Resource.ObjectMeta, containerName) {
return nil, nil
}
if !check.IsActionable(test.Target, test.Resource.Kind, test.IsInitContianer) {
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,
Success: passes,
}
if passes {
result.Message = check.SuccessMessage
} else {
result.Message = check.FailureMessage
}
return result
}
const exemptionAnnotationKey = "polaris.fairwinds.com/exempt"
const exemptionAnnotationPattern = "polaris.fairwinds.com/%s-exempt"
func hasExemptionAnnotation(objMeta metaV1.Object, checkID string) bool {
annot := objMeta.GetAnnotations()
val := annot[exemptionAnnotationKey]
if strings.ToLower(val) == "true" {
return true
}
checkKey := fmt.Sprintf(exemptionAnnotationPattern, checkID)
val = annot[checkKey]
if strings.ToLower(val) == "true" {
return true
}
return false
}
func ApplyAllSchemaChecks(conf *config.Configuration, resource kube.GenericResource) (Result, error) {
if resource.PodSpec == nil {
return applyNonControllerSchemaChecks(conf, resource)
} else {
return applyControllerSchemaChecks(conf, resource)
}
}
func applyNonControllerSchemaChecks(conf *config.Configuration, resource kube.GenericResource) (Result, error) {
finalResult := Result{
Kind: resource.Kind,
Name: resource.ObjectMeta.GetName(),
Namespace: resource.ObjectMeta.GetNamespace(),
}
resultSet, err := applyTopLevelSchemaChecks(conf, resource, false)
finalResult.Results = resultSet
return finalResult, err
}
func applyControllerSchemaChecks(conf *config.Configuration, resource kube.GenericResource) (Result, error) {
finalResult := Result{
Kind: resource.Kind,
Name: resource.ObjectMeta.GetName(),
Namespace: resource.ObjectMeta.GetNamespace(),
}
resultSet, err := applyTopLevelSchemaChecks(conf, resource, true)
if err != nil {
return finalResult, err
}
finalResult.Results = resultSet
podRS, err := applyPodSchemaChecks(conf, resource)
if err != nil {
return finalResult, err
}
podRes := PodResult{
Results: podRS,
ContainerResults: []ContainerResult{},
}
for _, container := range resource.PodSpec.InitContainers {
results, err := applyContainerSchemaChecks(conf, resource, &container, true)
if err != nil {
return finalResult, err
}
cRes := ContainerResult{
Name: container.Name,
Results: results,
}
podRes.ContainerResults = append(podRes.ContainerResults, cRes)
}
for _, container := range resource.PodSpec.Containers {
results, err := applyContainerSchemaChecks(conf, resource, &container, false)
if err != nil {
return finalResult, err
}
cRes := ContainerResult{
Name: container.Name,
Results: results,
}
podRes.ContainerResults = append(podRes.ContainerResults, cRes)
}
finalResult.PodResult = &podRes
return finalResult, nil
}
func applyTopLevelSchemaChecks(conf *config.Configuration, res kube.GenericResource, isController bool) (ResultSet, error) {
test := schemaTestCase{
Resource: res,
}
if isController {
test.Target = config.TargetController
}
return applySchemaChecks(conf, test)
}
func applyPodSchemaChecks(conf *config.Configuration, controller kube.GenericResource) (ResultSet, error) {
test := schemaTestCase{
Target: config.TargetPod,
Resource: controller,
}
return applySchemaChecks(conf, test)
}
func applyContainerSchemaChecks(conf *config.Configuration, controller kube.GenericResource, container *corev1.Container, isInit bool) (ResultSet, error) {
test := schemaTestCase{
Target: config.TargetContainer,
Resource: controller,
Container: container,
}
return applySchemaChecks(conf, test)
}
func applySchemaChecks(conf *config.Configuration, test schemaTestCase) (ResultSet, error) {
results := ResultSet{}
checkIDs := getSortedKeys(conf.Checks)
for _, checkID := range checkIDs {
result, err := applySchemaCheck(conf, checkID, test)
if err != nil {
return results, err
}
if result != nil {
results[checkID] = *result
}
}
return results, nil
}
func applySchemaCheck(conf *config.Configuration, checkID string, test schemaTestCase) (*ResultMessage, error) {
check, err := resolveCheck(conf, checkID, test)
if err != nil {
return nil, err
} else if check == nil {
return nil, nil
}
var passes bool
if check.SchemaTarget != "" {
if check.SchemaTarget == config.TargetPod && check.Target == config.TargetContainer {
podCopy := *test.Resource.PodSpec
podCopy.InitContainers = []corev1.Container{}
podCopy.Containers = []corev1.Container{*test.Container}
passes, err = check.CheckPod(&podCopy)
} else {
return nil, fmt.Errorf("Unknown combination of target (%s) and schema target (%s)", check.Target, check.SchemaTarget)
}
} else if check.Target == config.TargetPod {
passes, err = check.CheckPod(test.Resource.PodSpec)
} else if check.Target == config.TargetContainer {
passes, err = check.CheckContainer(test.Container)
} else {
passes, err = check.CheckObject(test.Resource.Resource.Object)
}
if err != nil {
return nil, err
}
result := makeResult(conf, check, passes)
return &result, nil
}
func getSortedKeys(m map[string]config.Severity) []string {
keys := make([]string, 0, len(m))
for key := range m {
keys = append(keys, key)
}
sort.Strings(keys)
return keys
}