From 6792fba91f0110afdad6d0c8de77a5c41eb341a3 Mon Sep 17 00:00:00 2001 From: Robert Brennan Date: Mon, 27 Apr 2020 10:43:02 -0400 Subject: [PATCH] Delete controllers package (#270) * rename root fs check * speed up docker build * refactor webhook to be more generic * delete controllers pkg * revert deploy * fix example config * remove controllersToScan config * fix lint error * fix webhook name * FileSystem -> Filesystem * update deps * skip node owners * clean up meta tracking Co-authored-by: Robert Brennan --- Dockerfile | 11 +- ...em.yaml => notReadOnlyRootFilesystem.yaml} | 0 cmd/polaris/webhook.go | 47 ++++- deploy/dashboard.yaml | 6 +- deploy/webhook.yaml | 6 +- docs/check-documentation/security.md | 2 +- examples/config-full.yaml | 11 +- examples/config.yaml | 13 +- go.mod | 9 +- go.sum | 3 + pkg/config/config.go | 1 - pkg/config/config_test.go | 6 +- pkg/config/supportedcontrollers.go | 165 ------------------ pkg/config/supportedcontrollers_test.go | 118 ------------- pkg/kube/resources.go | 46 +++-- pkg/kube/resources_test.go | 4 +- pkg/kube/workload.go | 64 +++++++ pkg/validator/container.go | 8 +- pkg/validator/container_test.go | 84 ++++----- pkg/validator/controller.go | 13 +- pkg/validator/controller_test.go | 39 ++--- pkg/validator/controllers/cronjob.go | 21 --- pkg/validator/controllers/daemonset.go | 17 -- pkg/validator/controllers/deployment.go | 17 -- pkg/validator/controllers/generic.go | 91 ---------- pkg/validator/controllers/job.go | 17 -- pkg/validator/controllers/naked-pod.go | 18 -- .../controllers/replicationcontroller.go | 17 -- pkg/validator/controllers/statefulsets.go | 21 --- pkg/validator/fullaudit_test.go | 8 - pkg/validator/pod.go | 4 +- pkg/validator/pod_test.go | 32 ++-- pkg/validator/schema.go | 22 +-- pkg/validator/schema_test.go | 4 +- pkg/webhook/{validator.go => webhook.go} | 110 +++++------- test/fixtures.go | 37 ++-- 36 files changed, 301 insertions(+), 791 deletions(-) rename checks/{notReadOnlyRootFileSystem.yaml => notReadOnlyRootFilesystem.yaml} (100%) delete mode 100644 pkg/config/supportedcontrollers.go delete mode 100644 pkg/config/supportedcontrollers_test.go create mode 100644 pkg/kube/workload.go delete mode 100644 pkg/validator/controllers/cronjob.go delete mode 100644 pkg/validator/controllers/daemonset.go delete mode 100644 pkg/validator/controllers/deployment.go delete mode 100644 pkg/validator/controllers/generic.go delete mode 100644 pkg/validator/controllers/job.go delete mode 100644 pkg/validator/controllers/naked-pod.go delete mode 100644 pkg/validator/controllers/replicationcontroller.go delete mode 100644 pkg/validator/controllers/statefulsets.go rename pkg/webhook/{validator.go => webhook.go} (54%) diff --git a/Dockerfile b/Dockerfile index 3be51aab..5f573a4c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,13 +3,16 @@ WORKDIR /go/src/github.com/fairwindsops/polaris/ ENV GO111MODULE=on ENV GOPROXY=https://proxy.golang.org - -COPY . . -RUN go get -u github.com/gobuffalo/packr/v2/packr2 - ENV CGO_ENABLED=0 ENV GOOS=linux ENV GOARCH=amd64 + +COPY go.mod . +COPY go.sum . +RUN go mod download +RUN go get -u github.com/gobuffalo/packr/v2/packr2 + +COPY . . RUN packr2 build -a -o polaris *.go FROM alpine:3.10 diff --git a/checks/notReadOnlyRootFileSystem.yaml b/checks/notReadOnlyRootFilesystem.yaml similarity index 100% rename from checks/notReadOnlyRootFileSystem.yaml rename to checks/notReadOnlyRootFilesystem.yaml diff --git a/cmd/polaris/webhook.go b/cmd/polaris/webhook.go index be9fa5ea..443947d8 100644 --- a/cmd/polaris/webhook.go +++ b/cmd/polaris/webhook.go @@ -15,7 +15,6 @@ package cmd import ( - "fmt" "io/ioutil" "os" "strings" @@ -23,6 +22,14 @@ import ( fwebhook "github.com/fairwindsops/polaris/pkg/webhook" "github.com/sirupsen/logrus" "github.com/spf13/cobra" + 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" + batchv2alpha1 "k8s.io/api/batch/v2alpha1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" apitypes "k8s.io/apimachinery/pkg/types" k8sConfig "sigs.k8s.io/controller-runtime/pkg/client/config" "sigs.k8s.io/controller-runtime/pkg/manager" @@ -30,6 +37,28 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook" ) +var supportedVersions = map[string]runtime.Object{ + "appsv1/Deployment": &appsv1.Deployment{}, + "appsv1beta1/Deployment": &appsv1beta1.Deployment{}, + "appsv1beta2/Deployment": &appsv1beta2.Deployment{}, + + "appsv1/StatefulSet": &appsv1.StatefulSet{}, + "appsv1beta1/StatefulSet": &appsv1beta1.StatefulSet{}, + "appsv1beta2/StatefulSet": &appsv1beta2.StatefulSet{}, + + "appsv1/DaemonSet": &appsv1.DaemonSet{}, + "appsv1beta2/DaemonSet": &appsv1beta2.DaemonSet{}, + + "batchv1/Job": &batchv1.Job{}, + + "batchv1beta1/CronJob": &batchv1beta1.CronJob{}, + "batchv2alpha1/CronJob": &batchv2alpha1.CronJob{}, + + "corev1/ReplicationController": &corev1.ReplicationController{}, + + "corev1/Pod": &corev1.Pod{}, +} + var webhookPort int var disableWebhookConfigInstaller bool @@ -102,14 +131,16 @@ var webhookCmd = &cobra.Command{ // Should only register controllers that are configured to be scanned logrus.Debug("Registering webhooks to the webhook server") var webhooks []webhook.Webhook - for index, controllerToScan := range config.ControllersToScan { - for innerIndex, supportedAPIType := range controllerToScan.ListSupportedAPIVersions() { - webhookName := strings.ToLower(fmt.Sprintf("%s-%d-%d", controllerToScan, index, innerIndex)) - hook := fwebhook.NewWebhook(webhookName, mgr, fwebhook.Validator{Config: config}, supportedAPIType) - if hook != nil { - webhooks = append(webhooks, hook) - } + for name, supportedAPIType := range supportedVersions { + webhookName := strings.ToLower(name) + webhookName = strings.ReplaceAll(webhookName, "/", "-") + hook, err := fwebhook.NewWebhook(webhookName, mgr, fwebhook.Validator{Config: config}, supportedAPIType) + if err != nil { + logrus.Warningf("Couldn't build webhook %s: %v", webhookName, err) + continue } + webhooks = append(webhooks, hook) + logrus.Infof("%s webhook started", webhookName) } if err = as.Register(webhooks...); err != nil { diff --git a/deploy/dashboard.yaml b/deploy/dashboard.yaml index ee514972..bc2cb776 100644 --- a/deploy/dashboard.yaml +++ b/deploy/dashboard.yaml @@ -33,7 +33,7 @@ data: # security hostIPCSet: error hostPIDSet: error - notReadOnlyRootFileSystem: warning + notReadOnlyRootFilesystem: warning privilegeEscalationAllowed: error runAsRootAllowed: warning runAsPrivileged: error @@ -100,7 +100,7 @@ data: - tiller - kube2iam rules: - - notReadOnlyRootFileSystem + - notReadOnlyRootFilesystem - controllerNames: - cert-manager - dns-controller @@ -128,7 +128,7 @@ data: - goldilocks - insights-agent-goldilocks-vpa-install rules: - - notReadOnlyRootFileSystem + - notReadOnlyRootFilesystem - controllerNames: - insights-agent-goldilocks-controller rules: diff --git a/deploy/webhook.yaml b/deploy/webhook.yaml index f288d958..55602485 100644 --- a/deploy/webhook.yaml +++ b/deploy/webhook.yaml @@ -46,7 +46,7 @@ data: # security hostIPCSet: error hostPIDSet: error - notReadOnlyRootFileSystem: warning + notReadOnlyRootFilesystem: warning privilegeEscalationAllowed: error runAsRootAllowed: warning runAsPrivileged: error @@ -113,7 +113,7 @@ data: - tiller - kube2iam rules: - - notReadOnlyRootFileSystem + - notReadOnlyRootFilesystem - controllerNames: - cert-manager - dns-controller @@ -141,7 +141,7 @@ data: - goldilocks - insights-agent-goldilocks-vpa-install rules: - - notReadOnlyRootFileSystem + - notReadOnlyRootFilesystem - controllerNames: - insights-agent-goldilocks-controller rules: diff --git a/docs/check-documentation/security.md b/docs/check-documentation/security.md index ba7b9a48..162d6f3c 100644 --- a/docs/check-documentation/security.md +++ b/docs/check-documentation/security.md @@ -6,7 +6,7 @@ key | default | description ----|---------|------------ `security.hostIPCSet` | `error` | Fails when `hostIPC` attribute is configured. `security.hostPIDSet` | `error` | Fails when `hostPID` attribute is configured. -`security.notReadOnlyRootFileSystem` | `warning` | Fails when `securityContext.readOnlyRootFilesystem` is not true. +`security.notReadOnlyRootFilesystem` | `warning` | Fails when `securityContext.readOnlyRootFilesystem` is not true. `security.privilegeEscalationAllowed` | `error` | Fails when `securityContext.allowPrivilegeEscalation` is true. `security.runAsRootAllowed` | `error` | Fails when `securityContext.runAsNonRoot` is not true. `security.runAsPrivileged` | `error` | Fails when `securityContext.privileged` is true. diff --git a/examples/config-full.yaml b/examples/config-full.yaml index 8b2e916d..213dc389 100644 --- a/examples/config-full.yaml +++ b/examples/config-full.yaml @@ -17,7 +17,7 @@ checks: # security hostIPCSet: error hostPIDSet: error - notReadOnlyRootFileSystem: warning + notReadOnlyRootFilesystem: warning privilegeEscalationAllowed: error runAsRootAllowed: warning runAsPrivileged: error @@ -72,13 +72,6 @@ customChecks: not: pattern: ^quay.io -controllersToScan: - - Deployments - - StatefulSets - - DaemonSets - - CronJobs - - Jobs - - ReplicationControllers exemptions: - controllerNames: - dns-controller @@ -133,7 +126,7 @@ exemptions: - tiller - kube2iam rules: - - notReadOnlyRootFileSystem + - notReadOnlyRootFilesystem - controllerNames: - cert-manager - dns-controller diff --git a/examples/config.yaml b/examples/config.yaml index e468175f..35095df2 100644 --- a/examples/config.yaml +++ b/examples/config.yaml @@ -16,19 +16,12 @@ checks: # security hostIPCSet: error hostPIDSet: error - notReadOnlyRootFileSystem: warning + notReadOnlyRootFilesystem: warning privilegeEscalationAllowed: error runAsRootAllowed: warning runAsPrivileged: error dangerousCapabilities: error insecureCapabilities: warning -controllersToScan: - - Deployments - - StatefulSets - - DaemonSets - - CronJobs - - Jobs - - ReplicationControllers exemptions: - controllerNames: - dns-controller @@ -83,7 +76,7 @@ exemptions: - tiller - kube2iam rules: - - notReadOnlyRootFileSystem + - notReadOnlyRootFilesystem - controllerNames: - cert-manager - dns-controller @@ -111,7 +104,7 @@ exemptions: - goldilocks - insights-agent-goldilocks-vpa-install rules: - - notReadOnlyRootFileSystem + - notReadOnlyRootFilesystem - controllerNames: - insights-agent-goldilocks-controller rules: diff --git a/go.mod b/go.mod index b4a47654..7932f474 100644 --- a/go.mod +++ b/go.mod @@ -47,16 +47,17 @@ require ( github.com/markbates/safe v1.0.1 github.com/matttproud/golang_protobuf_extensions v1.0.1 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd - github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 + github.com/modern-go/reflect2 v1.0.1 github.com/pborman/uuid v0.0.0-20180906182336-adf5a7427709 github.com/petar/GoLLRB v0.0.0-20190514000832-33fb24c13b99 github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/pkg/errors v0.9.1 github.com/pmezard/go-difflib v1.0.0 - github.com/prometheus/client_golang v0.9.3 - github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 + github.com/prometheus/client_golang v1.0.0 + github.com/prometheus/client_model v0.2.0 github.com/prometheus/common v0.9.1 github.com/prometheus/procfs v0.0.11 + github.com/prometheus/tsdb v0.7.1 // indirect github.com/qri-io/jsonschema v0.1.1 github.com/rogpeppe/go-internal v1.5.2 github.com/sirupsen/logrus v1.4.2 @@ -76,7 +77,7 @@ require ( golang.org/x/net v0.0.0-20190620200207-3b0461eec859 golang.org/x/oauth2 v0.0.0-20190517181255-950ef44c6e07 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e - golang.org/x/sys v0.0.0-20191218084908-4a24b4065292 + golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e golang.org/x/text v0.3.2 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 golang.org/x/tools v0.0.0-20191224055732-dd894d0a8a40 diff --git a/go.sum b/go.sum index 4d4ac983..af2983ee 100644 --- a/go.sum +++ b/go.sum @@ -250,6 +250,7 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ github.com/modern-go/reflect2 v0.0.0-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= @@ -284,11 +285,13 @@ github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXP github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= github.com/prometheus/client_golang v0.9.3 h1:9iH4JKXLzFbOAdtqv/a+j8aewx2Y8lAjAydhbaScPF8= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_golang v1.0.0 h1:vrDKnkGzuGvhNAL56c7DBz29ZL+KxnoR0x7enabFceM= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= diff --git a/pkg/config/config.go b/pkg/config/config.go index 11671700..faa07005 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -30,7 +30,6 @@ import ( type Configuration struct { DisplayName string `json:"displayName"` Checks map[string]Severity `json:"checks"` - ControllersToScan []SupportedController `json:"controllersToScan"` CustomChecks map[string]SchemaCheck `json:"customChecks"` Exemptions []Exemption `json:"exemptions"` DisallowExemptions bool `json:"disallowExemptions"` diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index b13feb75..c8fe1704 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -31,16 +31,13 @@ var confInvalid = `test` var confValidYAML = ` checks: cpuRequestsMissing: warning -controllersToScan: - - Deployments ` var confValidJSON = ` { "checks": { "cpuRequestsMissing": "warning" - }, - "controllersToScan": ["Deployments"] + } } ` @@ -160,5 +157,4 @@ func TestConfigWithCustomChecks(t *testing.T) { func testParsedConfig(t *testing.T, config *Configuration) { assert.Equal(t, SeverityWarning, config.Checks["cpuRequestsMissing"]) assert.Equal(t, Severity(""), config.Checks["cpuLimitsMissing"]) - assert.ElementsMatch(t, []SupportedController{Deployments}, config.ControllersToScan) } diff --git a/pkg/config/supportedcontrollers.go b/pkg/config/supportedcontrollers.go deleted file mode 100644 index 2562859b..00000000 --- a/pkg/config/supportedcontrollers.go +++ /dev/null @@ -1,165 +0,0 @@ -package config - -import ( - "bytes" - "encoding/json" - "fmt" - "strings" - - 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" - batchv2alpha1 "k8s.io/api/batch/v2alpha1" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/runtime" -) - -const ( - // Unsupported is the default enum for non-defined controller types - Unsupported SupportedController = iota - // Deployments are a supported controller for scanning pod specs - Deployments - // StatefulSets are a supported controller for scanning pod specs - StatefulSets - // DaemonSets are a supported controller for scanning pod specs - DaemonSets - // Jobs are a supported controller for scanning pod specs - Jobs - // CronJobs are a supported controller for scanning pod specs - CronJobs - // ReplicationControllers are supported controllers for scanning pod specs - ReplicationControllers - // NakedPods are a pseudo-controller for scanning pod specs - NakedPods -) - -// ControllerStrings are strongly ordered to match the SupportedController enum -var ControllerStrings = []string{ - "Unsupported", - "Deployment", - "StatefulSet", - "DaemonSet", - "Job", - "CronJob", - "ReplicationController", - "NakedPod", -} - -// stringLookupForSupportedControllers is the list of lowercase singular and plural strings for string to enum lookup -var stringLookupForSupportedControllers = map[string]SupportedController{ - "deployment": Deployments, - "deployments": Deployments, - "statefulset": StatefulSets, - "statefulsets": StatefulSets, - "daemonset": DaemonSets, - "daemonsets": DaemonSets, - "job": Jobs, - "jobs": Jobs, - "cronjob": CronJobs, - "cronjobs": CronJobs, - "replicationcontroller": ReplicationControllers, - "replicationcontrollers": ReplicationControllers, - "nakedpod": NakedPods, - "nakedpods": NakedPods, -} - -// SupportedController is a constant item of a controller that is supported for scanning pod specs -type SupportedController int - -// String returns the string name for a given SupportedController enum -func (s SupportedController) String() string { - return ControllerStrings[s] -} - -// MarshalJSON manages writing the enum into json data or error on unsupported value -func (s SupportedController) MarshalJSON() ([]byte, error) { - if s == Unsupported { - return []byte{}, fmt.Errorf("Unsupported is not a valid Supported Controller") - } - buffer := bytes.NewBufferString(`"`) - buffer.WriteString(s.String()) - buffer.WriteString(`"`) - return buffer.Bytes(), nil -} - -// UnmarshalJSON handles reading json data into enum -func (s *SupportedController) UnmarshalJSON(b []byte) error { - var j string - err := json.Unmarshal(b, &j) - if err != nil { - return err - } - - *s = GetSupportedControllerFromString(j) - if *s == Unsupported { - return fmt.Errorf("Unsupported controller kind: %s", j) - } - return nil -} - -// ListSupportedAPIVersions for SupportedController returns all the apimachinery object type supported -func (s SupportedController) ListSupportedAPIVersions() []runtime.Object { - var supportedVersions []runtime.Object - switch s { - case Deployments: - supportedVersions = []runtime.Object{ - &appsv1.Deployment{}, - &appsv1beta1.Deployment{}, - &appsv1beta2.Deployment{}, - } - case StatefulSets: - supportedVersions = []runtime.Object{ - &appsv1.StatefulSet{}, - &appsv1beta1.StatefulSet{}, - &appsv1beta2.StatefulSet{}, - } - case DaemonSets: - supportedVersions = []runtime.Object{ - &appsv1.DaemonSet{}, - &appsv1beta2.DaemonSet{}, - } - case Jobs: - supportedVersions = []runtime.Object{ - &batchv1.Job{}, - } - case CronJobs: - supportedVersions = []runtime.Object{ - &batchv1beta1.CronJob{}, - &batchv2alpha1.CronJob{}, - } - case ReplicationControllers: - supportedVersions = []runtime.Object{ - &corev1.ReplicationController{}, - } - case NakedPods: - supportedVersions = []runtime.Object{ - &corev1.Pod{}, - } - } - return supportedVersions -} - -// GetSupportedControllerFromString fuzzy matches a string with a SupportedController Enum -func GetSupportedControllerFromString(str string) SupportedController { - lowerStr := strings.ToLower(str) - controller, keyFound := stringLookupForSupportedControllers[lowerStr] - if !keyFound { - controller = Unsupported - } - return controller -} - -// CheckIfKindIsConfiguredForValidation takes a kind (in string format) and checks if Polaris is configured to scan this type of controller -func (c Configuration) CheckIfKindIsConfiguredForValidation(kind string) bool { - controller := GetSupportedControllerFromString(kind) - if controller != Unsupported { - for _, controllerToScan := range c.ControllersToScan { - if controller == controllerToScan { - return true - } - } - } - return false -} diff --git a/pkg/config/supportedcontrollers_test.go b/pkg/config/supportedcontrollers_test.go deleted file mode 100644 index 694f4ccf..00000000 --- a/pkg/config/supportedcontrollers_test.go +++ /dev/null @@ -1,118 +0,0 @@ -// Copyright 2019 FairwindsOps Inc -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package config - -import ( - "encoding/json" - "fmt" - "testing" - - "github.com/stretchr/testify/assert" -) - -type checkMarshal struct { - Controllers []SupportedController `json:"controllers"` -} - -func TestUnmarshalSupportedControllers(t *testing.T) { - for idx, controllerString := range ControllerStrings { - // Check taking all strings and convert them into enums - object := checkMarshal{} - jsonBytes := []byte(fmt.Sprintf(`{"controllers":["%v"]}`, controllerString)) - err := json.Unmarshal(jsonBytes, &object) - if idx == 0 { - if err == nil { - // Assure the first element always should NOT unmarshal - t.Errorf("Expected the first element (%s) to fail json unmarshal. First element in this array should always be 'Unsupported'", controllerString) - } - } else if err != nil { - t.Errorf("Could not unmarshal json (%s) to a Supported controller; Received (%v)", jsonBytes, err) - } - } - - badJSON := []byte(`{"controllers":[{"not":"valid_structure"}]}`) - err := json.Unmarshal(badJSON, &checkMarshal{}) - if err == nil { - t.Error("expected invalid schema json to fail unmarshal") - } -} - -func TestMarshalSupportedControllers(t *testing.T) { - for idx, controllerString := range ControllerStrings { - controllerType := GetSupportedControllerFromString(controllerString) - if idx == 0 { - assert.Equal(t, SupportedController(0), controllerType) - } else { - assert.NotEqual(t, SupportedController(0), controllerType) - } - - object := checkMarshal{ - Controllers: []SupportedController{controllerType}, - } - _, err := json.Marshal(object) - if idx == 0 { - if err == nil { - t.Errorf("Expected (%s) to throw an error. Reserving the first element in the enum to be an invalid config", controllerString) - } - } else if err != nil { - t.Errorf("Could not write json output for element (%s); Received Error: (%s)", controllerString, err) - } - } -} - -func TestCheckIfControllerKindIsConfiguredForValidation(t *testing.T) { - config := Configuration{} - for _, controllerString := range ControllerStrings[1:] { - controllerEnum := GetSupportedControllerFromString(controllerString) - assert.NotEqual(t, SupportedController(0), controllerEnum) - config.ControllersToScan = append(config.ControllersToScan, controllerEnum) - } - - validControllerKinds := []string{ - "deployment", - "statefulset", - } - - invalidControllerKinds := []string{ - "nonExistent", - } - - for _, kind := range validControllerKinds { - if ok := config.CheckIfKindIsConfiguredForValidation(kind); !ok { - t.Errorf("Kind (%s) expected to be valid for configuration.", kind) - } - } - - for _, kind := range invalidControllerKinds { - if ok := config.CheckIfKindIsConfiguredForValidation(kind); ok { - t.Errorf("Kind (%s) should not be a valid controller to check", kind) - } - } -} - -func TestGetSupportedControllerFromString(t *testing.T) { - fixture := map[string]SupportedController{ - "": Unsupported, - "asdfasdf": Unsupported, - "\000": Unsupported, - "deployMENTS": Deployments, - "JOB": Jobs, - } - - for inputString, expectedType := range fixture { - resolvedType := GetSupportedControllerFromString(inputString) - assert.Equal(t, expectedType, resolvedType, fmt.Sprintf("Expected (%s) to return (%s) controller type.", inputString, expectedType)) - } -} diff --git a/pkg/kube/resources.go b/pkg/kube/resources.go index fc8d74f6..ebd2e03f 100644 --- a/pkg/kube/resources.go +++ b/pkg/kube/resources.go @@ -9,7 +9,6 @@ import ( "strings" "time" - "github.com/fairwindsops/polaris/pkg/validator/controllers" "github.com/sirupsen/logrus" "gopkg.in/yaml.v3" corev1 "k8s.io/api/core/v1" @@ -31,13 +30,15 @@ type ResourceProvider struct { SourceType string Nodes []corev1.Node Namespaces []corev1.Namespace - Controllers []controllers.GenericController + Controllers []GenericWorkload } type k8sResource struct { Kind string `yaml:"kind"` } +var podSpecFields = []string{"jobTemplate", "spec", "template"} + // CreateResourceProvider returns a new ResourceProvider object to interact with k8s resources func CreateResourceProvider(directory string) (*ResourceProvider, error) { if directory != "" { @@ -54,7 +55,7 @@ func CreateResourceProviderFromPath(directory string) (*ResourceProvider, error) SourceName: directory, Nodes: []corev1.Node{}, Namespaces: []corev1.Namespace{}, - Controllers: []controllers.GenericController{}, + Controllers: []GenericWorkload{}, } addYaml := func(contents string) error { @@ -155,37 +156,46 @@ func CreateResourceProviderFromAPI(kube kubernetes.Interface, clusterName string } // 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{} +func LoadControllers(pods []corev1.Pod, dynamicClientPointer *dynamic.Interface, restMapperPointer *meta.RESTMapper) []GenericWorkload { + interfaces := []GenericWorkload{} + deduped := map[string]corev1.Pod{} for _, pod := range pods { - interfaces = append(interfaces, controllers.NewGenericPodController(pod, dynamicClientPointer, restMapperPointer)) + owners := pod.ObjectMeta.OwnerReferences + if len(owners) == 0 { + deduped[pod.ObjectMeta.Namespace+"/Pod/"+pod.ObjectMeta.Name] = pod + continue + } + deduped[pod.ObjectMeta.Namespace+"/"+owners[0].Kind+"/"+owners[0].Name] = pod + } + for _, pod := range deduped { + interfaces = append(interfaces, NewGenericWorkload(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) +func deduplicateControllers(inputControllers []GenericWorkload) []GenericWorkload { + controllerMap := make(map[string]GenericWorkload) for _, controller := range inputControllers { - key := controller.GetNamespace() + "/" + controller.GetKind() + "/" + controller.Name + key := controller.ObjectMeta.GetNamespace() + "/" + controller.Kind + "/" + controller.ObjectMeta.GetName() oldController, ok := controllerMap[key] - if !ok || controller.CreatedTime.After(oldController.CreatedTime) { + if !ok || controller.ObjectMeta.GetCreationTimestamp().Time.After(oldController.ObjectMeta.GetCreationTimestamp().Time) { controllerMap[key] = controller } } - results := make([]controllers.GenericController, 0) + results := make([]GenericWorkload, 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 { +// GetPodSpec looks inside arbitrary YAML for a PodSpec +func GetPodSpec(yaml map[string]interface{}) interface{} { + for _, child := range podSpecFields { if childYaml, ok := yaml[child]; ok { - return getPodSpec(childYaml.(map[string]interface{})) + return GetPodSpec(childYaml.(map[string]interface{})) } } return yaml @@ -209,7 +219,7 @@ func addResourceFromString(contents string, resources *ResourceProvider) error { } else if resource.Kind == "Pod" { pod := corev1.Pod{} err = decoder.Decode(&pod) - resources.Controllers = append(resources.Controllers, controllers.NewGenericPodController(pod, nil, nil)) + resources.Controllers = append(resources.Controllers, NewGenericWorkload(pod, nil, nil)) } else { yamlNode := make(map[string]interface{}) err = yaml.Unmarshal(contentBytes, &yamlNode) @@ -221,7 +231,7 @@ func addResourceFromString(contents string, resources *ResourceProvider) error { finalDoc["metadata"] = yamlNode["metadata"] finalDoc["apiVersion"] = "v1" finalDoc["kind"] = "Pod" - finalDoc["spec"] = getPodSpec(yamlNode) + finalDoc["spec"] = GetPodSpec(yamlNode) marshaledYaml, err := yaml.Marshal(finalDoc) if err != nil { logrus.Errorf("Could not marshal yaml: %v", err) @@ -230,7 +240,7 @@ func addResourceFromString(contents string, resources *ResourceProvider) error { decoder := k8sYaml.NewYAMLOrJSONDecoder(bytes.NewReader(marshaledYaml), 1000) pod := corev1.Pod{} err = decoder.Decode(&pod) - newController := controllers.NewGenericPodController(pod, nil, nil) + newController := NewGenericWorkload(pod, nil, nil) newController.Kind = resource.Kind resources.Controllers = append(resources.Controllers, newController) } diff --git a/pkg/kube/resources_test.go b/pkg/kube/resources_test.go index 742cc407..28517d40 100644 --- a/pkg/kube/resources_test.go +++ b/pkg/kube/resources_test.go @@ -26,7 +26,7 @@ func TestGetResourcesFromPath(t *testing.T) { assert.Equal(t, 8, len(resources.Controllers), "Should have eight controllers") namespaceCount := map[string]int{} for _, controller := range resources.Controllers { - namespaceCount[controller.GetNamespace()]++ + namespaceCount[controller.ObjectMeta.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'") @@ -72,5 +72,5 @@ func TestGetResourceFromAPI(t *testing.T) { assert.Equal(t, 0, len(resources.Nodes), "Should not have any nodes") assert.Equal(t, 1, len(resources.Controllers), "Should have 1 controller") - assert.Equal(t, "", resources.Controllers[0].ObjectMeta.Name) + assert.Equal(t, "", resources.Controllers[0].ObjectMeta.GetName()) } diff --git a/pkg/kube/workload.go b/pkg/kube/workload.go new file mode 100644 index 00000000..40fe5ba7 --- /dev/null +++ b/pkg/kube/workload.go @@ -0,0 +1,64 @@ +package kube + +import ( + "fmt" + + "github.com/sirupsen/logrus" + kubeAPICoreV1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" + kubeAPIMetaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/dynamic" +) + +// GenericWorkload is a base implementation with some free methods for inherited structs +type GenericWorkload struct { + Kind string + PodSpec kubeAPICoreV1.PodSpec + ObjectMeta kubeAPIMetaV1.Object +} + +// NewGenericWorkload builds a new workload for a given Pod +func NewGenericWorkload(originalResource kubeAPICoreV1.Pod, dynamicClientPointer *dynamic.Interface, restMapperPointer *meta.RESTMapper) GenericWorkload { + workload := GenericWorkload{} + workload.PodSpec = originalResource.Spec + workload.ObjectMeta = originalResource.ObjectMeta.GetObjectMeta() + workload.Kind = "Pod" + fmt.Println("get workload", workload.ObjectMeta.GetNamespace(), workload.ObjectMeta.GetName()) + + if dynamicClientPointer == nil || restMapperPointer == nil { + return workload + } + // If an owner exists then set the name to the workload. + // This allows us to handle CRDs creating Workloads or DeploymentConfigs in OpenShift. + owners := workload.ObjectMeta.GetOwnerReferences() + for len(owners) > 0 { + if len(owners) > 1 { + logrus.Warn("More than 1 owner found") + } + firstOwner := owners[0] + if firstOwner.Kind == "Node" { + break + } + workload.Kind = firstOwner.Kind + + 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 workload + } + parent, err := dynamicClient.Resource(mapping.Resource).Namespace(workload.ObjectMeta.GetNamespace()).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 workload + } + objMeta, err := meta.Accessor(parent) + workload.ObjectMeta = objMeta + owners = parent.GetOwnerReferences() + } + + return workload +} diff --git a/pkg/validator/container.go b/pkg/validator/container.go index 91add9ad..25c59346 100644 --- a/pkg/validator/container.go +++ b/pkg/validator/container.go @@ -16,13 +16,13 @@ package validator import ( "github.com/fairwindsops/polaris/pkg/config" - "github.com/fairwindsops/polaris/pkg/validator/controllers" + "github.com/fairwindsops/polaris/pkg/kube" corev1 "k8s.io/api/core/v1" ) // ValidateContainer validates a single container from a given controller -func ValidateContainer(conf *config.Configuration, controller controllers.GenericController, container *corev1.Container, isInit bool) (ContainerResult, error) { +func ValidateContainer(conf *config.Configuration, controller kube.GenericWorkload, container *corev1.Container, isInit bool) (ContainerResult, error) { results, err := applyContainerSchemaChecks(conf, controller, container, isInit) if err != nil { return ContainerResult{}, err @@ -37,9 +37,9 @@ func ValidateContainer(conf *config.Configuration, controller controllers.Generi } // ValidateAllContainers validates both init and regular containers -func ValidateAllContainers(conf *config.Configuration, controller controllers.GenericController) ([]ContainerResult, error) { +func ValidateAllContainers(conf *config.Configuration, controller kube.GenericWorkload) ([]ContainerResult, error) { results := []ContainerResult{} - pod := controller.GetPodSpec() + pod := controller.PodSpec for _, container := range pod.InitContainers { result, err := ValidateContainer(conf, controller, &container, true) if err != nil { diff --git a/pkg/validator/container_test.go b/pkg/validator/container_test.go index f3f4d822..65720909 100644 --- a/pkg/validator/container_test.go +++ b/pkg/validator/container_test.go @@ -19,10 +19,9 @@ import ( "testing" conf "github.com/fairwindsops/polaris/pkg/config" - "github.com/fairwindsops/polaris/pkg/validator/controllers" + "github.com/fairwindsops/polaris/pkg/kube" "github.com/stretchr/testify/assert" - appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -51,26 +50,24 @@ exemptions: - foo ` -func getEmptyController(name string) controllers.GenericController { - return controllers.NewDeploymentController(appsv1.Deployment{ +func getEmptyWorkload(name string) kube.GenericWorkload { + workload := kube.NewGenericWorkload(corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: name, }, - Spec: appsv1.DeploymentSpec{ - Template: corev1.PodTemplateSpec{}, - }, - }) + }, nil, nil) + return workload } func testValidate(t *testing.T, container *corev1.Container, resourceConf *string, controllerName string, expectedErrors []ResultMessage, expectedWarnings []ResultMessage, expectedSuccesses []ResultMessage) { - testValidateWithController(t, container, resourceConf, getEmptyController(controllerName), expectedErrors, expectedWarnings, expectedSuccesses) + testValidateWithWorkload(t, container, resourceConf, getEmptyWorkload(controllerName), expectedErrors, expectedWarnings, expectedSuccesses) } -func testValidateWithController(t *testing.T, container *corev1.Container, resourceConf *string, controller controllers.GenericController, expectedErrors []ResultMessage, expectedWarnings []ResultMessage, expectedSuccesses []ResultMessage) { +func testValidateWithWorkload(t *testing.T, container *corev1.Container, resourceConf *string, workload kube.GenericWorkload, expectedErrors []ResultMessage, expectedWarnings []ResultMessage, expectedSuccesses []ResultMessage) { parsedConf, err := conf.Parse([]byte(*resourceConf)) assert.NoError(t, err, "Expected no error when parsing config") - results, err := applyContainerSchemaChecks(&parsedConf, controller, container, false) + results, err := applyContainerSchemaChecks(&parsedConf, workload, container, false) if err != nil { panic(err) } @@ -91,7 +88,7 @@ func TestValidateResourcesEmptyConfig(t *testing.T) { Name: "Empty", } - results, err := applyContainerSchemaChecks(&conf.Configuration{}, getEmptyController(""), container, false) + results, err := applyContainerSchemaChecks(&conf.Configuration{}, getEmptyWorkload(""), container, false) if err != nil { panic(err) } @@ -187,7 +184,7 @@ func TestValidateHealthChecks(t *testing.T) { for idx, tt := range testCases { t.Run(tt.name, func(t *testing.T) { - controller := getEmptyController("") + controller := getEmptyWorkload("") results, err := applyContainerSchemaChecks(&conf.Configuration{Checks: tt.probes}, controller, tt.container, tt.isInit) if err != nil { panic(err) @@ -301,7 +298,7 @@ func TestValidateImage(t *testing.T) { for _, tt := range testCases { t.Run(tt.name, func(t *testing.T) { - controller := getEmptyController("") + controller := getEmptyWorkload("") results, err := applyContainerSchemaChecks(&conf.Configuration{Checks: tt.image}, controller, tt.container, false) if err != nil { panic(err) @@ -418,7 +415,7 @@ func TestValidateNetworking(t *testing.T) { for _, tt := range testCases { t.Run(tt.name, func(t *testing.T) { - controller := getEmptyController("") + controller := getEmptyWorkload("") results, err := applyContainerSchemaChecks(&conf.Configuration{Checks: tt.networkConf}, controller, tt.container, false) if err != nil { panic(err) @@ -442,7 +439,7 @@ func TestValidateSecurity(t *testing.T) { standardConf := map[string]conf.Severity{ "runAsRootAllowed": conf.SeverityWarning, "runAsPrivileged": conf.SeverityError, - "notReadOnlyRootFileSystem": conf.SeverityWarning, + "notReadOnlyRootFilesystem": conf.SeverityWarning, "privilegeEscalationAllowed": conf.SeverityError, "dangerousCapabilities": conf.SeverityError, "insecureCapabilities": conf.SeverityWarning, @@ -450,7 +447,7 @@ func TestValidateSecurity(t *testing.T) { strongConf := map[string]conf.Severity{ "runAsRootAllowed": conf.SeverityError, "runAsPrivileged": conf.SeverityError, - "notReadOnlyRootFileSystem": conf.SeverityError, + "notReadOnlyRootFilesystem": conf.SeverityError, "privilegeEscalationAllowed": conf.SeverityError, "dangerousCapabilities": conf.SeverityError, "insecureCapabilities": conf.SeverityError, @@ -543,7 +540,7 @@ func TestValidateSecurity(t *testing.T) { Severity: "warning", Category: "Security", }, { - ID: "notReadOnlyRootFileSystem", + ID: "notReadOnlyRootFilesystem", Message: "Filesystem should be read only", Success: false, Severity: "warning", @@ -610,7 +607,7 @@ func TestValidateSecurity(t *testing.T) { Severity: "warning", Category: "Security", }, { - ID: "notReadOnlyRootFileSystem", + ID: "notReadOnlyRootFilesystem", Message: "Filesystem should be read only", Success: false, Severity: "warning", @@ -653,7 +650,7 @@ func TestValidateSecurity(t *testing.T) { Severity: "warning", Category: "Security", }, { - ID: "notReadOnlyRootFileSystem", + ID: "notReadOnlyRootFilesystem", Message: "Filesystem should be read only", Success: false, Severity: "warning", @@ -696,7 +693,7 @@ func TestValidateSecurity(t *testing.T) { Severity: "warning", Category: "Security", }, { - ID: "notReadOnlyRootFileSystem", + ID: "notReadOnlyRootFilesystem", Message: "Filesystem should be read only", Success: false, Severity: "warning", @@ -715,7 +712,7 @@ func TestValidateSecurity(t *testing.T) { Severity: "warning", Category: "Security", }, { - ID: "notReadOnlyRootFileSystem", + ID: "notReadOnlyRootFilesystem", Message: "Filesystem is read only", Success: true, Severity: "warning", @@ -770,7 +767,7 @@ func TestValidateSecurity(t *testing.T) { Severity: "error", Category: "Security", }, { - ID: "notReadOnlyRootFileSystem", + ID: "notReadOnlyRootFilesystem", Message: "Filesystem is read only", Success: true, Severity: "error", @@ -801,7 +798,7 @@ func TestValidateSecurity(t *testing.T) { Severity: "error", Category: "Security", }, { - ID: "notReadOnlyRootFileSystem", + ID: "notReadOnlyRootFilesystem", Message: "Filesystem is read only", Success: true, Severity: "error", @@ -844,7 +841,7 @@ func TestValidateSecurity(t *testing.T) { Severity: "error", Category: "Security", }, { - ID: "notReadOnlyRootFileSystem", + ID: "notReadOnlyRootFilesystem", Message: "Filesystem is read only", Success: true, Severity: "error", @@ -887,7 +884,7 @@ func TestValidateSecurity(t *testing.T) { Severity: "error", Category: "Security", }, { - ID: "notReadOnlyRootFileSystem", + ID: "notReadOnlyRootFilesystem", Message: "Filesystem is read only", Success: true, Severity: "error", @@ -922,17 +919,8 @@ func TestValidateSecurity(t *testing.T) { for _, tt := range testCases { t.Run(tt.name, func(t *testing.T) { - controller := controllers.NewDeploymentController(appsv1.Deployment{ - ObjectMeta: metav1.ObjectMeta{ - Name: "", - }, - Spec: appsv1.DeploymentSpec{ - Template: corev1.PodTemplateSpec{ - Spec: *tt.pod, - }, - }, - }) - results, err := applyContainerSchemaChecks(&conf.Configuration{Checks: tt.securityConf}, controller, tt.container, false) + workload := kube.NewGenericWorkload(corev1.Pod{Spec: *tt.pod}, nil, nil) + results, err := applyContainerSchemaChecks(&conf.Configuration{Checks: tt.securityConf}, workload, tt.container, false) if err != nil { panic(err) } @@ -1075,17 +1063,8 @@ func TestValidateRunAsRoot(t *testing.T) { } for idx, tt := range testCases { t.Run(tt.name, func(t *testing.T) { - controller := controllers.NewDeploymentController(appsv1.Deployment{ - ObjectMeta: metav1.ObjectMeta{ - Name: "", - }, - Spec: appsv1.DeploymentSpec{ - Template: corev1.PodTemplateSpec{ - Spec: *tt.pod, - }, - }, - }) - results, err := applyContainerSchemaChecks(&config, controller, tt.container, false) + workload := kube.NewGenericWorkload(corev1.Pod{Spec: *tt.pod}, nil, nil) + results, err := applyContainerSchemaChecks(&config, workload, tt.container, false) if err != nil { panic(err) } @@ -1185,7 +1164,7 @@ func TestValidateResourcesEmptyContainerCPURequestsExempt(t *testing.T) { expectedSuccesses := []ResultMessage{} - controller := controllers.NewDeploymentController(appsv1.Deployment{ + workload := kube.NewGenericWorkload(corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "foo", Annotations: map[string]string{ @@ -1193,9 +1172,6 @@ func TestValidateResourcesEmptyContainerCPURequestsExempt(t *testing.T) { "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) + }, nil, nil) + testValidateWithWorkload(t, &container, &resourceConfMinimal, workload, expectedErrors, expectedWarnings, expectedSuccesses) } diff --git a/pkg/validator/controller.go b/pkg/validator/controller.go index b3d5ee5c..72962fef 100644 --- a/pkg/validator/controller.go +++ b/pkg/validator/controller.go @@ -21,21 +21,20 @@ import ( conf "github.com/fairwindsops/polaris/pkg/config" "github.com/fairwindsops/polaris/pkg/kube" - 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.GenericController) (ControllerResult, error) { +func ValidateController(conf *conf.Configuration, controller kube.GenericWorkload) (ControllerResult, error) { podResult, err := ValidatePod(conf, controller) if err != nil { return ControllerResult{}, err } result := ControllerResult{ - Kind: controller.GetKind(), - Name: controller.GetName(), - Namespace: controller.GetObjectMeta().Namespace, + Kind: controller.Kind, + Name: controller.ObjectMeta.GetName(), + Namespace: controller.ObjectMeta.GetNamespace(), Results: ResultSet{}, PodResult: podResult, } @@ -64,8 +63,8 @@ func ValidateControllers(config *conf.Configuration, kubeResources *kube.Resourc return results, nil } -func hasExemptionAnnotation(ctrl controller.GenericController) bool { - annot := ctrl.GetObjectMeta().Annotations +func hasExemptionAnnotation(ctrl kube.GenericWorkload) bool { + annot := ctrl.ObjectMeta.GetAnnotations() val := annot[exemptionAnnotationKey] return strings.ToLower(val) == "true" } diff --git a/pkg/validator/controller_test.go b/pkg/validator/controller_test.go index a3335309..9d722ba9 100644 --- a/pkg/validator/controller_test.go +++ b/pkg/validator/controller_test.go @@ -22,7 +22,6 @@ import ( conf "github.com/fairwindsops/polaris/pkg/config" "github.com/fairwindsops/polaris/pkg/kube" - controller "github.com/fairwindsops/polaris/pkg/validator/controllers" "github.com/fairwindsops/polaris/test" ) @@ -33,7 +32,8 @@ func TestValidateController(t *testing.T) { "hostPIDSet": conf.SeverityError, }, } - deployment := controller.NewDeploymentController(test.MockDeploy()) + deployment := kube.NewGenericWorkload(test.MockPod(), nil, nil) + deployment.Kind = "Deployment" expectedSum := CountSummary{ Successes: uint(2), Warnings: uint(0), @@ -62,18 +62,11 @@ func TestSkipHealthChecks(t *testing.T) { "readinessProbeMissing": conf.SeverityError, "livenessProbeMissing": conf.SeverityWarning, }, - ControllersToScan: []conf.SupportedController{ - conf.Deployments, - conf.StatefulSets, - conf.DaemonSets, - conf.Jobs, - conf.CronJobs, - conf.ReplicationControllers, - }, } - deploymentBase := test.MockDeploy() - deploymentBase.Spec.Template.Spec.InitContainers = []corev1.Container{test.MockContainer("test")} - deployment := controller.NewDeploymentController(deploymentBase) + pod := test.MockPod() + pod.Spec.InitContainers = []corev1.Container{test.MockContainer("test")} + deployment := kube.NewGenericWorkload(pod, nil, nil) + deployment.Kind = "Deployment" expectedSum := CountSummary{ Successes: uint(0), Warnings: uint(1), @@ -93,7 +86,8 @@ func TestSkipHealthChecks(t *testing.T) { assert.EqualValues(t, ResultSet{}, actualResult.PodResult.ContainerResults[0].Results) assert.EqualValues(t, expectedResults, actualResult.PodResult.ContainerResults[1].Results) - job := controller.NewJobController(test.MockJob()) + job := kube.NewGenericWorkload(test.MockPod(), nil, nil) + job.Kind = "Job" expectedSum = CountSummary{ Successes: uint(0), Warnings: uint(0), @@ -109,7 +103,8 @@ func TestSkipHealthChecks(t *testing.T) { assert.EqualValues(t, expectedSum, actualResult.GetSummary()) assert.EqualValues(t, expectedResults, actualResult.PodResult.ContainerResults[0].Results) - cronjob := controller.NewCronJobController(test.MockCronJob()) + cronjob := kube.NewGenericWorkload(test.MockPod(), nil, nil) + cronjob.Kind = "CronJob" expectedSum = CountSummary{ Successes: uint(0), Warnings: uint(0), @@ -132,14 +127,12 @@ func TestControllerExemptions(t *testing.T) { "readinessProbeMissing": conf.SeverityError, "livenessProbeMissing": conf.SeverityWarning, }, - ControllersToScan: []conf.SupportedController{ - conf.Deployments, - }, } - newController := test.MockGenericController() - newController.Kind = "Deployment" + pod := test.MockPod() + workload := kube.NewGenericWorkload(pod, nil, nil) + workload.Kind = "Deployment" resources := &kube.ResourceProvider{ - Controllers: []controller.GenericController{newController}, + Controllers: []kube.GenericWorkload{workload}, } expectedSum := CountSummary{ @@ -155,9 +148,9 @@ func TestControllerExemptions(t *testing.T) { assert.Equal(t, "Deployment", actualResults[0].Kind) assert.EqualValues(t, expectedSum, actualResults[0].GetSummary()) - resources.Controllers[0].ObjectMeta.Annotations = map[string]string{ + resources.Controllers[0].ObjectMeta.SetAnnotations(map[string]string{ exemptionAnnotationKey: "true", - } + }) actualResults, err = ValidateControllers(&c, resources) if err != nil { panic(err) diff --git a/pkg/validator/controllers/cronjob.go b/pkg/validator/controllers/cronjob.go deleted file mode 100644 index 33a2c72e..00000000 --- a/pkg/validator/controllers/cronjob.go +++ /dev/null @@ -1,21 +0,0 @@ -package controllers - -import ( - "github.com/fairwindsops/polaris/pkg/config" - "github.com/sirupsen/logrus" - kubeAPIBatchV1beta1 "k8s.io/api/batch/v1beta1" -) - -// NewCronJobController builds a new controller interface for Deployments -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 deleted file mode 100644 index c8ce6d3e..00000000 --- a/pkg/validator/controllers/daemonset.go +++ /dev/null @@ -1,17 +0,0 @@ -package controllers - -import ( - "github.com/fairwindsops/polaris/pkg/config" - kubeAPIAppsV1 "k8s.io/api/apps/v1" -) - -// NewDaemonSetController builds a new controller interface for Deployments -func NewDaemonSetController(originalResource kubeAPIAppsV1.DaemonSet) GenericController { - controller := GenericController{} - controller.Name = originalResource.Name - controller.Namespace = originalResource.Namespace - 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 deleted file mode 100644 index 2972d442..00000000 --- a/pkg/validator/controllers/deployment.go +++ /dev/null @@ -1,17 +0,0 @@ -package controllers - -import ( - "github.com/fairwindsops/polaris/pkg/config" - kubeAPIAppsV1 "k8s.io/api/apps/v1" -) - -// NewDeploymentController builds a new controller interface for Deployments -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 deleted file mode 100644 index 3d78023c..00000000 --- a/pkg/validator/controllers/generic.go +++ /dev/null @@ -1,91 +0,0 @@ -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/job.go b/pkg/validator/controllers/job.go deleted file mode 100644 index 3377249f..00000000 --- a/pkg/validator/controllers/job.go +++ /dev/null @@ -1,17 +0,0 @@ -package controllers - -import ( - "github.com/fairwindsops/polaris/pkg/config" - kubeAPIBatchV1 "k8s.io/api/batch/v1" -) - -// NewJobController builds a new controller interface for Deployments -func NewJobController(originalResource kubeAPIBatchV1.Job) GenericController { - controller := GenericController{} - controller.Name = originalResource.Name - controller.Namespace = originalResource.Namespace - 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 deleted file mode 100644 index b96a76ee..00000000 --- a/pkg/validator/controllers/naked-pod.go +++ /dev/null @@ -1,18 +0,0 @@ -package controllers - -import ( - "github.com/fairwindsops/polaris/pkg/config" - kubeAPICoreV1 "k8s.io/api/core/v1" -) - -// NewNakedPodController builds a new controller interface for NakedPods -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 deleted file mode 100644 index 1a9fa32f..00000000 --- a/pkg/validator/controllers/replicationcontroller.go +++ /dev/null @@ -1,17 +0,0 @@ -package controllers - -import ( - "github.com/fairwindsops/polaris/pkg/config" - kubeAPICoreV1 "k8s.io/api/core/v1" -) - -// NewReplicationControllerController builds a new controller interface for Deployments -func NewReplicationControllerController(originalResource kubeAPICoreV1.ReplicationController) GenericController { - controller := GenericController{} - controller.Name = originalResource.Name - controller.Namespace = originalResource.Namespace - 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 deleted file mode 100644 index fab6b865..00000000 --- a/pkg/validator/controllers/statefulsets.go +++ /dev/null @@ -1,21 +0,0 @@ -package controllers - -import ( - "github.com/fairwindsops/polaris/pkg/config" - "github.com/sirupsen/logrus" - kubeAPIAppsV1 "k8s.io/api/apps/v1" -) - -// NewStatefulSetController builds a statefulset controller -func NewStatefulSetController(originalResource kubeAPIAppsV1.StatefulSet) GenericController { - controller := GenericController{} - controller.Name = originalResource.Name - controller.Namespace = originalResource.Namespace - 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_test.go b/pkg/validator/fullaudit_test.go index 0135ed30..e1771af3 100644 --- a/pkg/validator/fullaudit_test.go +++ b/pkg/validator/fullaudit_test.go @@ -23,14 +23,6 @@ func TestGetTemplateData(t *testing.T) { "readinessProbeMissing": conf.SeverityError, "livenessProbeMissing": conf.SeverityWarning, }, - ControllersToScan: []conf.SupportedController{ - conf.Deployments, - conf.StatefulSets, - conf.DaemonSets, - conf.Jobs, - conf.CronJobs, - conf.ReplicationControllers, - }, } sum := CountSummary{ diff --git a/pkg/validator/pod.go b/pkg/validator/pod.go index f6678191..6bef7b91 100644 --- a/pkg/validator/pod.go +++ b/pkg/validator/pod.go @@ -16,11 +16,11 @@ package validator import ( "github.com/fairwindsops/polaris/pkg/config" - "github.com/fairwindsops/polaris/pkg/validator/controllers" + "github.com/fairwindsops/polaris/pkg/kube" ) // ValidatePod validates that each pod conforms to the Polaris config, returns a ResourceResult. -func ValidatePod(conf *config.Configuration, controller controllers.GenericController) (PodResult, error) { +func ValidatePod(conf *config.Configuration, controller kube.GenericWorkload) (PodResult, error) { podResults, err := applyPodSchemaChecks(conf, controller) if err != nil { return PodResult{}, err diff --git a/pkg/validator/pod_test.go b/pkg/validator/pod_test.go index 1c2139ec..5b103590 100644 --- a/pkg/validator/pod_test.go +++ b/pkg/validator/pod_test.go @@ -17,13 +17,12 @@ package validator import ( "testing" - conf "github.com/fairwindsops/polaris/pkg/config" - "github.com/fairwindsops/polaris/pkg/validator/controllers" - "github.com/fairwindsops/polaris/test" - "github.com/stretchr/testify/assert" - appsv1 "k8s.io/api/apps/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + conf "github.com/fairwindsops/polaris/pkg/config" + "github.com/fairwindsops/polaris/pkg/kube" + "github.com/fairwindsops/polaris/test" ) func TestValidatePod(t *testing.T) { @@ -39,7 +38,7 @@ func TestValidatePod(t *testing.T) { k8s, _ := test.SetupTestAPI() k8s = test.SetupAddControllers(k8s, "test") p := test.MockPod() - deployment := controllers.NewDeploymentController(appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: p}}) + deployment := kube.NewGenericWorkload(p, nil, nil) expectedSum := CountSummary{ Successes: uint(4), @@ -77,7 +76,7 @@ func TestInvalidIPCPod(t *testing.T) { k8s = test.SetupAddControllers(k8s, "test") p := test.MockPod() p.Spec.HostIPC = true - deployment := controllers.NewDeploymentController(appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: p}}) + workload := kube.NewGenericWorkload(p, nil, nil) expectedSum := CountSummary{ Successes: uint(3), @@ -90,7 +89,7 @@ func TestInvalidIPCPod(t *testing.T) { "hostPIDSet": {ID: "hostPIDSet", Message: "Host PID is not configured", Success: true, Severity: "error", Category: "Security"}, } - actualPodResult, err := ValidatePod(&c, deployment) + actualPodResult, err := ValidatePod(&c, workload) if err != nil { panic(err) } @@ -114,7 +113,7 @@ func TestInvalidNeworkPod(t *testing.T) { k8s = test.SetupAddControllers(k8s, "test") p := test.MockPod() p.Spec.HostNetwork = true - deployment := controllers.NewDeploymentController(appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: p}}) + workload := kube.NewGenericWorkload(p, nil, nil) expectedSum := CountSummary{ Successes: uint(3), @@ -128,7 +127,7 @@ func TestInvalidNeworkPod(t *testing.T) { "hostPIDSet": {ID: "hostPIDSet", Message: "Host PID is not configured", Success: true, Severity: "error", Category: "Security"}, } - actualPodResult, err := ValidatePod(&c, deployment) + actualPodResult, err := ValidatePod(&c, workload) if err != nil { panic(err) } @@ -152,7 +151,7 @@ func TestInvalidPIDPod(t *testing.T) { k8s = test.SetupAddControllers(k8s, "test") p := test.MockPod() p.Spec.HostPID = true - deployment := controllers.NewDeploymentController(appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: p}}) + workload := kube.NewGenericWorkload(p, nil, nil) expectedSum := CountSummary{ Successes: uint(3), @@ -166,7 +165,7 @@ func TestInvalidPIDPod(t *testing.T) { "hostNetworkSet": {ID: "hostNetworkSet", Message: "Host network is not configured", Success: true, Severity: "warning", Category: "Networking"}, } - actualPodResult, err := ValidatePod(&c, deployment) + actualPodResult, err := ValidatePod(&c, workload) if err != nil { panic(err) } @@ -196,13 +195,10 @@ func TestExemption(t *testing.T) { k8s = test.SetupAddControllers(k8s, "test") p := test.MockPod() p.Spec.HostIPC = true - meta := metav1.ObjectMeta{ + p.ObjectMeta = metav1.ObjectMeta{ Name: "foo", } - deploySpec := appsv1.DeploymentSpec{ - Template: p, - } - deployment := controllers.NewDeploymentController(appsv1.Deployment{ObjectMeta: meta, Spec: deploySpec}) + workload := kube.NewGenericWorkload(p, nil, nil) expectedSum := CountSummary{ Successes: uint(3), @@ -214,7 +210,7 @@ func TestExemption(t *testing.T) { "hostPIDSet": {ID: "hostPIDSet", Message: "Host PID is not configured", Success: true, Severity: "error", Category: "Security"}, } - actualPodResult, err := ValidatePod(&c, deployment) + actualPodResult, err := ValidatePod(&c, workload) if err != nil { panic(err) } diff --git a/pkg/validator/schema.go b/pkg/validator/schema.go index 89f8c872..ec2ed81c 100644 --- a/pkg/validator/schema.go +++ b/pkg/validator/schema.go @@ -12,7 +12,7 @@ import ( "k8s.io/apimachinery/pkg/util/yaml" "github.com/fairwindsops/polaris/pkg/config" - "github.com/fairwindsops/polaris/pkg/validator/controllers" + "github.com/fairwindsops/polaris/pkg/kube" ) var ( @@ -37,7 +37,7 @@ var ( "hostPortSet", "runAsRootAllowed", "runAsPrivileged", - "notReadOnlyRootFileSystem", + "notReadOnlyRootFilesystem", "privilegeEscalationAllowed", "dangerousCapabilities", "insecureCapabilities", @@ -74,7 +74,7 @@ func parseCheck(rawBytes []byte) (config.SchemaCheck, error) { } } -func resolveCheck(conf *config.Configuration, checkID string, controller controllers.GenericController, target config.TargetKind, isInitContainer bool) (*config.SchemaCheck, error) { +func resolveCheck(conf *config.Configuration, checkID string, controller kube.GenericWorkload, target config.TargetKind, isInitContainer bool) (*config.SchemaCheck, error) { check, ok := conf.CustomChecks[checkID] if !ok { check, ok = builtInChecks[checkID] @@ -82,10 +82,10 @@ func resolveCheck(conf *config.Configuration, checkID string, controller control if !ok { return nil, fmt.Errorf("Check %s not found", checkID) } - if !conf.IsActionable(check.ID, controller.GetName()) { + if !conf.IsActionable(check.ID, controller.ObjectMeta.GetName()) { return nil, nil } - if !check.IsActionable(target, controller.GetKind(), isInitContainer) { + if !check.IsActionable(target, controller.Kind, isInitContainer) { return nil, nil } return &check, nil @@ -110,10 +110,10 @@ func getExemptKey(checkID string) string { return fmt.Sprintf("polaris.fairwinds.com/%s-exempt", checkID) } -func applyPodSchemaChecks(conf *config.Configuration, controller controllers.GenericController) (ResultSet, error) { +func applyPodSchemaChecks(conf *config.Configuration, controller kube.GenericWorkload) (ResultSet, error) { results := ResultSet{} checkIDs := getSortedKeys(conf.Checks) - objectAnnotations := controller.GetObjectMeta().Annotations + objectAnnotations := controller.ObjectMeta.GetAnnotations() for _, checkID := range checkIDs { exemptValue := objectAnnotations[getExemptKey(checkID)] if strings.ToLower(exemptValue) == "true" { @@ -126,7 +126,7 @@ func applyPodSchemaChecks(conf *config.Configuration, controller controllers.Gen } else if check == nil { continue } - passes, err := check.CheckPod(controller.GetPodSpec()) + passes, err := check.CheckPod(&controller.PodSpec) if err != nil { return nil, err } @@ -135,10 +135,10 @@ func applyPodSchemaChecks(conf *config.Configuration, controller controllers.Gen return results, nil } -func applyContainerSchemaChecks(conf *config.Configuration, controller controllers.GenericController, container *corev1.Container, isInit bool) (ResultSet, error) { +func applyContainerSchemaChecks(conf *config.Configuration, controller kube.GenericWorkload, container *corev1.Container, isInit bool) (ResultSet, error) { results := ResultSet{} checkIDs := getSortedKeys(conf.Checks) - objectAnnotations := controller.GetObjectMeta().Annotations + objectAnnotations := controller.ObjectMeta.GetAnnotations() for _, checkID := range checkIDs { exemptValue := objectAnnotations[getExemptKey(checkID)] if strings.ToLower(exemptValue) == "true" { @@ -152,7 +152,7 @@ func applyContainerSchemaChecks(conf *config.Configuration, controller controlle } var passes bool if check.SchemaTarget == config.TargetPod { - podCopy := *controller.GetPodSpec() + podCopy := controller.PodSpec podCopy.InitContainers = []corev1.Container{} podCopy.Containers = []corev1.Container{*container} passes, err = check.CheckPod(&podCopy) diff --git a/pkg/validator/schema_test.go b/pkg/validator/schema_test.go index 3005558e..a4dc1137 100644 --- a/pkg/validator/schema_test.go +++ b/pkg/validator/schema_test.go @@ -138,7 +138,7 @@ func TestValidateResourcesPartiallyValid(t *testing.T) { func TestValidateResourcesInit(t *testing.T) { emptyContainer := &corev1.Container{} - controller := getEmptyController("") + controller := getEmptyWorkload("") parsedConf, err := conf.Parse([]byte(resourceConfRanges)) assert.NoError(t, err, "Expected no error when parsing config") @@ -259,4 +259,4 @@ func TestValidateCustomCheckExemptions(t *testing.T) { }, } testValidate(t, &container, &customCheckExemptions, "notexempt", expectedErrors, expectedWarnings, expectedSuccesses) -} \ No newline at end of file +} diff --git a/pkg/webhook/validator.go b/pkg/webhook/webhook.go similarity index 54% rename from pkg/webhook/validator.go rename to pkg/webhook/webhook.go index b395ae56..0f7b63b1 100644 --- a/pkg/webhook/validator.go +++ b/pkg/webhook/webhook.go @@ -16,18 +16,16 @@ package webhook import ( "context" + "encoding/json" "fmt" "net/http" "github.com/fairwindsops/polaris/pkg/config" + "github.com/fairwindsops/polaris/pkg/kube" validator "github.com/fairwindsops/polaris/pkg/validator" - "github.com/fairwindsops/polaris/pkg/validator/controllers" "github.com/sirupsen/logrus" admissionregistrationv1beta1 "k8s.io/api/admissionregistration/v1beta1" - appsv1 "k8s.io/api/apps/v1" - batchv1 "k8s.io/api/batch/v1" - batchv1beta1 "k8s.io/api/batch/v1beta1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -64,7 +62,7 @@ func (v *Validator) InjectDecoder(d types.Decoder) error { var _ admission.Handler = &Validator{} // NewWebhook creates a validating admission webhook for the apiType. -func NewWebhook(name string, mgr manager.Manager, validator Validator, apiType runtime.Object) *admission.Webhook { +func NewWebhook(name string, mgr manager.Manager, validator Validator, apiType runtime.Object) (*admission.Webhook, error) { name = fmt.Sprintf("%s.k8s.io", name) path := fmt.Sprintf("/validating-%s", name) @@ -78,86 +76,56 @@ func NewWebhook(name string, mgr manager.Manager, validator Validator, apiType r Handlers(&validator). Build() if err != nil { - logrus.Errorf("Error building webhook: %v", err) - return nil + return nil, err } - logrus.Info(name + " webhook started") - return webhook + return webhook, nil +} + +func (v *Validator) handleInternal(ctx context.Context, req types.Request) (*validator.PodResult, error) { + pod := corev1.Pod{} + if req.AdmissionRequest.Kind.Kind == "Pod" { + err := v.decoder.Decode(req, &pod) + if err != nil { + return nil, err + } + } else { + decoded := map[string]interface{}{} + err := json.Unmarshal(req.AdmissionRequest.Object.Raw, &decoded) + if err != nil { + return nil, err + } + podMap := kube.GetPodSpec(decoded) + encoded, err := json.Marshal(podMap) + if err != nil { + return nil, err + } + err = json.Unmarshal(encoded, &pod.Spec) + if err != nil { + return nil, err + } + } + controller := kube.NewGenericWorkload(pod, nil, nil) + controller.Kind = req.AdmissionRequest.Kind.Kind + controllerResult, err := validator.ValidateController(&v.Config, controller) + if err != nil { + return nil, err + } + return &controllerResult.PodResult, nil } // Handle for Validator to run validation checks. func (v *Validator) Handle(ctx context.Context, req types.Request) types.Response { - var err error - var podResult validator.PodResult - - if req.AdmissionRequest.Kind.Kind == "Pod" { - pod := corev1.Pod{} - err = v.decoder.Decode(req, &pod) // err is handled below - nakedPod := controllers.NewNakedPodController(pod) - if err == nil { - podResult, err = validator.ValidatePod(&v.Config, nakedPod) - } - } else { - 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)) - } - - // We should never hit this case unless something is misconfiured in CheckIfKindIsConfiguredForValidation - controllerType := config.GetSupportedControllerFromString(req.AdmissionRequest.Kind.Kind) - if controllerType == config.Unsupported { - msg := fmt.Errorf("Expected Kind (%s) to be a supported type", req.AdmissionRequest.Kind.Kind) - logrus.Error(msg) - return admission.ErrorResponse(http.StatusInternalServerError, msg) - } - - // For each type, perform the scan - // TODO: This isn't really that elegant due to the decoder and NewXXXController setup :( could use love - switch controllerType { - case config.Deployments: - deploy := appsv1.Deployment{} - err = v.decoder.Decode(req, &deploy) - controller = controllers.NewDeploymentController(deploy) - case config.StatefulSets: - statefulSet := appsv1.StatefulSet{} - err = v.decoder.Decode(req, &statefulSet) - controller = controllers.NewStatefulSetController(statefulSet) - case config.DaemonSets: - daemonSet := appsv1.DaemonSet{} - err = v.decoder.Decode(req, &daemonSet) - controller = controllers.NewDaemonSetController(daemonSet) - case config.Jobs: - job := batchv1.Job{} - err = v.decoder.Decode(req, &job) - controller = controllers.NewJobController(job) - case config.CronJobs: - cronJob := batchv1beta1.CronJob{} - err = v.decoder.Decode(req, &cronJob) - controller = controllers.NewCronJobController(cronJob) - case config.ReplicationControllers: - replicationController := corev1.ReplicationController{} - err = v.decoder.Decode(req, &replicationController) - controller = controllers.NewReplicationControllerController(replicationController) - } - if err == nil { - var controllerResult validator.ControllerResult - controllerResult, err = validator.ValidateController(&v.Config, controller) - podResult = controllerResult.PodResult - } - } - + podResult, err := v.handleInternal(ctx, req) if err != nil { logrus.Errorf("Error validating request: %v", err) return admission.ErrorResponse(http.StatusBadRequest, err) } - allowed := true reason := "" numErrors := podResult.GetSummary().Errors if numErrors > 0 { allowed = false - reason = getFailureReason(podResult) + reason = getFailureReason(*podResult) } logrus.Infof("%d validation errors found when validating %s", numErrors, podResult.Name) return admission.ValidationResponse(allowed, reason) diff --git a/test/fixtures.go b/test/fixtures.go index 7a5ad47c..b0178664 100644 --- a/test/fixtures.go +++ b/test/fixtures.go @@ -1,7 +1,6 @@ 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" @@ -24,9 +23,9 @@ func MockContainer(name string) corev1.Container { } // MockPod creates a pod object. -func MockPod() corev1.PodTemplateSpec { +func MockPod() corev1.Pod { c1 := MockContainer("test") - p := corev1.PodTemplateSpec{ + p := corev1.Pod{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ c1, @@ -36,13 +35,6 @@ 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{ @@ -55,7 +47,7 @@ func MockDeploy() appsv1.Deployment { p := MockPod() d := appsv1.Deployment{ Spec: appsv1.DeploymentSpec{ - Template: p, + Template: corev1.PodTemplateSpec{Spec: p.Spec}, }, } return d @@ -66,7 +58,7 @@ func MockStatefulSet() appsv1.StatefulSet { p := MockPod() s := appsv1.StatefulSet{ Spec: appsv1.StatefulSetSpec{ - Template: p, + Template: corev1.PodTemplateSpec{Spec: p.Spec}, }, } return s @@ -74,29 +66,32 @@ func MockStatefulSet() appsv1.StatefulSet { // MockDaemonSet creates a DaemonSet object. func MockDaemonSet() appsv1.DaemonSet { + p := MockPod() return appsv1.DaemonSet{ Spec: appsv1.DaemonSetSpec{ - Template: MockPod(), + Template: corev1.PodTemplateSpec{Spec: p.Spec}, }, } } // MockJob creates a Job object. func MockJob() batchv1.Job { + p := MockPod() return batchv1.Job{ Spec: batchv1.JobSpec{ - Template: MockPod(), + Template: corev1.PodTemplateSpec{Spec: p.Spec}, }, } } // MockCronJob creates a CronJob object. func MockCronJob() batchv1beta1.CronJob { + p := MockPod() return batchv1beta1.CronJob{ Spec: batchv1beta1.CronJobSpec{ JobTemplate: batchv1beta1.JobTemplateSpec{ Spec: batchv1.JobSpec{ - Template: MockPod(), + Template: corev1.PodTemplateSpec{Spec: p.Spec}, }, }, }, @@ -108,7 +103,7 @@ func MockReplicationController() corev1.ReplicationController { p := MockPod() return corev1.ReplicationController{ Spec: corev1.ReplicationControllerSpec{ - Template: &p, + Template: &corev1.PodTemplateSpec{Spec: p.Spec}, }, } } @@ -166,7 +161,7 @@ func SetupAddExtraControllerVersions(k kubernetes.Interface, namespace string) k dv1b1 := appsv1beta1.Deployment{ Spec: appsv1beta1.DeploymentSpec{ - Template: p, + Template: corev1.PodTemplateSpec{Spec: p.Spec}, }, } if _, err := k.AppsV1beta1().Deployments(namespace).Create(&dv1b1); err != nil { @@ -175,7 +170,7 @@ func SetupAddExtraControllerVersions(k kubernetes.Interface, namespace string) k dv1b2 := appsv1beta2.Deployment{ Spec: appsv1beta2.DeploymentSpec{ - Template: p, + Template: corev1.PodTemplateSpec{Spec: p.Spec}, }, } if _, err := k.AppsV1beta2().Deployments(namespace).Create(&dv1b2); err != nil { @@ -184,7 +179,7 @@ func SetupAddExtraControllerVersions(k kubernetes.Interface, namespace string) k ssv1b1 := appsv1beta1.StatefulSet{ Spec: appsv1beta1.StatefulSetSpec{ - Template: p, + Template: corev1.PodTemplateSpec{Spec: p.Spec}, }, } if _, err := k.AppsV1beta1().StatefulSets(namespace).Create(&ssv1b1); err != nil { @@ -193,7 +188,7 @@ func SetupAddExtraControllerVersions(k kubernetes.Interface, namespace string) k ssv1b2 := appsv1beta2.StatefulSet{ Spec: appsv1beta2.StatefulSetSpec{ - Template: p, + Template: corev1.PodTemplateSpec{Spec: p.Spec}, }, } if _, err := k.AppsV1beta2().StatefulSets(namespace).Create(&ssv1b2); err != nil { @@ -202,7 +197,7 @@ func SetupAddExtraControllerVersions(k kubernetes.Interface, namespace string) k dsv1b2 := appsv1beta2.DaemonSet{ Spec: appsv1beta2.DaemonSetSpec{ - Template: p, + Template: corev1.PodTemplateSpec{Spec: p.Spec}, }, } if _, err := k.AppsV1beta2().DaemonSets(namespace).Create(&dsv1b2); err != nil {