mirror of
https://github.com/FairwindsOps/polaris.git
synced 2026-05-17 06:36:48 +00:00
304 lines
8.1 KiB
Go
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
|
|
}
|