Merge pull request #240 from FairwindsOps/bb/openshift

Adding support for third-party Controllers (e.g. OpenShift)
This commit is contained in:
baderbuddy
2020-03-26 13:06:36 -04:00
committed by GitHub
29 changed files with 382 additions and 650 deletions

View File

@@ -1,5 +1,7 @@
# x.x.x (next release)
* Added the ability to exempt a particular controller from a particular check.
* Breaking changes in the config format.
* Added support for finding the Owners, this will allow Polaris to work with types of Controllers it doesn't even know about.
# 0.6.0
* Fixed webhook support in Kubernetes 1.16

8
go.mod
View File

@@ -7,6 +7,7 @@ require (
contrib.go.opencensus.io/exporter/ocagent v0.4.12
git.apache.org/thrift.git v0.12.0 // indirect
github.com/Azure/go-autorest v12.4.3+incompatible
github.com/Azure/go-autorest/autorest v0.10.0 // indirect
github.com/appscode/jsonpatch v0.0.0-20190108182946-7c0e3b262f30
github.com/beorn7/perks v1.0.0
github.com/census-instrumentation/opencensus-proto v0.2.1
@@ -68,7 +69,7 @@ require (
gitlab.com/golang-commonmark/mdurl v0.0.0-20180912090424-e5bce34c34f2
gitlab.com/golang-commonmark/puny v0.0.0-20180912090636-2cd490539afe
go.opencensus.io v0.21.0
go.uber.org/atomic v1.4.0
go.uber.org/atomic v1.6.0
go.uber.org/multierr v1.5.0
go.uber.org/zap v1.14.0
golang.org/x/crypto v0.0.0-20191219195013-becbf705a915
@@ -78,13 +79,14 @@ require (
golang.org/x/sys v0.0.0-20191218084908-4a24b4065292
golang.org/x/text v0.3.2
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4
golang.org/x/tools v0.0.0-20191219212307-145a1e401f50
golang.org/x/tools v0.0.0-20191224055732-dd894d0a8a40
google.golang.org/api v0.5.0
google.golang.org/appengine v1.6.0
google.golang.org/genproto v0.0.0-20190516172635-bb713bdc0e52
google.golang.org/grpc v1.20.1
gopkg.in/inf.v0 v0.9.1
gopkg.in/yaml.v2 v2.2.2
gopkg.in/yaml.v2 v2.2.7
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c
k8s.io/api v0.0.0-20181213150558-05914d821849
k8s.io/apimachinery v0.0.0-20181127025237-2b1284ed4c93
k8s.io/client-go v0.0.0-20181213151034-8d9ed539ba31

21
go.sum
View File

@@ -5,11 +5,28 @@ cloud.google.com/go v0.39.0/go.mod h1:rVLT6fkc8chs9sfPtFc1SBH6em7n+ZoXaG+87tDISt
contrib.go.opencensus.io/exporter/ocagent v0.4.12 h1:jGFvw3l57ViIVEPKKEUXPcLYIXJmQxLUh6ey1eJhwyc=
contrib.go.opencensus.io/exporter/ocagent v0.4.12/go.mod h1:450APlNTSR6FrvC3CTRqYosuDstRB9un7SOx2k/9ckA=
git.apache.org/thrift.git v0.12.0/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
github.com/Azure/go-autorest v1.1.1 h1:4G9tVCqooRY3vDTB2bA1Z01PlSALtnUbji0AfzthUSs=
github.com/Azure/go-autorest v12.0.0+incompatible h1:N+VqClcomLGD/sHb3smbSYYtNMgKpVV3Cd5r5i8z6bQ=
github.com/Azure/go-autorest v12.0.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
github.com/Azure/go-autorest v12.4.3+incompatible h1:tCkdkgLZqAk+43nZu3wda9n413Q2g+z7xp1wmjiJTPY=
github.com/Azure/go-autorest v12.4.3+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
github.com/Azure/go-autorest v14.0.0+incompatible h1:r/ug62X9o8vikt53/nkAPmFmzfSrCCAplPH7wa+mK0U=
github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI=
github.com/Azure/go-autorest/autorest v0.10.0 h1:mvdtztBqcL8se7MdrUweNieTNi4kfNG6GOJuurQJpuY=
github.com/Azure/go-autorest/autorest v0.10.0/go.mod h1:/FALq9T/kS7b5J5qsQ+RSTUdAmGFqi0vUdVNNx8q630=
github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0=
github.com/Azure/go-autorest/autorest/adal v0.8.2 h1:O1X4oexUxnZCaEUGsvMnr8ZGj8HI37tNezwY4npRqA0=
github.com/Azure/go-autorest/autorest/adal v0.8.2/go.mod h1:ZjhuQClTqx435SRJ2iMlOxPYt3d2C/T/7TiQCVZSn3Q=
github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA=
github.com/Azure/go-autorest/autorest/date v0.2.0 h1:yW+Zlqf26583pE43KhfnhFcdmSWlm5Ew6bxipnr/tbM=
github.com/Azure/go-autorest/autorest/date v0.2.0/go.mod h1:vcORJHLJEh643/Ioh9+vPmf1Ij9AEBM5FuBIXLmIy0g=
github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
github.com/Azure/go-autorest/autorest/mocks v0.3.0/go.mod h1:a8FDP3DYzQ4RYfVAxAN3SVSiiO77gL2j2ronKKP0syM=
github.com/Azure/go-autorest/logger v0.1.0 h1:ruG4BSDXONFRrZZJ2GUXDiUyVpayPmb1GnWeHDdaNKY=
github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc=
github.com/Azure/go-autorest/tracing v0.5.0 h1:TRn4WjSnkcSy5AEG3pnbtFSwNtwzjr4VYyQflFE619k=
github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
@@ -375,6 +392,7 @@ golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191122220453-ac88ee75c92c/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20191219195013-becbf705a915 h1:aJ0ex187qoXrJHPo8ZasVTASQB7llQP6YeNzgDALPRk=
golang.org/x/crypto v0.0.0-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -506,7 +524,10 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20180920025451-e3ad64cb4ed3/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@@ -144,13 +144,13 @@ func (check SchemaCheck) CheckObject(obj interface{}) (bool, error) {
}
// IsActionable decides if this check applies to a particular target
func (check SchemaCheck) IsActionable(target TargetKind, controllerType SupportedController, isInit bool) bool {
func (check SchemaCheck) IsActionable(target TargetKind, controllerType string, isInit bool) bool {
if check.Target != target {
return false
}
isIncluded := len(check.Controllers.Include) == 0
for _, inclusion := range check.Controllers.Include {
if GetSupportedControllerFromString(inclusion) == controllerType {
if inclusion == controllerType {
isIncluded = true
break
}
@@ -159,7 +159,7 @@ func (check SchemaCheck) IsActionable(target TargetKind, controllerType Supporte
return false
}
for _, exclusion := range check.Controllers.Exclude {
if GetSupportedControllerFromString(exclusion) == controllerType {
if exclusion == controllerType {
return false
}
}

View File

@@ -43,6 +43,10 @@
<span>Pods:</span>
<strong>{{.AuditData.ClusterInfo.Pods}}</strong>
</span>
<span class="kubernetes-stat">
<span>Controllers:</span>
<strong>{{.AuditData.ClusterInfo.Controllers}}</strong>
</span>
<span class="kubernetes-stat">
<span>Namespaces:</span>
<strong>{{.AuditData.ClusterInfo.Namespaces}}</strong>

View File

@@ -2,7 +2,6 @@ package kube
import (
"bytes"
"encoding/json"
"io/ioutil"
"os"
"path/filepath"
@@ -10,33 +9,29 @@ import (
"strings"
"time"
"github.com/fairwindsops/polaris/pkg/validator/controllers"
"github.com/sirupsen/logrus"
appsv1 "k8s.io/api/apps/v1"
batchv1 "k8s.io/api/batch/v1"
batchv1beta1 "k8s.io/api/batch/v1beta1"
"gopkg.in/yaml.v3"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
k8sYaml "k8s.io/apimachinery/pkg/util/yaml"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes"
_ "k8s.io/client-go/plugin/pkg/client/auth" // Required for other auth providers like GKE.
"k8s.io/client-go/restmapper"
"sigs.k8s.io/controller-runtime/pkg/client/config"
)
// ResourceProvider contains k8s resources to be audited
type ResourceProvider struct {
ServerVersion string
CreationTime time.Time
SourceName string
SourceType string
Nodes []corev1.Node
Deployments []appsv1.Deployment
StatefulSets []appsv1.StatefulSet
DaemonSets []appsv1.DaemonSet
Jobs []batchv1.Job
CronJobs []batchv1beta1.CronJob
ReplicationControllers []corev1.ReplicationController
Namespaces []corev1.Namespace
Pods []corev1.Pod
ServerVersion string
CreationTime time.Time
SourceName string
SourceType string
Nodes []corev1.Node
Namespaces []corev1.Namespace
Controllers []controllers.GenericController
}
type k8sResource struct {
@@ -54,18 +49,12 @@ func CreateResourceProvider(directory string) (*ResourceProvider, error) {
// CreateResourceProviderFromPath returns a new ResourceProvider using the YAML files in a directory
func CreateResourceProviderFromPath(directory string) (*ResourceProvider, error) {
resources := ResourceProvider{
ServerVersion: "unknown",
SourceType: "Path",
SourceName: directory,
Nodes: []corev1.Node{},
Deployments: []appsv1.Deployment{},
StatefulSets: []appsv1.StatefulSet{},
DaemonSets: []appsv1.DaemonSet{},
Jobs: []batchv1.Job{},
CronJobs: []batchv1beta1.CronJob{},
ReplicationControllers: []corev1.ReplicationController{},
Namespaces: []corev1.Namespace{},
Pods: []corev1.Pod{},
ServerVersion: "unknown",
SourceType: "Path",
SourceName: directory,
Nodes: []corev1.Node{},
Namespaces: []corev1.Namespace{},
Controllers: []controllers.GenericController{},
}
addYaml := func(contents string) error {
@@ -114,44 +103,23 @@ func CreateResourceProviderFromCluster() (*ResourceProvider, error) {
logrus.Errorf("Error creating Kubernetes client: %v", err)
return nil, err
}
return CreateResourceProviderFromAPI(api, kubeConf.Host)
dynamicInterface, err := dynamic.NewForConfig(kubeConf)
if err != nil {
logrus.Errorf("Error connecting to dynamic interface: %v", err)
return nil, err
}
return CreateResourceProviderFromAPI(api, kubeConf.Host, &dynamicInterface)
}
// CreateResourceProviderFromAPI creates a new ResourceProvider from an existing k8s interface
func CreateResourceProviderFromAPI(kube kubernetes.Interface, clusterName string) (*ResourceProvider, error) {
func CreateResourceProviderFromAPI(kube kubernetes.Interface, clusterName string, dynamic *dynamic.Interface) (*ResourceProvider, error) {
listOpts := metav1.ListOptions{}
serverVersion, err := kube.Discovery().ServerVersion()
if err != nil {
logrus.Errorf("Error fetching Cluster API version: %v", err)
return nil, err
}
deploys, err := getDeployments(kube)
if err != nil {
return nil, err
}
statefulSets, err := getStatefulSets(kube)
if err != nil {
return nil, err
}
cronJobs, err := getCronJobs(kube)
if err != nil {
return nil, err
}
daemonSets, err := getDaemonSets(kube)
if err != nil {
return nil, err
}
jobs, err := kube.BatchV1().Jobs("").List(listOpts)
if err != nil {
logrus.Errorf("Error fetching Jobs: %v", err)
return nil, err
}
replicationControllers, err := kube.CoreV1().ReplicationControllers("").List(listOpts)
if err != nil {
logrus.Errorf("Error fetching ReplicationControllers: %v", err)
return nil, err
}
nodes, err := kube.CoreV1().Nodes().List(listOpts)
if err != nil {
logrus.Errorf("Error fetching Nodes: %v", err)
@@ -168,222 +136,103 @@ func CreateResourceProviderFromAPI(kube kubernetes.Interface, clusterName string
return nil, err
}
resources, err := restmapper.GetAPIGroupResources(kube.Discovery())
if err != nil {
logrus.Errorf("Error getting API Group resources: %v", err)
return nil, err
}
restMapper := restmapper.NewDiscoveryRESTMapper(resources)
api := ResourceProvider{
ServerVersion: serverVersion.Major + "." + serverVersion.Minor,
SourceType: "Cluster",
SourceName: clusterName,
CreationTime: time.Now(),
Deployments: deploys,
StatefulSets: statefulSets,
DaemonSets: daemonSets,
CronJobs: cronJobs,
Jobs: jobs.Items,
ReplicationControllers: replicationControllers.Items,
Nodes: nodes.Items,
Namespaces: namespaces.Items,
Pods: pods.Items,
ServerVersion: serverVersion.Major + "." + serverVersion.Minor,
SourceType: "Cluster",
SourceName: clusterName,
CreationTime: time.Now(),
Nodes: nodes.Items,
Namespaces: namespaces.Items,
Controllers: LoadControllers(pods.Items, dynamic, &restMapper),
}
return &api, nil
}
// LoadControllers loads a list of controllers from the kubeResources Pods
func LoadControllers(pods []corev1.Pod, dynamicClientPointer *dynamic.Interface, restMapperPointer *meta.RESTMapper) []controllers.GenericController {
interfaces := []controllers.GenericController{}
for _, pod := range pods {
interfaces = append(interfaces, controllers.NewGenericPodController(pod, dynamicClientPointer, restMapperPointer))
}
return deduplicateControllers(interfaces)
}
// Because the controllers with an Owner take on the name of the Owner, this eliminates any duplicates.
// In cases like CronJobs older children can hang around, so this takes the most recent.
func deduplicateControllers(inputControllers []controllers.GenericController) []controllers.GenericController {
controllerMap := make(map[string]controllers.GenericController)
for _, controller := range inputControllers {
key := controller.GetNamespace() + "/" + controller.GetKind() + "/" + controller.Name
oldController, ok := controllerMap[key]
if !ok || controller.CreatedTime.After(oldController.CreatedTime) {
controllerMap[key] = controller
}
}
results := make([]controllers.GenericController, 0)
for _, controller := range controllerMap {
results = append(results, controller)
}
return results
}
func getPodSpec(yaml map[string]interface{}) interface{} {
allowedChildren := []string{"jobTemplate", "spec", "template"}
for _, child := range allowedChildren {
if childYaml, ok := yaml[child]; ok {
return getPodSpec(childYaml.(map[string]interface{}))
}
}
return yaml
}
func addResourceFromString(contents string, resources *ResourceProvider) error {
contentBytes := []byte(contents)
decoder := k8sYaml.NewYAMLOrJSONDecoder(bytes.NewReader(contentBytes), 1000)
resource := k8sResource{}
err := decoder.Decode(&resource)
decoder = k8sYaml.NewYAMLOrJSONDecoder(bytes.NewReader(contentBytes), 1000)
if err != nil {
logrus.Errorf("Invalid YAML: %s", string(contents))
return err
}
decoder = k8sYaml.NewYAMLOrJSONDecoder(bytes.NewReader(contentBytes), 1000)
if resource.Kind == "Deployment" {
controller := appsv1.Deployment{}
err = decoder.Decode(&controller)
resources.Deployments = append(resources.Deployments, controller)
} else if resource.Kind == "StatefulSet" {
controller := appsv1.StatefulSet{}
err = decoder.Decode(&controller)
resources.StatefulSets = append(resources.StatefulSets, controller)
} else if resource.Kind == "DaemonSet" {
controller := appsv1.DaemonSet{}
err = decoder.Decode(&controller)
resources.DaemonSets = append(resources.DaemonSets, controller)
} else if resource.Kind == "Job" {
controller := batchv1.Job{}
err = decoder.Decode(&controller)
resources.Jobs = append(resources.Jobs, controller)
} else if resource.Kind == "CronJob" {
controller := batchv1beta1.CronJob{}
err = decoder.Decode(&controller)
resources.CronJobs = append(resources.CronJobs, controller)
} else if resource.Kind == "ReplicationController" {
controller := corev1.ReplicationController{}
err = decoder.Decode(&controller)
resources.ReplicationControllers = append(resources.ReplicationControllers, controller)
} else if resource.Kind == "Namespace" {
if resource.Kind == "Namespace" {
ns := corev1.Namespace{}
err = decoder.Decode(&ns)
resources.Namespaces = append(resources.Namespaces, ns)
} else if resource.Kind == "Pod" {
pod := corev1.Pod{}
err = decoder.Decode(&pod)
resources.Pods = append(resources.Pods, pod)
resources.Controllers = append(resources.Controllers, controllers.NewGenericPodController(pod, nil, nil))
} else {
yamlNode := make(map[string]interface{})
err = yaml.Unmarshal(contentBytes, &yamlNode)
if err != nil {
logrus.Errorf("Invalid YAML: %s", string(contents))
return err
}
finalDoc := make(map[string]interface{})
finalDoc["metadata"] = yamlNode["metadata"]
finalDoc["apiVersion"] = "v1"
finalDoc["kind"] = "Pod"
finalDoc["spec"] = getPodSpec(yamlNode)
marshaledYaml, err := yaml.Marshal(finalDoc)
if err != nil {
logrus.Errorf("Could not marshal yaml: %v", err)
return err
}
decoder := k8sYaml.NewYAMLOrJSONDecoder(bytes.NewReader(marshaledYaml), 1000)
pod := corev1.Pod{}
err = decoder.Decode(&pod)
newController := controllers.NewGenericPodController(pod, nil, nil)
newController.Kind = resource.Kind
resources.Controllers = append(resources.Controllers, newController)
}
if err != nil {
logrus.Errorf("Error parsing %s: %v", resource.Kind, err)
return err
}
return nil
}
func getDeployments(kube kubernetes.Interface) ([]appsv1.Deployment, error) {
listOpts := metav1.ListOptions{}
deployList, err := kube.AppsV1().Deployments("").List(listOpts)
if err != nil {
logrus.Errorf("Error fetching Deployments: %v", err)
return nil, err
}
deploys := deployList.Items
oldDeploys := make([]interface{}, 0)
deploysV1B1, err := kube.AppsV1beta1().Deployments("").List(listOpts)
if err != nil {
logrus.Errorf("Error fetching Deployments v1beta1: %v", err)
return nil, err
}
for _, oldDeploy := range deploysV1B1.Items {
oldDeploys = append(oldDeploys, oldDeploy)
}
deploysV1B2, err := kube.AppsV1beta2().Deployments("").List(listOpts)
if err != nil {
logrus.Errorf("Error fetching Deployments v1beta2: %v", err)
return nil, err
}
for _, oldDeploy := range deploysV1B2.Items {
oldDeploys = append(oldDeploys, oldDeploy)
}
for _, oldDeploy := range oldDeploys {
str, err := json.Marshal(oldDeploy)
if err != nil {
logrus.Errorf("Error marshaling old deployment version: %v", err)
return nil, err
}
deploy := appsv1.Deployment{}
err = json.Unmarshal(str, &deploy)
if err != nil {
logrus.Errorf("Error unmarshaling old deployment version: %v", err)
return nil, err
}
deploys = append(deploys, deploy)
}
return deploys, nil
}
func getStatefulSets(kube kubernetes.Interface) ([]appsv1.StatefulSet, error) {
listOpts := metav1.ListOptions{}
controllerList, err := kube.AppsV1().StatefulSets("").List(listOpts)
if err != nil {
logrus.Errorf("Error fetching StatefulSets: %v", err)
return nil, err
}
controllers := controllerList.Items
oldControllers := make([]interface{}, 0)
controllersV1B1, err := kube.AppsV1beta1().StatefulSets("").List(listOpts)
if err != nil {
logrus.Errorf("Error fetching StatefulSets v1beta1: %v", err)
return nil, err
}
for _, oldController := range controllersV1B1.Items {
oldControllers = append(oldControllers, oldController)
}
controllersV1B2, err := kube.AppsV1beta2().StatefulSets("").List(listOpts)
if err != nil {
logrus.Errorf("Error fetching StatefulSets v1beta2: %v", err)
return nil, err
}
for _, oldController := range controllersV1B2.Items {
oldControllers = append(oldControllers, oldController)
}
for _, oldController := range oldControllers {
str, err := json.Marshal(oldController)
if err != nil {
logrus.Errorf("Error marshaling old StatefulSet version: %v", err)
return nil, err
}
controller := appsv1.StatefulSet{}
err = json.Unmarshal(str, &controller)
if err != nil {
logrus.Errorf("Error unmarshaling old StatefulSet version: %v", err)
return nil, err
}
controllers = append(controllers, controller)
}
return controllers, nil
}
func getDaemonSets(kube kubernetes.Interface) ([]appsv1.DaemonSet, error) {
listOpts := metav1.ListOptions{}
controllerList, err := kube.AppsV1().DaemonSets("").List(listOpts)
if err != nil {
logrus.Errorf("Error fetching DaemonSets: %v", err)
return nil, err
}
controllers := controllerList.Items
controllersV1B2, err := kube.AppsV1beta2().DaemonSets("").List(listOpts)
if err != nil {
logrus.Errorf("Error fetching DaemonSets v1beta2: %v", err)
return nil, err
}
for _, oldController := range controllersV1B2.Items {
str, err := json.Marshal(oldController)
if err != nil {
logrus.Errorf("Error marshaling old DaemonSet version: %v", err)
return nil, err
}
controller := appsv1.DaemonSet{}
err = json.Unmarshal(str, &controller)
if err != nil {
logrus.Errorf("Error unmarshaling old DaemonSet version: %v", err)
return nil, err
}
controllers = append(controllers, controller)
}
return controllers, nil
}
func getCronJobs(kube kubernetes.Interface) ([]batchv1beta1.CronJob, error) {
listOpts := metav1.ListOptions{}
controllerList, err := kube.BatchV1beta1().CronJobs("").List(listOpts)
if err != nil {
logrus.Errorf("Error fetching CronJobs: %v", err)
return nil, err
}
controllers := controllerList.Items
controllersV2A1, err := kube.BatchV2alpha1().CronJobs("").List(listOpts)
if err != nil {
logrus.Errorf("Error fetching CronJobs v2alpha1: %v", err)
return nil, err
}
for _, oldController := range controllersV2A1.Items {
str, err := json.Marshal(oldController)
if err != nil {
logrus.Errorf("Error marshaling old CronJob version: %v", err)
return nil, err
}
controller := batchv1beta1.CronJob{}
err = json.Unmarshal(str, &controller)
if err != nil {
logrus.Errorf("Error unmarshaling old CronJob version: %v", err)
return nil, err
}
controllers = append(controllers, controller)
}
return controllers, nil
return err
}

View File

@@ -20,18 +20,16 @@ func TestGetResourcesFromPath(t *testing.T) {
assert.Equal(t, 0, len(resources.Nodes), "Should not have any nodes")
assert.Equal(t, 1, len(resources.Deployments), "Should have a deployment")
assert.Equal(t, "ubuntu", resources.Deployments[0].Spec.Template.Spec.Containers[0].Name)
assert.Equal(t, 1, len(resources.StatefulSets), "Should have a stateful set")
assert.Equal(t, "nginx", resources.StatefulSets[0].Spec.Template.Spec.Containers[0].Name)
assert.Equal(t, 1, len(resources.Namespaces), "Should have a namespace")
assert.Equal(t, "two", resources.Namespaces[0].ObjectMeta.Name)
assert.Equal(t, 2, len(resources.Pods), "Should have two pods")
assert.Equal(t, "", resources.Pods[0].ObjectMeta.Namespace, "Should have one pod in default namespace")
assert.Equal(t, "two", resources.Pods[1].ObjectMeta.Namespace, "Should have one pod in namespace 'two'")
assert.Equal(t, 8, len(resources.Controllers), "Should have eight controllers")
namespaceCount := map[string]int{}
for _, controller := range resources.Controllers {
namespaceCount[controller.GetNamespace()]++
}
assert.Equal(t, 7, namespaceCount[""], "Should have seven controller in default namespace")
assert.Equal(t, 1, namespaceCount["two"], "Should have one controller in namespace 'two'")
}
func TestGetMultipleResourceFromSingleFile(t *testing.T) {
@@ -46,8 +44,8 @@ func TestGetMultipleResourceFromSingleFile(t *testing.T) {
assert.Equal(t, 0, len(resources.Nodes), "Should not have any nodes")
assert.Equal(t, 1, len(resources.Deployments), "Should have a deployment")
assert.Equal(t, "dashboard", resources.Deployments[0].Spec.Template.Spec.Containers[0].Name)
assert.Equal(t, 4, len(resources.Controllers), "Should have four controllers")
assert.Equal(t, "dashboard", resources.Controllers[0].PodSpec.Containers[0].Name)
assert.Equal(t, 2, len(resources.Namespaces), "Should have a namespace")
assert.Equal(t, "polaris", resources.Namespaces[0].ObjectMeta.Name)
@@ -60,9 +58,11 @@ func TestGetMultipleResourceFromBadFile(t *testing.T) {
}
func TestGetResourceFromAPI(t *testing.T) {
k8s := test.SetupTestAPI()
k8s, dynamicInterface := test.SetupTestAPI()
k8s = test.SetupAddControllers(k8s, "test")
resources, err := CreateResourceProviderFromAPI(k8s, "test")
// TODO find a way to mock out the dynamic client
// and create fake pods in order to find all of the controllers.
resources, err := CreateResourceProviderFromAPI(k8s, "test", &dynamicInterface)
assert.Equal(t, nil, err, "Error should be nil")
assert.Equal(t, "Cluster", resources.SourceType, "Should have type Path")
@@ -70,9 +70,7 @@ func TestGetResourceFromAPI(t *testing.T) {
assert.IsType(t, time.Now(), resources.CreationTime, "Creation time should be set")
assert.Equal(t, 0, len(resources.Nodes), "Should not have any nodes")
assert.Equal(t, 1, len(resources.Deployments), "Should have a deployment")
assert.Equal(t, 1, len(resources.StatefulSets), "Should have a stateful set")
assert.Equal(t, 0, len(resources.Pods), "Should have a pod")
assert.Equal(t, 1, len(resources.Controllers), "Should have 1 controller")
assert.Equal(t, "", resources.Deployments[0].ObjectMeta.Name)
assert.Equal(t, "", resources.Controllers[0].ObjectMeta.Name)
}

View File

@@ -22,7 +22,7 @@ import (
)
// ValidateContainer validates a single container from a given controller
func ValidateContainer(conf *config.Configuration, controller controllers.Interface, container *corev1.Container, isInit bool) (ContainerResult, error) {
func ValidateContainer(conf *config.Configuration, controller controllers.GenericController, container *corev1.Container, isInit bool) (ContainerResult, error) {
results, err := applyContainerSchemaChecks(conf, controller, container, isInit)
if err != nil {
return ContainerResult{}, err
@@ -37,7 +37,7 @@ func ValidateContainer(conf *config.Configuration, controller controllers.Interf
}
// ValidateAllContainers validates both init and regular containers
func ValidateAllContainers(conf *config.Configuration, controller controllers.Interface) ([]ContainerResult, error) {
func ValidateAllContainers(conf *config.Configuration, controller controllers.GenericController) ([]ContainerResult, error) {
results := []ContainerResult{}
pod := controller.GetPodSpec()
for _, container := range pod.InitContainers {

View File

@@ -51,7 +51,7 @@ exemptions:
- foo
`
func getEmptyController(name string) controllers.Interface {
func getEmptyController(name string) controllers.GenericController {
return controllers.NewDeploymentController(appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: name,
@@ -66,7 +66,7 @@ func testValidate(t *testing.T, container *corev1.Container, resourceConf *strin
testValidateWithController(t, container, resourceConf, getEmptyController(controllerName), expectedErrors, expectedWarnings, expectedSuccesses)
}
func testValidateWithController(t *testing.T, container *corev1.Container, resourceConf *string, controller controllers.Interface, expectedErrors []ResultMessage, expectedWarnings []ResultMessage, expectedSuccesses []ResultMessage) {
func testValidateWithController(t *testing.T, container *corev1.Container, resourceConf *string, controller controllers.GenericController, expectedErrors []ResultMessage, expectedWarnings []ResultMessage, expectedSuccesses []ResultMessage) {
parsedConf, err := conf.Parse([]byte(*resourceConf))
assert.NoError(t, err, "Expected no error when parsing config")
@@ -1184,18 +1184,18 @@ func TestValidateResourcesEmptyContainerCPURequestsExempt(t *testing.T) {
}
expectedSuccesses := []ResultMessage{}
controller := controllers.NewDeploymentController(appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
Annotations: map[string]string {
"polaris.fairwinds.com/cpuRequestsMissing-exempt": "true", // Exempt this controller from cpuRequestsMissing
Annotations: map[string]string{
"polaris.fairwinds.com/cpuRequestsMissing-exempt": "true", // Exempt this controller from cpuRequestsMissing
"polaris.fairwinds.com/memoryRequestsMissing-exempt": "truthy", // Don't actually exempt this controller from memoryRequestsMissing
} ,
},
},
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{},
},
})
testValidateWithController(t, &container, &resourceConfMinimal, controller, expectedErrors, expectedWarnings, expectedSuccesses)
}
}

View File

@@ -17,38 +17,36 @@ package validator
import (
"strings"
"github.com/sirupsen/logrus"
conf "github.com/fairwindsops/polaris/pkg/config"
"github.com/fairwindsops/polaris/pkg/kube"
"github.com/fairwindsops/polaris/pkg/validator/controllers"
controller "github.com/fairwindsops/polaris/pkg/validator/controllers"
)
const exemptionAnnotationKey = "polaris.fairwinds.com/exempt"
// ValidateController validates a single controller, returns a ControllerResult.
func ValidateController(conf *conf.Configuration, controller controller.Interface) (ControllerResult, error) {
func ValidateController(conf *conf.Configuration, controller controller.GenericController) (ControllerResult, error) {
podResult, err := ValidatePod(conf, controller)
if err != nil {
return ControllerResult{}, err
}
result := ControllerResult{
Kind: controller.GetKind().String(),
Kind: controller.GetKind(),
Name: controller.GetName(),
Namespace: controller.GetObjectMeta().Namespace,
Results: ResultSet{},
PodResult: podResult,
}
return result, nil
}
// 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) ([]ControllerResult, error) {
var controllersToAudit []controller.Interface
for _, supportedControllers := range config.ControllersToScan {
loadedControllers, _ := controllers.LoadControllersByKind(supportedControllers, kubeResources)
controllersToAudit = append(controllersToAudit, loadedControllers...)
}
controllersToAudit := kubeResources.Controllers
results := []ControllerResult{}
for _, controller := range controllersToAudit {
@@ -57,14 +55,16 @@ func ValidateControllers(config *conf.Configuration, kubeResources *kube.Resourc
}
result, err := ValidateController(config, controller)
if err != nil {
logrus.Warn("An error occured validating controller:", err)
return nil, err
}
results = append(results, result)
}
return results, nil
}
func hasExemptionAnnotation(ctrl controller.Interface) bool {
func hasExemptionAnnotation(ctrl controller.GenericController) bool {
annot := ctrl.GetObjectMeta().Annotations
val := annot[exemptionAnnotationKey]
return strings.ToLower(val) == "true"

View File

@@ -18,7 +18,6 @@ import (
"testing"
"github.com/stretchr/testify/assert"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
conf "github.com/fairwindsops/polaris/pkg/config"
@@ -137,8 +136,10 @@ func TestControllerExemptions(t *testing.T) {
conf.Deployments,
},
}
newController := test.MockGenericController()
newController.Kind = "Deployment"
resources := &kube.ResourceProvider{
Deployments: []appsv1.Deployment{test.MockDeploy()},
Controllers: []controller.GenericController{newController},
}
expectedSum := CountSummary{
@@ -154,7 +155,7 @@ func TestControllerExemptions(t *testing.T) {
assert.Equal(t, "Deployment", actualResults[0].Kind)
assert.EqualValues(t, expectedSum, actualResults[0].GetSummary())
resources.Deployments[0].ObjectMeta.Annotations = map[string]string{
resources.Controllers[0].ObjectMeta.Annotations = map[string]string{
exemptionAnnotationKey: "true",
}
actualResults, err = ValidateControllers(&c, resources)

View File

@@ -2,42 +2,20 @@ package controllers
import (
"github.com/fairwindsops/polaris/pkg/config"
"github.com/sirupsen/logrus"
kubeAPIBatchV1beta1 "k8s.io/api/batch/v1beta1"
kubeAPICoreV1 "k8s.io/api/core/v1"
kubeAPIMetaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// CronJobController is an implementation of controller for deployments
type CronJobController struct {
GenericController
K8SResource kubeAPIBatchV1beta1.CronJob
}
// GetPodTemplate returns the original template spec
func (c CronJobController) GetPodTemplate() *kubeAPICoreV1.PodTemplateSpec {
return &c.K8SResource.Spec.JobTemplate.Spec.Template
}
// GetPodSpec returns the original kubernetes template pod spec
func (c CronJobController) GetPodSpec() *kubeAPICoreV1.PodSpec {
return &c.K8SResource.Spec.JobTemplate.Spec.Template.Spec
}
// GetKind returns the supportedcontroller enum type
func (c CronJobController) GetKind() config.SupportedController {
return config.CronJobs
}
// GetObjectMeta returns the metadata
func (c CronJobController) GetObjectMeta() kubeAPIMetaV1.ObjectMeta {
return c.K8SResource.ObjectMeta
}
// NewCronJobController builds a new controller interface for Deployments
func NewCronJobController(originalDeploymentResource kubeAPIBatchV1beta1.CronJob) Interface {
controller := CronJobController{}
controller.Name = originalDeploymentResource.Name
controller.Namespace = originalDeploymentResource.Namespace
controller.K8SResource = originalDeploymentResource
func NewCronJobController(originalResource kubeAPIBatchV1beta1.CronJob) GenericController {
controller := GenericController{}
controller.Name = originalResource.Name
controller.Namespace = originalResource.Namespace
controller.PodSpec = originalResource.Spec.JobTemplate.Spec.Template.Spec
controller.ObjectMeta = originalResource.ObjectMeta
controller.Kind = config.CronJobs.String()
if controller.Name == "" {
logrus.Warn("Name is missing from controller", originalResource.Namespace)
}
return controller
}

View File

@@ -3,41 +3,15 @@ package controllers
import (
"github.com/fairwindsops/polaris/pkg/config"
kubeAPIAppsV1 "k8s.io/api/apps/v1"
kubeAPICoreV1 "k8s.io/api/core/v1"
kubeAPIMetaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// DaemonSetController is an implementation of controller for deployments
type DaemonSetController struct {
GenericController
K8SResource kubeAPIAppsV1.DaemonSet
}
// GetPodTemplate returns the original template spec
func (d DaemonSetController) GetPodTemplate() *kubeAPICoreV1.PodTemplateSpec {
return &d.K8SResource.Spec.Template
}
// GetPodSpec returns the original kubernetes template pod spec
func (d DaemonSetController) GetPodSpec() *kubeAPICoreV1.PodSpec {
return &d.K8SResource.Spec.Template.Spec
}
// GetObjectMeta returns the metadata
func (d DaemonSetController) GetObjectMeta() kubeAPIMetaV1.ObjectMeta {
return d.K8SResource.ObjectMeta
}
// GetKind returns the supportedcontroller enum type
func (d DaemonSetController) GetKind() config.SupportedController {
return config.DaemonSets
}
// NewDaemonSetController builds a new controller interface for Deployments
func NewDaemonSetController(originalResource kubeAPIAppsV1.DaemonSet) Interface {
controller := DaemonSetController{}
func NewDaemonSetController(originalResource kubeAPIAppsV1.DaemonSet) GenericController {
controller := GenericController{}
controller.Name = originalResource.Name
controller.Namespace = originalResource.Namespace
controller.K8SResource = originalResource
controller.PodSpec = originalResource.Spec.Template.Spec
controller.ObjectMeta = originalResource.ObjectMeta
controller.Kind = config.DaemonSets.String()
return controller
}

View File

@@ -3,41 +3,15 @@ package controllers
import (
"github.com/fairwindsops/polaris/pkg/config"
kubeAPIAppsV1 "k8s.io/api/apps/v1"
kubeAPICoreV1 "k8s.io/api/core/v1"
kubeAPIMetaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// DeploymentController is an implementation of controller for deployments
type DeploymentController struct {
GenericController
K8SResource kubeAPIAppsV1.Deployment
}
// GetPodTemplate returns the original template spec
func (d DeploymentController) GetPodTemplate() *kubeAPICoreV1.PodTemplateSpec {
return &d.K8SResource.Spec.Template
}
// GetPodSpec returns the original kubernetes template pod spec
func (d DeploymentController) GetPodSpec() *kubeAPICoreV1.PodSpec {
return &d.K8SResource.Spec.Template.Spec
}
// GetObjectMeta returns the metadata
func (d DeploymentController) GetObjectMeta() kubeAPIMetaV1.ObjectMeta {
return d.K8SResource.ObjectMeta
}
// GetKind returns the supportedcontroller enum type
func (d DeploymentController) GetKind() config.SupportedController {
return config.Deployments
}
// NewDeploymentController builds a new controller interface for Deployments
func NewDeploymentController(originalDeploymentResource kubeAPIAppsV1.Deployment) Interface {
controller := DeploymentController{}
controller.Name = originalDeploymentResource.Name
controller.Namespace = originalDeploymentResource.Namespace
controller.K8SResource = originalDeploymentResource
func NewDeploymentController(originalResource kubeAPIAppsV1.Deployment) GenericController {
controller := GenericController{}
controller.Name = originalResource.Name
controller.Namespace = originalResource.Namespace
controller.PodSpec = originalResource.Spec.Template.Spec
controller.ObjectMeta = originalResource.ObjectMeta
controller.Kind = config.Deployments.String()
return controller
}

View File

@@ -0,0 +1,91 @@
package controllers
import (
"time"
"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/runtime/schema"
"k8s.io/client-go/dynamic"
)
// GenericController is a base implementation with some free methods for inherited structs
type GenericController struct {
Name string
Namespace string
PodSpec kubeAPICoreV1.PodSpec
ObjectMeta kubeAPIMetaV1.ObjectMeta
Kind string
CreatedTime time.Time
}
// GetPodSpec returns the original kubernetes template pod spec
func (g GenericController) GetPodSpec() *kubeAPICoreV1.PodSpec {
return &g.PodSpec
}
// GetObjectMeta returns the metadata
func (g GenericController) GetObjectMeta() kubeAPIMetaV1.ObjectMeta {
return g.ObjectMeta
}
// GetKind returns the supportedcontroller enum type
func (g GenericController) GetKind() string {
return g.Kind
}
// GetName is inherited by all controllers using generic controller to get the name of the controller
func (g GenericController) GetName() string {
return g.Name
}
// GetNamespace is inherited by all controllers using generic controller to get the namespace of the controller
func (g GenericController) GetNamespace() string {
return g.Namespace
}
// NewGenericPodController builds a new controller interface for anytype of Pod
func NewGenericPodController(originalResource kubeAPICoreV1.Pod, dynamicClientPointer *dynamic.Interface, restMapperPointer *meta.RESTMapper) GenericController {
controller := GenericController{}
controller.Name = originalResource.Name
controller.Namespace = originalResource.Namespace
controller.PodSpec = originalResource.Spec
controller.ObjectMeta = originalResource.ObjectMeta
controller.Kind = "Pod"
controller.CreatedTime = controller.GetObjectMeta().CreationTimestamp.Time
owners := controller.GetObjectMeta().OwnerReferences
if dynamicClientPointer == nil || restMapperPointer == nil {
return controller
}
// If an owner exists then set the name to the controller.
// This allows us to handle CRDs creating Controllers or DeploymentConfigs in OpenShift.
for len(owners) > 0 {
if len(owners) > 1 {
logrus.Warn("More than 1 owner found")
}
firstOwner := owners[0]
controller.Kind = firstOwner.Kind
controller.Name = firstOwner.Name
dynamicClient := *dynamicClientPointer
restMapper := *restMapperPointer
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 controller
}
getParents, err := dynamicClient.Resource(mapping.Resource).Namespace(controller.GetObjectMeta().Namespace).Get(firstOwner.Name, kubeAPIMetaV1.GetOptions{})
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 controller
}
owners = getParents.GetOwnerReferences()
}
return controller
}

View File

@@ -1,71 +0,0 @@
package controllers
import (
"fmt"
"github.com/fairwindsops/polaris/pkg/config"
"github.com/fairwindsops/polaris/pkg/kube"
kubeAPICoreV1 "k8s.io/api/core/v1"
kubeAPIMetaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// Interface is an interface for k8s controllers (e.g. Deployments and StatefulSets)
type Interface interface {
GetName() string
GetNamespace() string
GetPodTemplate() *kubeAPICoreV1.PodTemplateSpec
GetPodSpec() *kubeAPICoreV1.PodSpec
GetKind() config.SupportedController
GetObjectMeta() kubeAPIMetaV1.ObjectMeta
}
// GenericController is a base implementation with some free methods for inherited structs
type GenericController struct {
Name string
Namespace string
}
// GetName is inherited by all controllers using generic controller to get the name of the controller
func (g GenericController) GetName() string {
return g.Name
}
// GetNamespace is inherited by all controllers using generic controller to get the namespace of the controller
func (g GenericController) GetNamespace() string {
return g.Namespace
}
// LoadControllersByKind loads a list of controllers from the kubeResources by detecting their type
func LoadControllersByKind(controllerKind config.SupportedController, kubeResources *kube.ResourceProvider) ([]Interface, error) {
interfaces := []Interface{}
switch controllerKind {
case config.Deployments:
for _, deploy := range kubeResources.Deployments {
interfaces = append(interfaces, NewDeploymentController(deploy))
}
case config.StatefulSets:
for _, statefulSet := range kubeResources.StatefulSets {
interfaces = append(interfaces, NewStatefulSetController(statefulSet))
}
case config.DaemonSets:
for _, daemonSet := range kubeResources.DaemonSets {
interfaces = append(interfaces, NewDaemonSetController(daemonSet))
}
case config.Jobs:
for _, job := range kubeResources.Jobs {
interfaces = append(interfaces, NewJobController(job))
}
case config.CronJobs:
for _, cronJob := range kubeResources.CronJobs {
interfaces = append(interfaces, NewCronJobController(cronJob))
}
case config.ReplicationControllers:
for _, replicationController := range kubeResources.ReplicationControllers {
interfaces = append(interfaces, NewReplicationControllerController(replicationController))
}
}
if len(interfaces) > 0 {
return interfaces, nil
}
return nil, fmt.Errorf("Controller type (%s) does not have a generator", controllerKind)
}

View File

@@ -3,41 +3,15 @@ package controllers
import (
"github.com/fairwindsops/polaris/pkg/config"
kubeAPIBatchV1 "k8s.io/api/batch/v1"
kubeAPICoreV1 "k8s.io/api/core/v1"
kubeAPIMetaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// JobController is an implementation of controller for deployments
type JobController struct {
GenericController
K8SResource kubeAPIBatchV1.Job
}
// GetPodTemplate returns the original template spec
func (j JobController) GetPodTemplate() *kubeAPICoreV1.PodTemplateSpec {
return &j.K8SResource.Spec.Template
}
// GetPodSpec returns the original kubernetes template pod spec
func (j JobController) GetPodSpec() *kubeAPICoreV1.PodSpec {
return &j.K8SResource.Spec.Template.Spec
}
// GetObjectMeta returns the metadata
func (j JobController) GetObjectMeta() kubeAPIMetaV1.ObjectMeta {
return j.K8SResource.ObjectMeta
}
// GetKind returns the supportedcontroller enum type
func (j JobController) GetKind() config.SupportedController {
return config.Jobs
}
// NewJobController builds a new controller interface for Deployments
func NewJobController(originalResource kubeAPIBatchV1.Job) Interface {
controller := JobController{}
func NewJobController(originalResource kubeAPIBatchV1.Job) GenericController {
controller := GenericController{}
controller.Name = originalResource.Name
controller.Namespace = originalResource.Namespace
controller.K8SResource = originalResource
controller.PodSpec = originalResource.Spec.Template.Spec
controller.ObjectMeta = originalResource.ObjectMeta
controller.Kind = config.Jobs.String()
return controller
}

View File

@@ -3,40 +3,16 @@ package controllers
import (
"github.com/fairwindsops/polaris/pkg/config"
kubeAPICoreV1 "k8s.io/api/core/v1"
kubeAPIMetaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// NakedPodController is an implementation of controller for deployments
type NakedPodController struct {
GenericController
K8SResource kubeAPICoreV1.Pod
}
// GetPodTemplate returns the original template spec
func (n NakedPodController) GetPodTemplate() *kubeAPICoreV1.PodTemplateSpec {
return nil
}
// GetPodSpec returns the original kubernetes template pod spec
func (n NakedPodController) GetPodSpec() *kubeAPICoreV1.PodSpec {
return &n.K8SResource.Spec
}
// GetObjectMeta returns the metadata
func (n NakedPodController) GetObjectMeta() kubeAPIMetaV1.ObjectMeta {
return n.K8SResource.ObjectMeta
}
// GetKind returns the supportedcontroller enum type
func (n NakedPodController) GetKind() config.SupportedController {
return config.NakedPods
}
// NewNakedPodController builds a new controller interface for NakedPods
func NewNakedPodController(originalNakedPodResource kubeAPICoreV1.Pod) Interface {
controller := NakedPodController{}
controller.Name = originalNakedPodResource.Name
controller.Namespace = originalNakedPodResource.Namespace
controller.K8SResource = originalNakedPodResource
func NewNakedPodController(originalResource kubeAPICoreV1.Pod) GenericController {
controller := GenericController{}
controller.Name = originalResource.Name
controller.Namespace = originalResource.Namespace
controller.PodSpec = originalResource.Spec
controller.ObjectMeta = originalResource.ObjectMeta
controller.Kind = config.NakedPods.String()
return controller
}

View File

@@ -3,43 +3,15 @@ package controllers
import (
"github.com/fairwindsops/polaris/pkg/config"
kubeAPICoreV1 "k8s.io/api/core/v1"
kubeAPIMetaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// NOTE: Maybe this name of ReplicationController is duplicative but it's more explicit since
// that's how kubernetes refers the the object.
// ReplicationControllerController is an implementation of controller for deployments
type ReplicationControllerController struct {
GenericController
K8SResource kubeAPICoreV1.ReplicationController
}
// GetPodTemplate returns the original template spec
func (r ReplicationControllerController) GetPodTemplate() *kubeAPICoreV1.PodTemplateSpec {
return r.K8SResource.Spec.Template
}
// GetPodSpec returns the original kubernetes template pod spec
func (r ReplicationControllerController) GetPodSpec() *kubeAPICoreV1.PodSpec {
return &r.K8SResource.Spec.Template.Spec
}
// GetObjectMeta returns the metadata
func (r ReplicationControllerController) GetObjectMeta() kubeAPIMetaV1.ObjectMeta {
return r.K8SResource.ObjectMeta
}
// GetKind returns the supportedcontroller enum type
func (r ReplicationControllerController) GetKind() config.SupportedController {
return config.ReplicationControllers
}
// NewReplicationControllerController builds a new controller interface for Deployments
func NewReplicationControllerController(originalResource kubeAPICoreV1.ReplicationController) Interface {
controller := ReplicationControllerController{}
func NewReplicationControllerController(originalResource kubeAPICoreV1.ReplicationController) GenericController {
controller := GenericController{}
controller.Name = originalResource.Name
controller.Namespace = originalResource.Namespace
controller.K8SResource = originalResource
controller.PodSpec = originalResource.Spec.Template.Spec
controller.ObjectMeta = originalResource.ObjectMeta
controller.Kind = config.ReplicationControllers.String()
return controller
}

View File

@@ -2,42 +2,20 @@ package controllers
import (
"github.com/fairwindsops/polaris/pkg/config"
"github.com/sirupsen/logrus"
kubeAPIAppsV1 "k8s.io/api/apps/v1"
kubeAPICoreV1 "k8s.io/api/core/v1"
kubeAPIMetaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// StatefulSetController is an implementation of controller for deployments
type StatefulSetController struct {
GenericController
K8SResource kubeAPIAppsV1.StatefulSet
}
// GetPodTemplate returns the kubernetes template spec
func (s StatefulSetController) GetPodTemplate() *kubeAPICoreV1.PodTemplateSpec {
return &s.K8SResource.Spec.Template
}
// GetPodSpec returns the podspec from the original kubernetes resource
func (s StatefulSetController) GetPodSpec() *kubeAPICoreV1.PodSpec {
return &s.K8SResource.Spec.Template.Spec
}
// GetObjectMeta returns the metadata
func (s StatefulSetController) GetObjectMeta() kubeAPIMetaV1.ObjectMeta {
return s.K8SResource.ObjectMeta
}
// GetKind returns the supportedcontroller enum type
func (s StatefulSetController) GetKind() config.SupportedController {
return config.StatefulSets
}
// NewStatefulSetController builds a statefulset controller
func NewStatefulSetController(originalResource kubeAPIAppsV1.StatefulSet) Interface {
controller := StatefulSetController{}
func NewStatefulSetController(originalResource kubeAPIAppsV1.StatefulSet) GenericController {
controller := GenericController{}
controller.Name = originalResource.Name
controller.Namespace = originalResource.Namespace
controller.K8SResource = originalResource
controller.PodSpec = originalResource.Spec.Template.Spec
controller.ObjectMeta = originalResource.ObjectMeta
controller.Kind = config.StatefulSets.String()
if controller.Name == "" {
logrus.Warn("Name is missing from controller", originalResource.Namespace)
}
return controller
}

View File

@@ -34,16 +34,11 @@ func RunAudit(config conf.Configuration, kubeResources *kube.ResourceProvider) (
SourceName: kubeResources.SourceName,
DisplayName: displayName,
ClusterInfo: ClusterInfo{
Version: kubeResources.ServerVersion,
Nodes: len(kubeResources.Nodes),
Pods: len(kubeResources.Pods),
Namespaces: len(kubeResources.Namespaces),
Deployments: len(kubeResources.Deployments),
StatefulSets: len(kubeResources.StatefulSets),
DaemonSets: len(kubeResources.DaemonSets),
Jobs: len(kubeResources.Jobs),
CronJobs: len(kubeResources.CronJobs),
ReplicationControllers: len(kubeResources.ReplicationControllers),
Version: kubeResources.ServerVersion,
Nodes: len(kubeResources.Nodes),
Pods: len(kubeResources.Controllers), // TODO validate that this is still valuable
Namespaces: len(kubeResources.Namespaces),
Controllers: len(results),
},
Results: results,
}

View File

@@ -10,10 +10,12 @@ import (
)
func TestGetTemplateData(t *testing.T) {
k8s := test.SetupTestAPI()
k8s, dynamicClient := test.SetupTestAPI()
k8s = test.SetupAddControllers(k8s, "test")
k8s = test.SetupAddExtraControllerVersions(k8s, "test-extra")
resources, err := kube.CreateResourceProviderFromAPI(k8s, "test")
// TODO figure out how to mock out dynamic client.
// and add in pods for all controllers to fill out tests.
resources, err := kube.CreateResourceProviderFromAPI(k8s, "test", &dynamicClient)
assert.Equal(t, err, nil, "error should be nil")
c := conf.Configuration{
@@ -33,8 +35,8 @@ func TestGetTemplateData(t *testing.T) {
sum := CountSummary{
Successes: uint(0),
Warnings: uint(9),
Errors: uint(9),
Warnings: uint(1),
Errors: uint(1),
}
actualAudit, err := RunAudit(c, resources)
@@ -48,17 +50,7 @@ func TestGetTemplateData(t *testing.T) {
kind string
results int
}{
{kind: "Deployment", results: 2},
{kind: "Deployment", results: 2},
{kind: "Deployment", results: 2},
{kind: "StatefulSet", results: 2},
{kind: "StatefulSet", results: 2},
{kind: "StatefulSet", results: 2},
{kind: "DaemonSet", results: 2},
{kind: "DaemonSet", results: 2},
{kind: "Job", results: 0},
{kind: "CronJob", results: 0},
{kind: "ReplicationController", results: 2},
{kind: "Pod", results: 2},
}
assert.Equal(t, len(expected), len(actualAudit.Results))

View File

@@ -15,6 +15,8 @@
package validator
import (
"time"
"github.com/fairwindsops/polaris/pkg/config"
)
@@ -36,16 +38,11 @@ type AuditData struct {
// 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
Version string
Nodes int
Pods int
Namespaces int
Controllers int
}
// ResultMessage is the result of a given check
@@ -62,11 +59,12 @@ type ResultSet map[string]ResultMessage
// ControllerResult provides results for a controller
type ControllerResult struct {
Name string
Namespace string
Kind string
Results ResultSet
PodResult PodResult
Name string
Namespace string
Kind string
Results ResultSet
PodResult PodResult
CreatedTime time.Time
}
// PodResult provides a list of validation messages for each pod.

View File

@@ -20,12 +20,11 @@ import (
)
// ValidatePod validates that each pod conforms to the Polaris config, returns a ResourceResult.
func ValidatePod(conf *config.Configuration, controller controllers.Interface) (PodResult, error) {
func ValidatePod(conf *config.Configuration, controller controllers.GenericController) (PodResult, error) {
podResults, err := applyPodSchemaChecks(conf, controller)
if err != nil {
return PodResult{}, err
}
pRes := PodResult{
Results: podResults,
ContainerResults: []ContainerResult{},
@@ -35,6 +34,5 @@ func ValidatePod(conf *config.Configuration, controller controllers.Interface) (
if err != nil {
return pRes, err
}
return pRes, nil
}

View File

@@ -36,7 +36,7 @@ func TestValidatePod(t *testing.T) {
},
}
k8s := test.SetupTestAPI()
k8s, _ := test.SetupTestAPI()
k8s = test.SetupAddControllers(k8s, "test")
p := test.MockPod()
deployment := controllers.NewDeploymentController(appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: p}})
@@ -73,7 +73,7 @@ func TestInvalidIPCPod(t *testing.T) {
},
}
k8s := test.SetupTestAPI()
k8s, _ := test.SetupTestAPI()
k8s = test.SetupAddControllers(k8s, "test")
p := test.MockPod()
p.Spec.HostIPC = true
@@ -110,7 +110,7 @@ func TestInvalidNeworkPod(t *testing.T) {
},
}
k8s := test.SetupTestAPI()
k8s, _ := test.SetupTestAPI()
k8s = test.SetupAddControllers(k8s, "test")
p := test.MockPod()
p.Spec.HostNetwork = true
@@ -148,7 +148,7 @@ func TestInvalidPIDPod(t *testing.T) {
},
}
k8s := test.SetupTestAPI()
k8s, _ := test.SetupTestAPI()
k8s = test.SetupAddControllers(k8s, "test")
p := test.MockPod()
p.Spec.HostPID = true
@@ -192,7 +192,7 @@ func TestExemption(t *testing.T) {
},
}
k8s := test.SetupTestAPI()
k8s, _ := test.SetupTestAPI()
k8s = test.SetupAddControllers(k8s, "test")
p := test.MockPod()
p.Spec.HostIPC = true

View File

@@ -74,7 +74,7 @@ func parseCheck(rawBytes []byte) (config.SchemaCheck, error) {
}
}
func resolveCheck(conf *config.Configuration, checkID string, controller controllers.Interface, target config.TargetKind, isInitContainer bool) (*config.SchemaCheck, error) {
func resolveCheck(conf *config.Configuration, checkID string, controller controllers.GenericController, target config.TargetKind, isInitContainer bool) (*config.SchemaCheck, error) {
check, ok := conf.CustomChecks[checkID]
if !ok {
check, ok = builtInChecks[checkID]
@@ -110,7 +110,7 @@ func getExemptKey(checkID string) string {
return fmt.Sprintf("polaris.fairwinds.com/%s-exempt", checkID)
}
func applyPodSchemaChecks(conf *config.Configuration, controller controllers.Interface) (ResultSet, error) {
func applyPodSchemaChecks(conf *config.Configuration, controller controllers.GenericController) (ResultSet, error) {
results := ResultSet{}
checkIDs := getSortedKeys(conf.Checks)
objectAnnotations := controller.GetObjectMeta().Annotations
@@ -120,9 +120,7 @@ func applyPodSchemaChecks(conf *config.Configuration, controller controllers.Int
continue
}
check, err := resolveCheck(conf, checkID, controller, config.TargetPod, false)
if err != nil {
return nil, err
}
if err != nil {
return nil, err
} else if check == nil {
@@ -137,7 +135,7 @@ func applyPodSchemaChecks(conf *config.Configuration, controller controllers.Int
return results, nil
}
func applyContainerSchemaChecks(conf *config.Configuration, controller controllers.Interface, container *corev1.Container, isInit bool) (ResultSet, error) {
func applyContainerSchemaChecks(conf *config.Configuration, controller controllers.GenericController, container *corev1.Container, isInit bool) (ResultSet, error) {
results := ResultSet{}
checkIDs := getSortedKeys(conf.Checks)
objectAnnotations := controller.GetObjectMeta().Annotations

View File

@@ -17,6 +17,9 @@ type CountSummaryByCategory map[string]CountSummary
// 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)
if total == 0 {
return 0 // Prevent divide by 0.
}
return uint((float64(cs.Successes*2) / float64(total)) * 100)
}

View File

@@ -98,7 +98,7 @@ func (v *Validator) Handle(ctx context.Context, req types.Request) types.Respons
podResult, err = validator.ValidatePod(&v.Config, nakedPod)
}
} else {
var controller controllers.Interface
var controller controllers.GenericController
if yes := v.Config.CheckIfKindIsConfiguredForValidation(req.AdmissionRequest.Kind.Kind); !yes {
logrus.Warnf("Skipping, kind (%s) isn't something we are configured to scan", req.AdmissionRequest.Kind.Kind)
return admission.ValidationResponse(true, fmt.Sprintf("Skipping: (%s) isn't something we're configured to scan.", req.AdmissionRequest.Kind.Kind))

View File

@@ -1,12 +1,16 @@
package test
import (
"github.com/fairwindsops/polaris/pkg/validator/controllers"
appsv1 "k8s.io/api/apps/v1"
appsv1beta1 "k8s.io/api/apps/v1beta1"
appsv1beta2 "k8s.io/api/apps/v1beta2"
batchv1 "k8s.io/api/batch/v1"
batchv1beta1 "k8s.io/api/batch/v1beta1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/dynamic"
dynamicFake "k8s.io/client-go/dynamic/fake"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/fake"
)
@@ -32,6 +36,20 @@ func MockPod() corev1.PodTemplateSpec {
return p
}
// MockGenericController creates a generic controller object for testing.
func MockGenericController() controllers.GenericController {
return controllers.GenericController{
PodSpec: MockPod().Spec,
}
}
// MockNakedPod creates a pod object.
func MockNakedPod() corev1.Pod {
return corev1.Pod{
Spec: MockPod().Spec,
}
}
// MockDeploy creates a Deployment object.
func MockDeploy() appsv1.Deployment {
p := MockPod()
@@ -96,8 +114,10 @@ func MockReplicationController() corev1.ReplicationController {
}
// SetupTestAPI creates a test kube API struct.
func SetupTestAPI() kubernetes.Interface {
return fake.NewSimpleClientset()
func SetupTestAPI() (kubernetes.Interface, dynamic.Interface) {
scheme := runtime.NewScheme()
return fake.NewSimpleClientset(), dynamicFake.NewSimpleDynamicClient(scheme)
}
// SetupAddControllers creates mock controllers and adds them to the test clientset.
@@ -132,6 +152,11 @@ func SetupAddControllers(k kubernetes.Interface, namespace string) kubernetes.In
panic(err)
}
p1 := MockNakedPod()
if _, err := k.CoreV1().Pods(namespace).Create(&p1); err != nil {
panic(err)
}
return k
}