repo_description: Validation of best practices in your Kubernetes clusters
repo_topics: audit best-practices cluster dashboard fairwinds-official kubernetes
code: // Copyright 2022 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 config
  
  import (
  	"bytes"
  	"context"
  	"encoding/json"
  	"errors"
  	"fmt"
  	"io"
  	"maps"
  	"slices"
  	"strings"
  	"text/template"
  
  	"github.com/qri-io/jsonpointer"
  	"github.com/qri-io/jsonschema"
  	"github.com/thoas/go-funk"
  	corev1 "k8s.io/api/core/v1"
  	"k8s.io/apimachinery/pkg/api/resource"
  	k8sYaml "k8s.io/apimachinery/pkg/util/yaml"
  )
  
  // TargetKind represents the part of the config to be validated
  type TargetKind string
  
  const (
  	// TargetController points to the controller's spec
  	TargetController TargetKind = "Controller"
  	// TargetContainer points to the container spec
  	TargetContainer TargetKind = "Container"
  	// TargetPodSpec points to the pod spec
  	TargetPodSpec TargetKind = "PodSpec"
  	// TargetPodTemplate points to the pod template
  	TargetPodTemplate TargetKind = "PodTemplate"
  )
  
  // HandledTargets is a list of target names that are explicitly handled
  var HandledTargets = []TargetKind{
  	TargetController,
  	TargetContainer,
  	TargetPodSpec,
  	TargetPodTemplate,
  }
  
  // Mutation defines how to change a YAML file, in the style of JSON Patch
  type Mutation struct {
  	Path    string
  	Op      string
  	Value   any
  	Comment string
  }
  
  // SchemaCheck is a Polaris check that runs using JSON Schema
  type SchemaCheck struct {
  	ID                      string                       `yaml:"id" json:"id"`
  	Category                string                       `yaml:"category" json:"category"`
  	SuccessMessage          string                       `yaml:"successMessage" json:"successMessage"`
  	FailureMessage          string                       `yaml:"failureMessage" json:"failureMessage"`
  	Controllers             includeExcludeList           `yaml:"controllers" json:"controllers"`
  	Containers              includeExcludeList           `yaml:"containers" json:"containers"`
  	Target                  TargetKind                   `yaml:"target" json:"target"`
  	SchemaTarget            TargetKind                   `yaml:"schemaTarget" json:"schemaTarget"`
  	Schema                  map[string]any               `yaml:"schema" json:"schema"`
  	SchemaString            string                       `yaml:"schemaString" json:"schemaString"`
  	Validator               jsonschema.Schema            `yaml:"-" json:"-"`
  	AdditionalSchemas       map[string]map[string]any    `yaml:"additionalSchemas" json:"additionalSchemas"`
  	AdditionalSchemaStrings map[string]string            `yaml:"additionalSchemaStrings" json:"additionalSchemaStrings"`
  	AdditionalValidators    map[string]jsonschema.Schema `yaml:"-" json:"-"`
  	Mutations               []Mutation                   `yaml:"mutations" json:"mutations"`
  }
  
  type resourceMinimum string
  type resourceMaximum string
  
  // UnmarshalYAMLOrJSON is a helper function to unmarshal data in an arbitrary format
  func UnmarshalYAMLOrJSON(raw []byte, dest any) error {
  	reader := bytes.NewReader(raw)
  	d := k8sYaml.NewYAMLOrJSONDecoder(reader, 4096)
  	for {
  		if err := d.Decode(dest); err != nil {
  			if err == io.EOF {
  				break
  			}
  			return fmt.Errorf("Decoding schema check failed: %v", err)
  		}
  	}
  	return nil
  }
  
  // ParseCheck parses a check from a byte array
  func ParseCheck(id string, rawBytes []byte) (SchemaCheck, error) {
  	check := SchemaCheck{}
  	err := UnmarshalYAMLOrJSON(rawBytes, &check)
  	if err != nil {
  		return check, err
  	}
  	check.Initialize(id)
  	return check, nil
  }
  
  func init() {
  	jsonschema.RegisterKeyword("resourceMinimum", newResourceMinimum)
  	jsonschema.RegisterKeyword("resourceMaximum", newResourceMaximum)
  	jsonschema.LoadDraft2019_09()
  }
  
  type includeExcludeList struct {
  	Include []string `yaml:"include"`
  	Exclude []string `yaml:"exclude"`
  }
  
  func newResourceMinimum() jsonschema.Keyword {
  	return new(resourceMinimum)
  }
  
  func newResourceMaximum() jsonschema.Keyword {
  	return new(resourceMaximum)
  }
  
  func (min resourceMinimum) ValidateKeyword(ctx context.Context, currentState *jsonschema.ValidationState, data any) {
  	err := validateRange(string(min), data, true)
  	if err != nil {
  		errs := currentState.Errs
  		*errs = append(*errs, *err...)
  		currentState.Errs = errs
  	}
  }
  func (max resourceMaximum) ValidateKeyword(ctx context.Context, currentState *jsonschema.ValidationState, data any) {
  	err := validateRange(string(max), data, false)
  	if err != nil {
  		errs := currentState.Errs
  		*errs = append(*errs, *err...)
  		currentState.Errs = errs
  	}
  }
  
  func (min resourceMinimum) Resolve(pointer jsonpointer.Pointer, uri string) *jsonschema.Schema {
  	// Not implemented
  	return nil
  }
  
  func (min resourceMinimum) Register(uri string, registry *jsonschema.SchemaRegistry) {
  	// Not implemented
  }
  
  func (max resourceMaximum) Resolve(pointer jsonpointer.Pointer, uri string) *jsonschema.Schema {
  	// Not implemented
  	return nil
  }
  func (max resourceMaximum) Register(uri string, registry *jsonschema.SchemaRegistry) {
  	// Not implemented
  }
  
  func parseQuantity(i any) (resource.Quantity, *[]jsonschema.KeyError) {
  	if resNum, ok := i.(float64); ok {
  		i = fmt.Sprintf("%f", resNum)
  	}
  	resStr, ok := i.(string)
  	if !ok {
  		return resource.Quantity{}, &[]jsonschema.KeyError{
  			{Message: fmt.Sprintf("Resource quantity %v is not a string", i)},
  		}
  	}
  	q, err := resource.ParseQuantity(resStr)
  	if err != nil {
  		return resource.Quantity{}, &[]jsonschema.KeyError{
  			{Message: fmt.Sprintf("Could not parse resource quantity: %s", resStr)},
  		}
  	}
  	return q, nil
  }
  
  func validateRange(limit any, data any, isMinimum bool) *[]jsonschema.KeyError {
  	limitQuantity, err := parseQuantity(limit)
  	if err != nil {
  		return err
  	}
  	actualQuantity, err := parseQuantity(data)
  	if err != nil {
  		return err
  	}
  	cmp := limitQuantity.Cmp(actualQuantity)
  	if isMinimum {
  		if cmp == 1 {
  			return &[]jsonschema.KeyError{
  				{Message: fmt.Sprintf("quantity %v is > %v", actualQuantity, limitQuantity)},
  			}
  		}
  	} else {
  		if cmp == -1 {
  			return &[]jsonschema.KeyError{
  				{Message: fmt.Sprintf("quantity %v is < %v", actualQuantity, limitQuantity)},
  			}
  		}
  	}
  	return nil
  }
  
  // Initialize sets up the schema
  func (check *SchemaCheck) Initialize(id string) error {
  	check.ID = id
  	if check.SchemaString == "" {
  		jsonBytes, err := json.Marshal(check.Schema)
  		if err != nil {
  			return err
  		}
  		check.SchemaString = string(jsonBytes)
  	}
  	if check.AdditionalSchemaStrings == nil {
  		check.AdditionalSchemaStrings = make(map[string]string)
  	}
  	for kind, schema := range check.AdditionalSchemas {
  		jsonBytes, err := json.Marshal(schema)
  		if err != nil {
  			return err
  		}
  		check.AdditionalSchemaStrings[kind] = string(jsonBytes)
  	}
  	check.Schema = map[string]any{}
  	check.AdditionalSchemas = map[string]map[string]any{}
  	return nil
  }
  
  // TemplateForResource fills out a check's templated fields given a particular resource
  func (check SchemaCheck) TemplateForResource(res any) (*SchemaCheck, error) {
  	newCheck := check // Make a copy of the check, since we're going to modify the schema
  
  	templateStrings := map[string]string{
  		"": newCheck.SchemaString,
  	}
  	maps.Copy(templateStrings, newCheck.AdditionalSchemaStrings)
  	newCheck.SchemaString = ""
  	newCheck.AdditionalSchemaStrings = map[string]string{}
  
  	for kind, tmplString := range templateStrings {
  		tmpl := template.New(newCheck.ID).Funcs(template.FuncMap{
  			"hasPrefix": strings.HasPrefix,
  			"hasSuffix": strings.HasSuffix,
  		})
  		tmpl, err := tmpl.Parse(tmplString)
  		if err != nil {
  			return nil, err
  		}
  		w := bytes.Buffer{}
  		err = tmpl.Execute(&w, res)
  		if err != nil {
  			return nil, err
  		}
  		templated := w.String()
  		if strings.TrimSpace(templated) == "" {
  			continue
  		}
  
  		if kind == "" {
  			newCheck.SchemaString = templated
  		} else {
  			newCheck.AdditionalSchemaStrings[kind] = templated
  		}
  	}
  
  	newCheck.AdditionalValidators = map[string]jsonschema.Schema{}
  	for kind, schemaStr := range newCheck.AdditionalSchemaStrings {
  		val := jsonschema.Schema{}
  		err := UnmarshalYAMLOrJSON([]byte(schemaStr), &val)
  		if err != nil {
  			return nil, err
  		}
  		newCheck.AdditionalValidators[kind] = val
  	}
  	err := UnmarshalYAMLOrJSON([]byte(newCheck.SchemaString), &newCheck.Validator)
  	if err != nil {
  		return nil, err
  	}
  	return &newCheck, err
  }
  
  // CheckPodSpec checks a pod spec against the schema
  func (check SchemaCheck) CheckPodSpec(ctx context.Context, pod *corev1.PodSpec) (bool, []jsonschema.KeyError, error) {
  	return check.CheckObject(ctx, pod)
  }
  
  // CheckPodTemplate checks a pod template against the schema
  func (check SchemaCheck) CheckPodTemplate(ctx context.Context, podTemplate any) (bool, []jsonschema.KeyError, error) {
  	return check.CheckObject(ctx, podTemplate)
  }
  
  // CheckController checks a controler's spec against the schema
  func (check SchemaCheck) CheckController(ctx context.Context, bytes []byte) (bool, []jsonschema.KeyError, error) {
  	errs, err := check.Validator.ValidateBytes(ctx, bytes)
  	return len(errs) == 0, errs, err
  }
  
  // CheckContainer checks a container spec against the schema
  func (check SchemaCheck) CheckContainer(ctx context.Context, container *corev1.Container) (bool, []jsonschema.KeyError, error) {
  	return check.CheckObject(ctx, container)
  }
  
  // CheckObject checks arbitrary data against the schema
  func (check SchemaCheck) CheckObject(ctx context.Context, obj any) (bool, []jsonschema.KeyError, error) {
  	bytes, err := json.Marshal(obj)
  	if err != nil {
  		return false, nil, err
  	}
  	errs, err := check.Validator.ValidateBytes(ctx, bytes)
  	return len(errs) == 0, errs, err
  }
  
  // CheckAdditionalObjects looks for an object that passes the specified additional schema
  func (check SchemaCheck) CheckAdditionalObjects(ctx context.Context, groupkind string, objects []any) (bool, error) {
  	val, ok := check.AdditionalValidators[groupkind]
  	if !ok {
  		return false, errors.New("No validator found for " + groupkind)
  	}
  	for _, obj := range objects {
  		bytes, err := json.Marshal(obj)
  		if err != nil {
  			return false, err
  		}
  		errs, err := val.ValidateBytes(ctx, bytes)
  		if err != nil {
  			return false, err
  		}
  		if len(errs) == 0 {
  			return true, nil
  		}
  	}
  	return false, nil
  }
  
  // IsActionable decides if this check applies to a particular target
  func (check SchemaCheck) IsActionable(target TargetKind, kind string, isInit bool) bool {
  	if funk.Contains(HandledTargets, target) {
  		if check.Target == TargetPodTemplate && target == TargetPodSpec {
  			// A target=PodSpec and check.Target=PodTemplate is expected
  			// because applyPodSchemaChecks() explicitly sets check.Target
  			return true
  		}
  		if check.Target != target {
  			return false
  		}
  	} else if string(check.Target) != kind && !strings.HasSuffix(string(check.Target), "/"+kind) {
  		return false
  	}
  	isIncluded := len(check.Controllers.Include) == 0
  	if slices.Contains(check.Controllers.Include, kind) {
  		isIncluded = true
  	}
  	if !isIncluded {
  		return false
  	}
  	if slices.Contains(check.Controllers.Exclude, kind) {
  		return false
  	}
  	if check.Target == TargetContainer {
  		isIncluded := len(check.Containers.Include) == 0
  		for _, inclusion := range check.Containers.Include {
  			if (inclusion == "initContainer" && isInit) || (inclusion == "container" && !isInit) {
  				isIncluded = true
  				break
  			}
  		}
  		if !isIncluded {
  			return false
  		}
  		for _, exclusion := range check.Containers.Exclude {
  			if (exclusion == "initContainer" && isInit) || (exclusion == "container" && !isInit) {
  				return false
  			}
  		}
  	}
  	return true
  }
  