diff --git a/CHANGELOG.md b/CHANGELOG.md index 40817857..2905dcec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/go.mod b/go.mod index 0497f489..09acf22f 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index d5cc884a..97939e79 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/pkg/config/schema.go b/pkg/config/schema.go index 320a3843..2870eb2c 100644 --- a/pkg/config/schema.go +++ b/pkg/config/schema.go @@ -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 } } diff --git a/pkg/dashboard/templates/dashboard.gohtml b/pkg/dashboard/templates/dashboard.gohtml index 7b3e85cf..69a6f40b 100644 --- a/pkg/dashboard/templates/dashboard.gohtml +++ b/pkg/dashboard/templates/dashboard.gohtml @@ -43,6 +43,10 @@ Pods: {{.AuditData.ClusterInfo.Pods}} + + Controllers: + {{.AuditData.ClusterInfo.Controllers}} + Namespaces: {{.AuditData.ClusterInfo.Namespaces}} diff --git a/pkg/kube/resources.go b/pkg/kube/resources.go index 67a3be66..fc8d74f6 100644 --- a/pkg/kube/resources.go +++ b/pkg/kube/resources.go @@ -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 } diff --git a/pkg/kube/resources_test.go b/pkg/kube/resources_test.go index b3326ea4..742cc407 100644 --- a/pkg/kube/resources_test.go +++ b/pkg/kube/resources_test.go @@ -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) } diff --git a/pkg/validator/container.go b/pkg/validator/container.go index 66f7f5a2..91add9ad 100644 --- a/pkg/validator/container.go +++ b/pkg/validator/container.go @@ -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 { diff --git a/pkg/validator/container_test.go b/pkg/validator/container_test.go index c298586b..f3f4d822 100644 --- a/pkg/validator/container_test.go +++ b/pkg/validator/container_test.go @@ -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) -} \ No newline at end of file +} diff --git a/pkg/validator/controller.go b/pkg/validator/controller.go index 9aa01ba6..b3d5ee5c 100644 --- a/pkg/validator/controller.go +++ b/pkg/validator/controller.go @@ -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" diff --git a/pkg/validator/controller_test.go b/pkg/validator/controller_test.go index e8e832fd..a3335309 100644 --- a/pkg/validator/controller_test.go +++ b/pkg/validator/controller_test.go @@ -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) diff --git a/pkg/validator/controllers/cronjob.go b/pkg/validator/controllers/cronjob.go index e55fc485..33a2c72e 100644 --- a/pkg/validator/controllers/cronjob.go +++ b/pkg/validator/controllers/cronjob.go @@ -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 } diff --git a/pkg/validator/controllers/daemonset.go b/pkg/validator/controllers/daemonset.go index a3c67dad..c8ce6d3e 100644 --- a/pkg/validator/controllers/daemonset.go +++ b/pkg/validator/controllers/daemonset.go @@ -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 } diff --git a/pkg/validator/controllers/deployment.go b/pkg/validator/controllers/deployment.go index e02a60bd..2972d442 100644 --- a/pkg/validator/controllers/deployment.go +++ b/pkg/validator/controllers/deployment.go @@ -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 } diff --git a/pkg/validator/controllers/generic.go b/pkg/validator/controllers/generic.go new file mode 100644 index 00000000..3d78023c --- /dev/null +++ b/pkg/validator/controllers/generic.go @@ -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 +} diff --git a/pkg/validator/controllers/interface.go b/pkg/validator/controllers/interface.go deleted file mode 100644 index fbcca9aa..00000000 --- a/pkg/validator/controllers/interface.go +++ /dev/null @@ -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) -} diff --git a/pkg/validator/controllers/job.go b/pkg/validator/controllers/job.go index 3efe181c..3377249f 100644 --- a/pkg/validator/controllers/job.go +++ b/pkg/validator/controllers/job.go @@ -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 } diff --git a/pkg/validator/controllers/naked-pod.go b/pkg/validator/controllers/naked-pod.go index c5ab8e66..b96a76ee 100644 --- a/pkg/validator/controllers/naked-pod.go +++ b/pkg/validator/controllers/naked-pod.go @@ -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 } diff --git a/pkg/validator/controllers/replicationcontroller.go b/pkg/validator/controllers/replicationcontroller.go index cb510f88..1a9fa32f 100644 --- a/pkg/validator/controllers/replicationcontroller.go +++ b/pkg/validator/controllers/replicationcontroller.go @@ -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 } diff --git a/pkg/validator/controllers/statefulsets.go b/pkg/validator/controllers/statefulsets.go index 4ac1f62b..fab6b865 100644 --- a/pkg/validator/controllers/statefulsets.go +++ b/pkg/validator/controllers/statefulsets.go @@ -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 } diff --git a/pkg/validator/fullaudit.go b/pkg/validator/fullaudit.go index 06eb6e93..1fe4b020 100644 --- a/pkg/validator/fullaudit.go +++ b/pkg/validator/fullaudit.go @@ -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, } diff --git a/pkg/validator/fullaudit_test.go b/pkg/validator/fullaudit_test.go index c5cdd7f8..0135ed30 100644 --- a/pkg/validator/fullaudit_test.go +++ b/pkg/validator/fullaudit_test.go @@ -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)) diff --git a/pkg/validator/output.go b/pkg/validator/output.go index 49fc7626..845daaeb 100644 --- a/pkg/validator/output.go +++ b/pkg/validator/output.go @@ -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. diff --git a/pkg/validator/pod.go b/pkg/validator/pod.go index 420ef39f..f6678191 100644 --- a/pkg/validator/pod.go +++ b/pkg/validator/pod.go @@ -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 } diff --git a/pkg/validator/pod_test.go b/pkg/validator/pod_test.go index a0e72b12..1c2139ec 100644 --- a/pkg/validator/pod_test.go +++ b/pkg/validator/pod_test.go @@ -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 diff --git a/pkg/validator/schema.go b/pkg/validator/schema.go index 1d5033f5..89f8c872 100644 --- a/pkg/validator/schema.go +++ b/pkg/validator/schema.go @@ -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 diff --git a/pkg/validator/summary.go b/pkg/validator/summary.go index 6801de8a..19e5198e 100644 --- a/pkg/validator/summary.go +++ b/pkg/validator/summary.go @@ -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) } diff --git a/pkg/webhook/validator.go b/pkg/webhook/validator.go index f93064a3..b395ae56 100644 --- a/pkg/webhook/validator.go +++ b/pkg/webhook/validator.go @@ -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)) diff --git a/test/fixtures.go b/test/fixtures.go index 303b0613..7a5ad47c 100644 --- a/test/fixtures.go +++ b/test/fixtures.go @@ -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 }