Compare commits

..
Author SHA1 Message Date
Robert Brennan 634557eec9 fix up goreleaser 2020-05-18 18:07:57 +00:00
66 changed files with 1767 additions and 2487 deletions
+9 -8
View File
@@ -27,22 +27,24 @@ references:
sudo apt-get install -yqq jq git
echo "Installing KIND"
curl -sLO https://github.com/kubernetes-sigs/kind/releases/download/v0.8.1/kind-linux-amd64
curl -sLO https://github.com/kubernetes-sigs/kind/releases/download/0.2.1/kind-linux-amd64
chmod 0755 kind-linux-amd64
sudo mv kind-linux-amd64 /usr/local/bin/kind
kind version
echo "Installing Kubectl"
curl -sLO https://storage.googleapis.com/kubernetes-release/release/v1.18.6/bin/linux/amd64/kubectl
curl -sLO https://storage.googleapis.com/kubernetes-release/release/v1.12.7/bin/linux/amd64/kubectl
chmod 0755 kubectl
sudo mv kubectl /usr/local/bin/
kubectl version --client
echo "Creating Kubernetes Cluster with Kind"
kind create cluster --wait=90s --image kindest/node:v1.15.11
kind create cluster --wait=90s
docker ps -a
echo "Setting up kubecfg"
cp $(kind get kubeconfig-path --name=kind) ~/.kube/config
kubectl version
# Test scripts
@@ -116,7 +118,7 @@ references:
jobs:
build:
docker:
- image: quay.io/reactiveops/ci-images:v11.0-stretch
- image: quay.io/reactiveops/ci-images:v8.0-stretch
steps:
- checkout
- setup_remote_docker
@@ -125,7 +127,7 @@ jobs:
push:
docker:
- image: quay.io/reactiveops/ci-images:v11.0-stretch
- image: quay.io/reactiveops/ci-images:v8.0-stretch
steps:
- checkout
- setup_remote_docker
@@ -152,7 +154,7 @@ jobs:
- run: go list ./... | grep -v vendor | xargs golint -set_exit_status
- run: go list ./... | grep -v vendor | xargs go vet
- run: go test ./pkg/... -coverprofile=coverage.txt -covermode=count
- run: go run main.go audit --audit-path ./deploy --set-exit-code-below-score 100 --set-exit-code-on-danger
- run: go run main.go audit --audit-path ./deploy --set-exit-code-below-score 100 --set-exit-code-on-error
- *update_coverage
- *test_binary_dashboard
@@ -166,7 +168,6 @@ jobs:
- *set_environment_variables
- *install_goreleaser
- run: go get -u github.com/gobuffalo/packr/v2/packr2
- run: git checkout -- . # FIXME: the go get makes the directory dirty
- run: packr2
- run: goreleaser
- *release_deploy_configs
@@ -174,7 +175,7 @@ jobs:
release_images:
working_directory: /go/src/github.com/fairwindsops/polaris/
docker:
- image: quay.io/reactiveops/ci-images:v11.0-stretch
- image: quay.io/reactiveops/ci-images:v8.0-stretch
steps:
- checkout
- setup_remote_docker
+1
View File
@@ -1,4 +1,5 @@
deploy
examples
.gitignore
.git/*
Dockerfile
-3
View File
@@ -21,6 +21,3 @@ main
*-packr.go
dist
.vscode
*-test.yaml
-13
View File
@@ -6,19 +6,6 @@ changelog:
exclude:
- '^docs:'
- '^test:'
builds:
- id: watcher
env:
- CGO_ENABLED=0
- GO111MODULE=on
goarch:
- amd64
- arm
- arm64
- 386
goarm:
- 6
- 7
brews:
- name: polaris
github:
+2 -48
View File
@@ -1,53 +1,7 @@
# x.x.x (next release)
# 1.2.1
* Update date on dashboard footer
# 1.2.0
* Add ability to audit a single workload
* Enable `pullPolicyAlways` by default
* Fix for finding parent resources
# 1.1.1
* Show controller checks on dashboard
* Fix for orphaned pods w/ controller checks
# 1.1.0
* Add namespace filter in UI
* Add priorityClass check
* Support reading from STDIN
* Ensure severity is set for all custom checks
* Support audit files which use \r or \r\n as newline character
* Add option to exempt an entire controller from checks via config file
* Fixed case where parent resources trigger error
* Fixed UI zero-state
# 1.0.3
* Fixed case where parent resources trigger error
* Fixed dashboard link when `--base-path` is set
# 1.0.2
* Fixed case where custom CRDs are not covered by RBAC
# 1.0.1
* Added ARM binaries to releases
# 1.0.0
## New Features
* Added support for custom checks using JSON Schema
* Added support for arbitrary controllers, rather than a pre-configured set
* removed support for `controllers_to_scan` in config
* Added the ability to exempt a particular controller from a particular check.
* Docker image now includes the default config
## Breaking Changes
* Breaking changes in both input and output formats. See [Examples](/examples) for examples of the new formats.
* removed config-level configuration for checks like max/min memory settings
* changed severity `error` to `danger`
* Breaking changes [to the CLI](/docs/usage.md#cli-options)
* CLI flag `--set-exit-code-on-error` is now `--set-exit-code-on-danger`
* Flags `--version`, `--dashboard`, `--webhook`, and `--audit` are now arguments
* Port flags are now just `--port`
* 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
+5 -8
View File
@@ -1,11 +1,11 @@
<div align="center">
<img src="/img/polaris-logo.png" alt="Polaris Logo" />
<img src="/polaris-logo.png" alt="Polaris Logo" />
<br>
[![Version][version-image]][version-link] [![CircleCI][circleci-image]][circleci-link] [![Go Report Card][goreport-image]][goreport-link]
</div>
[version-image]: https://img.shields.io/static/v1.svg?label=Version&message=1.2.0&color=239922
[version-image]: https://img.shields.io/static/v1.svg?label=Version&message=0.6.0&color=239922
[version-link]: https://github.com/FairwindsOps/polaris
[goreport-image]: https://goreportcard.com/badge/github.com/FairwindsOps/polaris
@@ -43,7 +43,7 @@ With the port forwarding in place, you can open http://localhost:8080 in your br
The Polaris dashboard is a way to get a simple visual overview of the current state of your Kubernetes workloads as well as a roadmap for what can be improved. The dashboard provides a cluster wide overview as well as breaking out results by category, namespace, and workload.
<p align="center">
<img src="/img/dashboard-screenshot.png" alt="Polaris Dashboard" width="550"/>
<img src="/dashboard-screenshot.png" alt="Polaris Dashboard" />
</p>
Our default standards in Polaris are rather high, so dont be surprised if your score is lower than you might expect. A key goal for Polaris was to set a high standard and aim for great configuration by default. If the defaults weve included are too strict, its easy to adjust the configuration as part of the deployment configuration to better suit your workloads.
@@ -52,10 +52,7 @@ Our default standards in Polaris are rather high, so dont be surprised if you
## Admission Controller: Validating Webhook
> [View installation instructions](docs/usage.md#webhook)
Polaris can be run as an admission controller that acts as a validating webhook. This accepts the same configuration as the dashboard, and can run the same validations. This webhook will reject any workloads that trigger a danger-level check. This is indicative of the greater goal of Polaris, not just to encourage better configuration through dashboard visibility, but to actually enforce it with this webhook. Polaris will not fix your workloads, only block them.
The webhook only supports a fixed set of controller types. You can see the
[list of supported controllers here](https://github.com/FairwindsOps/polaris/blob/master/cmd/polaris/webhook.go#L40)
Polaris can be run as an admission controller that acts as a validating webhook. This accepts the same configuration as the dashboard, and can run the same validations. This webhook will reject any workloads that trigger a validation error. This is indicative of the greater goal of Polaris, not just to encourage better configuration through dashboard visibility, but to actually enforce it with this webhook. Polaris will not fix your workloads, only block them.
Unfortunately we have not found a way to display warnings as part of `kubectl` output unless we are rejecting a workload altogether. That means that any checks with a severity of `warning` will still pass webhook validation, and the only evidence of that warning will either be in the Polaris dashboard or the Polaris webhook logs.
@@ -65,7 +62,7 @@ Unfortunately we have not found a way to display warnings as part of `kubectl` o
Polaris can also be used on the command line, either to audit local files or a running cluster.
This is particularly helpful for running Polaris against your infrastructure-as-code as part of a
CI/CD pipeline. Use the available [command line flags](docs/usage.md#running-with-ci-cd)
to cause CI/CD to fail if your Polaris score drops below a certain threshold, or if any danger-level issues arise.
to cause CI/CD to fail if your Polaris score drops below a certain threshold, or if any errors arise.
# Installation and Usage
See the [Usage Guide](/docs/usage.md) for details on different methods for installing and using Polaris.
+50 -16
View File
@@ -5,21 +5,55 @@ as well as the project's overall functionality.
If you have ideas for a new check, or for new Polaris features,
you can submit a pull request for this file, or open an issue in GitHub.
## Check non-controllers
## Q3 2019
Below is a list of work we plan to get done this quarter. Some more details can be found under
[Future Work](#future_work), or
[in the milestone](https://github.com/FairwindsOps/polaris/milestone/1)
* Rule exceptions - we'd like to provide a way of overriding Polaris checks on individual controllers, e.g. through annotations
* More controller types - we plan to implement checks for more than just deployments
* Image repo checks - we plan to provide a check that ensures all repos conform to a set of user-defined patterns
* OPA integration (investigate only) - we plan to investigate the potential use of OPA to let users define custom Polaris checks
## Future Work
### Validating Webhook
The validating webhook rejects incoming workloads if they cause `error`-level
issues, as defined by the Polaris configuration.
The webhook server is currently functional, but largely untested. Because
the validating webhook has the potential to cause headaches for users,
we have marked it as `beta` for now.
We will continue to gather feedback here and will move the webhook out of `beta`
when we feel it's ready.
#### TODO
* Pass `warning`-level messages back to `kubectl` if possible (may require changes
to core k8s or `kubectl`)
### Ability to override checks
We plan to add the ability to override Polaris checks for particular namespaces
and workloads. This may be something that is set in the Polaris configuration,
or an annotation that can be added to the resource definition.
This is important, as certain workloads have good reason to skip particular Polaris
checks - for instance, the `dns-controller` deployment in `kube-system` needs to have
the host network configured.
### Images Registries Check
We'd like to allow users to restrict images to a list of allowed/disallowed registries.
We'll likely allow patterns, e.g. `*.dkr.ecr.*.amazonaws.com`
### New controller types
Currently we only look at Deployments. We plan to also validate these types of controllers:
* StatefulSet
* DaemonSet
* Job
* CronJob
* ReplicaSet
### Check non-controllers
We would like to implement checks for non-controller types, such as:
* Ingresses
* Services
* RBAC roles/bindings
* Ingress
* Service
* Secret
## Expand list of checks
We'd like to increase our library of checks. Not all checks need to be enabled by default,
so anything that would be useful across different organizations is worth checking in.
## Support more controllers in the validating webhook
Currently the webhook only checks for a fixed set of controllers.
While we can't listen for _all_ possible controller types, we can block Pods that violate policy.
We've chosen not to do this for now, because it could prevent existing controllers from scaling.
However, there may be a way to check for the owner's age, in order to determine if it's a new
controller or a pre-existing controller.
-26
View File
@@ -1,26 +0,0 @@
successMessage: Multiple replicas are scheduled
failureMessage: Only one replica is scheduled
category: Reliability
target: Controller
controllers:
include:
- Deployment
schema:
'$schema': http://json-schema.org/draft-07/schema
type: object
required:
- Object
properties:
Object:
type: object
required:
- spec
properties:
spec:
type: object
required:
- replicas
properties:
replicas:
type: integer
minimum: 2
-9
View File
@@ -1,9 +0,0 @@
successMessage: Priority class has been set
failureMessage: Priority class should be set
category: Security
target: Pod
schema:
'$schema': http://json-schema.org/draft-07/schema
type: object
required:
- priorityClassName
+7 -10
View File
@@ -16,7 +16,6 @@ package cmd
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io/ioutil"
@@ -36,18 +35,16 @@ var minScore int
var auditOutputURL string
var auditOutputFile string
var auditOutputFormat string
var resourceToAudit string
func init() {
rootCmd.AddCommand(auditCmd)
auditCmd.PersistentFlags().StringVar(&auditPath, "audit-path", "", "If specified, audits one or more YAML files instead of a cluster.")
auditCmd.PersistentFlags().BoolVar(&setExitCode, "set-exit-code-on-danger", false, "Set an exit code of 3 when the audit contains danger-level issues.")
auditCmd.PersistentFlags().BoolVar(&setExitCode, "set-exit-code-on-error", false, "Set an exit code of 3 when the audit contains error-level issues.")
auditCmd.PersistentFlags().IntVar(&minScore, "set-exit-code-below-score", 0, "Set an exit code of 4 when the score is below this threshold (1-100).")
auditCmd.PersistentFlags().StringVar(&auditOutputURL, "output-url", "", "Destination URL to send audit results.")
auditCmd.PersistentFlags().StringVar(&auditOutputFile, "output-file", "", "Destination file for audit results.")
auditCmd.PersistentFlags().StringVarP(&auditOutputFormat, "format", "f", "json", "Output format for results - json, yaml, or score.")
auditCmd.PersistentFlags().StringVar(&displayName, "display-name", "", "An optional identifier for the audit.")
auditCmd.PersistentFlags().StringVar(&resourceToAudit, "resource", "", "Audit a specific resource, in the format namespace/kind/version/name, e.g. nginx-ingress/Deployment.apps/v1/default-backend.")
}
var auditCmd = &cobra.Command{
@@ -59,12 +56,12 @@ var auditCmd = &cobra.Command{
config.DisplayName = displayName
}
auditData := runAndReportAudit(cmd.Context(), config, auditPath, resourceToAudit, auditOutputFile, auditOutputURL, auditOutputFormat)
auditData := runAndReportAudit(config, auditPath, auditOutputFile, auditOutputURL, auditOutputFormat)
summary := auditData.GetSummary()
score := summary.GetScore()
if setExitCode && summary.Dangers > 0 {
logrus.Infof("%d danger items found in audit", summary.Dangers)
if setExitCode && summary.Errors > 0 {
logrus.Infof("%d errors found in audit", summary.Errors)
os.Exit(3)
} else if minScore != 0 && score < uint(minScore) {
logrus.Infof("Audit score of %d is less than the provided minimum of %d", score, minScore)
@@ -73,14 +70,14 @@ var auditCmd = &cobra.Command{
},
}
func runAndReportAudit(ctx context.Context, c conf.Configuration, auditPath, workload, outputFile, outputURL, outputFormat string) validator.AuditData {
func runAndReportAudit(c conf.Configuration, auditPath string, outputFile string, outputURL string, outputFormat string) validator.AuditData {
// Create a kubernetes client resource provider
k, err := kube.CreateResourceProvider(ctx, auditPath, workload)
k, err := kube.CreateResourceProvider(auditPath)
if err != nil {
logrus.Errorf("Error fetching Kubernetes resources %v", err)
os.Exit(1)
}
auditData, err := validator.RunAudit(ctx, c, k)
auditData, err := validator.RunAudit(c, k)
if err != nil {
logrus.Errorf("Error while running audit on resources: %v", err)
+101 -18
View File
@@ -15,18 +15,50 @@
package cmd
import (
"io/ioutil"
"os"
"time"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"strings"
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"
"sigs.k8s.io/controller-runtime/pkg/runtime/signals"
"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
@@ -42,30 +74,81 @@ var webhookCmd = &cobra.Command{
Long: `Runs the webhook webserver.`,
Run: func(cmd *cobra.Command, args []string) {
logrus.Debug("Setting up controller manager")
mgr, err := manager.New(k8sConfig.GetConfigOrDie(), manager.Options{
CertDir: "/opt/cert",
Port: webhookPort,
})
mgr, err := manager.New(k8sConfig.GetConfigOrDie(), manager.Options{})
if err != nil {
logrus.Errorf("Unable to set up overall controller manager: %v", err)
os.Exit(1)
}
_, err = os.Stat("/opt/cert/tls.crt")
if os.IsNotExist(err) {
time.Sleep(time.Second * 10)
panic("Cert does not exist")
polarisAppName := "polaris"
polarisResourceName := "polaris-webhook"
polarisNamespaceBytes, err := ioutil.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace")
if err != nil {
// Not exiting here as we have fallback options
logrus.Debugf("Error reading namespace information: %v", err)
}
server := mgr.GetWebhookServer()
server.CertName = "tls.crt"
server.KeyName = "tls.key"
polarisNamespace := string(polarisNamespaceBytes)
if polarisNamespace == "" {
polarisNamespace = polarisResourceName
logrus.Debugf("Could not determine current namespace, creating resources in %s namespace", polarisNamespace)
}
logrus.Info("Setting up webhook server")
as, err := webhook.NewServer(polarisResourceName, mgr, webhook.ServerOptions{
Port: int32(webhookPort),
CertDir: "/opt/cert",
DisableWebhookConfigInstaller: &disableWebhookConfigInstaller,
BootstrapOptions: &webhook.BootstrapOptions{
ValidatingWebhookConfigName: polarisResourceName,
Secret: &apitypes.NamespacedName{
Namespace: polarisNamespace,
Name: polarisResourceName,
},
Service: &webhook.Service{
Namespace: polarisNamespace,
Name: polarisResourceName,
// Selectors should select the pods that runs this webhook server.
Selectors: map[string]string{
"app": polarisAppName,
"component": "webhook",
},
},
},
})
if err != nil {
logrus.Errorf("Error setting up webhook server: %v", err)
os.Exit(1)
}
logrus.Infof("Polaris webhook server listening on port %d", webhookPort)
// Iterate all the configurations supported controllers to scan and register them for webhooks
// Should only register controllers that are configured to be scanned
fwebhook.NewWebhook(mgr, fwebhook.Validator{Config: config, Client: mgr.GetClient()})
logrus.Debug("Registering webhooks to the webhook server")
var webhooks []webhook.Webhook
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)
}
logrus.Infof("Polaris webhook server listening on port %d", webhookPort)
if err = as.Register(webhooks...); err != nil {
logrus.Debugf("Unable to register webhooks in the admission server: %v", err)
os.Exit(1)
}
logrus.Debug("Starting webhook manager")
if err := mgr.Start(signals.SetupSignalHandler()); err != nil {
logrus.Errorf("Error starting manager: %v", err)
os.Exit(1)
+203 -32
View File
@@ -5,63 +5,200 @@ kind: Namespace
metadata:
name: polaris
---
# Source: polaris/templates/rbac.yaml
# Source: polaris/templates/configmap.yaml
apiVersion: v1
kind: ServiceAccount
kind: ConfigMap
metadata:
name: polaris
namespace: polaris
labels:
app: polaris
data:
config.yaml: |-
checks:
# resources
cpuRequestsMissing: warning
cpuLimitsMissing: warning
memoryRequestsMissing: warning
memoryLimitsMissing: warning
# images
tagNotSpecified: error
pullPolicyNotAlways: ignore
# healthChecks
readinessProbeMissing: warning
livenessProbeMissing: warning
# networking
hostNetworkSet: warning
hostPortSet: warning
# security
hostIPCSet: error
hostPIDSet: error
notReadOnlyRootFilesystem: warning
privilegeEscalationAllowed: error
runAsRootAllowed: warning
runAsPrivileged: error
dangerousCapabilities: error
insecureCapabilities: warning
controllersToScan:
- Deployments
- StatefulSets
- DaemonSets
- CronJobs
- Jobs
- ReplicationControllers
exemptions:
- controllerNames:
- dns-controller
- datadog-datadog
- kube-flannel-ds
- kube2iam
- aws-iam-authenticator
- datadog
- kube2iam
rules:
- hostNetworkSet
- controllerNames:
- aws-iam-authenticator
- aws-cluster-autoscaler
- kube-state-metrics
- dns-controller
- external-dns
- dnsmasq
- autoscaler
- kubernetes-dashboard
- install-cni
- kube2iam
rules:
- readinessProbeMissing
- livenessProbeMissing
- controllerNames:
- aws-iam-authenticator
- nginx-ingress-controller
- nginx-ingress-default-backend
- aws-cluster-autoscaler
- kube-state-metrics
- dns-controller
- external-dns
- kubedns
- dnsmasq
- autoscaler
- tiller
- kube2iam
rules:
- runAsRootAllowed
- controllerNames:
- aws-iam-authenticator
- nginx-ingress-controller
- nginx-ingress-default-backend
- aws-cluster-autoscaler
- kube-state-metrics
- dns-controller
- external-dns
- kubedns
- dnsmasq
- autoscaler
- tiller
- kube2iam
rules:
- notReadOnlyRootFilesystem
- controllerNames:
- cert-manager
- dns-controller
- kubedns
- dnsmasq
- autoscaler
- insights-agent-goldilocks-vpa-install
rules:
- cpuRequestsMissing
- cpuLimitsMissing
- memoryRequestsMissing
- memoryLimitsMissing
- controllerNames:
- kube2iam
- kube-flannel-ds
rules:
- runAsPrivileged
- controllerNames:
- kube-hunter
rules:
- hostPIDSet
- controllerNames:
- polaris
- kube-hunter
- goldilocks
- insights-agent-goldilocks-vpa-install
rules:
- notReadOnlyRootFilesystem
- controllerNames:
- insights-agent-goldilocks-controller
rules:
- livenessProbeMissing
- readinessProbeMissing
- controllerNames:
- insights-agent-goldilocks-vpa-install
- kube-hunter
rules:
- runAsRootAllowed
---
# Source: polaris/templates/dashboard.rbac.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: polaris-dashboard
namespace: polaris
labels:
app: polaris
---
# Source: polaris/templates/rbac.yaml
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
name: polaris
name: polaris-dashboard
labels:
app: polaris
rules:
# required by controller-runtime code doing a cluster wide lookup
# when it seems namespace would suffice
- apiGroups:
- 'apps'
- 'extensions'
resources:
- 'deployments'
- 'statefulsets'
- 'daemonsets'
verbs:
- 'get'
- 'list'
- apiGroups:
- 'batch'
resources:
- 'jobs'
- 'cronjobs'
verbs:
- 'get'
- 'list'
- apiGroups:
- ''
resources:
- 'nodes'
- 'namespaces'
- 'pods'
- 'replicationcontrollers'
verbs:
- 'get'
- 'list'
---
# Source: polaris/templates/rbac.yaml
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: polaris-view
name: polaris-dashboard
labels:
app: polaris
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: view
name: polaris-dashboard
subjects:
- kind: ServiceAccount
name: polaris
namespace: polaris
---
# Source: polaris/templates/rbac.yaml
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: polaris
labels:
app: polaris
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: polaris
subjects:
- kind: ServiceAccount
name: polaris
name: polaris-dashboard
namespace: polaris
---
# Source: polaris/templates/dashboard.service.yaml
@@ -75,7 +212,7 @@ metadata:
annotations:
spec:
ports:
- name: http-dashboard
- name: dashboard
port: 80
protocol: TCP
targetPort: 8080
@@ -101,15 +238,23 @@ spec:
component: dashboard
template:
metadata:
annotations:
checksum/config: 'eb6d6b194c6786d62400fc0578dd5ea5158212b5b29d93d3cde3fa14da8ac501'
labels:
app: polaris
component: dashboard
spec:
volumes:
- name: config
configMap:
name: polaris
containers:
- command:
- polaris
- dashboard
image: 'quay.io/fairwinds/polaris:1.2'
- --dashboard
- --config
- /opt/app/config.yaml
image: 'quay.io/fairwinds/polaris:0.6'
imagePullPolicy: 'Always'
name: dashboard
ports:
@@ -128,8 +273,8 @@ spec:
periodSeconds: 20
resources:
limits:
cpu: 150m
memory: 512Mi
cpu: 100m
memory: 128Mi
requests:
cpu: 100m
memory: 128Mi
@@ -141,6 +286,32 @@ spec:
capabilities:
drop:
- ALL
serviceAccountName: polaris
volumeMounts:
- name: config
mountPath: /opt/app/config.yaml
subPath: config.yaml
readOnly: true
serviceAccountName: polaris-dashboard
nodeSelector:
tolerations:
---
# Source: polaris/templates/audit.job.yaml
---
# Source: polaris/templates/audit.rbac.yaml
---
# Source: polaris/templates/ingress.yaml
---
# Source: polaris/templates/webhook.deployment.yaml
---
# Source: polaris/templates/webhook.rbac.yaml
---
# Source: polaris/templates/webhook.secret.yaml
---
# Source: polaris/templates/webhook.service.yaml
+205 -215
View File
@@ -5,24 +5,6 @@ kind: Namespace
metadata:
name: polaris
---
# Source: polaris/templates/rbac.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: polaris
namespace: polaris
labels:
app: polaris
---
# Source: polaris/templates/webhook.rbac.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: polaris-certificates
namespace: polaris
labels:
app: polaris
---
# Source: polaris/templates/webhook.secret.yaml
# The name of this secret is static as it is populated by the webhook pod.
apiVersion: v1
@@ -32,30 +14,155 @@ metadata:
namespace: polaris
labels:
app: polaris
type: kubernetes.io/tls
data:
tls.crt: ''
tls.key: ''
type: Opaque
stringData:
cert.pem: ''
---
# Source: polaris/templates/rbac.yaml
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
# Source: polaris/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: polaris
namespace: polaris
labels:
app: polaris
rules:
# required by controller-runtime code doing a cluster wide lookup
# when it seems namespace would suffice
- apiGroups:
- ''
resources:
- 'nodes'
verbs:
- 'get'
- 'list'
data:
config.yaml: |-
checks:
# resources
cpuRequestsMissing: warning
cpuLimitsMissing: warning
memoryRequestsMissing: warning
memoryLimitsMissing: warning
# images
tagNotSpecified: error
pullPolicyNotAlways: ignore
# healthChecks
readinessProbeMissing: warning
livenessProbeMissing: warning
# networking
hostNetworkSet: warning
hostPortSet: warning
# security
hostIPCSet: error
hostPIDSet: error
notReadOnlyRootFilesystem: warning
privilegeEscalationAllowed: error
runAsRootAllowed: warning
runAsPrivileged: error
dangerousCapabilities: error
insecureCapabilities: warning
controllersToScan:
- Deployments
- StatefulSets
- DaemonSets
- CronJobs
- Jobs
- ReplicationControllers
exemptions:
- controllerNames:
- dns-controller
- datadog-datadog
- kube-flannel-ds
- kube2iam
- aws-iam-authenticator
- datadog
- kube2iam
rules:
- hostNetworkSet
- controllerNames:
- aws-iam-authenticator
- aws-cluster-autoscaler
- kube-state-metrics
- dns-controller
- external-dns
- dnsmasq
- autoscaler
- kubernetes-dashboard
- install-cni
- kube2iam
rules:
- readinessProbeMissing
- livenessProbeMissing
- controllerNames:
- aws-iam-authenticator
- nginx-ingress-controller
- nginx-ingress-default-backend
- aws-cluster-autoscaler
- kube-state-metrics
- dns-controller
- external-dns
- kubedns
- dnsmasq
- autoscaler
- tiller
- kube2iam
rules:
- runAsRootAllowed
- controllerNames:
- aws-iam-authenticator
- nginx-ingress-controller
- nginx-ingress-default-backend
- aws-cluster-autoscaler
- kube-state-metrics
- dns-controller
- external-dns
- kubedns
- dnsmasq
- autoscaler
- tiller
- kube2iam
rules:
- notReadOnlyRootFilesystem
- controllerNames:
- cert-manager
- dns-controller
- kubedns
- dnsmasq
- autoscaler
- insights-agent-goldilocks-vpa-install
rules:
- cpuRequestsMissing
- cpuLimitsMissing
- memoryRequestsMissing
- memoryLimitsMissing
- controllerNames:
- kube2iam
- kube-flannel-ds
rules:
- runAsPrivileged
- controllerNames:
- kube-hunter
rules:
- hostPIDSet
- controllerNames:
- polaris
- kube-hunter
- goldilocks
- insights-agent-goldilocks-vpa-install
rules:
- notReadOnlyRootFilesystem
- controllerNames:
- insights-agent-goldilocks-controller
rules:
- livenessProbeMissing
- readinessProbeMissing
- controllerNames:
- insights-agent-goldilocks-vpa-install
- kube-hunter
rules:
- runAsRootAllowed
---
# Source: polaris/templates/webhook.rbac.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: polaris-webhook
namespace: polaris
labels:
app: polaris
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
@@ -63,6 +170,37 @@ metadata:
labels:
app: polaris
rules:
# auditor rules rbac
- apiGroups:
- 'apps'
- 'extensions'
resources:
- 'deployments'
- 'statefulsets'
- 'daemonsets'
verbs:
- 'get'
- 'list'
- apiGroups:
- 'batch'
resources:
- 'jobs'
- 'cronjobs'
verbs:
- 'get'
- 'list'
- apiGroups:
- ''
resources:
- 'nodes'
- 'namespaces'
- 'pods'
- 'replicationcontrollers'
verbs:
- 'get'
- 'list'
# required by controller-runtime code doing a cluster wide lookup
# when it seems namespace would suffice
- apiGroups:
- ''
resources:
@@ -72,24 +210,6 @@ rules:
- 'get'
- 'list'
- 'watch'
- apiGroups:
- 'certificates.k8s.io'
resources:
- 'certificatesigningrequests'
- 'certificatesigningrequests/approval'
verbs:
- 'get'
- 'update'
- 'create'
- 'delete'
- apiGroups:
- 'certificates.k8s.io'
resources:
- 'signers'
resourceNames:
- 'kubernetes.io/legacy-unknown'
verbs:
- 'approve'
- apiGroups:
- 'admissionregistration.k8s.io'
resources:
@@ -97,39 +217,6 @@ rules:
verbs:
- '*'
---
# Source: polaris/templates/rbac.yaml
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: polaris-view
labels:
app: polaris
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: view
subjects:
- kind: ServiceAccount
name: polaris
namespace: polaris
---
# Source: polaris/templates/rbac.yaml
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: polaris
labels:
app: polaris
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: polaris
subjects:
- kind: ServiceAccount
name: polaris
namespace: polaris
---
# Source: polaris/templates/webhook.rbac.yaml
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
@@ -142,10 +229,9 @@ roleRef:
name: polaris-webhook
subjects:
- kind: ServiceAccount
name: polaris-certificates
name: polaris-webhook
namespace: polaris
---
# Source: polaris/templates/webhook.rbac.yaml
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: Role
metadata:
@@ -163,7 +249,6 @@ rules:
verbs:
- '*'
---
# Source: polaris/templates/webhook.rbac.yaml
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: RoleBinding
metadata:
@@ -177,7 +262,7 @@ roleRef:
name: polaris-webhook
subjects:
- kind: ServiceAccount
name: polaris-certificates
name: polaris-webhook
namespace: polaris
---
# Source: polaris/templates/webhook.service.yaml
@@ -216,6 +301,8 @@ spec:
component: webhook
template:
metadata:
annotations:
checksum/config: 'eb6d6b194c6786d62400fc0578dd5ea5158212b5b29d93d3cde3fa14da8ac501'
labels:
app: polaris
component: webhook
@@ -224,8 +311,10 @@ spec:
- name: webhook
command:
- polaris
- webhook
image: 'quay.io/fairwinds/polaris:1.2'
- --webhook
- --config
- /opt/app/config.yaml
image: 'quay.io/fairwinds/polaris:0.6'
imagePullPolicy: 'Always'
ports:
- containerPort: 9876
@@ -264,142 +353,43 @@ spec:
drop:
- ALL
volumeMounts:
- name: config
mountPath: /opt/app/config.yaml
subPath: config.yaml
readOnly: true
- name: secret
mountPath: /opt/cert/
readOnly: true
- name: cr-logs
mountPath: /tmp/
readOnly: false
serviceAccountName: polaris
serviceAccountName: polaris-webhook
nodeSelector:
tolerations:
volumes:
- name: config
configMap:
name: polaris
- name: secret
secret:
secretName: polaris-webhook
- name: cr-logs
emptyDir: {}
---
# Source: polaris/templates/webhook.job.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: polaris-certificate-updater
namespace: polaris
labels:
app: polaris
component: certificate-updater
spec:
template:
metadata:
labels:
app: polaris
component: certificate-updater
spec:
containers:
- name: webhook-certificate-generator
image: 'newrelic/k8s-webhook-cert-manager:1.3.0'
imagePullPolicy: Always
command:
- ./generate_certificate.sh
- --service
- polaris-webhook
- --namespace
- polaris
- --secret
- polaris-webhook
- --webhook
- polaris-webhook
- --webhook-kind
- ValidatingWebhookConfiguration
resources:
limits:
cpu: 150m
memory: 512Mi
requests:
cpu: 100m
memory: 128Mi
volumeMounts:
- name: tmp
mountPath: /tmp/
readOnly: false
securityContext:
allowPrivilegeEscalation: false
privileged: false
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 1000
capabilities:
drop:
- ALL
serviceAccountName: polaris-certificates
restartPolicy: Never
volumes:
- name: tmp
emptyDir: {}
# Source: polaris/templates/audit.job.yaml
---
# Source: polaris/templates/webhook.configuration.yaml
apiVersion: admissionregistration.k8s.io/v1beta1
kind: ValidatingWebhookConfiguration
metadata:
name: polaris-webhook
webhooks:
- admissionReviewVersions:
- v1beta1
clientConfig:
caBundle: ""
service:
name: polaris-webhook
namespace: polaris
path: /validate
port: 443
failurePolicy: Ignore
matchPolicy: Exact
name: polaris.fairwinds.com
namespaceSelector:
matchExpressions:
- key: control-plane
operator: DoesNotExist
objectSelector: {}
rules:
- apiGroups:
- apps
apiVersions:
- v1
- v1beta1
- v1beta2
operations:
- CREATE
- UPDATE
resources:
- daemonsets
- deployments
- statefulsets
scope: Namespaced
- apiGroups:
- batch
apiVersions:
- v1
- v1beta1
operations:
- CREATE
- UPDATE
resources:
- jobs
- cronjobs
scope: Namespaced
- apiGroups:
- ""
apiVersions:
- v1
operations:
- CREATE
- UPDATE
resources:
- pods
- replicationcontrollers
scope: Namespaced
sideEffects: None
timeoutSeconds: 30
# Source: polaris/templates/audit.rbac.yaml
---
# Source: polaris/templates/dashboard.deployment.yaml
---
# Source: polaris/templates/dashboard.rbac.yaml
---
# Source: polaris/templates/dashboard.service.yaml
---
# Source: polaris/templates/ingress.yaml
+3 -3
View File
@@ -4,14 +4,14 @@ Polaris supports a number of checks related to the image specified by pods.
key | default | description
----|---------|------------
`images.tagNotSpecified` | `danger` | Fails when an image tag is either not specified or `latest`.
`images.pullPolicyNotAlways` | `warning` | Fails when an image pull policy is not `always`.
`images.tagNotSpecified` | `error` | Fails when an image tag is either not specified or `latest`.
`images.pullPolicyNotAlways` | `ignore` | Fails when an image pull policy is not `always`.
## Background
Docker's `latest` tag is applied by default to images where a tag hasn't been specified. Not specifying a specific version of an image can lead to a wide variety of problems. The underlying image could include unexpected breaking changes that break your application whenever the latest image is pulled. Reusing the same tag for multiple versions of an image can lead to different nodes in the same cluster having different versions of an image, even if the tag is identical.
Related to that, relying on cached versions of a Docker image can become a security vulnerability. By default, an image will be pulled if it isn't already cached on the node attempting to run it. This can result in variations in images that are running per node, or potentially provide a way to gain access to an image without having direct access to the ImagePullSecret. With that in mind, it's often better to ensure the a pod has `pullPolicy: Always` specified, so images are always pulled directly from their source.
Related to that, relying on cached versions of a Docker image can become a security vulnerability. By default, an image will be pulled if it isn't already cached on the node attempting to run it. This can result in variations in images that are running per node, or potentially provide a way to gain access to an image without having direct access to the ImagePullSecret. With that in mind, it's often better to ensure the a pod has `pullPolicy: Always` specified, so images are always pulled directly from their source. This is not a check enabled by default with Polaris as organizations may not wish to add the overhead involved with pulling images for each pod.
## Further Reading
+15 -4
View File
@@ -8,10 +8,21 @@ To simplify ensure that these values have been set, the following attributes are
key | default | description
----|---------|------------
`resources.cpuRequestsMissing` | `danger` | Fails when `resources.requests.cpu` attribute is not configured.
`resources.memoryRequestsMissing` | `danger` | Fails when `resources.requests.memory` attribute is not configured.
`resources.cpuLimitsMissing` | `danger` | Fails when `resources.limits.cpu` attribute is not configured.
`resources.memoryLimitsMissing` | `danger` | Fails when `resources.limits.memory` attribute is not configured.
`resources.cpuRequestsMissing` | `error` | Fails when `resources.requests.cpu` attribute is not configured.
`resources.memoryRequestsMissing` | `error` | Fails when `resources.requests.memory` attribute is not configured.
`resources.cpuLimitsMissing` | `error` | Fails when `resources.limits.cpu` attribute is not configured.
`resources.memoryLimitsMissing` | `error` | Fails when `resources.limits.memory` attribute is not configured.
## Range Checks
Polaris can also verify that those values fall within a certain range. These checks are not enabled by default, and as such do not have default values. The `cpuRequestRanges`, `cpuLimitRanges`, `memoryRequestRanges`, and `memoryLimitRanges` all support the following attributes:
key | description
----|------------
`warning.below` | Warn when resource is below this value (or not defined)
`warning.above` | Warn when resource is above this value
`error.below` | Error when resource is below this value (or not defined)
`error.above` | Error when resource is above this value
## Background
@@ -0,0 +1,23 @@
# Security Capabilities
Polaris supports a number of checks to ensure pods are running with a limited set of capabilities. Under `security.capabilities`, there are `error` and `warning` sections indicating the severity of failures for the following checks.
key | default | description
----|---------|------------
`security.capabilities.error.ifAnyAdded` | [`SYS_ADMIN`, `NET_ADMIN`, `ALL`] | Fails when any of the listed capabilities have been added.
`security.capabilities.error.ifAnyAddedBeyond` | `nil` | Fails when any capabilities have been added beyond the specified list.
`security.capabilities.error.ifAnyNotDropped` | `nil` | Fails when any of the listed capabilities have not been dropped.
`security.capabilities.warning.ifAnyAdded` | `nil` | Fails when any of the listed capabilities have been added.
`security.capabilities.warning.ifAnyAddedBeyond` | [`CHOWN`, `DAC_OVERRIDE`, `FSETID`, `FOWNER`, `MKNOD`, `NET_RAW`, `SETGID`, `SETUID`, `SETFCAP`, `SETPCAP`, `NET_BIND_SERVICE`, `SYS_CHROOT`, `KILL`,`AUDIT_WRITE`] | Fails when any capabilities have been added beyond the specified list.
`security.capabilities.warning.ifAnyNotDropped` | `nil` | Fails when any of the listed capabilities have not been dropped.
## Background
Linux Capabilities allow you to specify privileges for a process at a granular level. The [default list of capabilities](https://github.com/moby/moby/blob/master/oci/defaults.go#L15) included with a container are already fairly minimal, but often can be further restricted.
With Kubernetes configuration, these capabilities can be added or removed by adjusting `securityContext.capabilities`.
## Further Reading
- [Kubernetes Docs: Set capabilities for a Container](https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-capabilities-for-a-container)
- [Linux Programmer's Manual: Capabilities](http://man7.org/linux/man-pages/man7/capabilities.7.html)
+9 -9
View File
@@ -4,14 +4,16 @@ Polaris supports a number of checks related to security.
key | default | description
----|---------|------------
`security.hostIPCSet` | `danger` | Fails when `hostIPC` attribute is configured.
`security.hostPIDSet` | `danger` | Fails when `hostPID` attribute is configured.
`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.privilegeEscalationAllowed` | `danger` | Fails when `securityContext.allowPrivilegeEscalation` is true.
`security.runAsRootAllowed` | `danger` | Fails when `securityContext.runAsNonRoot` is not true.
`security.runAsPrivileged` | `danger` | Fails when `securityContext.privileged` is true.
`security.insecureCapabilities` | `warning` | Fails when `securityContext.capabilities` includes one of the capabilities [listed here](/checks/insecureCapabilities.yaml)
`security.dangerousCapabilities` | `danger` | Fails when `securityContext.capabilities` includes one of the capabilities [listed here](/checks/dangerousCapabilities.yaml)
`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.
## Security Capabilities
Additional validations are available to ensure pods are running with a limited set of capabilities. More information is available in our [Security Capabilities documentation](security-capabilities.md).
## Background
@@ -23,5 +25,3 @@ Much of this configuration can be found in the `securityContext` attribute for b
- [Kubernetes Docs: Configure a Security Context for a Pod or Container](https://kubernetes.io/docs/tasks/configure-pod-container/security-context/)
- [KubeCon 2018 Keynote: Running with Scissors](https://www.youtube.com/watch?v=ltrV-Qmh3oY)
- [Kubernetes Security Book](https://kubernetes-security.info/)
- [Kubernetes Docs: Set capabilities for a Container](https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-capabilities-for-a-container)
- [Linux Programmer's Manual: Capabilities](http://man7.org/linux/man-pages/man7/capabilities.7.html)
+1 -1
View File
@@ -7,7 +7,7 @@
<dt>Exit 2</dt>
<dd>Unused</dd>
<dt>Exit 3</dt>
<dd>Exiting due to `--set-exit-code-on-danger` being set and at least one danger was found after an audit.</dd>
<dd>Exiting due to `--set-exit-code-on-error` being set and at least one error was found after an audit.</dd>
<dt>Edit 4</dd>
<dd>Exiting due to `--set-exit-code-below-score` being set and the audit resulted in a score less than the minimum score value.</dd>
</dl>
+9 -33
View File
@@ -10,7 +10,7 @@ The [default configuration](/examples/config.yaml) contains a number of those ch
### Checks
Each check can be assigned a `severity`. Only checks with a severity of `danger` or `warning` will be validated. The results of these validations are visible on the dashboard. In the case of the validating webhook, only failures with a severity of `danger` will result in a change being rejected.
Each check can be assigned a `severity`. Only checks with a severity of `error` or `warning` will be validated. The results of these validations are visible on the dashboard. In the case of the validating webhook, only failures with a severity of `error` will result in a change being rejected.
Polaris validation checks fall into several different categories:
@@ -60,10 +60,6 @@ strings like `1000m` and `1G`. You can see an example in [the extended config](/
There are additional examples in the [checks folder](/checks).
### Exemptions
Sometimes a workload really does need to do things that Polaris considers insecure. For instance,
many of the `kube-system` workloads need to run as root, or need access to the host network. In these
cases, we can add **exemptions** to allow the workload to pass Polaris checks.
Exemptions can be added two ways: by annotating a controller, or editing the Polaris config.
To exempt a controller from all checks via annotations, use the annotation `polaris.fairwinds.com/exempt=true`, e.g.
@@ -115,17 +111,6 @@ brew install reactiveops/tap/polaris
polaris dashboard --port 8080
```
You can also point the dashboard to the local filesystem, instead of a live cluster:
```bash
polaris dashboard --port 8080 --audit-path=./deploy/
```
### Local Docker container
```
docker run -d -p8080:8080 -v ~/.kube/config:/opt/app/config:ro quay.io/fairwinds/polaris:1.2 polaris dashboard --kubeconfig /opt/app/config
```
## Webhook
### kubectl
```bash
@@ -156,25 +141,18 @@ polaris audit --format score
# 92
```
Audits can run against a local directory or YAML file rather than a cluster:
Both the dashboard and audits can run against a local directory or YAML file
rather than a cluster:
```bash
polaris audit --audit-path ./deploy/
# or to use STDIN
cat pod.yaml | polaris audit --audit-path -
```
You can also run the audit on a single resource instead of the entire cluster:
```bash
polaris audit --resource "nginx-ingress/Deployment.apps/v1/default-backend"
```
#### Running with CI/CD
You can integrate Polaris into CI/CD for repositories containing infrastructure-as-code.
For example, to fail if polaris detects *any* danger-level issues, or if the score drops below 90%:
For example, to fail if polaris detects *any* error-level issues, or if the score drops below 90%:
```bash
polaris audit --audit-path ./deploy/ \
--set-exit-code-on-danger \
--set-exit-code-on-error \
--set-exit-code-below-score 90
```
@@ -224,20 +202,18 @@ webhook
# audit flags
--audit-path string
If specified, audits one or more YAML files instead of a cluster
--resource string
If specified, audit a specific resource, in the format namespace/kind/version/name, e.g. nginx-ingress/Deployment.apps/v1/default-backend
--display-name string
An optional identifier for the audit
--format string
Output format for results - json, yaml, or score (default "json")
--output-file string
Destination file for audit results
--output-format string
Output format for results - json, yaml, or score (default "json")
--output-url string
Destination URL to send audit results
--set-exit-code-below-score int
Set an exit code of 4 when the score is below this threshold (1-100)
--set-exit-code-on-danger
Set an exit code of 3 when the audit contains danger-level issues.
--set-exit-code-on-error
Set an exit code of 3 when the audit contains error-level issues.
# webhook flags
--disable-webhook-config-installer
+78 -20
View File
@@ -1,15 +1,13 @@
checks:
# reliability
multipleReplicasForDeployment: warning
priorityClassNotSet: warning
# resources
cpuRequestsMissing: warning
cpuLimitsMissing: warning
memoryRequestsMissing: warning
memoryLimitsMissing: warning
# images
tagNotSpecified: danger
pullPolicyNotAlways: warning
tagNotSpecified: error
pullPolicyNotAlways: ignore
imageRegistry: warning
# healthChecks
readinessProbeMissing: warning
livenessProbeMissing: warning
@@ -17,24 +15,14 @@ checks:
hostNetworkSet: warning
hostPortSet: warning
# security
hostIPCSet: danger
hostPIDSet: danger
hostIPCSet: error
hostPIDSet: error
notReadOnlyRootFilesystem: warning
privilegeEscalationAllowed: danger
privilegeEscalationAllowed: error
runAsRootAllowed: warning
runAsPrivileged: danger
dangerousCapabilities: danger
runAsPrivileged: error
dangerousCapabilities: error
insecureCapabilities: warning
# custom
resourceLimits: warning
imageRegistry: danger
exemptions:
- controllerNames:
- my-network-controller
rules:
- hostNetworkSet
- hostPortSet
customChecks:
resourceLimits:
@@ -84,3 +72,73 @@ customChecks:
not:
pattern: ^quay.io
exemptions:
- controllerNames:
- dns-controller
- datadog-datadog
- kube-flannel-ds
- kube2iam
- aws-iam-authenticator
- datadog
- kube2iam
rules:
- hostNetworkSet
- controllerNames:
- aws-iam-authenticator
- aws-cluster-autoscaler
- kube-state-metrics
- dns-controller
- external-dns
- dnsmasq
- autoscaler
- kubernetes-dashboard
- install-cni
- kube2iam
rules:
- readinessProbeMissing
- livenessProbeMissing
- controllerNames:
- aws-iam-authenticator
- nginx-ingress-controller
- nginx-ingress-default-backend
- aws-cluster-autoscaler
- kube-state-metrics
- dns-controller
- external-dns
- kubedns
- dnsmasq
- autoscaler
- tiller
- kube2iam
rules:
- runAsRootAllowed
- controllerNames:
- aws-iam-authenticator
- nginx-ingress-controller
- nginx-ingress-default-backend
- aws-cluster-autoscaler
- kube-state-metrics
- dns-controller
- external-dns
- kubedns
- dnsmasq
- autoscaler
- tiller
- kube2iam
rules:
- notReadOnlyRootFilesystem
- controllerNames:
- cert-manager
- dns-controller
- kubedns
- dnsmasq
- autoscaler
rules:
- cpuRequestsMissing
- cpuLimitsMissing
- memoryRequestsMissing
- memoryLimitsMissing
- controllerNames:
- kube2iam
rules:
- runAsPrivileged
+7 -10
View File
@@ -1,15 +1,12 @@
checks:
# reliability
multipleReplicasForDeployment: ignore
priorityClassNotSet: ignore
# resources
cpuRequestsMissing: warning
cpuLimitsMissing: warning
memoryRequestsMissing: warning
memoryLimitsMissing: warning
# images
tagNotSpecified: danger
pullPolicyNotAlways: warning
tagNotSpecified: error
pullPolicyNotAlways: ignore
# healthChecks
readinessProbeMissing: warning
livenessProbeMissing: warning
@@ -17,13 +14,13 @@ checks:
hostNetworkSet: warning
hostPortSet: warning
# security
hostIPCSet: danger
hostPIDSet: danger
hostIPCSet: error
hostPIDSet: error
notReadOnlyRootFilesystem: warning
privilegeEscalationAllowed: danger
privilegeEscalationAllowed: error
runAsRootAllowed: warning
runAsPrivileged: danger
dangerousCapabilities: danger
runAsPrivileged: error
dangerousCapabilities: error
insecureCapabilities: warning
exemptions:
+29
View File
@@ -0,0 +1,29 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: polaris-failing-deployment
labels:
app: polaris-failing-deployment
spec:
replicas: 1
selector:
matchLabels:
app: polaris-failing-deployment
template:
metadata:
labels:
app: polaris-failing-deployment
spec:
containers:
- name: this-will-fail
image: 'alpine'
command: [ "/bin/sh", "-c", "--" ]
args: [ "while true; do sleep 30; done;" ]
securityContext:
privileged: true
allowPrivilegeEscalation: true
readOnlyRootFilesystem: false
runAsNonRoot: false
capabilities:
add:
- ALL
+12 -12
View File
@@ -29,7 +29,7 @@
"ID": "hostIPCSet",
"Message": "Host IPC is not configured",
"Success": true,
"Severity": "danger",
"Severity": "error",
"Category": "Security"
},
"hostNetworkSet": {
@@ -43,7 +43,7 @@
"ID": "hostPIDSet",
"Message": "Host PID is not configured",
"Success": true,
"Severity": "danger",
"Severity": "error",
"Category": "Security"
}
},
@@ -69,7 +69,7 @@
"ID": "dangerousCapabilities",
"Message": "Container does not have any dangerous capabilities",
"Success": true,
"Severity": "danger",
"Severity": "error",
"Category": "Security"
},
"hostPortSet": {
@@ -111,7 +111,7 @@
"ID": "privilegeEscalationAllowed",
"Message": "Privilege escalation not allowed",
"Success": true,
"Severity": "danger",
"Severity": "error",
"Category": "Security"
},
"readinessProbeMissing": {
@@ -125,7 +125,7 @@
"ID": "runAsPrivileged",
"Message": "Not running as privileged",
"Success": true,
"Severity": "danger",
"Severity": "error",
"Category": "Security"
},
"runAsRootAllowed": {
@@ -139,7 +139,7 @@
"ID": "tagNotSpecified",
"Message": "Image tag is specified",
"Success": true,
"Severity": "danger",
"Severity": "error",
"Category": "Images"
}
}
@@ -159,7 +159,7 @@
"ID": "hostIPCSet",
"Message": "Host IPC is not configured",
"Success": true,
"Severity": "danger",
"Severity": "error",
"Category": "Security"
},
"hostNetworkSet": {
@@ -173,7 +173,7 @@
"ID": "hostPIDSet",
"Message": "Host PID is not configured",
"Success": true,
"Severity": "danger",
"Severity": "error",
"Category": "Security"
}
},
@@ -199,7 +199,7 @@
"ID": "dangerousCapabilities",
"Message": "Container does not have any dangerous capabilities",
"Success": true,
"Severity": "danger",
"Severity": "error",
"Category": "Security"
},
"hostPortSet": {
@@ -241,7 +241,7 @@
"ID": "privilegeEscalationAllowed",
"Message": "Privilege escalation not allowed",
"Success": true,
"Severity": "danger",
"Severity": "error",
"Category": "Security"
},
"readinessProbeMissing": {
@@ -255,7 +255,7 @@
"ID": "runAsPrivileged",
"Message": "Not running as privileged",
"Success": true,
"Severity": "danger",
"Severity": "error",
"Category": "Security"
},
"runAsRootAllowed": {
@@ -269,7 +269,7 @@
"ID": "tagNotSpecified",
"Message": "Image tag is specified",
"Success": true,
"Severity": "danger",
"Severity": "error",
"Category": "Images"
}
}
+52
View File
@@ -0,0 +1,52 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: polaris-passing-deployment
labels:
app: polaris-passing-deployment
spec:
replicas: 1
selector:
matchLabels:
app: polaris-passing-deployment
template:
metadata:
labels:
app: polaris-passing-deployment
spec:
containers:
- command:
- polaris
- --dashboard
image: 'quay.io/reactiveops/polaris:0.1.0'
imagePullPolicy: 'Always'
name: dashboard
ports:
- containerPort: 8080
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 5
periodSeconds: 20
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 5
periodSeconds: 20
resources:
limits:
cpu: 100m
memory: 128Mi
requests:
cpu: 100m
memory: 128Mi
securityContext:
allowPrivilegeEscalation: false
privileged: false
readOnlyRootFilesystem: true
runAsNonRoot: true
capabilities:
drop:
- ALL
+89 -32
View File
@@ -3,39 +3,96 @@ module github.com/fairwindsops/polaris
go 1.13
require (
cloud.google.com/go v0.65.0 // indirect
github.com/Azure/go-autorest/autorest v0.11.4 // indirect
github.com/Azure/go-autorest/autorest/adal v0.9.2 // indirect
github.com/fairwindsops/controller-utils v0.1.0
github.com/gobuffalo/packr/v2 v2.8.0
github.com/google/go-cmp v0.5.2 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/gophercloud/gophercloud v0.12.0 // indirect
github.com/gorilla/mux v1.8.0
github.com/imdario/mergo v0.3.11 // indirect
github.com/jessevdk/go-flags v1.4.0 // indirect
github.com/karrick/godirwalk v1.16.1 // indirect
github.com/kr/pretty v0.2.0 // indirect
github.com/prometheus/common v0.13.0 // indirect
github.com/qri-io/jsonpointer v0.1.1 // indirect
cloud.google.com/go v0.39.0
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.1
github.com/census-instrumentation/opencensus-proto v0.2.1
github.com/davecgh/go-spew v1.1.1
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/evanphx/json-patch v4.5.0+incompatible
github.com/go-logr/logr v0.1.0
github.com/go-logr/zapr v0.1.1
github.com/gobuffalo/depgen v0.1.0 // indirect
github.com/gobuffalo/envy v1.8.1
github.com/gobuffalo/genny v0.6.0
github.com/gobuffalo/gogen v0.2.0
github.com/gobuffalo/logger v1.0.3
github.com/gobuffalo/mapi v1.2.0
github.com/gobuffalo/packd v0.4.0
github.com/gobuffalo/packr/v2 v2.7.1
github.com/gobuffalo/syncx v0.1.0
github.com/gogo/protobuf v1.2.1
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7 // indirect
github.com/golang/protobuf v1.4.0
github.com/google/btree v1.0.0
github.com/google/gofuzz v1.1.0
github.com/google/uuid v1.1.1
github.com/googleapis/gnostic v0.3.1
github.com/gophercloud/gophercloud v0.0.0-20190516165734-b3a23cc94cc5
github.com/gorilla/mux v1.7.4
github.com/gregjones/httpcache v0.0.0-20190212212710-3befbb6ad0cc
github.com/grpc-ecosystem/grpc-gateway v1.9.0
github.com/hashicorp/golang-lru v0.5.4
github.com/imdario/mergo v0.3.7
github.com/joho/godotenv v1.3.0
github.com/json-iterator/go v1.1.9
github.com/karrick/godirwalk v1.15.6
github.com/konsorten/go-windows-terminal-sequences v1.0.3
github.com/markbates/oncer v1.0.0
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 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 v1.6.0
github.com/prometheus/client_model v0.2.0
github.com/prometheus/common v0.10.0
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.6.2 // indirect
github.com/sirupsen/logrus v1.7.0
github.com/spf13/cobra v1.0.0
github.com/rogpeppe/go-internal v1.5.2
github.com/sirupsen/logrus v1.5.0
github.com/spf13/cobra v0.0.5
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.6.1
gitlab.com/golang-commonmark/linkify v0.0.0-20200225224916-64bca66f6ad3 // indirect
gitlab.com/golang-commonmark/markdown v0.0.0-20191127184510-91b5b3c99c19
go.uber.org/zap v1.16.0 // indirect
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a // indirect
golang.org/x/sys v0.0.0-20200824131525-c12d262b63d8 // indirect
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e // indirect
gomodules.xyz/jsonpatch/v2 v2.1.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776
k8s.io/api v0.18.9
k8s.io/apimachinery v0.18.9
k8s.io/client-go v0.18.9
k8s.io/klog/v2 v2.1.0 // indirect
sigs.k8s.io/controller-runtime v0.6.3
github.com/stretchr/testify v1.5.1
gitlab.com/golang-commonmark/html v0.0.0-20180917080848-cfaf75183c4a
gitlab.com/golang-commonmark/linkify v0.0.0-20180917065525-c22b7bdb1179
gitlab.com/golang-commonmark/markdown v0.0.0-20181102083822-772775880e1f
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.6.0
go.uber.org/multierr v1.5.0
go.uber.org/zap v1.15.0
golang.org/x/crypto v0.0.0-20191219195013-becbf705a915
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-20200420163511-1957bb5e6d1f
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
google.golang.org/api v0.5.0
google.golang.org/appengine v1.6.6
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.8
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
k8s.io/klog v0.3.0
k8s.io/kube-openapi v0.0.0-20190510232812-a01b7d5d6c22
sigs.k8s.io/controller-runtime v0.1.10
sigs.k8s.io/yaml v1.2.0
)
+291 -705
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 186 KiB

+1 -1
View File
@@ -20,7 +20,7 @@ import (
const (
// Version represents the current release version of Polaris
Version = "1.2.1"
Version = "1.0.0"
)
func main() {
+3 -23
View File
@@ -16,7 +16,6 @@ package config
import (
"bytes"
"errors"
"fmt"
"io"
"io/ioutil"
@@ -42,21 +41,13 @@ type Exemption struct {
ControllerNames []string `json:"controllerNames"`
}
var configBox = (*packr.Box)(nil)
func getConfigBox() *packr.Box {
if configBox == (*packr.Box)(nil) {
configBox = packr.New("Config", "../../examples")
}
return configBox
}
// ParseFile parses config from a file.
func ParseFile(path string) (Configuration, error) {
var rawBytes []byte
var err error
if path == "" {
rawBytes, err = getConfigBox().Find("config.yaml")
configBox := packr.New("Config", "../../examples")
rawBytes, err = configBox.Find("config.yaml")
} else if strings.HasPrefix(path, "https://") || strings.HasPrefix(path, "http://") {
//path is a url
response, err2 := http.Get(path)
@@ -93,17 +84,6 @@ func Parse(rawBytes []byte) (Configuration, error) {
return conf, err
}
conf.CustomChecks[key] = check
if _, ok := conf.Checks[key]; !ok {
return conf, fmt.Errorf("no severity specified for custom check %s. Please add the following to your configuration:\n\nchecks:\n %s: warning # or danger/ignore\n\nto enable your check", key, key)
}
}
return conf, conf.Validate()
}
// Validate checks if a config is valid
func (c Configuration) Validate() error {
if len(c.Checks) == 0 {
return errors.New("No checks were enabled")
}
return nil
return conf, nil
}
-20
View File
@@ -74,21 +74,6 @@ customChecks:
}
`
var confCustomChecksMissing = `
customChecks:
foo:
successMessage: Security context is set
failureMessage: Security context should be set
category: Security
target: Container
schema:
'$schema': http://json-schema.org/draft-07/schema
type: object
required:
- securityContext
`
func TestParseError(t *testing.T) {
_, err := Parse([]byte(confInvalid))
expectedErr := "Decoding config failed: error unmarshaling JSON: while decoding JSON: json: cannot unmarshal string into Go value of type config.Configuration"
@@ -169,11 +154,6 @@ func TestConfigWithCustomChecks(t *testing.T) {
assert.Equal(t, false, isValid)
}
func TestCustomChecksMissingSeverity(t *testing.T) {
_, err := Parse([]byte(confCustomChecksMissing))
assert.Error(t, err, "Expected error when check has no severity set")
}
func testParsedConfig(t *testing.T, config *Configuration) {
assert.Equal(t, SeverityWarning, config.Checks["cpuRequestsMissing"])
assert.Equal(t, Severity(""), config.Checks["cpuLimitsMissing"])
-8
View File
@@ -12,7 +12,6 @@ func (conf Configuration) IsActionable(ruleID, controllerName string) bool {
if conf.DisallowExemptions {
return true
}
for _, example := range conf.Exemptions {
for _, rule := range example.Rules {
if rule != ruleID {
@@ -24,13 +23,6 @@ func (conf Configuration) IsActionable(ruleID, controllerName string) bool {
}
}
}
if len(example.Rules) == 0 {
for _, controller := range example.ControllerNames {
if strings.HasPrefix(controllerName, controller) {
return false
}
}
}
}
return true
}
-62
View File
@@ -1,62 +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 (
"testing"
"github.com/stretchr/testify/assert"
)
var confExemptRuleTest = `
checks:
ANY: warning
OTHER: warning
exemptions:
- controllerNames:
- test
rules:
- ANY
`
var confExemptTest = `
checks:
ANY: warning
exemptions:
- controllerNames:
- test
`
func TestInclusiveExemption(t *testing.T) {
parsedConf, _ := Parse([]byte(confExemptTest))
applicable := parsedConf.IsActionable("ANY", "test")
applicableOtherController := parsedConf.IsActionable("ANY", "other")
assert.False(t, applicable, "Expected all checks to be exempted when their controller is specified.")
assert.True(t, applicableOtherController, "Expected checks to only be exempted when their controller is specified.")
}
func TestIndividualRuleException(t *testing.T) {
parsedConf, _ := Parse([]byte(confExemptRuleTest))
applicable := parsedConf.IsActionable("ANY", "test")
applicableOtherRule := parsedConf.IsActionable("OTHER", "test")
applicableOtherRuleOtherController := parsedConf.IsActionable("OTHER", "other")
applicableRuleOtherController := parsedConf.IsActionable("ANY", "other")
assert.False(t, applicable, "Expected all checks to be exempted when their controller and rule are specified.")
assert.True(t, applicableOtherRule, "Expected checks to only be exempted when their controller and rule are specified.")
assert.True(t, applicableOtherRuleOtherController, "Expected checks to only be exempted when their controller and rule are specified.")
assert.True(t, applicableRuleOtherController, "Expected checks to only be exempted when their controller and rule are specified.")
}
-8
View File
@@ -17,8 +17,6 @@ const (
TargetContainer TargetKind = "Container"
// TargetPod points to the pod spec
TargetPod TargetKind = "Pod"
// TargetController points to the controller's spec
TargetController TargetKind = "Controller"
)
// SchemaCheck is a Polaris check that runs using JSON Schema
@@ -130,12 +128,6 @@ func (check SchemaCheck) CheckPod(pod *corev1.PodSpec) (bool, error) {
return check.CheckObject(pod)
}
// CheckController checks a controler's spec against the schema
func (check SchemaCheck) CheckController(bytes []byte) (bool, error) {
errs, err := check.Schema.ValidateBytes(bytes)
return len(errs) == 0, err
}
// CheckContainer checks a container spec against the schema
func (check SchemaCheck) CheckContainer(container *corev1.Container) (bool, error) {
return check.CheckObject(container)
+3 -3
View File
@@ -24,11 +24,11 @@ const (
// SeverityWarning warns on validation failures
SeverityWarning Severity = "warning"
// SeverityDanger errors on validation failures
SeverityDanger Severity = "danger"
// SeverityError errors on validation failures
SeverityError Severity = "error"
)
// IsActionable returns true if the severity level is warning or error
func (severity *Severity) IsActionable() bool {
return *severity == SeverityWarning || *severity == SeverityDanger
return *severity == SeverityWarning || *severity == SeverityError
}
+36 -77
View File
@@ -4,7 +4,6 @@
position: relative;
}
.filters .title,
.card h3 {
margin: 0;
font-weight: 300;
@@ -41,10 +40,12 @@
width: 32%;
vertical-align: top;
}
.cluster-overview .cluster-score,
.cluster-overview .result-messages {
.cluster-overview .cluster-score{
padding-top: 15px;
}
.cluster-overview .result-messages {
padding-top: 70px;
}
.cluster-overview .graph canvas {
margin-left: auto;
@@ -92,24 +93,45 @@
.cluster-overview .result-messages ul {
font-size: 20px;
line-height: 42px;
line-height: 35px;
}
.cluster-overview-footer {
position: absolute;
bottom: 0px;
right: 40px;
right: 10px;
left: 40px;
}
.cluster-overview-footer .kubernetes-stats {
.cluster-overview-footer p {
display: inline-block;
font-size: 11px;
color: #777;
width: 48%;
}
.cluster-overview-footer .kubernetes-stats {
display: flex;
justify-content: space-between;
.cluster-overview-footer p.kubernetes-overview {
text-align: right;
}
.cluster-overview-footer p.kubernetes-overview .kubernetes-stat {
margin-left: 15px;
}
.kubernetes-overview ul {
list-style-type: none;
margin: 0;
padding: 0 10px;
text-align: right;
}
.kubernetes-overview li {
display: inline-block;
margin: 20px 0 0 10px;
}
.kubernetes-overview li span {
margin-right: 3px;
}
.category .name {
@@ -169,7 +191,7 @@
cursor: pointer;
}
.resource-info .caret-expander {
.expandable-table .resource-info .caret-expander {
display: inline-block;
width: 15px;
height: 15px;
@@ -180,16 +202,16 @@
background-position: 2px center;
}
.resource-info.expanded .caret-expander {
.expandable-table .resource-info.expanded .caret-expander {
background-image: url('../images/caret-bottom.svg');
background-position: center 2px;
}
.resource-info .expandable-content {
.expandable-table .resource-info .expandable-content {
display: none;
}
.resource-info.expanded .expandable-content {
.expandable-table .resource-info.expanded .expandable-content {
display: block;
}
@@ -214,26 +236,6 @@ ul.message-list {
.cluster-overview ul.message-list {
margin: 0px;
text-align: right;
}
.cluster-overview ul.message-list .message-group {
display: inline-block;
min-width: 130px;
}
.cluster-overview ul.message-list .message {
display: block;
font-size: 12px;
line-height: 12px;
}
.cluster-overview ul.message-list li i.message-icon {
font-size: 42px;
text-align: center;
min-width: 42px;
vertical-align: top;
margin-top: 8px;
}
ul.message-list li {
@@ -259,7 +261,7 @@ ul.message-list li i.message-icon {
color: #f26c21;
}
.result-messages .failure.danger i.message-icon {
.result-messages .failure.error i.message-icon {
color: #a11f4c;
}
@@ -317,46 +319,3 @@ a.more-info:hover {
0% {opacity: 0;}
100% {opacity: 1;}
}
.filters .title {
padding-left: 0;
}
.filters .caret-expander {
margin-bottom: 2px;
margin-top: 8px;
}
.filters input[type="checkbox"] {
margin-top: 4px;
}
.filters input[type="submit"] {
padding: 4px 10px;
box-shadow: none;
background-color: #23103A;
border: 1px solid #ccc;
border-radius: 4px;
color: #fff;
cursor: pointer;
font-size: 20px;
width: 100px;
margin-top: 15px;
}
form.namespace-list {
line-height: 20px;
margin: 5px 35px;
padding: 0;
}
.namespace-row {
display: flex;
padding: 3px 0;
}
.namespace-row label {
margin-left: 4px;
font-size: 20px;
}
+1 -1
View File
@@ -7,7 +7,7 @@ $(function () {
data: [
polarisSummary.Successes,
polarisSummary.Warnings,
polarisSummary.Dangers,
polarisSummary.Errors,
],
backgroundColor: ['#8BD2DC', '#f26c21', '#a11f4c'],
}]
-22
View File
@@ -1,22 +0,0 @@
$(function () {
// Check selected namespace options on page load
const urlParams = new URLSearchParams(window.location.search);
const currentNamespaces = urlParams.getAll('ns');
currentNamespaces.forEach(ns => {
$(`input#namespace-${ns}`).prop('checked', true);
});
// Handle new filter submissions
$('#namespaceFiltersForm').on('submit', e => {
e.preventDefault();
let newParams = new URLSearchParams();
$('#namespaceFiltersForm input[type="checkbox"]').each((index, checkbox) => {
if (checkbox.checked) {
newParams.append('ns', checkbox.name);
}
});
window.location = new URL(`?${newParams.toString()}`, window.location).toString();
});
});
+12 -30
View File
@@ -81,11 +81,10 @@ func GetMarkdownBox() *packr.Box {
// templateData is passed to the dashboard HTML template
type templateData struct {
BasePath string
Config config.Configuration
AuditData validator.AuditData
FilteredAuditData validator.AuditData
JSON template.JS
BasePath string
Config config.Configuration
AuditData validator.AuditData
JSON template.JS
}
// GetBaseTemplate puts together the dashboard template. Individual pieces can be overridden before rendering.
@@ -151,16 +150,6 @@ func getConfigForQuery(base config.Configuration, query url.Values) config.Confi
return c
}
func stripUnselectedNamespaces(data *validator.AuditData, selectedNamespaces []string) {
newResults := []validator.ControllerResult{}
for _, res := range data.Results {
if stringInSlice(res.Namespace, selectedNamespaces) {
newResults = append(newResults, res)
}
}
data.Results = newResults
}
// GetRouter returns a mux router serving all routes necessary for the dashboard
func GetRouter(c config.Configuration, auditPath string, port int, basePath string, auditData *validator.AuditData) *mux.Router {
router := mux.NewRouter().PathPrefix(basePath).Subrouter()
@@ -184,14 +173,14 @@ func GetRouter(c config.Configuration, auditPath string, port int, basePath stri
router.HandleFunc("/results.json", func(w http.ResponseWriter, r *http.Request) {
adjustedConf := getConfigForQuery(c, r.URL.Query())
if auditData == nil {
k, err := kube.CreateResourceProvider(r.Context(), auditPath, "")
k, err := kube.CreateResourceProvider(auditPath)
if err != nil {
logrus.Errorf("Error fetching Kubernetes resources %v", err)
http.Error(w, "Error fetching Kubernetes resources", http.StatusInternalServerError)
return
}
auditDataObj, err := validator.RunAudit(r.Context(), adjustedConf, k)
auditDataObj, err := validator.RunAudit(adjustedConf, k)
if err != nil {
http.Error(w, "Error Fetching Deployments", http.StatusInternalServerError)
return
@@ -217,14 +206,14 @@ func GetRouter(c config.Configuration, auditPath string, port int, basePath stri
adjustedConf := getConfigForQuery(c, r.URL.Query())
if auditData == nil {
k, err := kube.CreateResourceProvider(r.Context(), auditPath, "")
k, err := kube.CreateResourceProvider(auditPath)
if err != nil {
logrus.Errorf("Error fetching Kubernetes resources %v", err)
http.Error(w, "Error fetching Kubernetes resources", http.StatusInternalServerError)
return
}
auditData, err := validator.RunAudit(r.Context(), adjustedConf, k)
auditData, err := validator.RunAudit(adjustedConf, k)
if err != nil {
logrus.Errorf("Error getting audit data: %v", err)
http.Error(w, "Error running audit", 500)
@@ -248,18 +237,11 @@ func MainHandler(w http.ResponseWriter, r *http.Request, c config.Configuration,
return
}
filteredAuditData := auditData
namespaces := r.URL.Query()["ns"]
if len(namespaces) > 0 {
stripUnselectedNamespaces(&filteredAuditData, namespaces)
}
data := templateData{
BasePath: basePath,
AuditData: auditData,
FilteredAuditData: filteredAuditData,
JSON: template.JS(jsonData),
Config: c,
BasePath: basePath,
AuditData: auditData,
JSON: template.JS(jsonData),
Config: c,
}
tmpl, err := GetBaseTemplate("main")
if err != nil {
+2 -21
View File
@@ -23,21 +23,11 @@ import (
)
func getWarningWidth(counts validator.CountSummary, fullWidth int) uint {
denom := counts.Successes + counts.Warnings + counts.Dangers
if denom == 0 {
return uint(0)
}
res := float64(counts.Successes+counts.Warnings) / float64(denom) * float64(fullWidth)
return uint(res)
return uint(float64(counts.Successes+counts.Warnings) / float64(counts.Successes+counts.Warnings+counts.Errors) * float64(fullWidth))
}
func getSuccessWidth(counts validator.CountSummary, fullWidth int) uint {
denom := counts.Successes + counts.Warnings + counts.Dangers
if denom == 0 {
return uint(0)
}
res := float64(counts.Successes) / float64(denom) * float64(fullWidth)
return uint(res)
return uint(float64(counts.Successes) / float64(counts.Successes+counts.Warnings+counts.Errors) * float64(fullWidth))
}
func getGrade(counts validator.CountSummary) string {
@@ -171,12 +161,3 @@ func getCategoryInfo(category string) string {
return ""
}
}
func stringInSlice(a string, list []string) bool {
for _, b := range list {
if b == a {
return true
}
}
return false
}
+33 -88
View File
@@ -10,12 +10,9 @@
<div class="cluster-score">
<div class="score-details">
<div class="weather"><i class="fas {{ getWeatherIcon .AuditData.GetSummary }}"></i></div>
<div class="sailing">{{ getWeatherText .FilteredAuditData.GetSummary }}</div>
<div class="scores"><span>Grade: </span><strong>{{ getGrade .FilteredAuditData.GetSummary }}</strong></div>
<div class="scores"><span>Score: </span><strong>{{ .FilteredAuditData.GetSummary.GetScore }}%</strong></div>
<p class="score-description">
Score is the percentage of passing checks. Warnings get half the weight of dangerous checks.
</p>
<div class="sailing">{{ getWeatherText .AuditData.GetSummary }}</div>
<div class="scores"><span>Grade: </span><strong>{{ getGrade .AuditData.GetSummary }}</strong></div>
<div class="scores"><span>Score: </span><strong>{{ .AuditData.GetSummary.GetScore }}%</strong></div>
</div>
</div>
<div class="graph">
@@ -23,60 +20,45 @@
</div>
<div class="result-messages">
<ul class="message-list">
<li class="success">
<i class="message-icon fas fa-check"></i>
<div class="message-group">
<span class="count"> {{ .FilteredAuditData.GetSummary.Successes }}</span>
<span class="message"> passing checks</span>
</div>
</li>
<li class="failure warning">
<i class="message-icon fas fa-exclamation"></i>
<div class="message-group">
<span class="count"> {{ .FilteredAuditData.GetSummary.Warnings }}</span>
<span class="message"> warning checks</span>
</div>
</li>
<li class="failure danger">
<i class="message-icon fas fa-times"></i>
<div class="message-group">
<span class="count"> {{ .FilteredAuditData.GetSummary.Dangers }}</span>
<span class="message"> dangerous checks</span>
</div>
</li>
<li class="success"><i class="message-icon fas fa-check"></i> {{ .AuditData.GetSummary.Successes }} checks passed</li>
<li class="failure warning"><i class="message-icon fas fa-exclamation"></i> {{ .AuditData.GetSummary.Warnings }} checks had warnings</li>
<li class="failure error"><i class="message-icon fas fa-times"></i> {{ .AuditData.GetSummary.Errors }} checks had errors</li>
</ul>
</div>
</div>
<div class="cluster-overview-footer">
<div class="kubernetes-stats">
<div class="kubernetes-stat">
<p class="score-description">
Score is the percentage of passing checks. Warnings get half the weight of errors.
</p>
<p class="kubernetes-overview">
<span class="kubernetes-stat">
<span>Kubernetes Version:</span>
<strong>{{.FilteredAuditData.ClusterInfo.Version}}</strong>
</div>
<div class="kubernetes-stat">
<strong>{{.AuditData.ClusterInfo.Version}}</strong>
</span>
<span class="kubernetes-stat">
<span>Nodes:</span>
<strong>{{.FilteredAuditData.ClusterInfo.Nodes}}</strong>
</div>
<div class="kubernetes-stat">
<span>Namespaces:</span>
<strong>{{.FilteredAuditData.ClusterInfo.Namespaces}}</strong>
</div>
<div class="kubernetes-stat">
<span>Controllers:</span>
<strong>{{.FilteredAuditData.ClusterInfo.Controllers}}</strong>
</div>
<div class="kubernetes-stat">
<strong>{{.AuditData.ClusterInfo.Nodes}}</strong>
</span>
<span class="kubernetes-stat">
<span>Pods:</span>
<strong>{{.FilteredAuditData.ClusterInfo.Pods}}</strong>
</div>
</div>
<strong>{{.AuditData.ClusterInfo.Pods}}</strong>
</span>
<span class="kubernetes-stat">
<span>Controllers:</span>
<strong>{{.AuditData.ClusterInfo.Controllers}}</strong>
</span>
<span class="kubernetes-stat">
<span>Namespaces:</span>
<strong>{{.AuditData.ClusterInfo.Namespaces}}</strong>
</span>
</p>
</div>
</div>
<div class="card category">
<h3>Results by Category</h3>
<div class="expandable-table">
{{ range $category, $summary := .FilteredAuditData.GetSummaryByCategory }}
{{ range $category, $summary := .AuditData.GetSummaryByCategory }}
<div class="resource-info">
<div class="status-bar">
<div class="status">
@@ -88,7 +70,7 @@
</div>
</div>
<div class="name"><span class="caret-expander"></span>{{ $category }}<span class="category-score">Score: <strong>{{ $summary.GetScore }}%</strong></span></div>
<div class="result-messages expandable-content">
<div class="result-messages expandable-content">
<p class="category-info">{{ getCategoryInfo $category }} Refer to the <a href="details/{{ getCategoryLink $category }}">Polaris documentation about {{ $category }}</a> for more information.</p>
</div>
</div>
@@ -96,27 +78,7 @@
</div>
</div>
<div class="card filters">
<div class="resource-info">
<div class="name">
<span class="caret-expander"></span>
<span class="title">Filter by Namespace</span>
</div>
<div class="result-messages expandable-content">
<form id="namespaceFiltersForm" class="namespace-list">
{{ range $namespace, $ctrlResults := .AuditData.GetResultsByNamespace }}
<div class="namespace-row">
<input type="checkbox" name="{{ $namespace }}" id="namespace-{{ $namespace }}">
<label for="namespace-{{ $namespace }}">{{ $namespace }}</label>
</div>
{{ end }}
<input type="submit" value="Apply">
</form>
</div>
</div>
</div>
{{ range $namespace, $ctrlResults := .FilteredAuditData.GetResultsByNamespace }}
{{ range $namespace, $ctrlResults := .AuditData.GetResultsByNamespace }}
<div class="card namespace">
<h3>Namespace: <strong>{{ $namespace }}</strong></h3>
<div class="expandable-table">
@@ -135,22 +97,6 @@
<div class="name"><span class="caret-expander"></span>
<span class="controller-type">{{ .Kind }}:</span>
<strong>{{ .Name }}</strong></div>
<div class="result-messages expandable-content">
<h4>Controller Spec:</h4>
<ul class="message-list">
{{ range $message := .Results.GetSortedResults }}
<li class="{{ getResultClass . }}">
<i class="message-icon {{ getIcon $message }}"></i>
<span class="message">{{ .Message }}</span>
<a class="more-info" href="details/{{ getCategoryLink .Category }}">
<i class="far fa-question-circle"></i>
</a>
</li>
{{ end }}
</ul>
</div>
<div class="result-messages expandable-content">
<h4>Pod Spec:</h4>
<ul class="message-list">
@@ -165,7 +111,6 @@
{{ end }}
</ul>
</div>
{{ range .PodResult.ContainerResults }}
<div class="result-messages expandable-content">
<h4>Container: {{ .Name }}</h4>
@@ -187,6 +132,6 @@
</div>
</div>
{{ end }} {{/* end range .AuditData.GetResultsByNamespace */}}
<script src="static/js/charts.js"></script>
<script src="static/js/filter.js"></script>
<script src="static/js/charts.js">
</script>
{{end}}
+1 -1
View File
@@ -1,5 +1,5 @@
{{define "footer"}}
<div class="footer">
<a href="https://fairwinds.com?source=polaris" target="_blank">&copy;2020 FairwindsOps Inc.</a>
<a href="https://fairwinds.com?source=polaris" target="_blank">&copy;2019 FairwindsOps Inc.</a>
</div>
{{end}}
+1 -1
View File
@@ -1,7 +1,7 @@
{{define "navbar"}}
<div class="navbar">
<div class="navbar-content">
<a href="{{ .BasePath }}">
<a href="/">
<img class="logo" src="static/images/polaris-logo.png" alt="Polaris" />
</a>
<div class="navbar-right">
+75 -133
View File
@@ -2,9 +2,7 @@ package kube
import (
"bytes"
"context"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
@@ -13,10 +11,11 @@ import (
"time"
"github.com/sirupsen/logrus"
"gopkg.in/yaml.v3"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
k8sYaml "k8s.io/apimachinery/pkg/util/yaml"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes"
@@ -43,75 +42,11 @@ type k8sResource struct {
var podSpecFields = []string{"jobTemplate", "spec", "template"}
// CreateResourceProvider returns a new ResourceProvider object to interact with k8s resources
func CreateResourceProvider(ctx context.Context, directory, workload string) (*ResourceProvider, error) {
if workload != "" {
return CreateResourceProviderFromWorkload(ctx, workload)
}
func CreateResourceProvider(directory string) (*ResourceProvider, error) {
if directory != "" {
return CreateResourceProviderFromPath(directory)
}
return CreateResourceProviderFromCluster(ctx)
}
// CreateResourceProviderFromWorkload creates a new ResourceProvider that just contains one workload
func CreateResourceProviderFromWorkload(ctx context.Context, workload string) (*ResourceProvider, error) {
kubeConf, configError := config.GetConfig()
if configError != nil {
logrus.Errorf("Error fetching KubeConfig: %v", configError)
return nil, configError
}
kube, err := kubernetes.NewForConfig(kubeConf)
if err != nil {
logrus.Errorf("Error creating Kubernetes client: %v", err)
return nil, err
}
serverVersion, err := kube.Discovery().ServerVersion()
if err != nil {
logrus.Errorf("Error fetching Cluster API version: %v", err)
return nil, err
}
resources := ResourceProvider{
ServerVersion: serverVersion.Major + "." + serverVersion.Minor,
SourceType: "Workload",
SourceName: workload,
CreationTime: time.Now(),
Nodes: []corev1.Node{},
Namespaces: []corev1.Namespace{},
}
parts := strings.Split(workload, "/")
if len(parts) != 4 {
return nil, fmt.Errorf("Invalid workload identifier %s. Should be in format namespace/kind/version/name, e.g. nginx-ingress/Deployment.apps/v1/default-backend", workload)
}
namespace := parts[0]
kind := parts[1]
version := parts[2]
name := parts[3]
dynamicInterface, err := dynamic.NewForConfig(kubeConf)
if err != nil {
logrus.Errorf("Error connecting to dynamic interface: %v", err)
return nil, err
}
groupResources, err := restmapper.GetAPIGroupResources(kube.Discovery())
if err != nil {
logrus.Errorf("Error getting API Group resources: %v", err)
return nil, err
}
restMapper := restmapper.NewDiscoveryRESTMapper(groupResources)
obj, err := getObject(ctx, namespace, kind, version, name, &dynamicInterface, &restMapper)
if err != nil {
logrus.Errorf("Could not find workload %s: %v", workload, err)
return nil, err
}
workloadObj, err := NewGenericWorkloadFromUnstructured(kind, obj)
if err != nil {
logrus.Errorf("Could not parse workload %s: %v", workload, err)
return nil, err
}
resources.Controllers = []GenericWorkload{workloadObj}
return &resources, nil
return CreateResourceProviderFromCluster()
}
// CreateResourceProviderFromPath returns a new ResourceProvider using the YAML files in a directory
@@ -125,14 +60,8 @@ func CreateResourceProviderFromPath(directory string) (*ResourceProvider, error)
Controllers: []GenericWorkload{},
}
if directory == "-" {
fi, err := os.Stdin.Stat()
if err == nil && fi.Mode()&os.ModeNamedPipe == os.ModeNamedPipe {
if err := addResourcesFromReader(os.Stdin, &resources); err != nil {
return nil, err
}
return &resources, nil
}
addYaml := func(contents string) error {
return addResourceFromString(contents, &resources)
}
visitFile := func(path string, f os.FileInfo, err error) error {
@@ -144,7 +73,18 @@ func CreateResourceProviderFromPath(directory string) (*ResourceProvider, error)
logrus.Errorf("Error reading file: %v", path)
return err
}
return addResourcesFromYaml(string(contents), &resources)
specs := regexp.MustCompile("\n-+\n").Split(string(contents), -1)
for _, spec := range specs {
if strings.TrimSpace(spec) == "" {
continue
}
err = addYaml(spec)
if err != nil {
logrus.Errorf("Error parsing YAML: (%v)", err)
return err
}
}
return nil
}
err := filepath.Walk(directory, visitFile)
@@ -155,7 +95,7 @@ func CreateResourceProviderFromPath(directory string) (*ResourceProvider, error)
}
// CreateResourceProviderFromCluster creates a new ResourceProvider using live data from a cluster
func CreateResourceProviderFromCluster(ctx context.Context) (*ResourceProvider, error) {
func CreateResourceProviderFromCluster() (*ResourceProvider, error) {
kubeConf, configError := config.GetConfig()
if configError != nil {
logrus.Errorf("Error fetching KubeConfig: %v", configError)
@@ -171,11 +111,11 @@ func CreateResourceProviderFromCluster(ctx context.Context) (*ResourceProvider,
logrus.Errorf("Error connecting to dynamic interface: %v", err)
return nil, err
}
return CreateResourceProviderFromAPI(ctx, api, kubeConf.Host, &dynamicInterface)
return CreateResourceProviderFromAPI(api, kubeConf.Host, &dynamicInterface)
}
// CreateResourceProviderFromAPI creates a new ResourceProvider from an existing k8s interface
func CreateResourceProviderFromAPI(ctx context.Context, kube kubernetes.Interface, clusterName string, dynamic *dynamic.Interface) (*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 {
@@ -183,17 +123,17 @@ func CreateResourceProviderFromAPI(ctx context.Context, kube kubernetes.Interfac
return nil, err
}
nodes, err := kube.CoreV1().Nodes().List(ctx, listOpts)
nodes, err := kube.CoreV1().Nodes().List(listOpts)
if err != nil {
logrus.Errorf("Error fetching Nodes: %v", err)
return nil, err
}
namespaces, err := kube.CoreV1().Namespaces().List(ctx, listOpts)
namespaces, err := kube.CoreV1().Namespaces().List(listOpts)
if err != nil {
logrus.Errorf("Error fetching Namespaces: %v", err)
return nil, err
}
pods, err := kube.CoreV1().Pods("").List(ctx, listOpts)
pods, err := kube.CoreV1().Pods("").List(listOpts)
if err != nil {
logrus.Errorf("Error fetching Pods: %v", err)
return nil, err
@@ -206,13 +146,7 @@ func CreateResourceProviderFromAPI(ctx context.Context, kube kubernetes.Interfac
}
restMapper := restmapper.NewDiscoveryRESTMapper(resources)
objectCache := map[string]unstructured.Unstructured{}
controllers, err := LoadControllers(ctx, pods.Items, dynamic, &restMapper, objectCache)
if err != nil {
logrus.Errorf("Error loading controllers from pods: %v", err)
return nil, err
}
objectCache := map[string]metav1.Object{}
api := ResourceProvider{
ServerVersion: serverVersion.Major + "." + serverVersion.Minor,
@@ -221,13 +155,32 @@ func CreateResourceProviderFromAPI(ctx context.Context, kube kubernetes.Interfac
CreationTime: time.Now(),
Nodes: nodes.Items,
Namespaces: namespaces.Items,
Controllers: controllers,
Controllers: LoadControllers(pods.Items, dynamic, &restMapper, objectCache),
}
return &api, nil
}
func cacheAllObjectsOfKind(dynamicClient dynamic.Interface, groupVersionResource schema.GroupVersionResource, objectCache map[string]metav1.Object) error {
objects, err := dynamicClient.Resource(groupVersionResource).Namespace("").List(metav1.ListOptions{})
if err != nil {
logrus.Warnf("Error retrieving parent object API %s and Kind %s because of error: %v ", groupVersionResource.Version, groupVersionResource.Resource, err)
return err
}
for idx, object := range objects.Items {
key := fmt.Sprintf("%s/%s/%s", object.GetKind(), object.GetNamespace(), object.GetName())
objMeta, err := meta.Accessor(&objects.Items[idx])
if err != nil {
logrus.Warnf("Error converting object to meta object %s %v", object.GetName(), err)
return err
}
objectCache[key] = objMeta
}
return nil
}
// LoadControllers loads a list of controllers from the kubeResources Pods
func LoadControllers(ctx context.Context, pods []corev1.Pod, dynamicClientPointer *dynamic.Interface, restMapperPointer *meta.RESTMapper, objectCache map[string]unstructured.Unstructured) ([]GenericWorkload, error) {
func LoadControllers(pods []corev1.Pod, dynamicClientPointer *dynamic.Interface, restMapperPointer *meta.RESTMapper, objectCache map[string]metav1.Object) []GenericWorkload {
interfaces := []GenericWorkload{}
deduped := map[string]corev1.Pod{}
for _, pod := range pods {
@@ -239,13 +192,9 @@ func LoadControllers(ctx context.Context, pods []corev1.Pod, dynamicClientPointe
deduped[pod.ObjectMeta.Namespace+"/"+owners[0].Kind+"/"+owners[0].Name] = pod
}
for _, pod := range deduped {
workload, err := NewGenericWorkload(ctx, pod, dynamicClientPointer, restMapperPointer, objectCache)
if err != nil {
return nil, err
}
interfaces = append(interfaces, workload)
interfaces = append(interfaces, NewGenericWorkload(pod, dynamicClientPointer, restMapperPointer, objectCache))
}
return deduplicateControllers(interfaces), nil
return deduplicateControllers(interfaces)
}
// Because the controllers with an Owner take on the name of the Owner, this eliminates any duplicates.
@@ -266,31 +215,14 @@ func deduplicateControllers(inputControllers []GenericWorkload) []GenericWorkloa
return results
}
func addResourcesFromReader(reader io.Reader, resources *ResourceProvider) error {
contents, err := ioutil.ReadAll(reader)
if err != nil {
logrus.Errorf("Error reading from %v: %v", reader, err)
return err
}
if err := addResourcesFromYaml(string(contents), resources); err != nil {
return err
}
return nil
}
func addResourcesFromYaml(contents string, resources *ResourceProvider) error {
specs := regexp.MustCompile("[\r\n]-+[\r\n]").Split(string(contents), -1)
for _, spec := range specs {
if strings.TrimSpace(spec) == "" {
continue
}
err := addResourceFromString(spec, resources)
if err != nil {
logrus.Errorf("Error parsing YAML: (%v)", err)
return err
// 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 nil
return yaml
}
func addResourceFromString(contents string, resources *ResourceProvider) error {
@@ -311,20 +243,30 @@ func addResourceFromString(contents string, resources *ResourceProvider) error {
} else if resource.Kind == "Pod" {
pod := corev1.Pod{}
err = decoder.Decode(&pod)
if err != nil {
return err
}
workload, err := NewGenericWorkloadFromPod(pod, pod)
if err != nil {
return err
}
resources.Controllers = append(resources.Controllers, workload)
resources.Controllers = append(resources.Controllers, NewGenericWorkloadFromPod(pod))
} else {
newController, err := GetWorkloadFromBytes(contentBytes)
if err != nil || newController == nil {
yamlNode := make(map[string]interface{})
err = yaml.Unmarshal(contentBytes, &yamlNode)
if err != nil {
logrus.Errorf("Invalid YAML: %s", string(contents))
return err
}
resources.Controllers = append(resources.Controllers, *newController)
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 := NewGenericWorkloadFromPod(pod)
newController.Kind = resource.Kind
resources.Controllers = append(resources.Controllers, newController)
}
return err
}
+3 -32
View File
@@ -1,15 +1,11 @@
package kube
import (
"bytes"
"context"
"io/ioutil"
"testing"
"time"
"github.com/fairwindsops/polaris/test"
"github.com/stretchr/testify/assert"
corev1 "k8s.io/api/core/v1"
)
func TestGetResourcesFromPath(t *testing.T) {
@@ -48,7 +44,7 @@ func TestGetMultipleResourceFromSingleFile(t *testing.T) {
assert.Equal(t, 0, len(resources.Nodes), "Should not have any nodes")
assert.Equal(t, 1, len(resources.Controllers), "Should have one controller")
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")
@@ -61,37 +57,12 @@ func TestGetMultipleResourceFromBadFile(t *testing.T) {
assert.NotEqual(t, nil, err, "CreateResource From Path should fail with bad yaml")
}
func TestAddResourcesFromReader(t *testing.T) {
contents, err := ioutil.ReadFile("./test_files/test_2/multi.yaml")
assert.NoError(t, err)
reader := bytes.NewBuffer(contents)
resources := &ResourceProvider{
ServerVersion: "unknown",
SourceType: "Path",
SourceName: "-",
Nodes: []corev1.Node{},
Namespaces: []corev1.Namespace{},
Controllers: []GenericWorkload{},
}
err = addResourcesFromReader(reader, resources)
assert.NoError(t, err)
assert.Equal(t, 0, len(resources.Nodes), "Should not have any nodes")
assert.Equal(t, 1, len(resources.Controllers), "Should have one controller")
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)
assert.Equal(t, "polaris-2", resources.Namespaces[1].ObjectMeta.Name)
}
func TestGetResourceFromAPI(t *testing.T) {
k8s, dynamicInterface := test.SetupTestAPI()
k8s = test.SetupAddControllers(context.Background(), k8s, "test")
k8s = test.SetupAddControllers(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(context.Background(), k8s, "test", &dynamicInterface)
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")
-11
View File
@@ -1,11 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: test-service
spec:
ports:
- port: 8080
protocol: TCP
targetPort: 8080
selector:
app: test
+69 -156
View File
@@ -1,180 +1,93 @@
package kube
import (
"bytes"
"context"
"encoding/json"
"fmt"
"github.com/fairwindsops/controller-utils/pkg/controller"
"github.com/sirupsen/logrus"
"gopkg.in/yaml.v3"
kubeAPICoreV1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/meta"
kubeAPIMetaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
k8sYaml "k8s.io/apimachinery/pkg/util/yaml"
"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
OriginalObjectJSON []byte
}
// NewGenericWorkloadFromUnstructured creates a workload from an unstructured.Unstructured
func NewGenericWorkloadFromUnstructured(kind string, unst *unstructured.Unstructured) (GenericWorkload, error) {
workload := GenericWorkload{
Kind: kind,
}
objMeta, err := meta.Accessor(unst)
if err != nil {
return workload, err
}
workload.ObjectMeta = objMeta
b, err := json.Marshal(unst)
if err != nil {
return workload, err
}
workload.OriginalObjectJSON = b
m := make(map[string]interface{})
err = json.Unmarshal(b, &m)
if err != nil {
return workload, err
}
podSpecMap := controller.GetPodSpec(m)
b, err = json.Marshal(podSpecMap)
if err != nil {
return workload, err
}
podSpecObject := kubeAPICoreV1.PodSpec{}
err = json.Unmarshal(b, &podSpecObject)
if err != nil {
return workload, err
}
workload.PodSpec = podSpecObject
return workload, nil
Kind string
PodSpec kubeAPICoreV1.PodSpec
ObjectMeta kubeAPIMetaV1.Object
}
// NewGenericWorkloadFromPod builds a new workload for a given Pod without looking at parents
func NewGenericWorkloadFromPod(podResource kubeAPICoreV1.Pod, originalObject interface{}) (GenericWorkload, error) {
workload := GenericWorkload{
Kind: "Pod",
PodSpec: podResource.Spec,
ObjectMeta: podResource.ObjectMeta.GetObjectMeta(),
}
if originalObject != nil {
bytes, err := json.Marshal(originalObject)
if err != nil {
return workload, err
}
workload.OriginalObjectJSON = bytes
}
return workload, nil
func NewGenericWorkloadFromPod(originalResource kubeAPICoreV1.Pod) GenericWorkload {
workload := GenericWorkload{}
workload.PodSpec = originalResource.Spec
workload.ObjectMeta = originalResource.ObjectMeta.GetObjectMeta()
workload.Kind = "Pod"
return workload
}
// NewGenericWorkload builds a new workload for a given Pod
func NewGenericWorkload(ctx context.Context, podResource kubeAPICoreV1.Pod, dynamicClient *dynamic.Interface, restMapper *meta.RESTMapper, objectCache map[string]unstructured.Unstructured) (GenericWorkload, error) {
workload, err := newGenericWorkload(ctx, podResource, dynamicClient, restMapper, objectCache)
if err != nil {
return workload, err
func NewGenericWorkload(originalResource kubeAPICoreV1.Pod, dynamicClientPointer *dynamic.Interface, restMapperPointer *meta.RESTMapper, objectCache map[string]kubeAPIMetaV1.Object) GenericWorkload {
workload := NewGenericWorkloadFromPod(originalResource)
dynamicClient := *dynamicClientPointer
restMapper := *restMapperPointer
// If an owner exists then set the name to the workload.
// This allows us to handle CRDs creating Workloads or DeploymentConfigs in OpenShift.
owners := workload.ObjectMeta.GetOwnerReferences()
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
key := fmt.Sprintf("%s/%s/%s", firstOwner.Kind, workload.ObjectMeta.GetNamespace(), firstOwner.Name)
objMeta, ok := objectCache[key]
if ok {
workload.ObjectMeta = objMeta
owners = objMeta.GetOwnerReferences()
continue
}
fqKind := schema.FromAPIVersionAndKind(firstOwner.APIVersion, firstOwner.Kind)
mapping, err := restMapper.RESTMapping(fqKind.GroupKind(), fqKind.Version)
if err != nil {
logrus.Warnf("Error retrieving mapping %s of API %s and Kind %s because of error: %v ", firstOwner.Name, firstOwner.APIVersion, firstOwner.Kind, err)
return workload
}
err = cacheAllObjectsOfKind(dynamicClient, mapping.Resource, objectCache)
if err != nil {
logrus.Warnf("Error getting objects of Kind %s %v", firstOwner.Kind, err)
return workload
}
objMeta, ok = objectCache[key]
if ok {
workload.ObjectMeta = objMeta
owners = objMeta.GetOwnerReferences()
continue
} else {
logrus.Errorf("Cache missed again %s", key)
}
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)
if err != nil {
logrus.Warnf("Error retrieving parent metadata %s of API %s and Kind %s because of error: %v ", firstOwner.Name, firstOwner.APIVersion, firstOwner.Kind, err)
return workload
}
workload.ObjectMeta = objMeta
objectCache[key] = objMeta
owners = parent.GetOwnerReferences()
}
if len(workload.OriginalObjectJSON) == 0 {
return NewGenericWorkloadFromPod(podResource, podResource)
}
return workload, err
}
func newGenericWorkload(ctx context.Context, podResource kubeAPICoreV1.Pod, dynamicClient *dynamic.Interface, restMapper *meta.RESTMapper, objectCache map[string]unstructured.Unstructured) (GenericWorkload, error) {
workload, err := NewGenericWorkloadFromPod(podResource, nil)
if err != nil {
return workload, err
}
objMeta, err := meta.Accessor(&podResource)
if err != nil {
return workload, err
}
controllerObject, err := controller.GetTopController(ctx, *dynamicClient, *restMapper, objMeta)
if err != nil {
return workload, err
}
bytes, err := json.Marshal(controllerObject)
if err != nil {
return workload, err
}
workload.OriginalObjectJSON = bytes
return workload, nil
}
func cacheAllObjectsOfKind(ctx context.Context, apiVersion, kind string, dynamicClient *dynamic.Interface, restMapper *meta.RESTMapper, objectCache map[string]unstructured.Unstructured) error {
fqKind := schema.FromAPIVersionAndKind(apiVersion, kind)
mapping, err := (*restMapper).RESTMapping(fqKind.GroupKind(), fqKind.Version)
if err != nil {
logrus.Warnf("Error retrieving mapping of API %s and Kind %s because of error: %v ", apiVersion, kind, err)
return err
}
objects, err := (*dynamicClient).Resource(mapping.Resource).Namespace("").List(ctx, kubeAPIMetaV1.ListOptions{})
if err != nil {
logrus.Warnf("Error retrieving parent object API %s and Kind %s because of error: %v ", mapping.Resource.Version, mapping.Resource.Resource, err)
return err
}
for idx, object := range objects.Items {
key := fmt.Sprintf("%s/%s/%s", object.GetKind(), object.GetNamespace(), object.GetName())
objectCache[key] = objects.Items[idx]
}
return nil
}
func getObject(ctx context.Context, namespace, kind, version, name string, dynamicClient *dynamic.Interface, restMapper *meta.RESTMapper) (*unstructured.Unstructured, error) {
fqKind := schema.ParseGroupKind(kind)
mapping, err := (*restMapper).RESTMapping(fqKind, version)
if err != nil {
return nil, err
}
object, err := (*dynamicClient).Resource(mapping.Resource).Namespace(namespace).Get(ctx, name, kubeAPIMetaV1.GetOptions{})
return object, err
}
// GetWorkloadFromBytes parses a GenericWorkload
func GetWorkloadFromBytes(contentBytes []byte) (*GenericWorkload, error) {
yamlNode := make(map[string]interface{})
err := yaml.Unmarshal(contentBytes, &yamlNode)
if err != nil {
logrus.Errorf("Invalid YAML: %s", string(contentBytes))
return nil, err
}
finalDoc := make(map[string]interface{})
finalDoc["metadata"] = yamlNode["metadata"]
finalDoc["apiVersion"] = "v1"
finalDoc["kind"] = "Pod"
podSpecObject := podspec.GetPodSpec(yamlNode)
if podSpecObject == nil {
return nil, nil
}
finalDoc["spec"] = podSpecObject
marshaledYaml, err := yaml.Marshal(finalDoc)
if err != nil {
logrus.Errorf("Could not marshal yaml: %v", err)
return nil, err
}
decoder := k8sYaml.NewYAMLOrJSONDecoder(bytes.NewReader(marshaledYaml), 1000)
pod := kubeAPICoreV1.Pod{}
err = decoder.Decode(&pod)
newController, err := NewGenericWorkloadFromPod(pod, yamlNode)
if err != nil {
return nil, err
}
newController.Kind = yamlNode["kind"].(string)
return &newController, nil
return workload
}
+5 -7
View File
@@ -15,8 +15,6 @@
package validator
import (
"context"
"github.com/fairwindsops/polaris/pkg/config"
"github.com/fairwindsops/polaris/pkg/kube"
@@ -24,8 +22,8 @@ import (
)
// ValidateContainer validates a single container from a given controller
func ValidateContainer(ctx context.Context, conf *config.Configuration, controller kube.GenericWorkload, container *corev1.Container, isInit bool) (ContainerResult, error) {
results, err := applyContainerSchemaChecks(ctx, conf, controller, container, isInit)
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
}
@@ -39,18 +37,18 @@ func ValidateContainer(ctx context.Context, conf *config.Configuration, controll
}
// ValidateAllContainers validates both init and regular containers
func ValidateAllContainers(ctx context.Context, conf *config.Configuration, controller kube.GenericWorkload) ([]ContainerResult, error) {
func ValidateAllContainers(conf *config.Configuration, controller kube.GenericWorkload) ([]ContainerResult, error) {
results := []ContainerResult{}
pod := controller.PodSpec
for _, container := range pod.InitContainers {
result, err := ValidateContainer(ctx, conf, controller, &container, true)
result, err := ValidateContainer(conf, controller, &container, true)
if err != nil {
return nil, err
}
results = append(results, result)
}
for _, container := range pod.Containers {
result, err := ValidateContainer(ctx, conf, controller, &container, false)
result, err := ValidateContainer(conf, controller, &container, false)
if err != nil {
return nil, err
}
+115 -120
View File
@@ -15,7 +15,6 @@
package validator
import (
"context"
"fmt"
"testing"
@@ -31,16 +30,16 @@ var resourceConfMinimal = `---
checks:
cpuRequestsMissing: warning
memoryRequestsMissing: warning
cpuLimitsMissing: danger
memoryLimitsMissing: danger
cpuLimitsMissing: error
memoryLimitsMissing: error
`
var resourceConfExemptions = `---
checks:
cpuRequestsMissing: warning
memoryRequestsMissing: warning
cpuLimitsMissing: danger
memoryLimitsMissing: danger
cpuLimitsMissing: error
memoryLimitsMissing: error
exemptions:
- rules:
- cpuRequestsMissing
@@ -51,25 +50,24 @@ exemptions:
- foo
`
func getEmptyWorkload(t *testing.T, name string) kube.GenericWorkload {
workload, err := kube.NewGenericWorkloadFromPod(corev1.Pod{
func getEmptyWorkload(name string) kube.GenericWorkload {
workload := kube.NewGenericWorkloadFromPod(corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
}, nil)
assert.NoError(t, err)
})
return workload
}
func testValidate(t *testing.T, container *corev1.Container, resourceConf *string, controllerName string, expectedDangers []ResultMessage, expectedWarnings []ResultMessage, expectedSuccesses []ResultMessage) {
testValidateWithWorkload(t, container, resourceConf, getEmptyWorkload(t, controllerName), expectedDangers, expectedWarnings, expectedSuccesses)
func testValidate(t *testing.T, container *corev1.Container, resourceConf *string, controllerName string, expectedErrors []ResultMessage, expectedWarnings []ResultMessage, expectedSuccesses []ResultMessage) {
testValidateWithWorkload(t, container, resourceConf, getEmptyWorkload(controllerName), expectedErrors, expectedWarnings, expectedSuccesses)
}
func testValidateWithWorkload(t *testing.T, container *corev1.Container, resourceConf *string, workload kube.GenericWorkload, expectedDangers []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(context.Background(), &parsedConf, workload, container, false)
results, err := applyContainerSchemaChecks(&parsedConf, workload, container, false)
if err != nil {
panic(err)
}
@@ -78,8 +76,8 @@ func testValidateWithWorkload(t *testing.T, container *corev1.Container, resourc
assert.Equal(t, uint(len(expectedWarnings)), summary.Warnings)
assert.ElementsMatch(t, expectedWarnings, results.GetWarnings())
assert.Equal(t, uint(len(expectedDangers)), summary.Dangers)
assert.ElementsMatch(t, expectedDangers, results.GetDangers())
assert.Equal(t, uint(len(expectedErrors)), summary.Errors)
assert.ElementsMatch(t, expectedErrors, results.GetErrors())
assert.Equal(t, uint(len(expectedSuccesses)), summary.Successes)
assert.ElementsMatch(t, expectedSuccesses, results.GetSuccesses())
@@ -90,11 +88,11 @@ func TestValidateResourcesEmptyConfig(t *testing.T) {
Name: "Empty",
}
results, err := applyContainerSchemaChecks(context.Background(), &conf.Configuration{}, getEmptyWorkload(t, ""), container, false)
results, err := applyContainerSchemaChecks(&conf.Configuration{}, getEmptyWorkload(""), container, false)
if err != nil {
panic(err)
}
assert.Equal(t, uint(0), results.GetSummary().Dangers)
assert.Equal(t, uint(0), results.GetSummary().Errors)
}
func TestValidateResourcesEmptyContainer(t *testing.T) {
@@ -119,18 +117,18 @@ func TestValidateResourcesEmptyContainer(t *testing.T) {
},
}
expectedDangers := []ResultMessage{
expectedErrors := []ResultMessage{
{
ID: "cpuLimitsMissing",
Success: false,
Severity: "danger",
Severity: "error",
Message: "CPU limits should be set",
Category: "Resources",
},
{
ID: "memoryLimitsMissing",
Success: false,
Severity: "danger",
Severity: "error",
Message: "Memory limits should be set",
Category: "Resources",
},
@@ -138,7 +136,7 @@ func TestValidateResourcesEmptyContainer(t *testing.T) {
expectedSuccesses := []ResultMessage{}
testValidate(t, &container, &resourceConfMinimal, "foo", expectedDangers, expectedWarnings, expectedSuccesses)
testValidate(t, &container, &resourceConfMinimal, "foo", expectedErrors, expectedWarnings, expectedSuccesses)
}
func TestValidateHealthChecks(t *testing.T) {
@@ -150,7 +148,7 @@ func TestValidateHealthChecks(t *testing.T) {
"livenessProbeMissing": conf.SeverityIgnore,
}
p3 := map[string]conf.Severity{
"readinessProbeMissing": conf.SeverityDanger,
"readinessProbeMissing": conf.SeverityError,
"livenessProbeMissing": conf.SeverityWarning,
}
@@ -163,7 +161,7 @@ func TestValidateHealthChecks(t *testing.T) {
}
l := ResultMessage{ID: "livenessProbeMissing", Success: false, Severity: "warning", Message: "Liveness probe should be configured", Category: "Health Checks"}
r := ResultMessage{ID: "readinessProbeMissing", Success: false, Severity: "danger", Message: "Readiness probe should be configured", Category: "Health Checks"}
r := ResultMessage{ID: "readinessProbeMissing", Success: false, Severity: "error", Message: "Readiness probe should be configured", Category: "Health Checks"}
f1 := []ResultMessage{}
f2 := []ResultMessage{r}
w1 := []ResultMessage{l}
@@ -173,21 +171,21 @@ func TestValidateHealthChecks(t *testing.T) {
probes map[string]conf.Severity
container *corev1.Container
isInit bool
dangers *[]ResultMessage
errors *[]ResultMessage
warnings *[]ResultMessage
}{
{name: "probes not configured", probes: p1, container: emptyContainer, dangers: &f1},
{name: "probes not required", probes: p2, container: emptyContainer, dangers: &f1},
{name: "probes required & configured", probes: p3, container: goodContainer, dangers: &f1},
{name: "probes required, not configured, but init", probes: p3, container: emptyContainer, isInit: true, dangers: &f1},
{name: "probes required & not configured", probes: p3, container: emptyContainer, dangers: &f2, warnings: &w1},
{name: "probes configured, but not required", probes: p2, container: goodContainer, dangers: &f1},
{name: "probes not configured", probes: p1, container: emptyContainer, errors: &f1},
{name: "probes not required", probes: p2, container: emptyContainer, errors: &f1},
{name: "probes required & configured", probes: p3, container: goodContainer, errors: &f1},
{name: "probes required, not configured, but init", probes: p3, container: emptyContainer, isInit: true, errors: &f1},
{name: "probes required & not configured", probes: p3, container: emptyContainer, errors: &f2, warnings: &w1},
{name: "probes configured, but not required", probes: p2, container: goodContainer, errors: &f1},
}
for idx, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
controller := getEmptyWorkload(t, "")
results, err := applyContainerSchemaChecks(context.Background(), &conf.Configuration{Checks: tt.probes}, controller, tt.container, tt.isInit)
controller := getEmptyWorkload("")
results, err := applyContainerSchemaChecks(&conf.Configuration{Checks: tt.probes}, controller, tt.container, tt.isInit)
if err != nil {
panic(err)
}
@@ -199,10 +197,10 @@ func TestValidateHealthChecks(t *testing.T) {
assert.ElementsMatch(t, warnings, *tt.warnings, message)
}
if tt.dangers != nil {
dangers := results.GetDangers()
assert.Len(t, dangers, len(*tt.dangers), message)
assert.ElementsMatch(t, dangers, *tt.dangers, message)
if tt.errors != nil {
errors := results.GetErrors()
assert.Len(t, errors, len(*tt.errors), message)
assert.ElementsMatch(t, errors, *tt.errors, message)
}
})
}
@@ -211,12 +209,12 @@ func TestValidateHealthChecks(t *testing.T) {
func TestValidateImage(t *testing.T) {
emptyConf := make(map[string]conf.Severity)
standardConf := map[string]conf.Severity{
"tagNotSpecified": conf.SeverityDanger,
"tagNotSpecified": conf.SeverityError,
"pullPolicyNotAlways": conf.SeverityIgnore,
}
strongConf := map[string]conf.Severity{
"tagNotSpecified": conf.SeverityDanger,
"pullPolicyNotAlways": conf.SeverityDanger,
"tagNotSpecified": conf.SeverityError,
"pullPolicyNotAlways": conf.SeverityError,
}
emptyContainer := &corev1.Container{}
@@ -244,7 +242,7 @@ func TestValidateImage(t *testing.T) {
ID: "tagNotSpecified",
Message: "Image tag should be specified",
Success: false,
Severity: "danger",
Severity: "error",
Category: "Images",
}},
},
@@ -256,7 +254,7 @@ func TestValidateImage(t *testing.T) {
ID: "tagNotSpecified",
Message: "Image tag should be specified",
Success: false,
Severity: "danger",
Severity: "error",
Category: "Images",
}},
},
@@ -268,7 +266,7 @@ func TestValidateImage(t *testing.T) {
ID: "tagNotSpecified",
Message: "Image tag should be specified",
Success: false,
Severity: "danger",
Severity: "error",
Category: "Images",
}},
},
@@ -280,13 +278,13 @@ func TestValidateImage(t *testing.T) {
ID: "pullPolicyNotAlways",
Message: "Image pull policy should be \"Always\"",
Success: false,
Severity: "danger",
Severity: "error",
Category: "Images",
}, {
ID: "tagNotSpecified",
Message: "Image tag should be specified",
Success: false,
Severity: "danger",
Severity: "error",
Category: "Images",
}},
},
@@ -300,14 +298,14 @@ func TestValidateImage(t *testing.T) {
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
controller := getEmptyWorkload(t, "")
results, err := applyContainerSchemaChecks(context.Background(), &conf.Configuration{Checks: tt.image}, controller, tt.container, false)
controller := getEmptyWorkload("")
results, err := applyContainerSchemaChecks(&conf.Configuration{Checks: tt.image}, controller, tt.container, false)
if err != nil {
panic(err)
}
dangers := results.GetDangers()
assert.Len(t, dangers, len(tt.expected))
assert.ElementsMatch(t, dangers, tt.expected)
errors := results.GetErrors()
assert.Len(t, errors, len(tt.expected))
assert.ElementsMatch(t, errors, tt.expected)
})
}
}
@@ -319,7 +317,7 @@ func TestValidateNetworking(t *testing.T) {
"hostPortSet": conf.SeverityWarning,
}
strongConf := map[string]conf.Severity{
"hostPortSet": conf.SeverityDanger,
"hostPortSet": conf.SeverityError,
}
emptyContainer := &corev1.Container{Name: ""}
@@ -409,7 +407,7 @@ func TestValidateNetworking(t *testing.T) {
ID: "hostPortSet",
Message: "Host port should not be configured",
Success: false,
Severity: "danger",
Severity: "error",
Category: "Networking",
}},
},
@@ -417,8 +415,8 @@ func TestValidateNetworking(t *testing.T) {
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
controller := getEmptyWorkload(t, "")
results, err := applyContainerSchemaChecks(context.Background(), &conf.Configuration{Checks: tt.networkConf}, controller, tt.container, false)
controller := getEmptyWorkload("")
results, err := applyContainerSchemaChecks(&conf.Configuration{Checks: tt.networkConf}, controller, tt.container, false)
if err != nil {
panic(err)
}
@@ -440,19 +438,19 @@ func TestValidateSecurity(t *testing.T) {
emptyConf := map[string]conf.Severity{}
standardConf := map[string]conf.Severity{
"runAsRootAllowed": conf.SeverityWarning,
"runAsPrivileged": conf.SeverityDanger,
"runAsPrivileged": conf.SeverityError,
"notReadOnlyRootFilesystem": conf.SeverityWarning,
"privilegeEscalationAllowed": conf.SeverityDanger,
"dangerousCapabilities": conf.SeverityDanger,
"privilegeEscalationAllowed": conf.SeverityError,
"dangerousCapabilities": conf.SeverityError,
"insecureCapabilities": conf.SeverityWarning,
}
strongConf := map[string]conf.Severity{
"runAsRootAllowed": conf.SeverityDanger,
"runAsPrivileged": conf.SeverityDanger,
"notReadOnlyRootFilesystem": conf.SeverityDanger,
"privilegeEscalationAllowed": conf.SeverityDanger,
"dangerousCapabilities": conf.SeverityDanger,
"insecureCapabilities": conf.SeverityDanger,
"runAsRootAllowed": conf.SeverityError,
"runAsPrivileged": conf.SeverityError,
"notReadOnlyRootFilesystem": conf.SeverityError,
"privilegeEscalationAllowed": conf.SeverityError,
"dangerousCapabilities": conf.SeverityError,
"insecureCapabilities": conf.SeverityError,
}
emptyContainer := &corev1.Container{Name: ""}
@@ -551,13 +549,13 @@ func TestValidateSecurity(t *testing.T) {
ID: "runAsPrivileged",
Message: "Not running as privileged",
Success: true,
Severity: "danger",
Severity: "error",
Category: "Security",
}, {
ID: "privilegeEscalationAllowed",
Message: "Privilege escalation not allowed",
Success: true,
Severity: "danger",
Severity: "error",
Category: "Security",
}, {
ID: "insecureCapabilities",
@@ -569,7 +567,7 @@ func TestValidateSecurity(t *testing.T) {
ID: "dangerousCapabilities",
Message: "Container does not have any dangerous capabilities",
Success: true,
Severity: "danger",
Severity: "error",
Category: "Security",
}},
},
@@ -582,19 +580,19 @@ func TestValidateSecurity(t *testing.T) {
ID: "dangerousCapabilities",
Message: "Container should not have dangerous capabilities",
Success: false,
Severity: "danger",
Severity: "error",
Category: "Security",
}, {
ID: "privilegeEscalationAllowed",
Message: "Privilege escalation should not be allowed",
Success: false,
Severity: "danger",
Severity: "error",
Category: "Security",
}, {
ID: "runAsPrivileged",
Message: "Should not be running as privileged",
Success: false,
Severity: "danger",
Severity: "error",
Category: "Security",
}, {
ID: "insecureCapabilities",
@@ -625,19 +623,19 @@ func TestValidateSecurity(t *testing.T) {
ID: "dangerousCapabilities",
Message: "Container should not have dangerous capabilities",
Success: false,
Severity: "danger",
Severity: "error",
Category: "Security",
}, {
ID: "privilegeEscalationAllowed",
Message: "Privilege escalation should not be allowed",
Success: false,
Severity: "danger",
Severity: "error",
Category: "Security",
}, {
ID: "runAsPrivileged",
Message: "Should not be running as privileged",
Success: false,
Severity: "danger",
Severity: "error",
Category: "Security",
}, {
ID: "insecureCapabilities",
@@ -668,7 +666,7 @@ func TestValidateSecurity(t *testing.T) {
ID: "dangerousCapabilities",
Message: "Container should not have dangerous capabilities",
Success: false,
Severity: "danger",
Severity: "error",
Category: "Security",
}, {
ID: "insecureCapabilities",
@@ -680,13 +678,13 @@ func TestValidateSecurity(t *testing.T) {
ID: "privilegeEscalationAllowed",
Message: "Privilege escalation should not be allowed",
Success: false,
Severity: "danger",
Severity: "error",
Category: "Security",
}, {
ID: "runAsPrivileged",
Message: "Should not be running as privileged",
Success: false,
Severity: "danger",
Severity: "error",
Category: "Security",
}, {
ID: "runAsRootAllowed",
@@ -723,19 +721,19 @@ func TestValidateSecurity(t *testing.T) {
ID: "runAsPrivileged",
Message: "Not running as privileged",
Success: true,
Severity: "danger",
Severity: "error",
Category: "Security",
}, {
ID: "privilegeEscalationAllowed",
Message: "Privilege escalation not allowed",
Success: true,
Severity: "danger",
Severity: "error",
Category: "Security",
}, {
ID: "dangerousCapabilities",
Message: "Container does not have any dangerous capabilities",
Success: true,
Severity: "danger",
Severity: "error",
Category: "Security",
}, {
ID: "insecureCapabilities",
@@ -754,37 +752,37 @@ func TestValidateSecurity(t *testing.T) {
ID: "dangerousCapabilities",
Message: "Container does not have any dangerous capabilities",
Success: true,
Severity: "danger",
Severity: "error",
Category: "Security",
}, {
ID: "insecureCapabilities",
Message: "Container does not have any insecure capabilities",
Success: true,
Severity: "danger",
Severity: "error",
Category: "Security",
}, {
ID: "runAsRootAllowed",
Message: "Is not allowed to run as root",
Success: true,
Severity: "danger",
Severity: "error",
Category: "Security",
}, {
ID: "notReadOnlyRootFilesystem",
Message: "Filesystem is read only",
Success: true,
Severity: "danger",
Severity: "error",
Category: "Security",
}, {
ID: "runAsPrivileged",
Message: "Not running as privileged",
Success: true,
Severity: "danger",
Severity: "error",
Category: "Security",
}, {
ID: "privilegeEscalationAllowed",
Message: "Privilege escalation not allowed",
Success: true,
Severity: "danger",
Severity: "error",
Category: "Security",
}},
},
@@ -797,37 +795,37 @@ func TestValidateSecurity(t *testing.T) {
ID: "runAsRootAllowed",
Message: "Is not allowed to run as root",
Success: true,
Severity: "danger",
Severity: "error",
Category: "Security",
}, {
ID: "notReadOnlyRootFilesystem",
Message: "Filesystem is read only",
Success: true,
Severity: "danger",
Severity: "error",
Category: "Security",
}, {
ID: "runAsPrivileged",
Message: "Not running as privileged",
Success: true,
Severity: "danger",
Severity: "error",
Category: "Security",
}, {
ID: "privilegeEscalationAllowed",
Message: "Privilege escalation not allowed",
Success: true,
Severity: "danger",
Severity: "error",
Category: "Security",
}, {
ID: "dangerousCapabilities",
Message: "Container does not have any dangerous capabilities",
Success: true,
Severity: "danger",
Severity: "error",
Category: "Security",
}, {
ID: "insecureCapabilities",
Message: "Container does not have any insecure capabilities",
Success: true,
Severity: "danger",
Severity: "error",
Category: "Security",
}},
},
@@ -840,37 +838,37 @@ func TestValidateSecurity(t *testing.T) {
ID: "runAsRootAllowed",
Message: "Is not allowed to run as root",
Success: true,
Severity: "danger",
Severity: "error",
Category: "Security",
}, {
ID: "notReadOnlyRootFilesystem",
Message: "Filesystem is read only",
Success: true,
Severity: "danger",
Severity: "error",
Category: "Security",
}, {
ID: "runAsPrivileged",
Message: "Not running as privileged",
Success: true,
Severity: "danger",
Severity: "error",
Category: "Security",
}, {
ID: "privilegeEscalationAllowed",
Message: "Privilege escalation not allowed",
Success: true,
Severity: "danger",
Severity: "error",
Category: "Security",
}, {
ID: "dangerousCapabilities",
Message: "Container does not have any dangerous capabilities",
Success: true,
Severity: "danger",
Severity: "error",
Category: "Security",
}, {
ID: "insecureCapabilities",
Message: "Container does not have any insecure capabilities",
Success: true,
Severity: "danger",
Severity: "error",
Category: "Security",
}},
},
@@ -883,37 +881,37 @@ func TestValidateSecurity(t *testing.T) {
ID: "runAsRootAllowed",
Message: "Is not allowed to run as root",
Success: true,
Severity: "danger",
Severity: "error",
Category: "Security",
}, {
ID: "notReadOnlyRootFilesystem",
Message: "Filesystem is read only",
Success: true,
Severity: "danger",
Severity: "error",
Category: "Security",
}, {
ID: "runAsPrivileged",
Message: "Not running as privileged",
Success: true,
Severity: "danger",
Severity: "error",
Category: "Security",
}, {
ID: "privilegeEscalationAllowed",
Message: "Privilege escalation not allowed",
Success: true,
Severity: "danger",
Severity: "error",
Category: "Security",
}, {
ID: "dangerousCapabilities",
Message: "Container does not have any dangerous capabilities",
Success: true,
Severity: "danger",
Severity: "error",
Category: "Security",
}, {
ID: "insecureCapabilities",
Message: "Container does not have any insecure capabilities",
Success: true,
Severity: "danger",
Severity: "error",
Category: "Security",
}},
},
@@ -921,9 +919,8 @@ func TestValidateSecurity(t *testing.T) {
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
workload, err := kube.NewGenericWorkloadFromPod(corev1.Pod{Spec: *tt.pod}, nil)
assert.NoError(t, err)
results, err := applyContainerSchemaChecks(context.Background(), &conf.Configuration{Checks: tt.securityConf}, workload, tt.container, false)
workload := kube.NewGenericWorkloadFromPod(corev1.Pod{Spec: *tt.pod})
results, err := applyContainerSchemaChecks(&conf.Configuration{Checks: tt.securityConf}, workload, tt.container, false)
if err != nil {
panic(err)
}
@@ -1066,9 +1063,8 @@ func TestValidateRunAsRoot(t *testing.T) {
}
for idx, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
workload, err := kube.NewGenericWorkloadFromPod(corev1.Pod{Spec: *tt.pod}, nil)
assert.NoError(t, err)
results, err := applyContainerSchemaChecks(context.Background(), &config, workload, tt.container, false)
workload := kube.NewGenericWorkloadFromPod(corev1.Pod{Spec: *tt.pod})
results, err := applyContainerSchemaChecks(&config, workload, tt.container, false)
if err != nil {
panic(err)
}
@@ -1090,10 +1086,10 @@ func TestValidateResourcesExemption(t *testing.T) {
}
expectedWarnings := []ResultMessage{}
expectedDangers := []ResultMessage{}
expectedErrors := []ResultMessage{}
expectedSuccesses := []ResultMessage{}
testValidate(t, &container, &resourceConfExemptions, "foo", expectedDangers, expectedWarnings, expectedSuccesses)
testValidate(t, &container, &resourceConfExemptions, "foo", expectedErrors, expectedWarnings, expectedSuccesses)
expectedWarnings = []ResultMessage{
{
@@ -1112,18 +1108,18 @@ func TestValidateResourcesExemption(t *testing.T) {
},
}
expectedDangers = []ResultMessage{
expectedErrors = []ResultMessage{
{
ID: "cpuLimitsMissing",
Success: false,
Severity: "danger",
Severity: "error",
Message: "CPU limits should be set",
Category: "Resources",
},
{
ID: "memoryLimitsMissing",
Success: false,
Severity: "danger",
Severity: "error",
Message: "Memory limits should be set",
Category: "Resources",
},
@@ -1131,7 +1127,7 @@ func TestValidateResourcesExemption(t *testing.T) {
disallowExemptionsConf := resourceConfExemptions + "\ndisallowExemptions: true"
testValidate(t, &container, &disallowExemptionsConf, "foo", expectedDangers, expectedWarnings, expectedSuccesses)
testValidate(t, &container, &disallowExemptionsConf, "foo", expectedErrors, expectedWarnings, expectedSuccesses)
}
func TestValidateResourcesEmptyContainerCPURequestsExempt(t *testing.T) {
@@ -1149,18 +1145,18 @@ func TestValidateResourcesEmptyContainerCPURequestsExempt(t *testing.T) {
},
}
expectedDangers := []ResultMessage{
expectedErrors := []ResultMessage{
{
ID: "cpuLimitsMissing",
Success: false,
Severity: "danger",
Severity: "error",
Message: "CPU limits should be set",
Category: "Resources",
},
{
ID: "memoryLimitsMissing",
Success: false,
Severity: "danger",
Severity: "error",
Message: "Memory limits should be set",
Category: "Resources",
},
@@ -1168,7 +1164,7 @@ func TestValidateResourcesEmptyContainerCPURequestsExempt(t *testing.T) {
expectedSuccesses := []ResultMessage{}
workload, err := kube.NewGenericWorkloadFromPod(corev1.Pod{
workload := kube.NewGenericWorkloadFromPod(corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
Annotations: map[string]string{
@@ -1176,7 +1172,6 @@ func TestValidateResourcesEmptyContainerCPURequestsExempt(t *testing.T) {
"polaris.fairwinds.com/memoryRequestsMissing-exempt": "truthy", // Don't actually exempt this controller from memoryRequestsMissing
},
},
}, nil)
assert.NoError(t, err)
testValidateWithWorkload(t, &container, &resourceConfMinimal, workload, expectedDangers, expectedWarnings, expectedSuccesses)
})
testValidateWithWorkload(t, &container, &resourceConfMinimal, workload, expectedErrors, expectedWarnings, expectedSuccesses)
}
+5 -12
View File
@@ -15,7 +15,6 @@
package validator
import (
"context"
"strings"
"github.com/sirupsen/logrus"
@@ -27,22 +26,16 @@ import (
const exemptionAnnotationKey = "polaris.fairwinds.com/exempt"
// ValidateController validates a single controller, returns a ControllerResult.
func ValidateController(ctx context.Context, conf *conf.Configuration, controller kube.GenericWorkload) (ControllerResult, error) {
podResult, err := ValidatePod(ctx, conf, controller)
func ValidateController(conf *conf.Configuration, controller kube.GenericWorkload) (ControllerResult, error) {
podResult, err := ValidatePod(conf, controller)
if err != nil {
return ControllerResult{}, err
}
controllerResult, err := applyControllerSchemaChecks(ctx, conf, controller)
if err != nil {
return ControllerResult{}, err
}
result := ControllerResult{
Kind: controller.Kind,
Name: controller.ObjectMeta.GetName(),
Namespace: controller.ObjectMeta.GetNamespace(),
Results: controllerResult,
Results: ResultSet{},
PodResult: podResult,
}
@@ -51,7 +44,7 @@ func ValidateController(ctx context.Context, conf *conf.Configuration, controlle
// ValidateControllers validates that each deployment conforms to the Polaris config,
// builds a list of ResourceResults organized by namespace.
func ValidateControllers(ctx context.Context, config *conf.Configuration, kubeResources *kube.ResourceProvider) ([]ControllerResult, error) {
func ValidateControllers(config *conf.Configuration, kubeResources *kube.ResourceProvider) ([]ControllerResult, error) {
controllersToAudit := kubeResources.Controllers
results := []ControllerResult{}
@@ -59,7 +52,7 @@ func ValidateControllers(ctx context.Context, config *conf.Configuration, kubeRe
if !config.DisallowExemptions && hasExemptionAnnotation(controller) {
continue
}
result, err := ValidateController(ctx, config, controller)
result, err := ValidateController(config, controller)
if err != nil {
logrus.Warn("An error occured validating controller:", err)
return nil, err
+23 -67
View File
@@ -15,7 +15,6 @@
package validator
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
@@ -29,25 +28,24 @@ import (
func TestValidateController(t *testing.T) {
c := conf.Configuration{
Checks: map[string]conf.Severity{
"hostIPCSet": conf.SeverityDanger,
"hostPIDSet": conf.SeverityDanger,
"hostIPCSet": conf.SeverityError,
"hostPIDSet": conf.SeverityError,
},
}
deployment, err := kube.NewGenericWorkloadFromPod(test.MockPod(), nil)
assert.NoError(t, err)
deployment := kube.NewGenericWorkloadFromPod(test.MockPod())
deployment.Kind = "Deployment"
expectedSum := CountSummary{
Successes: uint(2),
Warnings: uint(0),
Dangers: uint(0),
Errors: uint(0),
}
expectedResults := ResultSet{
"hostIPCSet": {ID: "hostIPCSet", Message: "Host IPC is not configured", Success: true, Severity: "danger", Category: "Security"},
"hostPIDSet": {ID: "hostPIDSet", Message: "Host PID is not configured", Success: true, Severity: "danger", Category: "Security"},
"hostIPCSet": {ID: "hostIPCSet", Message: "Host IPC is not configured", Success: true, Severity: "error", Category: "Security"},
"hostPIDSet": {ID: "hostPIDSet", Message: "Host PID is not configured", Success: true, Severity: "error", Category: "Security"},
}
actualResult, err := ValidateController(context.Background(), &c, deployment)
actualResult, err := ValidateController(&c, deployment)
if err != nil {
panic(err)
}
@@ -58,66 +56,27 @@ func TestValidateController(t *testing.T) {
assert.EqualValues(t, expectedResults, actualResult.PodResult.Results)
}
func TestControllerLevelChecks(t *testing.T) {
c := conf.Configuration{
Checks: map[string]conf.Severity{
"multipleReplicasForDeployment": conf.SeverityDanger,
},
}
resources, err := kube.CreateResourceProviderFromPath("../kube/test_files/test_1")
assert.Equal(t, nil, err, "Error should be nil")
assert.Equal(t, 8, len(resources.Controllers), "Should have eight controllers")
expectedSum := CountSummary{
Successes: uint(0),
Warnings: uint(0),
Dangers: uint(1),
}
expectedResults := ResultSet{
"multipleReplicasForDeployment": {ID: "multipleReplicasForDeployment", Message: "Only one replica is scheduled", Success: false, Severity: "danger", Category: "Reliability"},
}
for _, controller := range resources.Controllers {
if controller.Kind == "Deployment" && controller.ObjectMeta.GetName() == "test-deployment" {
actualResult, err := ValidateController(context.Background(), &c, controller)
if err != nil {
panic(err)
}
assert.Equal(t, "Deployment", actualResult.Kind)
assert.Equal(t, 1, len(actualResult.Results), "should be equal")
assert.EqualValues(t, expectedSum, actualResult.GetSummary())
assert.EqualValues(t, expectedResults, actualResult.Results)
}
}
}
func TestSkipHealthChecks(t *testing.T) {
c := conf.Configuration{
Checks: map[string]conf.Severity{
"readinessProbeMissing": conf.SeverityDanger,
"readinessProbeMissing": conf.SeverityError,
"livenessProbeMissing": conf.SeverityWarning,
},
}
pod := test.MockPod()
pod.Spec.InitContainers = []corev1.Container{test.MockContainer("test")}
deployment, err := kube.NewGenericWorkloadFromPod(pod, nil)
assert.NoError(t, err)
deployment := kube.NewGenericWorkloadFromPod(pod)
deployment.Kind = "Deployment"
expectedSum := CountSummary{
Successes: uint(0),
Warnings: uint(1),
Dangers: uint(1),
Errors: uint(1),
}
expectedResults := ResultSet{
"readinessProbeMissing": {ID: "readinessProbeMissing", Message: "Readiness probe should be configured", Success: false, Severity: "danger", Category: "Health Checks"},
"readinessProbeMissing": {ID: "readinessProbeMissing", Message: "Readiness probe should be configured", Success: false, Severity: "error", Category: "Health Checks"},
"livenessProbeMissing": {ID: "livenessProbeMissing", Message: "Liveness probe should be configured", Success: false, Severity: "warning", Category: "Health Checks"},
}
actualResult, err := ValidateController(context.Background(), &c, deployment)
actualResult, err := ValidateController(&c, deployment)
if err != nil {
panic(err)
}
@@ -127,16 +86,15 @@ func TestSkipHealthChecks(t *testing.T) {
assert.EqualValues(t, ResultSet{}, actualResult.PodResult.ContainerResults[0].Results)
assert.EqualValues(t, expectedResults, actualResult.PodResult.ContainerResults[1].Results)
job, err := kube.NewGenericWorkloadFromPod(test.MockPod(), nil)
assert.NoError(t, err)
job := kube.NewGenericWorkloadFromPod(test.MockPod())
job.Kind = "Job"
expectedSum = CountSummary{
Successes: uint(0),
Warnings: uint(0),
Dangers: uint(0),
Errors: uint(0),
}
expectedResults = ResultSet{}
actualResult, err = ValidateController(context.Background(), &c, job)
actualResult, err = ValidateController(&c, job)
if err != nil {
panic(err)
}
@@ -145,16 +103,15 @@ func TestSkipHealthChecks(t *testing.T) {
assert.EqualValues(t, expectedSum, actualResult.GetSummary())
assert.EqualValues(t, expectedResults, actualResult.PodResult.ContainerResults[0].Results)
cronjob, err := kube.NewGenericWorkloadFromPod(test.MockPod(), nil)
assert.NoError(t, err)
cronjob := kube.NewGenericWorkloadFromPod(test.MockPod())
cronjob.Kind = "CronJob"
expectedSum = CountSummary{
Successes: uint(0),
Warnings: uint(0),
Dangers: uint(0),
Errors: uint(0),
}
expectedResults = ResultSet{}
actualResult, err = ValidateController(context.Background(), &c, cronjob)
actualResult, err = ValidateController(&c, cronjob)
if err != nil {
panic(err)
}
@@ -167,13 +124,12 @@ func TestSkipHealthChecks(t *testing.T) {
func TestControllerExemptions(t *testing.T) {
c := conf.Configuration{
Checks: map[string]conf.Severity{
"readinessProbeMissing": conf.SeverityDanger,
"readinessProbeMissing": conf.SeverityError,
"livenessProbeMissing": conf.SeverityWarning,
},
}
pod := test.MockPod()
workload, err := kube.NewGenericWorkloadFromPod(pod, nil)
assert.NoError(t, err)
workload := kube.NewGenericWorkloadFromPod(pod)
workload.Kind = "Deployment"
resources := &kube.ResourceProvider{
Controllers: []kube.GenericWorkload{workload},
@@ -182,9 +138,9 @@ func TestControllerExemptions(t *testing.T) {
expectedSum := CountSummary{
Successes: uint(0),
Warnings: uint(1),
Dangers: uint(1),
Errors: uint(1),
}
actualResults, err := ValidateControllers(context.Background(), &c, resources)
actualResults, err := ValidateControllers(&c, resources)
if err != nil {
panic(err)
}
@@ -195,7 +151,7 @@ func TestControllerExemptions(t *testing.T) {
resources.Controllers[0].ObjectMeta.SetAnnotations(map[string]string{
exemptionAnnotationKey: "true",
})
actualResults, err = ValidateControllers(context.Background(), &c, resources)
actualResults, err = ValidateControllers(&c, resources)
if err != nil {
panic(err)
}
+2 -3
View File
@@ -2,7 +2,6 @@ package validator
import (
"bytes"
"context"
"fmt"
"io"
"io/ioutil"
@@ -17,13 +16,13 @@ import (
)
// RunAudit runs a full Polaris audit and returns an AuditData object
func RunAudit(ctx context.Context, config conf.Configuration, kubeResources *kube.ResourceProvider) (AuditData, error) {
func RunAudit(config conf.Configuration, kubeResources *kube.ResourceProvider) (AuditData, error) {
displayName := config.DisplayName
if displayName == "" {
displayName = kubeResources.SourceName
}
results, err := ValidateControllers(ctx, &config, kubeResources)
results, err := ValidateControllers(&config, kubeResources)
if err != nil {
return AuditData{}, err
}
+6 -8
View File
@@ -1,7 +1,6 @@
package validator
import (
"context"
"testing"
conf "github.com/fairwindsops/polaris/pkg/config"
@@ -12,16 +11,16 @@ import (
func TestGetTemplateData(t *testing.T) {
k8s, dynamicClient := test.SetupTestAPI()
k8s = test.SetupAddControllers(context.Background(), k8s, "test")
k8s = test.SetupAddExtraControllerVersions(context.Background(), k8s, "test-extra")
k8s = test.SetupAddControllers(k8s, "test")
k8s = test.SetupAddExtraControllerVersions(k8s, "test-extra")
// TODO figure out how to mock out dynamic client.
// and add in pods for all controllers to fill out tests.
resources, err := kube.CreateResourceProviderFromAPI(context.Background(), k8s, "test", &dynamicClient)
resources, err := kube.CreateResourceProviderFromAPI(k8s, "test", &dynamicClient)
assert.Equal(t, err, nil, "error should be nil")
c := conf.Configuration{
Checks: map[string]conf.Severity{
"readinessProbeMissing": conf.SeverityDanger,
"readinessProbeMissing": conf.SeverityError,
"livenessProbeMissing": conf.SeverityWarning,
},
}
@@ -29,11 +28,10 @@ func TestGetTemplateData(t *testing.T) {
sum := CountSummary{
Successes: uint(0),
Warnings: uint(1),
Dangers: uint(1),
Errors: uint(1),
}
actualAudit, err := RunAudit(context.Background(), c, resources)
actualAudit, err := RunAudit(c, resources)
assert.Equal(t, err, nil, "error should be nil")
assert.EqualValues(t, sum, actualAudit.GetSummary())
+3 -5
View File
@@ -15,15 +15,13 @@
package validator
import (
"context"
"github.com/fairwindsops/polaris/pkg/config"
"github.com/fairwindsops/polaris/pkg/kube"
)
// ValidatePod validates that each pod conforms to the Polaris config, returns a ResourceResult.
func ValidatePod(ctx context.Context, conf *config.Configuration, controller kube.GenericWorkload) (PodResult, error) {
podResults, err := applyPodSchemaChecks(ctx, conf, controller)
func ValidatePod(conf *config.Configuration, controller kube.GenericWorkload) (PodResult, error) {
podResults, err := applyPodSchemaChecks(conf, controller)
if err != nil {
return PodResult{}, err
}
@@ -32,7 +30,7 @@ func ValidatePod(ctx context.Context, conf *config.Configuration, controller kub
ContainerResults: []ContainerResult{},
}
pRes.ContainerResults, err = ValidateAllContainers(ctx, conf, controller)
pRes.ContainerResults, err = ValidateAllContainers(conf, controller)
if err != nil {
return pRes, err
}
+49 -50
View File
@@ -15,7 +15,6 @@
package validator
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
@@ -29,31 +28,31 @@ import (
func TestValidatePod(t *testing.T) {
c := conf.Configuration{
Checks: map[string]conf.Severity{
"hostIPCSet": conf.SeverityDanger,
"hostPIDSet": conf.SeverityDanger,
"hostIPCSet": conf.SeverityError,
"hostPIDSet": conf.SeverityError,
"hostNetworkSet": conf.SeverityWarning,
"hostPortSet": conf.SeverityDanger,
"hostPortSet": conf.SeverityError,
},
}
k8s, _ := test.SetupTestAPI()
k8s = test.SetupAddControllers(context.Background(), k8s, "test")
k8s = test.SetupAddControllers(k8s, "test")
p := test.MockPod()
deployment, err := kube.NewGenericWorkloadFromPod(p, nil)
assert.NoError(t, err)
deployment := kube.NewGenericWorkloadFromPod(p)
expectedSum := CountSummary{
Successes: uint(4),
Warnings: uint(0),
Dangers: uint(0),
Errors: uint(0),
}
expectedResults := ResultSet{
"hostIPCSet": {ID: "hostIPCSet", Message: "Host IPC is not configured", Success: true, Severity: "danger", Category: "Security"},
"hostIPCSet": {ID: "hostIPCSet", Message: "Host IPC is not configured", Success: true, Severity: "error", Category: "Security"},
"hostNetworkSet": {ID: "hostNetworkSet", Message: "Host network is not configured", Success: true, Severity: "warning", Category: "Networking"},
"hostPIDSet": {ID: "hostPIDSet", Message: "Host PID is not configured", Success: true, Severity: "danger", Category: "Security"},
"hostPIDSet": {ID: "hostPIDSet", Message: "Host PID is not configured", Success: true, Severity: "error", Category: "Security"},
}
actualPodResult, err := ValidatePod(context.Background(), &c, deployment)
actualPodResult, err := ValidatePod(&c, deployment)
if err != nil {
panic(err)
}
@@ -66,31 +65,31 @@ func TestValidatePod(t *testing.T) {
func TestInvalidIPCPod(t *testing.T) {
c := conf.Configuration{
Checks: map[string]conf.Severity{
"hostIPCSet": conf.SeverityDanger,
"hostPIDSet": conf.SeverityDanger,
"hostIPCSet": conf.SeverityError,
"hostPIDSet": conf.SeverityError,
"hostNetworkSet": conf.SeverityWarning,
"hostPortSet": conf.SeverityDanger,
"hostPortSet": conf.SeverityError,
},
}
k8s, _ := test.SetupTestAPI()
k8s = test.SetupAddControllers(context.Background(), k8s, "test")
k8s = test.SetupAddControllers(k8s, "test")
p := test.MockPod()
p.Spec.HostIPC = true
workload, err := kube.NewGenericWorkloadFromPod(p, nil)
assert.NoError(t, err)
workload := kube.NewGenericWorkloadFromPod(p)
expectedSum := CountSummary{
Successes: uint(3),
Warnings: uint(0),
Dangers: uint(1),
Errors: uint(1),
}
expectedResults := ResultSet{
"hostIPCSet": {ID: "hostIPCSet", Message: "Host IPC should not be configured", Success: false, Severity: "danger", Category: "Security"},
"hostIPCSet": {ID: "hostIPCSet", Message: "Host IPC should not be configured", Success: false, Severity: "error", Category: "Security"},
"hostNetworkSet": {ID: "hostNetworkSet", Message: "Host network is not configured", Success: true, Severity: "warning", Category: "Networking"},
"hostPIDSet": {ID: "hostPIDSet", Message: "Host PID is not configured", Success: true, Severity: "danger", Category: "Security"},
"hostPIDSet": {ID: "hostPIDSet", Message: "Host PID is not configured", Success: true, Severity: "error", Category: "Security"},
}
actualPodResult, err := ValidatePod(context.Background(), &c, workload)
actualPodResult, err := ValidatePod(&c, workload)
if err != nil {
panic(err)
}
@@ -104,31 +103,31 @@ func TestInvalidNeworkPod(t *testing.T) {
c := conf.Configuration{
Checks: map[string]conf.Severity{
"hostNetworkSet": conf.SeverityWarning,
"hostPortSet": conf.SeverityDanger,
"hostIPCSet": conf.SeverityDanger,
"hostPIDSet": conf.SeverityDanger,
"hostPortSet": conf.SeverityError,
"hostIPCSet": conf.SeverityError,
"hostPIDSet": conf.SeverityError,
},
}
k8s, _ := test.SetupTestAPI()
k8s = test.SetupAddControllers(context.Background(), k8s, "test")
k8s = test.SetupAddControllers(k8s, "test")
p := test.MockPod()
p.Spec.HostNetwork = true
workload, err := kube.NewGenericWorkloadFromPod(p, nil)
assert.NoError(t, err)
workload := kube.NewGenericWorkloadFromPod(p)
expectedSum := CountSummary{
Successes: uint(3),
Warnings: uint(1),
Dangers: uint(0),
Errors: uint(0),
}
expectedResults := ResultSet{
"hostNetworkSet": {ID: "hostNetworkSet", Message: "Host network should not be configured", Success: false, Severity: "warning", Category: "Networking"},
"hostIPCSet": {ID: "hostIPCSet", Message: "Host IPC is not configured", Success: true, Severity: "danger", Category: "Security"},
"hostPIDSet": {ID: "hostPIDSet", Message: "Host PID is not configured", Success: true, Severity: "danger", Category: "Security"},
"hostIPCSet": {ID: "hostIPCSet", Message: "Host IPC is not configured", Success: true, Severity: "error", Category: "Security"},
"hostPIDSet": {ID: "hostPIDSet", Message: "Host PID is not configured", Success: true, Severity: "error", Category: "Security"},
}
actualPodResult, err := ValidatePod(context.Background(), &c, workload)
actualPodResult, err := ValidatePod(&c, workload)
if err != nil {
panic(err)
}
@@ -141,32 +140,32 @@ func TestInvalidNeworkPod(t *testing.T) {
func TestInvalidPIDPod(t *testing.T) {
c := conf.Configuration{
Checks: map[string]conf.Severity{
"hostIPCSet": conf.SeverityDanger,
"hostPIDSet": conf.SeverityDanger,
"hostIPCSet": conf.SeverityError,
"hostPIDSet": conf.SeverityError,
"hostNetworkSet": conf.SeverityWarning,
"hostPortSet": conf.SeverityDanger,
"hostPortSet": conf.SeverityError,
},
}
k8s, _ := test.SetupTestAPI()
k8s = test.SetupAddControllers(context.Background(), k8s, "test")
k8s = test.SetupAddControllers(k8s, "test")
p := test.MockPod()
p.Spec.HostPID = true
workload, err := kube.NewGenericWorkloadFromPod(p, nil)
assert.NoError(t, err)
workload := kube.NewGenericWorkloadFromPod(p)
expectedSum := CountSummary{
Successes: uint(3),
Warnings: uint(0),
Dangers: uint(1),
Errors: uint(1),
}
expectedResults := ResultSet{
"hostPIDSet": {ID: "hostPIDSet", Message: "Host PID should not be configured", Success: false, Severity: "danger", Category: "Security"},
"hostIPCSet": {ID: "hostIPCSet", Message: "Host IPC is not configured", Success: true, Severity: "danger", Category: "Security"},
"hostPIDSet": {ID: "hostPIDSet", Message: "Host PID should not be configured", Success: false, Severity: "error", Category: "Security"},
"hostIPCSet": {ID: "hostIPCSet", Message: "Host IPC is not configured", Success: true, Severity: "error", Category: "Security"},
"hostNetworkSet": {ID: "hostNetworkSet", Message: "Host network is not configured", Success: true, Severity: "warning", Category: "Networking"},
}
actualPodResult, err := ValidatePod(context.Background(), &c, workload)
actualPodResult, err := ValidatePod(&c, workload)
if err != nil {
panic(err)
}
@@ -179,10 +178,10 @@ func TestInvalidPIDPod(t *testing.T) {
func TestExemption(t *testing.T) {
c := conf.Configuration{
Checks: map[string]conf.Severity{
"hostIPCSet": conf.SeverityDanger,
"hostIPCSet": conf.SeverityError,
"hostNetworkSet": conf.SeverityWarning,
"hostPIDSet": conf.SeverityDanger,
"hostPortSet": conf.SeverityDanger,
"hostPIDSet": conf.SeverityError,
"hostPortSet": conf.SeverityError,
},
Exemptions: []conf.Exemption{
conf.Exemption{
@@ -193,25 +192,25 @@ func TestExemption(t *testing.T) {
}
k8s, _ := test.SetupTestAPI()
k8s = test.SetupAddControllers(context.Background(), k8s, "test")
k8s = test.SetupAddControllers(k8s, "test")
p := test.MockPod()
p.Spec.HostIPC = true
p.ObjectMeta = metav1.ObjectMeta{
Name: "foo",
}
workload, err := kube.NewGenericWorkloadFromPod(p, nil)
assert.NoError(t, err)
workload := kube.NewGenericWorkloadFromPod(p)
expectedSum := CountSummary{
Successes: uint(3),
Warnings: uint(0),
Dangers: uint(0),
Errors: uint(0),
}
expectedResults := ResultSet{
"hostNetworkSet": {ID: "hostNetworkSet", Message: "Host network is not configured", Success: true, Severity: "warning", Category: "Networking"},
"hostPIDSet": {ID: "hostPIDSet", Message: "Host PID is not configured", Success: true, Severity: "danger", Category: "Security"},
"hostPIDSet": {ID: "hostPIDSet", Message: "Host PID is not configured", Success: true, Severity: "error", Category: "Security"},
}
actualPodResult, err := ValidatePod(context.Background(), &c, workload)
actualPodResult, err := ValidatePod(&c, workload)
if err != nil {
panic(err)
}
+2 -31
View File
@@ -2,7 +2,6 @@ package validator
import (
"bytes"
"context"
"fmt"
"io"
"sort"
@@ -22,8 +21,6 @@ var (
// We explicitly set the order to avoid thrash in the
// tests as we migrate toward JSON schema
checkOrder = []string{
// Controller Checks
"multipleReplicasForDeployment",
// Pod checks
"hostIPCSet",
"hostPIDSet",
@@ -44,7 +41,6 @@ var (
"privilegeEscalationAllowed",
"dangerousCapabilities",
"insecureCapabilities",
"priorityClassNotSet",
}
)
@@ -114,7 +110,7 @@ func getExemptKey(checkID string) string {
return fmt.Sprintf("polaris.fairwinds.com/%s-exempt", checkID)
}
func applyPodSchemaChecks(ctx context.Context, conf *config.Configuration, controller kube.GenericWorkload) (ResultSet, error) {
func applyPodSchemaChecks(conf *config.Configuration, controller kube.GenericWorkload) (ResultSet, error) {
results := ResultSet{}
checkIDs := getSortedKeys(conf.Checks)
objectAnnotations := controller.ObjectMeta.GetAnnotations()
@@ -139,32 +135,7 @@ func applyPodSchemaChecks(ctx context.Context, conf *config.Configuration, contr
return results, nil
}
func applyControllerSchemaChecks(ctx context.Context, conf *config.Configuration, controller kube.GenericWorkload) (ResultSet, error) {
results := ResultSet{}
checkIDs := getSortedKeys(conf.Checks)
objectAnnotations := controller.ObjectMeta.GetAnnotations()
for _, checkID := range checkIDs {
exemptValue := objectAnnotations[getExemptKey(checkID)]
if strings.ToLower(exemptValue) == "true" {
continue
}
check, err := resolveCheck(conf, checkID, controller, config.TargetController, false)
if err != nil {
return nil, err
} else if check == nil {
continue
}
passes, err := check.CheckController(controller.OriginalObjectJSON)
if err != nil {
return nil, err
}
results[check.ID] = makeResult(conf, check, passes)
}
return results, nil
}
func applyContainerSchemaChecks(ctx context.Context, conf *config.Configuration, controller kube.GenericWorkload, 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.ObjectMeta.GetAnnotations()
+18 -19
View File
@@ -1,7 +1,6 @@
package validator
import (
"context"
"testing"
conf "github.com/fairwindsops/polaris/pkg/config"
@@ -13,7 +12,7 @@ import (
var customCheckExemptions = `
checks:
foo: danger
foo: error
customChecks:
foo:
successMessage: success!
@@ -33,7 +32,7 @@ exemptions:
var resourceConfRanges = `
checks:
memoryRequestsRange: danger
memoryRequestsRange: error
memoryLimitsRange: warning
customChecks:
memoryLimitsRange:
@@ -122,11 +121,11 @@ func TestValidateResourcesPartiallyValid(t *testing.T) {
},
}
expectedDangers := []ResultMessage{
expectedErrors := []ResultMessage{
{
ID: "memoryRequestsRange",
Success: false,
Severity: "danger",
Severity: "error",
Message: "Memory requests should be within the required range",
Category: "Resources",
},
@@ -134,28 +133,28 @@ func TestValidateResourcesPartiallyValid(t *testing.T) {
expectedSuccesses := []ResultMessage{}
testValidate(t, &container, &resourceConfRanges, "foo", expectedDangers, expectedWarnings, expectedSuccesses)
testValidate(t, &container, &resourceConfRanges, "foo", expectedErrors, expectedWarnings, expectedSuccesses)
}
func TestValidateResourcesInit(t *testing.T) {
emptyContainer := &corev1.Container{}
controller := getEmptyWorkload(t, "")
controller := getEmptyWorkload("")
parsedConf, err := conf.Parse([]byte(resourceConfRanges))
assert.NoError(t, err, "Expected no error when parsing config")
results, err := applyContainerSchemaChecks(context.Background(), &parsedConf, controller, emptyContainer, false)
results, err := applyContainerSchemaChecks(&parsedConf, controller, emptyContainer, false)
if err != nil {
panic(err)
}
assert.Equal(t, uint(1), results.GetSummary().Dangers)
assert.Equal(t, uint(1), results.GetSummary().Errors)
assert.Equal(t, uint(1), results.GetSummary().Warnings)
results, err = applyContainerSchemaChecks(context.Background(), &parsedConf, controller, emptyContainer, true)
results, err = applyContainerSchemaChecks(&parsedConf, controller, emptyContainer, true)
if err != nil {
panic(err)
}
assert.Equal(t, uint(0), results.GetSummary().Dangers)
assert.Equal(t, uint(0), results.GetSummary().Errors)
assert.Equal(t, uint(0), results.GetSummary().Warnings)
}
@@ -190,7 +189,7 @@ func TestValidateResourcesFullyValid(t *testing.T) {
{
ID: "memoryRequestsRange",
Success: true,
Severity: "danger",
Severity: "error",
Message: "Memory requests are within the required range",
Category: "Resources",
},
@@ -223,14 +222,14 @@ func TestValidateResourcesFullyValid(t *testing.T) {
{
ID: "cpuLimitsMissing",
Success: true,
Severity: "danger",
Severity: "error",
Message: "CPU limits are set",
Category: "Resources",
},
{
ID: "memoryLimitsMissing",
Success: true,
Severity: "danger",
Severity: "error",
Message: "Memory limits are set",
Category: "Resources",
},
@@ -246,18 +245,18 @@ func TestValidateCustomCheckExemptions(t *testing.T) {
}
expectedWarnings := []ResultMessage{}
expectedDangers := []ResultMessage{}
expectedErrors := []ResultMessage{}
expectedSuccesses := []ResultMessage{}
testValidate(t, &container, &customCheckExemptions, "exempt", expectedDangers, expectedWarnings, expectedSuccesses)
testValidate(t, &container, &customCheckExemptions, "exempt", expectedErrors, expectedWarnings, expectedSuccesses)
expectedDangers = []ResultMessage{
expectedErrors = []ResultMessage{
{
ID: "foo",
Success: false,
Severity: "danger",
Severity: "error",
Message: "fail!",
Category: "Security",
},
}
testValidate(t, &container, &customCheckExemptions, "notexempt", expectedDangers, expectedWarnings, expectedSuccesses)
testValidate(t, &container, &customCheckExemptions, "notexempt", expectedErrors, expectedWarnings, expectedSuccesses)
}
+8 -24
View File
@@ -1,9 +1,6 @@
package validator
import (
"sort"
"strings"
"github.com/fairwindsops/polaris/pkg/config"
)
@@ -11,7 +8,7 @@ import (
type CountSummary struct {
Successes uint
Warnings uint
Dangers uint
Errors uint
}
// CountSummaryByCategory is a map from category to CountSummary
@@ -19,7 +16,7 @@ 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.Dangers * 2)
total := (cs.Successes * 2) + cs.Warnings + (cs.Errors * 2)
if total == 0 {
return 0 // Prevent divide by 0.
}
@@ -30,7 +27,7 @@ func (cs CountSummary) GetScore() uint {
func (cs *CountSummary) AddSummary(other CountSummary) {
cs.Successes += other.Successes
cs.Warnings += other.Warnings
cs.Dangers += other.Dangers
cs.Errors += other.Errors
}
// AddResult adds a single result to the summary
@@ -39,7 +36,7 @@ func (cs *CountSummary) AddResult(result ResultMessage) {
if result.Severity == config.SeverityWarning {
cs.Warnings++
} else {
cs.Dangers++
cs.Errors++
}
} else {
cs.Successes++
@@ -146,19 +143,6 @@ func (a AuditData) GetResultsByNamespace() map[string][]*ControllerResult {
nsResults = append(nsResults, &a.Results[idx])
allResults[ctrlResult.Namespace] = nsResults
}
for ns := range allResults {
sort.SliceStable(allResults[ns], func(i, j int) bool {
kind := strings.Compare(allResults[ns][i].Kind, allResults[ns][j].Kind)
if kind != 0 {
return kind == -1
}
name := strings.Compare(allResults[ns][i].Name, allResults[ns][j].Name)
if name != 0 {
return name == -1
}
return true
})
}
return allResults
}
@@ -184,11 +168,11 @@ func (rs ResultSet) GetWarnings() []ResultMessage {
return warnings
}
// GetDangers returns the error messages in a result set
func (rs ResultSet) GetDangers() []ResultMessage {
// GetErrors returns the error messages in a result set
func (rs ResultSet) GetErrors() []ResultMessage {
errors := []ResultMessage{}
for _, msg := range rs {
if msg.Success == false && msg.Severity == config.SeverityDanger {
if msg.Success == false && msg.Severity == config.SeverityError {
errors = append(errors, msg)
}
}
@@ -198,7 +182,7 @@ func (rs ResultSet) GetDangers() []ResultMessage {
// GetSortedResults returns messages sorted as errors, then warnings, then successes
func (rs ResultSet) GetSortedResults() []ResultMessage {
messages := []ResultMessage{}
messages = append(messages, rs.GetDangers()...)
messages = append(messages, rs.GetErrors()...)
messages = append(messages, rs.GetWarnings()...)
messages = append(messages, rs.GetSuccesses()...)
return messages
+60 -62
View File
@@ -17,7 +17,6 @@ package webhook
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
@@ -25,25 +24,37 @@ import (
"github.com/fairwindsops/polaris/pkg/kube"
validator "github.com/fairwindsops/polaris/pkg/validator"
"github.com/fairwindsops/controller-utils/pkg/podspec"
"github.com/sirupsen/logrus"
admissionregistrationv1beta1 "k8s.io/api/admissionregistration/v1beta1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/webhook"
"sigs.k8s.io/controller-runtime/pkg/runtime/inject"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission/builder"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission/types"
)
// Validator validates k8s resources.
type Validator struct {
Client client.Client
decoder *admission.Decoder
client client.Client
decoder types.Decoder
Config config.Configuration
}
var _ inject.Client = &Validator{}
// InjectClient injects the client.
func (v *Validator) InjectClient(c client.Client) error {
v.client = c
return nil
}
var _ inject.Decoder = &Validator{}
// InjectDecoder injects the decoder.
func (v *Validator) InjectDecoder(d *admission.Decoder) error {
logrus.Info("Injecting decoder")
func (v *Validator) InjectDecoder(d types.Decoder) error {
v.decoder = d
return nil
}
@@ -51,61 +62,51 @@ func (v *Validator) InjectDecoder(d *admission.Decoder) error {
var _ admission.Handler = &Validator{}
// NewWebhook creates a validating admission webhook for the apiType.
func NewWebhook(mgr manager.Manager, validator Validator) {
path := "/validate"
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)
mgr.GetWebhookServer().Register(path, &webhook.Admission{Handler: &validator})
webhook, err := builder.NewWebhookBuilder().
Name(name).
Validating().
Path(path).
Operations(admissionregistrationv1beta1.Create, admissionregistrationv1beta1.Update).
WithManager(mgr).
ForType(apiType).
Handlers(&validator).
Build()
if err != nil {
return nil, err
}
return webhook, nil
}
// GetObjectFromRawRequest returns the pod object and the controller's object from the raw json bytes.
func GetObjectFromRawRequest(raw []byte) (corev1.Pod, interface{}, error) {
func (v *Validator) handleInternal(ctx context.Context, req types.Request) (*validator.PodResult, error) {
pod := corev1.Pod{}
var originalObject interface{}
decoded := map[string]interface{}{}
err := json.Unmarshal(raw, &decoded)
if err != nil {
return pod, originalObject, err
}
podMap := podspec.GetPodSpec(decoded)
if podMap == nil {
return pod, originalObject, errors.New("Object does not contain pods")
}
encoded, err := json.Marshal(podMap)
if err != nil {
return pod, originalObject, err
}
err = json.Unmarshal(encoded, &pod.Spec)
if err != nil {
return pod, originalObject, err
}
originalObject = decoded
return pod, originalObject, err
}
func (v *Validator) handleInternal(ctx context.Context, req admission.Request) (*validator.PodResult, error) {
pod := corev1.Pod{}
var originalObject interface{}
var err error
if req.AdmissionRequest.Kind.Kind == "Pod" {
err := v.decoder.Decode(req, &pod)
if err != nil {
return nil, err
}
if len(pod.ObjectMeta.OwnerReferences) > 0 {
logrus.Infof("Allowing owned pod %s/%s to pass through webhook", pod.ObjectMeta.Namespace, pod.ObjectMeta.Name)
return nil, nil
}
originalObject = pod
} else {
pod, originalObject, err = GetObjectFromRawRequest(req.Object.Raw)
}
controller, err := kube.NewGenericWorkloadFromPod(pod, originalObject)
if err != nil {
return nil, err
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.NewGenericWorkloadFromPod(pod)
controller.Kind = req.AdmissionRequest.Kind.Kind
controllerResult, err := validator.ValidateController(ctx, &v.Config, controller)
controllerResult, err := validator.ValidateController(&v.Config, controller)
if err != nil {
return nil, err
}
@@ -113,23 +114,20 @@ func (v *Validator) handleInternal(ctx context.Context, req admission.Request) (
}
// Handle for Validator to run validation checks.
func (v *Validator) Handle(ctx context.Context, req admission.Request) admission.Response {
logrus.Info("Starting request")
func (v *Validator) Handle(ctx context.Context, req types.Request) types.Response {
podResult, err := v.handleInternal(ctx, req)
if err != nil {
logrus.Errorf("Error validating request: %v", err)
return admission.Errored(http.StatusBadRequest, err)
return admission.ErrorResponse(http.StatusBadRequest, err)
}
allowed := true
reason := ""
if podResult != nil {
numDangers := podResult.GetSummary().Dangers
if numDangers > 0 {
allowed = false
reason = getFailureReason(*podResult)
}
logrus.Infof("%d validation errors found when validating %s", numDangers, podResult.Name)
numErrors := podResult.GetSummary().Errors
if numErrors > 0 {
allowed = false
reason = getFailureReason(*podResult)
}
logrus.Infof("%d validation errors found when validating %s", numErrors, podResult.Name)
return admission.ValidationResponse(allowed, reason)
}
@@ -137,14 +135,14 @@ func getFailureReason(podResult validator.PodResult) string {
reason := "\nPolaris prevented this deployment due to configuration problems:\n"
for _, message := range podResult.Results {
if !message.Success && message.Severity == config.SeverityDanger {
if !message.Success && message.Severity == config.SeverityError {
reason += fmt.Sprintf("- Pod: %s\n", message.Message)
}
}
for _, containerResult := range podResult.ContainerResults {
for _, message := range containerResult.Results {
if !message.Success && message.Severity == config.SeverityDanger {
if !message.Success && message.Severity == config.SeverityError {
reason += fmt.Sprintf("- Container %s: %s\n", containerResult.Name, message.Message)
}
}

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

+7 -5
View File
@@ -1,15 +1,17 @@
# /bin/bash
set -e
set -eo pipefail
helm template polaris $CHARTS_DIR/stable/polaris/ \
--namespace polaris \
helm template $CHARTS_DIR/stable/polaris/ \
--name polaris --namespace polaris \
--set templateOnly=true \
--set config="$(cat ./examples/config.yaml)" \
> deploy/dashboard.yaml
helm template polaris $CHARTS_DIR/stable/polaris/ \
--namespace polaris \
helm template $CHARTS_DIR/stable/polaris/ \
--name polaris --namespace polaris \
--set templateOnly=true \
--set webhook.enable=true \
--set dashboard.enable=false \
--set config="$(cat ./examples/config.yaml)" \
> deploy/webhook.yaml
@@ -1,11 +0,0 @@
apiVersion: v1
kind: Pod
metadata:
name: nginx
labels:
env: test
spec:
containers:
- name: nginx
image: nginx
imagePullPolicy: IfNotPresent
@@ -1,12 +0,0 @@
apiVersion: v1
kind: Pod
metadata:
name: nginx
labels:
env: test
spec:
containers:
- name: nginx
image: nginx
imagePullPolicy: IfNotPresent
priorityClassName: high-priority
-73
View File
@@ -1,73 +0,0 @@
package test
import (
"context"
"io/ioutil"
"path/filepath"
"runtime"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/fairwindsops/polaris/pkg/config"
"github.com/fairwindsops/polaris/pkg/kube"
"github.com/fairwindsops/polaris/pkg/validator"
)
var testCases = []testCase{}
type testCase struct {
check string
input []byte
failure bool
}
func init() {
_, baseDir, _, _ := runtime.Caller(0)
baseDir = filepath.Dir(baseDir) + "/checks"
dirs, err := ioutil.ReadDir(baseDir)
if err != nil {
panic(err)
}
for _, dir := range dirs {
check := dir.Name()
checkDir := baseDir + "/" + check
cases, err := ioutil.ReadDir(checkDir)
if err != nil {
panic(err)
}
for _, tc := range cases {
body, err := ioutil.ReadFile(checkDir + "/" + tc.Name())
if err != nil {
panic(err)
}
testCases = append(testCases, testCase{
check: check,
input: body,
failure: strings.Contains(tc.Name(), "failure"),
})
}
}
}
func TestChecks(t *testing.T) {
for _, tc := range testCases {
workload, err := kube.GetWorkloadFromBytes(tc.input)
assert.NoError(t, err)
c, err := config.Parse([]byte("checks:\n " + tc.check + ": danger"))
assert.NoError(t, err)
result, err := validator.ValidateController(context.Background(), &c, *workload)
assert.NoError(t, err)
summary := result.GetSummary()
if tc.failure {
message := "Check " + tc.check + " passed unexpectedly"
assert.Equal(t, uint(0), summary.Successes, message)
assert.Equal(t, uint(1), summary.Dangers, message)
} else {
message := "Check " + tc.check + " failed unexpectedly"
assert.Equal(t, uint(1), summary.Successes, message)
assert.Equal(t, uint(0), summary.Dangers, message)
}
}
}
+14 -17
View File
@@ -1,15 +1,12 @@
package test
import (
"context"
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"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/dynamic"
dynamicFake "k8s.io/client-go/dynamic/fake"
@@ -119,39 +116,39 @@ func SetupTestAPI() (kubernetes.Interface, dynamic.Interface) {
}
// SetupAddControllers creates mock controllers and adds them to the test clientset.
func SetupAddControllers(ctx context.Context, k kubernetes.Interface, namespace string) kubernetes.Interface {
func SetupAddControllers(k kubernetes.Interface, namespace string) kubernetes.Interface {
d1 := MockDeploy()
if _, err := k.AppsV1().Deployments(namespace).Create(ctx, &d1, metav1.CreateOptions{}); err != nil {
if _, err := k.AppsV1().Deployments(namespace).Create(&d1); err != nil {
panic(err)
}
s1 := MockStatefulSet()
if _, err := k.AppsV1().StatefulSets(namespace).Create(ctx, &s1, metav1.CreateOptions{}); err != nil {
if _, err := k.AppsV1().StatefulSets(namespace).Create(&s1); err != nil {
panic(err)
}
ds1 := MockDaemonSet()
if _, err := k.AppsV1().DaemonSets(namespace).Create(ctx, &ds1, metav1.CreateOptions{}); err != nil {
if _, err := k.AppsV1().DaemonSets(namespace).Create(&ds1); err != nil {
panic(err)
}
j1 := MockJob()
if _, err := k.BatchV1().Jobs(namespace).Create(ctx, &j1, metav1.CreateOptions{}); err != nil {
if _, err := k.BatchV1().Jobs(namespace).Create(&j1); err != nil {
panic(err)
}
cj1 := MockCronJob()
if _, err := k.BatchV1beta1().CronJobs(namespace).Create(ctx, &cj1, metav1.CreateOptions{}); err != nil {
if _, err := k.BatchV1beta1().CronJobs(namespace).Create(&cj1); err != nil {
panic(err)
}
rc1 := MockReplicationController()
if _, err := k.CoreV1().ReplicationControllers(namespace).Create(ctx, &rc1, metav1.CreateOptions{}); err != nil {
if _, err := k.CoreV1().ReplicationControllers(namespace).Create(&rc1); err != nil {
panic(err)
}
p1 := MockNakedPod()
if _, err := k.CoreV1().Pods(namespace).Create(ctx, &p1, metav1.CreateOptions{}); err != nil {
if _, err := k.CoreV1().Pods(namespace).Create(&p1); err != nil {
panic(err)
}
@@ -159,7 +156,7 @@ func SetupAddControllers(ctx context.Context, k kubernetes.Interface, namespace
}
// SetupAddExtraControllerVersions creates mock controllers and adds them to the test clientset.
func SetupAddExtraControllerVersions(ctx context.Context, k kubernetes.Interface, namespace string) kubernetes.Interface {
func SetupAddExtraControllerVersions(k kubernetes.Interface, namespace string) kubernetes.Interface {
p := MockPod()
dv1b1 := appsv1beta1.Deployment{
@@ -167,7 +164,7 @@ func SetupAddExtraControllerVersions(ctx context.Context, k kubernetes.Interface
Template: corev1.PodTemplateSpec{Spec: p.Spec},
},
}
if _, err := k.AppsV1beta1().Deployments(namespace).Create(ctx, &dv1b1, metav1.CreateOptions{}); err != nil {
if _, err := k.AppsV1beta1().Deployments(namespace).Create(&dv1b1); err != nil {
panic(err)
}
@@ -176,7 +173,7 @@ func SetupAddExtraControllerVersions(ctx context.Context, k kubernetes.Interface
Template: corev1.PodTemplateSpec{Spec: p.Spec},
},
}
if _, err := k.AppsV1beta2().Deployments(namespace).Create(ctx, &dv1b2, metav1.CreateOptions{}); err != nil {
if _, err := k.AppsV1beta2().Deployments(namespace).Create(&dv1b2); err != nil {
panic(err)
}
@@ -185,7 +182,7 @@ func SetupAddExtraControllerVersions(ctx context.Context, k kubernetes.Interface
Template: corev1.PodTemplateSpec{Spec: p.Spec},
},
}
if _, err := k.AppsV1beta1().StatefulSets(namespace).Create(ctx, &ssv1b1, metav1.CreateOptions{}); err != nil {
if _, err := k.AppsV1beta1().StatefulSets(namespace).Create(&ssv1b1); err != nil {
panic(err)
}
@@ -194,7 +191,7 @@ func SetupAddExtraControllerVersions(ctx context.Context, k kubernetes.Interface
Template: corev1.PodTemplateSpec{Spec: p.Spec},
},
}
if _, err := k.AppsV1beta2().StatefulSets(namespace).Create(ctx, &ssv1b2, metav1.CreateOptions{}); err != nil {
if _, err := k.AppsV1beta2().StatefulSets(namespace).Create(&ssv1b2); err != nil {
panic(err)
}
@@ -203,7 +200,7 @@ func SetupAddExtraControllerVersions(ctx context.Context, k kubernetes.Interface
Template: corev1.PodTemplateSpec{Spec: p.Spec},
},
}
if _, err := k.AppsV1beta2().DaemonSets(namespace).Create(ctx, &dsv1b2, metav1.CreateOptions{}); err != nil {
if _, err := k.AppsV1beta2().DaemonSets(namespace).Create(&dsv1b2); err != nil {
panic(err)
}
return k
@@ -5,7 +5,7 @@ metadata:
labels:
app: nginx
spec:
replicas: 1
replicas: 3
selector:
matchLabels:
app: nginx
@@ -21,9 +21,9 @@ spec:
- containerPort: 80
securityContext:
allowPrivilegeEscalation: true
privileged: false
privileged: false
readOnlyRootFilesystem: true
runAsNonRoot: true
capabilities:
drop:
- ALL
- ALL
+6 -25
View File
@@ -2,7 +2,9 @@
set -e
#sed is replacing the polaris version with this commit sha so we are testing exactly this verison.
sed -r "s|'(quay.io/fairwinds/polaris:).+'|'\1${CIRCLE_SHA1}'|" ./deploy/webhook.yaml > ./deploy/webhook-test.yaml
sed -ri "s|'(quay.io/fairwinds/polaris:).+'|'\1${CIRCLE_SHA1}'|" ./deploy/webhook.yaml
# TODO: remove this after 1.0 is released
sed -i "s/--webhook/webhook/" ./deploy/webhook.yaml
# Testing to ensure that the webhook starts up, allows a correct deployment to pass,
# and prevents a incorrectly formatted deployment.
@@ -10,13 +12,8 @@ function check_webhook_is_ready() {
# Get the epoch time in one minute from now
local timeout_epoch
# Reset another 4 minutes to wait for webhook
timeout_epoch=$(date -d "+4 minutes" +%s)
while ! kubectl get csr | grep -E "polaris-webhook.polaris"; do
check_timeout "${timeout_epoch}"
echo -n "."
done
# Reset another 2 minutes to wait for webhook
timeout_epoch=$(date -d "+2 minutes" +%s)
# loop until this fails (desired condition is we cannot apply this yaml doc, which means the webhook is working
echo "Waiting for webhook to be ready"
@@ -25,8 +22,6 @@ function check_webhook_is_ready() {
echo -n "."
done
check_timeout "${timeout_epoch}"
echo "Webhook started!"
}
@@ -57,16 +52,11 @@ function clean_up() {
function grab_logs() {
kubectl -n polaris get pods -oyaml -l app=polaris
kubectl -n polaris describe pods -l app=polaris
kubectl -n polaris logs -l app=polaris -c webhook-certificate-generator
kubectl -n polaris logs -l app=polaris
}
# Install a bad deployment
kubectl create ns scale-test
kubectl apply -n scale-test -f ./test/webhook_cases/failing_test.deployment.yaml
# Install the webhook
kubectl apply -f ./deploy/webhook-test.yaml &> /dev/null
kubectl apply -f ./deploy/webhook.yaml &> /dev/null
# wait for the webhook to come online
@@ -96,15 +86,6 @@ for filename in test/webhook_cases/failing_test.*.yaml; do
fi
done
kubectl -n scale-test scale deployment nginx-deployment --replicas=2
sleep 5
kubectl get po -n scale-test
pod_count=$(kubectl get po -n scale-test -oname | wc -l)
if [ $pod_count != 2 ]; then
ALL_TESTS_PASSED=0
echo "Existing deployment was unable to scale after webhook installed: found $pod_count pods"
fi
clean_up
#Verify that all the tests passed.