Skip to content

Commit

Permalink
Add the capability for controller level checks (#285)
Browse files Browse the repository at this point in the history
* Add controller level checks

* Add check for multipleReplicas

* Fixed spec

* Add controller level check

* Move controller schema checks to their own function.
  • Loading branch information
baderbuddy committed May 18, 2020
1 parent de98d9f commit d50d9c8
Show file tree
Hide file tree
Showing 13 changed files with 240 additions and 71 deletions.
26 changes: 26 additions & 0 deletions checks/multipleReplicasForDeployment.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
successMessage: Multiple replicas are scheduled
failureMessage: Only one replica is scheduled
category: Reliability
target: Controller
controllers:
include:
- Deployment
schema:
'$schema': http:https://json-schema.org/draft-07/schema
type: object
required:
- Object
properties:
Object:
type: object
required:
- spec
properties:
spec:
type: object
required:
- replicas
properties:
replicas:
type: integer
minimum: 2
2 changes: 2 additions & 0 deletions examples/config-full.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
checks:
# reliability
multipleReplicasForDeployment: warning
# resources
cpuRequestsMissing: warning
cpuLimitsMissing: warning
Expand Down
2 changes: 2 additions & 0 deletions examples/config.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
checks:
# reliability
multipleReplicasForDeployment: ignore
# resources
cpuRequestsMissing: warning
cpuLimitsMissing: warning
Expand Down
8 changes: 8 additions & 0 deletions pkg/config/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ const (
TargetContainer TargetKind = "Container"
// TargetPod points to the pod spec
TargetPod TargetKind = "Pod"
// TargetController points to the controller's spec
TargetController TargetKind = "Controller"
)

// SchemaCheck is a Polaris check that runs using JSON Schema
Expand Down Expand Up @@ -128,6 +130,12 @@ func (check SchemaCheck) CheckPod(pod *corev1.PodSpec) (bool, error) {
return check.CheckObject(pod)
}

// CheckController checks a controler's spec against the schema
func (check SchemaCheck) CheckController(bytes []byte) (bool, error) {
errs, err := check.Schema.ValidateBytes(bytes)
return len(errs) == 0, err
}

// CheckContainer checks a container spec against the schema
func (check SchemaCheck) CheckContainer(container *corev1.Container) (bool, error) {
return check.CheckObject(container)
Expand Down
45 changes: 31 additions & 14 deletions pkg/kube/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
k8sYaml "k8s.io/apimachinery/pkg/util/yaml"
"k8s.io/client-go/dynamic"
Expand Down Expand Up @@ -146,7 +147,13 @@ func CreateResourceProviderFromAPI(kube kubernetes.Interface, clusterName string
}
restMapper := restmapper.NewDiscoveryRESTMapper(resources)

objectCache := map[string]metav1.Object{}
objectCache := map[string]unstructured.Unstructured{}

controllers, err := LoadControllers(pods.Items, dynamic, &restMapper, objectCache)
if err != nil {
logrus.Errorf("Error loading controllers from pods: %v", err)
return nil, err
}

api := ResourceProvider{
ServerVersion: serverVersion.Major + "." + serverVersion.Minor,
Expand All @@ -155,12 +162,12 @@ func CreateResourceProviderFromAPI(kube kubernetes.Interface, clusterName string
CreationTime: time.Now(),
Nodes: nodes.Items,
Namespaces: namespaces.Items,
Controllers: LoadControllers(pods.Items, dynamic, &restMapper, objectCache),
Controllers: controllers,
}
return &api, nil
}

func cacheAllObjectsOfKind(dynamicClient dynamic.Interface, groupVersionResource schema.GroupVersionResource, objectCache map[string]metav1.Object) error {
func cacheAllObjectsOfKind(dynamicClient dynamic.Interface, groupVersionResource schema.GroupVersionResource, objectCache map[string]unstructured.Unstructured) error {
objects, err := dynamicClient.Resource(groupVersionResource).Namespace("").List(metav1.ListOptions{})
if err != nil {
logrus.Warnf("Error retrieving parent object API %s and Kind %s because of error: %v ", groupVersionResource.Version, groupVersionResource.Resource, err)
Expand All @@ -169,18 +176,13 @@ func cacheAllObjectsOfKind(dynamicClient dynamic.Interface, groupVersionResource
for idx, object := range objects.Items {

key := fmt.Sprintf("%s/%s/%s", object.GetKind(), object.GetNamespace(), object.GetName())
objMeta, err := meta.Accessor(&objects.Items[idx])
if err != nil {
logrus.Warnf("Error converting object to meta object %s %v", object.GetName(), err)
return err
}
objectCache[key] = objMeta
objectCache[key] = objects.Items[idx]
}
return nil
}

// LoadControllers loads a list of controllers from the kubeResources Pods
func LoadControllers(pods []corev1.Pod, dynamicClientPointer *dynamic.Interface, restMapperPointer *meta.RESTMapper, objectCache map[string]metav1.Object) []GenericWorkload {
func LoadControllers(pods []corev1.Pod, dynamicClientPointer *dynamic.Interface, restMapperPointer *meta.RESTMapper, objectCache map[string]unstructured.Unstructured) ([]GenericWorkload, error) {
interfaces := []GenericWorkload{}
deduped := map[string]corev1.Pod{}
for _, pod := range pods {
Expand All @@ -192,9 +194,13 @@ func LoadControllers(pods []corev1.Pod, dynamicClientPointer *dynamic.Interface,
deduped[pod.ObjectMeta.Namespace+"/"+owners[0].Kind+"/"+owners[0].Name] = pod
}
for _, pod := range deduped {
interfaces = append(interfaces, NewGenericWorkload(pod, dynamicClientPointer, restMapperPointer, objectCache))
workload, err := NewGenericWorkload(pod, dynamicClientPointer, restMapperPointer, objectCache)
if err != nil {
return nil, err
}
interfaces = append(interfaces, workload)
}
return deduplicateControllers(interfaces)
return deduplicateControllers(interfaces), nil
}

// Because the controllers with an Owner take on the name of the Owner, this eliminates any duplicates.
Expand Down Expand Up @@ -243,7 +249,14 @@ func addResourceFromString(contents string, resources *ResourceProvider) error {
} else if resource.Kind == "Pod" {
pod := corev1.Pod{}
err = decoder.Decode(&pod)
resources.Controllers = append(resources.Controllers, NewGenericWorkloadFromPod(pod))
if err != nil {
return err
}
workload, err := NewGenericWorkloadFromPod(pod, pod)
if err != nil {
return err
}
resources.Controllers = append(resources.Controllers, workload)
} else {
yamlNode := make(map[string]interface{})
err = yaml.Unmarshal(contentBytes, &yamlNode)
Expand All @@ -264,7 +277,11 @@ func addResourceFromString(contents string, resources *ResourceProvider) error {
decoder := k8sYaml.NewYAMLOrJSONDecoder(bytes.NewReader(marshaledYaml), 1000)
pod := corev1.Pod{}
err = decoder.Decode(&pod)
newController := NewGenericWorkloadFromPod(pod)
newController, err := NewGenericWorkloadFromPod(pod, yamlNode)

if err != nil {
return err
}
newController.Kind = resource.Kind
resources.Controllers = append(resources.Controllers, newController)
}
Expand Down
82 changes: 55 additions & 27 deletions pkg/kube/workload.go
Original file line number Diff line number Diff line change
@@ -1,41 +1,55 @@
package kube

import (
"encoding/json"
"errors"
"fmt"

"github.com/sirupsen/logrus"
kubeAPICoreV1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/meta"
kubeAPIMetaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic"
)

// GenericWorkload is a base implementation with some free methods for inherited structs
type GenericWorkload struct {
Kind string
PodSpec kubeAPICoreV1.PodSpec
ObjectMeta kubeAPIMetaV1.Object
Kind string
PodSpec kubeAPICoreV1.PodSpec
ObjectMeta kubeAPIMetaV1.Object
OriginalObjectJSON []byte
}

// NewGenericWorkloadFromPod builds a new workload for a given Pod without looking at parents
func NewGenericWorkloadFromPod(originalResource kubeAPICoreV1.Pod) GenericWorkload {
func NewGenericWorkloadFromPod(podResource kubeAPICoreV1.Pod, originalObject interface{}) (GenericWorkload, error) {
workload := GenericWorkload{}
workload.PodSpec = originalResource.Spec
workload.ObjectMeta = originalResource.ObjectMeta.GetObjectMeta()
workload.PodSpec = podResource.Spec
workload.ObjectMeta = podResource.ObjectMeta.GetObjectMeta()
workload.Kind = "Pod"
return workload
if originalObject != nil {
bytes, err := json.Marshal(originalObject)
if err != nil {
return workload, err
}
workload.OriginalObjectJSON = bytes
}
return workload, nil
}

// NewGenericWorkload builds a new workload for a given Pod
func NewGenericWorkload(originalResource kubeAPICoreV1.Pod, dynamicClientPointer *dynamic.Interface, restMapperPointer *meta.RESTMapper, objectCache map[string]kubeAPIMetaV1.Object) GenericWorkload {
workload := NewGenericWorkloadFromPod(originalResource)

func NewGenericWorkload(podResource kubeAPICoreV1.Pod, dynamicClientPointer *dynamic.Interface, restMapperPointer *meta.RESTMapper, objectCache map[string]unstructured.Unstructured) (GenericWorkload, error) {
workload, err := NewGenericWorkloadFromPod(podResource, nil)
if err != nil {
return workload, err
}
dynamicClient := *dynamicClientPointer
restMapper := *restMapperPointer
// If an owner exists then set the name to the workload.
// This allows us to handle CRDs creating Workloads or DeploymentConfigs in OpenShift.
owners := workload.ObjectMeta.GetOwnerReferences()
lastKey := ""
for len(owners) > 0 {
if len(owners) > 1 {
logrus.Warn("More than 1 owner found")
Expand All @@ -46,48 +60,62 @@ func NewGenericWorkload(originalResource kubeAPICoreV1.Pod, dynamicClientPointer
}
workload.Kind = firstOwner.Kind
key := fmt.Sprintf("%s/%s/%s", firstOwner.Kind, workload.ObjectMeta.GetNamespace(), firstOwner.Name)
objMeta, ok := objectCache[key]
lastKey = key
abstractObject, ok := objectCache[key]
if ok {

objMeta, err := meta.Accessor(&abstractObject)
if err != nil {
logrus.Warnf("Error retrieving parent metadata %s of API %s and Kind %s because of error: %v ", firstOwner.Name, firstOwner.APIVersion, firstOwner.Kind, err)
return workload, err
}
workload.ObjectMeta = objMeta
owners = objMeta.GetOwnerReferences()
owners = abstractObject.GetOwnerReferences()

continue
}
fqKind := schema.FromAPIVersionAndKind(firstOwner.APIVersion, firstOwner.Kind)
mapping, err := restMapper.RESTMapping(fqKind.GroupKind(), fqKind.Version)
if err != nil {
logrus.Warnf("Error retrieving mapping %s of API %s and Kind %s because of error: %v ", firstOwner.Name, firstOwner.APIVersion, firstOwner.Kind, err)
return workload
return workload, nil
}
err = cacheAllObjectsOfKind(dynamicClient, mapping.Resource, objectCache)
if err != nil {
logrus.Warnf("Error getting objects of Kind %s %v", firstOwner.Kind, err)
return workload
return workload, err
}

objMeta, ok = objectCache[key]
abstractObject, ok = objectCache[key]
if ok {

objMeta, err := meta.Accessor(&abstractObject)
if err != nil {
logrus.Warnf("Error retrieving parent metadata %s of API %s and Kind %s because of error: %v ", firstOwner.Name, firstOwner.APIVersion, firstOwner.Kind, err)
return workload, err
}
workload.ObjectMeta = objMeta
owners = objMeta.GetOwnerReferences()
owners = abstractObject.GetOwnerReferences()

continue
} else {
logrus.Errorf("Cache missed again %s", key)
return workload, errors.New("Could not retrieve parent object")
}
parent, err := dynamicClient.Resource(mapping.Resource).Namespace(workload.ObjectMeta.GetNamespace()).Get(firstOwner.Name, kubeAPIMetaV1.GetOptions{})

}
if lastKey != "" {
bytes, err := json.Marshal(objectCache[lastKey])
if err != nil {
logrus.Warnf("Error retrieving parent object %s of API %s and Kind %s because of error: %v ", firstOwner.Name, firstOwner.APIVersion, firstOwner.Kind, err)
return workload
return workload, err
}
objMeta, err = meta.Accessor(parent)
workload.OriginalObjectJSON = bytes
} else {
bytes, err := json.Marshal(podResource)
if err != nil {
logrus.Warnf("Error retrieving parent metadata %s of API %s and Kind %s because of error: %v ", firstOwner.Name, firstOwner.APIVersion, firstOwner.Kind, err)
return workload
return workload, err
}
workload.ObjectMeta = objMeta
objectCache[key] = objMeta
owners = parent.GetOwnerReferences()
workload.OriginalObjectJSON = bytes
}

return workload
return workload, nil
}
Loading

0 comments on commit d50d9c8

Please sign in to comment.