mirror of
https://github.com/FairwindsOps/polaris.git
synced 2026-08-23 22:26:34 +00:00
Compare commits
99
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
567e16d056 | ||
|
|
f6e6f7ce2f | ||
|
|
9cce9fcec7 | ||
|
|
f05d3caa0b | ||
|
|
3c1533790f | ||
|
|
20746518bc | ||
|
|
75808b1dc1 | ||
|
|
7c9f01639b | ||
|
|
6cb9999864 | ||
|
|
a621249787 | ||
|
|
679f135b62 | ||
|
|
5c456ddb86 | ||
|
|
c1c07bcea2 | ||
|
|
c703aa4039 | ||
|
|
1051bc5922 | ||
|
|
155299a8d5 | ||
|
|
f0b63e3cb6 | ||
|
|
479ab1dae6 | ||
|
|
b5058cd906 | ||
|
|
c1115e556b | ||
|
|
617d3d28b5 | ||
|
|
582a631014 | ||
|
|
f4c2ae451f | ||
|
|
b76b2c3df4 | ||
|
|
0bbf43146c | ||
|
|
c33452bba0 | ||
|
|
300c8d9d94 | ||
|
|
7b1f046586 | ||
|
|
4e290e192d | ||
|
|
8d562f2490 | ||
|
|
47150155a0 | ||
|
|
b3d323d742 | ||
|
|
6060cf243f | ||
|
|
e044a540ed | ||
|
|
f6a4bffe88 | ||
|
|
343e24886d | ||
|
|
997d4bfb2b | ||
|
|
9ded3269c6 | ||
|
|
ca65a446bd | ||
|
|
4e9aa21cea | ||
|
|
ed014ecf8e | ||
|
|
2e11c04664 | ||
|
|
80662bd203 | ||
|
|
5705f81911 | ||
|
|
b4e3d40f4b | ||
|
|
cea0ca7fa8 | ||
|
|
69bed16e4f | ||
|
|
5b173cf4a7 | ||
|
|
0a0720a26c | ||
|
|
fa3504c350 | ||
|
|
4020000d68 | ||
|
|
ab7eeccfb0 | ||
|
|
78a16fa0ea | ||
|
|
b26c4be8d6 | ||
|
|
aada33f0e9 | ||
|
|
7b88c48a6e | ||
|
|
ded260ee19 | ||
|
|
3e9c270ac7 | ||
|
|
bf6e41566d | ||
|
|
ededabf00f | ||
|
|
3df9d35c57 | ||
|
|
a90c3b0f12 | ||
|
|
d19b0769b4 | ||
|
|
97f25cd7d1 | ||
|
|
03331147ca | ||
|
|
d44138bead | ||
|
|
eb21b030c2 | ||
|
|
3061756a55 | ||
|
|
2964280976 | ||
|
|
f657071a7f | ||
|
|
2cdc4775e0 | ||
|
|
af5b86e28c | ||
|
|
1021f6b1d7 | ||
|
|
0111b7ec2b | ||
|
|
33524757aa | ||
|
|
eea4a8e2c3 | ||
|
|
fb98d3112d | ||
|
|
97c81ccf60 | ||
|
|
e9b79fbdd4 | ||
|
|
217e3563d6 | ||
|
|
deb33bb05f | ||
|
|
e605eef755 | ||
|
|
c304036f97 | ||
|
|
c1061b50e0 | ||
|
|
3b07496a4d | ||
|
|
3eb2b6e2c9 | ||
|
|
5f5da069fd | ||
|
|
ceeffb97a0 | ||
|
|
27faab919c | ||
|
|
f4fc7c1b5d | ||
|
|
54d796b1f3 | ||
|
|
9065e3b2ae | ||
|
|
bd14ab8bc1 | ||
|
|
2ac6a2b540 | ||
|
|
9d81c393de | ||
|
|
b557786325 | ||
|
|
5572bb8118 | ||
|
|
ac9412920f | ||
|
|
d50d9c81f8 |
+10
-11
@@ -27,24 +27,22 @@ references:
|
||||
sudo apt-get install -yqq jq git
|
||||
|
||||
echo "Installing KIND"
|
||||
curl -sLO https://github.com/kubernetes-sigs/kind/releases/download/0.2.1/kind-linux-amd64
|
||||
curl -sLO https://github.com/kubernetes-sigs/kind/releases/download/v0.8.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.12.7/bin/linux/amd64/kubectl
|
||||
curl -sLO https://storage.googleapis.com/kubernetes-release/release/v1.18.6/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
|
||||
kind create cluster --wait=90s --image kindest/node:v1.15.11
|
||||
docker ps -a
|
||||
|
||||
echo "Setting up kubecfg"
|
||||
cp $(kind get kubeconfig-path --name=kind) ~/.kube/config
|
||||
kubectl version
|
||||
|
||||
# Test scripts
|
||||
@@ -85,8 +83,8 @@ references:
|
||||
run:
|
||||
name: Install GoReleaser
|
||||
command: |
|
||||
curl -fsSLo goreleaser.deb https://github.com/goreleaser/goreleaser/releases/download/v0.94.0/goreleaser_amd64.deb
|
||||
echo "8dbad6683d6fc9367e637e6eed8e01a0d63c9660 goreleaser.deb" | sha1sum -c
|
||||
curl -fsSLo goreleaser.deb https://github.com/goreleaser/goreleaser/releases/download/v0.131.1/goreleaser_amd64.deb
|
||||
echo "640790dcbfa864f26de4c26c2d491f293a64525c8c6641c5bbdec7136b38977e goreleaser.deb" | sha256sum -c -
|
||||
sudo dpkg -i goreleaser.deb
|
||||
rm goreleaser.deb
|
||||
|
||||
@@ -118,7 +116,7 @@ references:
|
||||
jobs:
|
||||
build:
|
||||
docker:
|
||||
- image: quay.io/reactiveops/ci-images:v8.0-stretch
|
||||
- image: quay.io/reactiveops/ci-images:v11.0-stretch
|
||||
steps:
|
||||
- checkout
|
||||
- setup_remote_docker
|
||||
@@ -127,7 +125,7 @@ jobs:
|
||||
|
||||
push:
|
||||
docker:
|
||||
- image: quay.io/reactiveops/ci-images:v8.0-stretch
|
||||
- image: quay.io/reactiveops/ci-images:v11.0-stretch
|
||||
steps:
|
||||
- checkout
|
||||
- setup_remote_docker
|
||||
@@ -154,7 +152,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-error
|
||||
- run: go run main.go audit --audit-path ./deploy --set-exit-code-below-score 100 --set-exit-code-on-danger
|
||||
- *update_coverage
|
||||
- *test_binary_dashboard
|
||||
|
||||
@@ -168,6 +166,7 @@ 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
|
||||
@@ -175,7 +174,7 @@ jobs:
|
||||
release_images:
|
||||
working_directory: /go/src/github.com/fairwindsops/polaris/
|
||||
docker:
|
||||
- image: quay.io/reactiveops/ci-images:v8.0-stretch
|
||||
- image: quay.io/reactiveops/ci-images:v11.0-stretch
|
||||
steps:
|
||||
- checkout
|
||||
- setup_remote_docker
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
deploy
|
||||
examples
|
||||
.gitignore
|
||||
.git/*
|
||||
Dockerfile
|
||||
|
||||
@@ -21,3 +21,6 @@ main
|
||||
|
||||
*-packr.go
|
||||
dist
|
||||
.vscode
|
||||
|
||||
*-test.yaml
|
||||
|
||||
+22
-21
@@ -1,29 +1,30 @@
|
||||
builds:
|
||||
- env:
|
||||
- CGO_ENABLED=0
|
||||
binary: polaris
|
||||
archive:
|
||||
replacements:
|
||||
darwin: Darwin
|
||||
linux: Linux
|
||||
windows: Windows
|
||||
386: i386
|
||||
amd64: x86_64
|
||||
checksum:
|
||||
name_template: 'checksums.txt'
|
||||
snapshot:
|
||||
name_template: "{{ .Tag }}-next"
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
exclude:
|
||||
- '^docs:'
|
||||
- '^test:'
|
||||
brew:
|
||||
github:
|
||||
owner: FairwindsOps
|
||||
name: homebrew-tap
|
||||
folder: Formula
|
||||
description: Open Source Best Practices for Kubernetes
|
||||
test: |
|
||||
system "#{bin}/polaris --version"
|
||||
builds:
|
||||
- id: watcher
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
- GO111MODULE=on
|
||||
goarch:
|
||||
- amd64
|
||||
- arm
|
||||
- arm64
|
||||
- 386
|
||||
goarm:
|
||||
- 6
|
||||
- 7
|
||||
brews:
|
||||
- name: polaris
|
||||
github:
|
||||
owner: FairwindsOps
|
||||
name: homebrew-tap
|
||||
folder: Formula
|
||||
description: Open Source Best Practices for Kubernetes
|
||||
test: |
|
||||
system "#{bin}/polaris version"
|
||||
|
||||
+48
-2
@@ -1,7 +1,53 @@
|
||||
# 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.
|
||||
* 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.
|
||||
* 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`
|
||||
|
||||
# 0.6.0
|
||||
* Fixed webhook support in Kubernetes 1.16
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<div align="center">
|
||||
<img src="/polaris-logo.png" alt="Polaris Logo" />
|
||||
<img src="/img/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=0.6.0&color=239922
|
||||
[version-image]: https://img.shields.io/static/v1.svg?label=Version&message=1.2.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="/dashboard-screenshot.png" alt="Polaris Dashboard" />
|
||||
<img src="/img/dashboard-screenshot.png" alt="Polaris Dashboard" width="550"/>
|
||||
</p>
|
||||
|
||||
Our default standards in Polaris are rather high, so don’t 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 we’ve included are too strict, it’s easy to adjust the configuration as part of the deployment configuration to better suit your workloads.
|
||||
@@ -52,7 +52,10 @@ Our default standards in Polaris are rather high, so don’t 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 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.
|
||||
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)
|
||||
|
||||
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.
|
||||
|
||||
@@ -62,7 +65,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 errors arise.
|
||||
to cause CI/CD to fail if your Polaris score drops below a certain threshold, or if any danger-level issues arise.
|
||||
|
||||
# Installation and Usage
|
||||
See the [Usage Guide](/docs/usage.md) for details on different methods for installing and using Polaris.
|
||||
|
||||
+16
-50
@@ -5,55 +5,21 @@ 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.
|
||||
|
||||
## 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
|
||||
## Check non-controllers
|
||||
We would like to implement checks for non-controller types, such as:
|
||||
* Ingress
|
||||
* Service
|
||||
* Secret
|
||||
* Ingresses
|
||||
* Services
|
||||
* RBAC roles/bindings
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
successMessage: Multiple replicas are scheduled
|
||||
failureMessage: Only one replica is scheduled
|
||||
category: Reliability
|
||||
target: Controller
|
||||
controllers:
|
||||
include:
|
||||
- Deployment
|
||||
schema:
|
||||
'$schema': http://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
|
||||
@@ -0,0 +1,9 @@
|
||||
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
|
||||
+10
-7
@@ -16,6 +16,7 @@ package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
@@ -35,16 +36,18 @@ 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-error", false, "Set an exit code of 3 when the audit contains error-level issues.")
|
||||
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().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{
|
||||
@@ -56,12 +59,12 @@ var auditCmd = &cobra.Command{
|
||||
config.DisplayName = displayName
|
||||
}
|
||||
|
||||
auditData := runAndReportAudit(config, auditPath, auditOutputFile, auditOutputURL, auditOutputFormat)
|
||||
auditData := runAndReportAudit(cmd.Context(), config, auditPath, resourceToAudit, auditOutputFile, auditOutputURL, auditOutputFormat)
|
||||
|
||||
summary := auditData.GetSummary()
|
||||
score := summary.GetScore()
|
||||
if setExitCode && summary.Errors > 0 {
|
||||
logrus.Infof("%d errors found in audit", summary.Errors)
|
||||
if setExitCode && summary.Dangers > 0 {
|
||||
logrus.Infof("%d danger items found in audit", summary.Dangers)
|
||||
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)
|
||||
@@ -70,14 +73,14 @@ var auditCmd = &cobra.Command{
|
||||
},
|
||||
}
|
||||
|
||||
func runAndReportAudit(c conf.Configuration, auditPath string, outputFile string, outputURL string, outputFormat string) validator.AuditData {
|
||||
func runAndReportAudit(ctx context.Context, c conf.Configuration, auditPath, workload, outputFile, outputURL, outputFormat string) validator.AuditData {
|
||||
// Create a kubernetes client resource provider
|
||||
k, err := kube.CreateResourceProvider(auditPath)
|
||||
k, err := kube.CreateResourceProvider(ctx, auditPath, workload)
|
||||
if err != nil {
|
||||
logrus.Errorf("Error fetching Kubernetes resources %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
auditData, err := validator.RunAudit(c, k)
|
||||
auditData, err := validator.RunAudit(ctx, c, k)
|
||||
|
||||
if err != nil {
|
||||
logrus.Errorf("Error while running audit on resources: %v", err)
|
||||
|
||||
+17
-100
@@ -15,50 +15,18 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
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"
|
||||
|
||||
fwebhook "github.com/fairwindsops/polaris/pkg/webhook"
|
||||
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
|
||||
|
||||
@@ -74,81 +42,30 @@ 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{})
|
||||
|
||||
mgr, err := manager.New(k8sConfig.GetConfigOrDie(), manager.Options{
|
||||
CertDir: "/opt/cert",
|
||||
Port: webhookPort,
|
||||
})
|
||||
if err != nil {
|
||||
logrus.Errorf("Unable to set up overall controller manager: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
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)
|
||||
_, err = os.Stat("/opt/cert/tls.crt")
|
||||
if os.IsNotExist(err) {
|
||||
time.Sleep(time.Second * 10)
|
||||
panic("Cert does not exist")
|
||||
}
|
||||
|
||||
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)
|
||||
server := mgr.GetWebhookServer()
|
||||
server.CertName = "tls.crt"
|
||||
server.KeyName = "tls.key"
|
||||
|
||||
// Iterate all the configurations supported controllers to scan and register them for webhooks
|
||||
// Should only register controllers that are configured to be scanned
|
||||
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)
|
||||
}
|
||||
fwebhook.NewWebhook(mgr, fwebhook.Validator{Config: config, Client: mgr.GetClient()})
|
||||
|
||||
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")
|
||||
logrus.Infof("Polaris webhook server listening on port %d", webhookPort)
|
||||
if err := mgr.Start(signals.SetupSignalHandler()); err != nil {
|
||||
logrus.Errorf("Error starting manager: %v", err)
|
||||
os.Exit(1)
|
||||
|
||||
+32
-203
@@ -5,200 +5,63 @@ kind: Namespace
|
||||
metadata:
|
||||
name: polaris
|
||||
---
|
||||
# Source: polaris/templates/configmap.yaml
|
||||
# Source: polaris/templates/rbac.yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
kind: ServiceAccount
|
||||
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-dashboard
|
||||
name: polaris
|
||||
labels:
|
||||
app: polaris
|
||||
rules:
|
||||
- apiGroups:
|
||||
- 'apps'
|
||||
- 'extensions'
|
||||
resources:
|
||||
- 'deployments'
|
||||
- 'statefulsets'
|
||||
- 'daemonsets'
|
||||
verbs:
|
||||
- 'get'
|
||||
- 'list'
|
||||
- apiGroups:
|
||||
- 'batch'
|
||||
resources:
|
||||
- 'jobs'
|
||||
- 'cronjobs'
|
||||
verbs:
|
||||
- 'get'
|
||||
- 'list'
|
||||
# required by controller-runtime code doing a cluster wide lookup
|
||||
# when it seems namespace would suffice
|
||||
- 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-dashboard
|
||||
name: polaris-view
|
||||
labels:
|
||||
app: polaris
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: polaris-dashboard
|
||||
name: view
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: polaris-dashboard
|
||||
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/dashboard.service.yaml
|
||||
@@ -212,7 +75,7 @@ metadata:
|
||||
annotations:
|
||||
spec:
|
||||
ports:
|
||||
- name: dashboard
|
||||
- name: http-dashboard
|
||||
port: 80
|
||||
protocol: TCP
|
||||
targetPort: 8080
|
||||
@@ -238,23 +101,15 @@ 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
|
||||
- --config
|
||||
- /opt/app/config.yaml
|
||||
image: 'quay.io/fairwinds/polaris:0.6'
|
||||
- dashboard
|
||||
image: 'quay.io/fairwinds/polaris:1.2'
|
||||
imagePullPolicy: 'Always'
|
||||
name: dashboard
|
||||
ports:
|
||||
@@ -273,8 +128,8 @@ spec:
|
||||
periodSeconds: 20
|
||||
resources:
|
||||
limits:
|
||||
cpu: 100m
|
||||
memory: 128Mi
|
||||
cpu: 150m
|
||||
memory: 512Mi
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 128Mi
|
||||
@@ -286,32 +141,6 @@ spec:
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
volumeMounts:
|
||||
- name: config
|
||||
mountPath: /opt/app/config.yaml
|
||||
subPath: config.yaml
|
||||
readOnly: true
|
||||
serviceAccountName: polaris-dashboard
|
||||
serviceAccountName: polaris
|
||||
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
|
||||
|
||||
|
||||
+215
-205
@@ -5,6 +5,24 @@ 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
|
||||
@@ -14,155 +32,30 @@ metadata:
|
||||
namespace: polaris
|
||||
labels:
|
||||
app: polaris
|
||||
type: Opaque
|
||||
stringData:
|
||||
cert.pem: ''
|
||||
type: kubernetes.io/tls
|
||||
data:
|
||||
tls.crt: ''
|
||||
tls.key: ''
|
||||
---
|
||||
# Source: polaris/templates/configmap.yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
# Source: polaris/templates/rbac.yaml
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
kind: ClusterRole
|
||||
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
|
||||
|
||||
rules:
|
||||
# required by controller-runtime code doing a cluster wide lookup
|
||||
# when it seems namespace would suffice
|
||||
- apiGroups:
|
||||
- ''
|
||||
resources:
|
||||
- 'nodes'
|
||||
verbs:
|
||||
- 'get'
|
||||
- 'list'
|
||||
---
|
||||
# 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:
|
||||
@@ -170,37 +63,6 @@ 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:
|
||||
@@ -210,6 +72,24 @@ 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:
|
||||
@@ -217,6 +97,39 @@ 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:
|
||||
@@ -229,9 +142,10 @@ roleRef:
|
||||
name: polaris-webhook
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: polaris-webhook
|
||||
name: polaris-certificates
|
||||
namespace: polaris
|
||||
---
|
||||
# Source: polaris/templates/webhook.rbac.yaml
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
kind: Role
|
||||
metadata:
|
||||
@@ -249,6 +163,7 @@ rules:
|
||||
verbs:
|
||||
- '*'
|
||||
---
|
||||
# Source: polaris/templates/webhook.rbac.yaml
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
@@ -262,7 +177,7 @@ roleRef:
|
||||
name: polaris-webhook
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: polaris-webhook
|
||||
name: polaris-certificates
|
||||
namespace: polaris
|
||||
---
|
||||
# Source: polaris/templates/webhook.service.yaml
|
||||
@@ -301,8 +216,6 @@ spec:
|
||||
component: webhook
|
||||
template:
|
||||
metadata:
|
||||
annotations:
|
||||
checksum/config: 'eb6d6b194c6786d62400fc0578dd5ea5158212b5b29d93d3cde3fa14da8ac501'
|
||||
labels:
|
||||
app: polaris
|
||||
component: webhook
|
||||
@@ -311,10 +224,8 @@ spec:
|
||||
- name: webhook
|
||||
command:
|
||||
- polaris
|
||||
- --webhook
|
||||
- --config
|
||||
- /opt/app/config.yaml
|
||||
image: 'quay.io/fairwinds/polaris:0.6'
|
||||
- webhook
|
||||
image: 'quay.io/fairwinds/polaris:1.2'
|
||||
imagePullPolicy: 'Always'
|
||||
ports:
|
||||
- containerPort: 9876
|
||||
@@ -353,43 +264,142 @@ 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-webhook
|
||||
serviceAccountName: polaris
|
||||
nodeSelector:
|
||||
tolerations:
|
||||
volumes:
|
||||
- name: config
|
||||
configMap:
|
||||
name: polaris
|
||||
- name: secret
|
||||
secret:
|
||||
secretName: polaris-webhook
|
||||
- name: cr-logs
|
||||
emptyDir: {}
|
||||
---
|
||||
# Source: polaris/templates/audit.job.yaml
|
||||
|
||||
# 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.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
|
||||
|
||||
# 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
|
||||
|
||||
@@ -4,14 +4,14 @@ Polaris supports a number of checks related to the image specified by pods.
|
||||
|
||||
key | default | description
|
||||
----|---------|------------
|
||||
`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`.
|
||||
`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`.
|
||||
|
||||
## 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. 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.
|
||||
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.
|
||||
|
||||
## Further Reading
|
||||
|
||||
|
||||
@@ -8,21 +8,10 @@ To simplify ensure that these values have been set, the following attributes are
|
||||
|
||||
key | default | description
|
||||
----|---------|------------
|
||||
`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
|
||||
`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.
|
||||
|
||||
## Background
|
||||
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
# 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)
|
||||
@@ -4,16 +4,14 @@ Polaris supports a number of checks related to security.
|
||||
|
||||
key | default | description
|
||||
----|---------|------------
|
||||
`security.hostIPCSet` | `error` | Fails when `hostIPC` attribute is configured.
|
||||
`security.hostPIDSet` | `error` | Fails when `hostPID` attribute is configured.
|
||||
`security.hostIPCSet` | `danger` | Fails when `hostIPC` attribute is configured.
|
||||
`security.hostPIDSet` | `danger` | Fails when `hostPID` attribute is configured.
|
||||
`security.notReadOnlyRootFilesystem` | `warning` | Fails when `securityContext.readOnlyRootFilesystem` is not true.
|
||||
`security.privilegeEscalationAllowed` | `error` | Fails when `securityContext.allowPrivilegeEscalation` is true.
|
||||
`security.runAsRootAllowed` | `error` | Fails when `securityContext.runAsNonRoot` is not true.
|
||||
`security.runAsPrivileged` | `error` | Fails when `securityContext.privileged` is true.
|
||||
|
||||
## 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).
|
||||
`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)
|
||||
|
||||
## Background
|
||||
|
||||
@@ -25,3 +23,5 @@ 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
@@ -7,7 +7,7 @@
|
||||
<dt>Exit 2</dt>
|
||||
<dd>Unused</dd>
|
||||
<dt>Exit 3</dt>
|
||||
<dd>Exiting due to `--set-exit-code-on-error` being set and at least one error was found after an audit.</dd>
|
||||
<dd>Exiting due to `--set-exit-code-on-danger` being set and at least one danger 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>
|
||||
|
||||
+33
-9
@@ -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 `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.
|
||||
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.
|
||||
|
||||
Polaris validation checks fall into several different categories:
|
||||
|
||||
@@ -60,6 +60,10 @@ 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.
|
||||
@@ -111,6 +115,17 @@ 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
|
||||
@@ -141,18 +156,25 @@ polaris audit --format score
|
||||
# 92
|
||||
```
|
||||
|
||||
Both the dashboard and audits can run against a local directory or YAML file
|
||||
rather than a cluster:
|
||||
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* error-level issues, or if the score drops below 90%:
|
||||
For example, to fail if polaris detects *any* danger-level issues, or if the score drops below 90%:
|
||||
```bash
|
||||
polaris audit --audit-path ./deploy/ \
|
||||
--set-exit-code-on-error \
|
||||
--set-exit-code-on-danger \
|
||||
--set-exit-code-below-score 90
|
||||
```
|
||||
|
||||
@@ -202,18 +224,20 @@ 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-error
|
||||
Set an exit code of 3 when the audit contains error-level issues.
|
||||
--set-exit-code-on-danger
|
||||
Set an exit code of 3 when the audit contains danger-level issues.
|
||||
|
||||
# webhook flags
|
||||
--disable-webhook-config-installer
|
||||
|
||||
+20
-78
@@ -1,13 +1,15 @@
|
||||
checks:
|
||||
# reliability
|
||||
multipleReplicasForDeployment: warning
|
||||
priorityClassNotSet: warning
|
||||
# resources
|
||||
cpuRequestsMissing: warning
|
||||
cpuLimitsMissing: warning
|
||||
memoryRequestsMissing: warning
|
||||
memoryLimitsMissing: warning
|
||||
# images
|
||||
tagNotSpecified: error
|
||||
pullPolicyNotAlways: ignore
|
||||
imageRegistry: warning
|
||||
tagNotSpecified: danger
|
||||
pullPolicyNotAlways: warning
|
||||
# healthChecks
|
||||
readinessProbeMissing: warning
|
||||
livenessProbeMissing: warning
|
||||
@@ -15,14 +17,24 @@ checks:
|
||||
hostNetworkSet: warning
|
||||
hostPortSet: warning
|
||||
# security
|
||||
hostIPCSet: error
|
||||
hostPIDSet: error
|
||||
hostIPCSet: danger
|
||||
hostPIDSet: danger
|
||||
notReadOnlyRootFilesystem: warning
|
||||
privilegeEscalationAllowed: error
|
||||
privilegeEscalationAllowed: danger
|
||||
runAsRootAllowed: warning
|
||||
runAsPrivileged: error
|
||||
dangerousCapabilities: error
|
||||
runAsPrivileged: danger
|
||||
dangerousCapabilities: danger
|
||||
insecureCapabilities: warning
|
||||
# custom
|
||||
resourceLimits: warning
|
||||
imageRegistry: danger
|
||||
|
||||
exemptions:
|
||||
- controllerNames:
|
||||
- my-network-controller
|
||||
rules:
|
||||
- hostNetworkSet
|
||||
- hostPortSet
|
||||
|
||||
customChecks:
|
||||
resourceLimits:
|
||||
@@ -72,73 +84,3 @@ 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
|
||||
|
||||
+10
-7
@@ -1,12 +1,15 @@
|
||||
checks:
|
||||
# reliability
|
||||
multipleReplicasForDeployment: ignore
|
||||
priorityClassNotSet: ignore
|
||||
# resources
|
||||
cpuRequestsMissing: warning
|
||||
cpuLimitsMissing: warning
|
||||
memoryRequestsMissing: warning
|
||||
memoryLimitsMissing: warning
|
||||
# images
|
||||
tagNotSpecified: error
|
||||
pullPolicyNotAlways: ignore
|
||||
tagNotSpecified: danger
|
||||
pullPolicyNotAlways: warning
|
||||
# healthChecks
|
||||
readinessProbeMissing: warning
|
||||
livenessProbeMissing: warning
|
||||
@@ -14,13 +17,13 @@ checks:
|
||||
hostNetworkSet: warning
|
||||
hostPortSet: warning
|
||||
# security
|
||||
hostIPCSet: error
|
||||
hostPIDSet: error
|
||||
hostIPCSet: danger
|
||||
hostPIDSet: danger
|
||||
notReadOnlyRootFilesystem: warning
|
||||
privilegeEscalationAllowed: error
|
||||
privilegeEscalationAllowed: danger
|
||||
runAsRootAllowed: warning
|
||||
runAsPrivileged: error
|
||||
dangerousCapabilities: error
|
||||
runAsPrivileged: danger
|
||||
dangerousCapabilities: danger
|
||||
insecureCapabilities: warning
|
||||
|
||||
exemptions:
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
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
@@ -29,7 +29,7 @@
|
||||
"ID": "hostIPCSet",
|
||||
"Message": "Host IPC is not configured",
|
||||
"Success": true,
|
||||
"Severity": "error",
|
||||
"Severity": "danger",
|
||||
"Category": "Security"
|
||||
},
|
||||
"hostNetworkSet": {
|
||||
@@ -43,7 +43,7 @@
|
||||
"ID": "hostPIDSet",
|
||||
"Message": "Host PID is not configured",
|
||||
"Success": true,
|
||||
"Severity": "error",
|
||||
"Severity": "danger",
|
||||
"Category": "Security"
|
||||
}
|
||||
},
|
||||
@@ -69,7 +69,7 @@
|
||||
"ID": "dangerousCapabilities",
|
||||
"Message": "Container does not have any dangerous capabilities",
|
||||
"Success": true,
|
||||
"Severity": "error",
|
||||
"Severity": "danger",
|
||||
"Category": "Security"
|
||||
},
|
||||
"hostPortSet": {
|
||||
@@ -111,7 +111,7 @@
|
||||
"ID": "privilegeEscalationAllowed",
|
||||
"Message": "Privilege escalation not allowed",
|
||||
"Success": true,
|
||||
"Severity": "error",
|
||||
"Severity": "danger",
|
||||
"Category": "Security"
|
||||
},
|
||||
"readinessProbeMissing": {
|
||||
@@ -125,7 +125,7 @@
|
||||
"ID": "runAsPrivileged",
|
||||
"Message": "Not running as privileged",
|
||||
"Success": true,
|
||||
"Severity": "error",
|
||||
"Severity": "danger",
|
||||
"Category": "Security"
|
||||
},
|
||||
"runAsRootAllowed": {
|
||||
@@ -139,7 +139,7 @@
|
||||
"ID": "tagNotSpecified",
|
||||
"Message": "Image tag is specified",
|
||||
"Success": true,
|
||||
"Severity": "error",
|
||||
"Severity": "danger",
|
||||
"Category": "Images"
|
||||
}
|
||||
}
|
||||
@@ -159,7 +159,7 @@
|
||||
"ID": "hostIPCSet",
|
||||
"Message": "Host IPC is not configured",
|
||||
"Success": true,
|
||||
"Severity": "error",
|
||||
"Severity": "danger",
|
||||
"Category": "Security"
|
||||
},
|
||||
"hostNetworkSet": {
|
||||
@@ -173,7 +173,7 @@
|
||||
"ID": "hostPIDSet",
|
||||
"Message": "Host PID is not configured",
|
||||
"Success": true,
|
||||
"Severity": "error",
|
||||
"Severity": "danger",
|
||||
"Category": "Security"
|
||||
}
|
||||
},
|
||||
@@ -199,7 +199,7 @@
|
||||
"ID": "dangerousCapabilities",
|
||||
"Message": "Container does not have any dangerous capabilities",
|
||||
"Success": true,
|
||||
"Severity": "error",
|
||||
"Severity": "danger",
|
||||
"Category": "Security"
|
||||
},
|
||||
"hostPortSet": {
|
||||
@@ -241,7 +241,7 @@
|
||||
"ID": "privilegeEscalationAllowed",
|
||||
"Message": "Privilege escalation not allowed",
|
||||
"Success": true,
|
||||
"Severity": "error",
|
||||
"Severity": "danger",
|
||||
"Category": "Security"
|
||||
},
|
||||
"readinessProbeMissing": {
|
||||
@@ -255,7 +255,7 @@
|
||||
"ID": "runAsPrivileged",
|
||||
"Message": "Not running as privileged",
|
||||
"Success": true,
|
||||
"Severity": "error",
|
||||
"Severity": "danger",
|
||||
"Category": "Security"
|
||||
},
|
||||
"runAsRootAllowed": {
|
||||
@@ -269,7 +269,7 @@
|
||||
"ID": "tagNotSpecified",
|
||||
"Message": "Image tag is specified",
|
||||
"Success": true,
|
||||
"Severity": "error",
|
||||
"Severity": "danger",
|
||||
"Category": "Images"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
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
|
||||
@@ -3,96 +3,39 @@ module github.com/fairwindsops/polaris
|
||||
go 1.13
|
||||
|
||||
require (
|
||||
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
|
||||
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
|
||||
github.com/qri-io/jsonschema v0.1.1
|
||||
github.com/rogpeppe/go-internal v1.5.2
|
||||
github.com/sirupsen/logrus v1.5.0
|
||||
github.com/spf13/cobra v0.0.5
|
||||
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/spf13/pflag v1.0.5
|
||||
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
|
||||
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
|
||||
sigs.k8s.io/yaml v1.2.0
|
||||
)
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 186 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
@@ -20,11 +20,9 @@ import (
|
||||
|
||||
const (
|
||||
// Version represents the current release version of Polaris
|
||||
Version = "0.6.0"
|
||||
|
||||
Version = "1.2.1"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cmd.Execute(Version)
|
||||
}
|
||||
|
||||
|
||||
+23
-3
@@ -16,6 +16,7 @@ package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
@@ -41,13 +42,21 @@ 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 == "" {
|
||||
configBox := packr.New("Config", "../../examples")
|
||||
rawBytes, err = configBox.Find("config.yaml")
|
||||
rawBytes, err = getConfigBox().Find("config.yaml")
|
||||
} else if strings.HasPrefix(path, "https://") || strings.HasPrefix(path, "http://") {
|
||||
//path is a url
|
||||
response, err2 := http.Get(path)
|
||||
@@ -84,6 +93,17 @@ 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, nil
|
||||
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
|
||||
}
|
||||
|
||||
@@ -74,6 +74,21 @@ 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"
|
||||
@@ -154,6 +169,11 @@ 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"])
|
||||
|
||||
@@ -12,6 +12,7 @@ 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 {
|
||||
@@ -23,6 +24,13 @@ 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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
// 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.")
|
||||
}
|
||||
@@ -17,6 +17,8 @@ const (
|
||||
TargetContainer TargetKind = "Container"
|
||||
// TargetPod points to the pod spec
|
||||
TargetPod TargetKind = "Pod"
|
||||
// TargetController points to the controller's spec
|
||||
TargetController TargetKind = "Controller"
|
||||
)
|
||||
|
||||
// SchemaCheck is a Polaris check that runs using JSON Schema
|
||||
@@ -128,6 +130,12 @@ func (check SchemaCheck) CheckPod(pod *corev1.PodSpec) (bool, error) {
|
||||
return check.CheckObject(pod)
|
||||
}
|
||||
|
||||
// CheckController checks a controler's spec against the schema
|
||||
func (check SchemaCheck) CheckController(bytes []byte) (bool, error) {
|
||||
errs, err := check.Schema.ValidateBytes(bytes)
|
||||
return len(errs) == 0, err
|
||||
}
|
||||
|
||||
// CheckContainer checks a container spec against the schema
|
||||
func (check SchemaCheck) CheckContainer(container *corev1.Container) (bool, error) {
|
||||
return check.CheckObject(container)
|
||||
|
||||
@@ -24,11 +24,11 @@ const (
|
||||
// SeverityWarning warns on validation failures
|
||||
SeverityWarning Severity = "warning"
|
||||
|
||||
// SeverityError errors on validation failures
|
||||
SeverityError Severity = "error"
|
||||
// SeverityDanger errors on validation failures
|
||||
SeverityDanger Severity = "danger"
|
||||
)
|
||||
|
||||
// IsActionable returns true if the severity level is warning or error
|
||||
func (severity *Severity) IsActionable() bool {
|
||||
return *severity == SeverityWarning || *severity == SeverityError
|
||||
return *severity == SeverityWarning || *severity == SeverityDanger
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.filters .title,
|
||||
.card h3 {
|
||||
margin: 0;
|
||||
font-weight: 300;
|
||||
@@ -40,11 +41,9 @@
|
||||
width: 32%;
|
||||
vertical-align: top;
|
||||
}
|
||||
.cluster-overview .cluster-score{
|
||||
padding-top: 15px;
|
||||
}
|
||||
.cluster-overview .cluster-score,
|
||||
.cluster-overview .result-messages {
|
||||
padding-top: 70px;
|
||||
padding-top: 15px;
|
||||
}
|
||||
|
||||
.cluster-overview .graph canvas {
|
||||
@@ -93,45 +92,24 @@
|
||||
|
||||
.cluster-overview .result-messages ul {
|
||||
font-size: 20px;
|
||||
line-height: 35px;
|
||||
line-height: 42px;
|
||||
}
|
||||
|
||||
.cluster-overview-footer {
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
right: 10px;
|
||||
right: 40px;
|
||||
left: 40px;
|
||||
}
|
||||
|
||||
.cluster-overview-footer p {
|
||||
display: inline-block;
|
||||
.cluster-overview-footer .kubernetes-stats {
|
||||
font-size: 11px;
|
||||
color: #777;
|
||||
width: 48%;
|
||||
}
|
||||
|
||||
.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;
|
||||
.cluster-overview-footer .kubernetes-stats {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.category .name {
|
||||
@@ -191,7 +169,7 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.expandable-table .resource-info .caret-expander {
|
||||
.resource-info .caret-expander {
|
||||
display: inline-block;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
@@ -202,16 +180,16 @@
|
||||
background-position: 2px center;
|
||||
}
|
||||
|
||||
.expandable-table .resource-info.expanded .caret-expander {
|
||||
.resource-info.expanded .caret-expander {
|
||||
background-image: url('../images/caret-bottom.svg');
|
||||
background-position: center 2px;
|
||||
}
|
||||
|
||||
.expandable-table .resource-info .expandable-content {
|
||||
.resource-info .expandable-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.expandable-table .resource-info.expanded .expandable-content {
|
||||
.resource-info.expanded .expandable-content {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -236,6 +214,26 @@ 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 {
|
||||
@@ -261,7 +259,7 @@ ul.message-list li i.message-icon {
|
||||
color: #f26c21;
|
||||
}
|
||||
|
||||
.result-messages .failure.error i.message-icon {
|
||||
.result-messages .failure.danger i.message-icon {
|
||||
color: #a11f4c;
|
||||
}
|
||||
|
||||
@@ -319,3 +317,46 @@ 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;
|
||||
}
|
||||
@@ -7,7 +7,7 @@ $(function () {
|
||||
data: [
|
||||
polarisSummary.Successes,
|
||||
polarisSummary.Warnings,
|
||||
polarisSummary.Errors,
|
||||
polarisSummary.Dangers,
|
||||
],
|
||||
backgroundColor: ['#8BD2DC', '#f26c21', '#a11f4c'],
|
||||
}]
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
$(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();
|
||||
});
|
||||
});
|
||||
|
||||
+30
-12
@@ -81,10 +81,11 @@ func GetMarkdownBox() *packr.Box {
|
||||
|
||||
// templateData is passed to the dashboard HTML template
|
||||
type templateData struct {
|
||||
BasePath string
|
||||
Config config.Configuration
|
||||
AuditData validator.AuditData
|
||||
JSON template.JS
|
||||
BasePath string
|
||||
Config config.Configuration
|
||||
AuditData validator.AuditData
|
||||
FilteredAuditData validator.AuditData
|
||||
JSON template.JS
|
||||
}
|
||||
|
||||
// GetBaseTemplate puts together the dashboard template. Individual pieces can be overridden before rendering.
|
||||
@@ -150,6 +151,16 @@ 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()
|
||||
@@ -173,14 +184,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(auditPath)
|
||||
k, err := kube.CreateResourceProvider(r.Context(), 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(adjustedConf, k)
|
||||
auditDataObj, err := validator.RunAudit(r.Context(), adjustedConf, k)
|
||||
if err != nil {
|
||||
http.Error(w, "Error Fetching Deployments", http.StatusInternalServerError)
|
||||
return
|
||||
@@ -206,14 +217,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(auditPath)
|
||||
k, err := kube.CreateResourceProvider(r.Context(), 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(adjustedConf, k)
|
||||
auditData, err := validator.RunAudit(r.Context(), adjustedConf, k)
|
||||
if err != nil {
|
||||
logrus.Errorf("Error getting audit data: %v", err)
|
||||
http.Error(w, "Error running audit", 500)
|
||||
@@ -237,11 +248,18 @@ 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,
|
||||
JSON: template.JS(jsonData),
|
||||
Config: c,
|
||||
BasePath: basePath,
|
||||
AuditData: auditData,
|
||||
FilteredAuditData: filteredAuditData,
|
||||
JSON: template.JS(jsonData),
|
||||
Config: c,
|
||||
}
|
||||
tmpl, err := GetBaseTemplate("main")
|
||||
if err != nil {
|
||||
|
||||
@@ -23,11 +23,21 @@ import (
|
||||
)
|
||||
|
||||
func getWarningWidth(counts validator.CountSummary, fullWidth int) uint {
|
||||
return uint(float64(counts.Successes+counts.Warnings) / float64(counts.Successes+counts.Warnings+counts.Errors) * float64(fullWidth))
|
||||
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)
|
||||
}
|
||||
|
||||
func getSuccessWidth(counts validator.CountSummary, fullWidth int) uint {
|
||||
return uint(float64(counts.Successes) / float64(counts.Successes+counts.Warnings+counts.Errors) * float64(fullWidth))
|
||||
denom := counts.Successes + counts.Warnings + counts.Dangers
|
||||
if denom == 0 {
|
||||
return uint(0)
|
||||
}
|
||||
res := float64(counts.Successes) / float64(denom) * float64(fullWidth)
|
||||
return uint(res)
|
||||
}
|
||||
|
||||
func getGrade(counts validator.CountSummary) string {
|
||||
@@ -161,3 +171,12 @@ func getCategoryInfo(category string) string {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func stringInSlice(a string, list []string) bool {
|
||||
for _, b := range list {
|
||||
if b == a {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -10,9 +10,12 @@
|
||||
<div class="cluster-score">
|
||||
<div class="score-details">
|
||||
<div class="weather"><i class="fas {{ getWeatherIcon .AuditData.GetSummary }}"></i></div>
|
||||
<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 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>
|
||||
</div>
|
||||
<div class="graph">
|
||||
@@ -20,45 +23,60 @@
|
||||
</div>
|
||||
<div class="result-messages">
|
||||
<ul class="message-list">
|
||||
<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>
|
||||
<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>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cluster-overview-footer">
|
||||
<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">
|
||||
<div class="kubernetes-stats">
|
||||
<div class="kubernetes-stat">
|
||||
<span>Kubernetes Version:</span>
|
||||
<strong>{{.AuditData.ClusterInfo.Version}}</strong>
|
||||
</span>
|
||||
<span class="kubernetes-stat">
|
||||
<strong>{{.FilteredAuditData.ClusterInfo.Version}}</strong>
|
||||
</div>
|
||||
<div class="kubernetes-stat">
|
||||
<span>Nodes:</span>
|
||||
<strong>{{.AuditData.ClusterInfo.Nodes}}</strong>
|
||||
</span>
|
||||
<span class="kubernetes-stat">
|
||||
<span>Pods:</span>
|
||||
<strong>{{.AuditData.ClusterInfo.Pods}}</strong>
|
||||
</span>
|
||||
<span class="kubernetes-stat">
|
||||
<span>Controllers:</span>
|
||||
<strong>{{.AuditData.ClusterInfo.Controllers}}</strong>
|
||||
</span>
|
||||
<span class="kubernetes-stat">
|
||||
<strong>{{.FilteredAuditData.ClusterInfo.Nodes}}</strong>
|
||||
</div>
|
||||
<div class="kubernetes-stat">
|
||||
<span>Namespaces:</span>
|
||||
<strong>{{.AuditData.ClusterInfo.Namespaces}}</strong>
|
||||
</span>
|
||||
</p>
|
||||
<strong>{{.FilteredAuditData.ClusterInfo.Namespaces}}</strong>
|
||||
</div>
|
||||
<div class="kubernetes-stat">
|
||||
<span>Controllers:</span>
|
||||
<strong>{{.FilteredAuditData.ClusterInfo.Controllers}}</strong>
|
||||
</div>
|
||||
<div class="kubernetes-stat">
|
||||
<span>Pods:</span>
|
||||
<strong>{{.FilteredAuditData.ClusterInfo.Pods}}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card category">
|
||||
<h3>Results by Category</h3>
|
||||
<div class="expandable-table">
|
||||
{{ range $category, $summary := .AuditData.GetSummaryByCategory }}
|
||||
{{ range $category, $summary := .FilteredAuditData.GetSummaryByCategory }}
|
||||
<div class="resource-info">
|
||||
<div class="status-bar">
|
||||
<div class="status">
|
||||
@@ -70,7 +88,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>
|
||||
@@ -78,7 +96,27 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ range $namespace, $ctrlResults := .AuditData.GetResultsByNamespace }}
|
||||
<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 }}
|
||||
<div class="card namespace">
|
||||
<h3>Namespace: <strong>{{ $namespace }}</strong></h3>
|
||||
<div class="expandable-table">
|
||||
@@ -97,6 +135,22 @@
|
||||
<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">
|
||||
@@ -111,6 +165,7 @@
|
||||
{{ end }}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{{ range .PodResult.ContainerResults }}
|
||||
<div class="result-messages expandable-content">
|
||||
<h4>Container: {{ .Name }}</h4>
|
||||
@@ -132,6 +187,6 @@
|
||||
</div>
|
||||
</div>
|
||||
{{ end }} {{/* end range .AuditData.GetResultsByNamespace */}}
|
||||
<script src="static/js/charts.js">
|
||||
</script>
|
||||
<script src="static/js/charts.js"></script>
|
||||
<script src="static/js/filter.js"></script>
|
||||
{{end}}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{{define "footer"}}
|
||||
<div class="footer">
|
||||
<a href="https://fairwinds.com?source=polaris" target="_blank">©2019 FairwindsOps Inc.</a>
|
||||
<a href="https://fairwinds.com?source=polaris" target="_blank">©2020 FairwindsOps Inc.</a>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{{define "navbar"}}
|
||||
<div class="navbar">
|
||||
<div class="navbar-content">
|
||||
<a href="/">
|
||||
<a href="{{ .BasePath }}">
|
||||
<img class="logo" src="static/images/polaris-logo.png" alt="Polaris" />
|
||||
</a>
|
||||
<div class="navbar-right">
|
||||
|
||||
+133
-75
@@ -2,7 +2,9 @@ package kube
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -11,11 +13,10 @@ 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/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
k8sYaml "k8s.io/apimachinery/pkg/util/yaml"
|
||||
"k8s.io/client-go/dynamic"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
@@ -42,11 +43,75 @@ type k8sResource struct {
|
||||
var podSpecFields = []string{"jobTemplate", "spec", "template"}
|
||||
|
||||
// CreateResourceProvider returns a new ResourceProvider object to interact with k8s resources
|
||||
func CreateResourceProvider(directory string) (*ResourceProvider, error) {
|
||||
func CreateResourceProvider(ctx context.Context, directory, workload string) (*ResourceProvider, error) {
|
||||
if workload != "" {
|
||||
return CreateResourceProviderFromWorkload(ctx, workload)
|
||||
}
|
||||
if directory != "" {
|
||||
return CreateResourceProviderFromPath(directory)
|
||||
}
|
||||
return CreateResourceProviderFromCluster()
|
||||
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
|
||||
}
|
||||
|
||||
// CreateResourceProviderFromPath returns a new ResourceProvider using the YAML files in a directory
|
||||
@@ -60,8 +125,14 @@ func CreateResourceProviderFromPath(directory string) (*ResourceProvider, error)
|
||||
Controllers: []GenericWorkload{},
|
||||
}
|
||||
|
||||
addYaml := func(contents string) error {
|
||||
return addResourceFromString(contents, &resources)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
visitFile := func(path string, f os.FileInfo, err error) error {
|
||||
@@ -73,18 +144,7 @@ func CreateResourceProviderFromPath(directory string) (*ResourceProvider, error)
|
||||
logrus.Errorf("Error reading file: %v", path)
|
||||
return err
|
||||
}
|
||||
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
|
||||
return addResourcesFromYaml(string(contents), &resources)
|
||||
}
|
||||
|
||||
err := filepath.Walk(directory, visitFile)
|
||||
@@ -95,7 +155,7 @@ func CreateResourceProviderFromPath(directory string) (*ResourceProvider, error)
|
||||
}
|
||||
|
||||
// CreateResourceProviderFromCluster creates a new ResourceProvider using live data from a cluster
|
||||
func CreateResourceProviderFromCluster() (*ResourceProvider, error) {
|
||||
func CreateResourceProviderFromCluster(ctx context.Context) (*ResourceProvider, error) {
|
||||
kubeConf, configError := config.GetConfig()
|
||||
if configError != nil {
|
||||
logrus.Errorf("Error fetching KubeConfig: %v", configError)
|
||||
@@ -111,11 +171,11 @@ func CreateResourceProviderFromCluster() (*ResourceProvider, error) {
|
||||
logrus.Errorf("Error connecting to dynamic interface: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
return CreateResourceProviderFromAPI(api, kubeConf.Host, &dynamicInterface)
|
||||
return CreateResourceProviderFromAPI(ctx, api, kubeConf.Host, &dynamicInterface)
|
||||
}
|
||||
|
||||
// CreateResourceProviderFromAPI creates a new ResourceProvider from an existing k8s interface
|
||||
func CreateResourceProviderFromAPI(kube kubernetes.Interface, clusterName string, dynamic *dynamic.Interface) (*ResourceProvider, error) {
|
||||
func CreateResourceProviderFromAPI(ctx context.Context, kube kubernetes.Interface, clusterName string, dynamic *dynamic.Interface) (*ResourceProvider, error) {
|
||||
listOpts := metav1.ListOptions{}
|
||||
serverVersion, err := kube.Discovery().ServerVersion()
|
||||
if err != nil {
|
||||
@@ -123,17 +183,17 @@ func CreateResourceProviderFromAPI(kube kubernetes.Interface, clusterName string
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nodes, err := kube.CoreV1().Nodes().List(listOpts)
|
||||
nodes, err := kube.CoreV1().Nodes().List(ctx, listOpts)
|
||||
if err != nil {
|
||||
logrus.Errorf("Error fetching Nodes: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
namespaces, err := kube.CoreV1().Namespaces().List(listOpts)
|
||||
namespaces, err := kube.CoreV1().Namespaces().List(ctx, listOpts)
|
||||
if err != nil {
|
||||
logrus.Errorf("Error fetching Namespaces: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
pods, err := kube.CoreV1().Pods("").List(listOpts)
|
||||
pods, err := kube.CoreV1().Pods("").List(ctx, listOpts)
|
||||
if err != nil {
|
||||
logrus.Errorf("Error fetching Pods: %v", err)
|
||||
return nil, err
|
||||
@@ -146,7 +206,13 @@ func CreateResourceProviderFromAPI(kube kubernetes.Interface, clusterName string
|
||||
}
|
||||
restMapper := restmapper.NewDiscoveryRESTMapper(resources)
|
||||
|
||||
objectCache := map[string]metav1.Object{}
|
||||
objectCache := map[string]unstructured.Unstructured{}
|
||||
|
||||
controllers, err := LoadControllers(ctx, pods.Items, dynamic, &restMapper, objectCache)
|
||||
if err != nil {
|
||||
logrus.Errorf("Error loading controllers from pods: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
api := ResourceProvider{
|
||||
ServerVersion: serverVersion.Major + "." + serverVersion.Minor,
|
||||
@@ -155,32 +221,13 @@ func CreateResourceProviderFromAPI(kube kubernetes.Interface, clusterName string
|
||||
CreationTime: time.Now(),
|
||||
Nodes: nodes.Items,
|
||||
Namespaces: namespaces.Items,
|
||||
Controllers: LoadControllers(pods.Items, dynamic, &restMapper, objectCache),
|
||||
Controllers: controllers,
|
||||
}
|
||||
return &api, nil
|
||||
}
|
||||
|
||||
func cacheAllObjectsOfKind(dynamicClient dynamic.Interface, groupVersionResource schema.GroupVersionResource, objectCache map[string]metav1.Object) error {
|
||||
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(pods []corev1.Pod, dynamicClientPointer *dynamic.Interface, restMapperPointer *meta.RESTMapper, objectCache map[string]metav1.Object) []GenericWorkload {
|
||||
func LoadControllers(ctx context.Context, pods []corev1.Pod, dynamicClientPointer *dynamic.Interface, restMapperPointer *meta.RESTMapper, objectCache map[string]unstructured.Unstructured) ([]GenericWorkload, error) {
|
||||
interfaces := []GenericWorkload{}
|
||||
deduped := map[string]corev1.Pod{}
|
||||
for _, pod := range pods {
|
||||
@@ -192,9 +239,13 @@ func LoadControllers(pods []corev1.Pod, dynamicClientPointer *dynamic.Interface,
|
||||
deduped[pod.ObjectMeta.Namespace+"/"+owners[0].Kind+"/"+owners[0].Name] = pod
|
||||
}
|
||||
for _, pod := range deduped {
|
||||
interfaces = append(interfaces, NewGenericWorkload(pod, dynamicClientPointer, restMapperPointer, objectCache))
|
||||
workload, err := NewGenericWorkload(ctx, pod, dynamicClientPointer, restMapperPointer, objectCache)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
interfaces = append(interfaces, workload)
|
||||
}
|
||||
return deduplicateControllers(interfaces)
|
||||
return deduplicateControllers(interfaces), nil
|
||||
}
|
||||
|
||||
// Because the controllers with an Owner take on the name of the Owner, this eliminates any duplicates.
|
||||
@@ -215,14 +266,31 @@ func deduplicateControllers(inputControllers []GenericWorkload) []GenericWorkloa
|
||||
return results
|
||||
}
|
||||
|
||||
// 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{}))
|
||||
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
|
||||
}
|
||||
}
|
||||
return yaml
|
||||
return nil
|
||||
}
|
||||
|
||||
func addResourceFromString(contents string, resources *ResourceProvider) error {
|
||||
@@ -243,30 +311,20 @@ func addResourceFromString(contents string, resources *ResourceProvider) error {
|
||||
} else if resource.Kind == "Pod" {
|
||||
pod := corev1.Pod{}
|
||||
err = decoder.Decode(&pod)
|
||||
resources.Controllers = append(resources.Controllers, NewGenericWorkloadFromPod(pod))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
workload, err := NewGenericWorkloadFromPod(pod, pod)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resources.Controllers = append(resources.Controllers, workload)
|
||||
} else {
|
||||
yamlNode := make(map[string]interface{})
|
||||
err = yaml.Unmarshal(contentBytes, &yamlNode)
|
||||
if err != nil {
|
||||
logrus.Errorf("Invalid YAML: %s", string(contents))
|
||||
newController, err := GetWorkloadFromBytes(contentBytes)
|
||||
if err != nil || newController == nil {
|
||||
return err
|
||||
}
|
||||
finalDoc := make(map[string]interface{})
|
||||
finalDoc["metadata"] = yamlNode["metadata"]
|
||||
finalDoc["apiVersion"] = "v1"
|
||||
finalDoc["kind"] = "Pod"
|
||||
finalDoc["spec"] = GetPodSpec(yamlNode)
|
||||
marshaledYaml, err := yaml.Marshal(finalDoc)
|
||||
if err != nil {
|
||||
logrus.Errorf("Could not marshal yaml: %v", err)
|
||||
return err
|
||||
}
|
||||
decoder := k8sYaml.NewYAMLOrJSONDecoder(bytes.NewReader(marshaledYaml), 1000)
|
||||
pod := corev1.Pod{}
|
||||
err = decoder.Decode(&pod)
|
||||
newController := NewGenericWorkloadFromPod(pod)
|
||||
newController.Kind = resource.Kind
|
||||
resources.Controllers = append(resources.Controllers, newController)
|
||||
resources.Controllers = append(resources.Controllers, *newController)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
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) {
|
||||
@@ -44,7 +48,7 @@ func TestGetMultipleResourceFromSingleFile(t *testing.T) {
|
||||
|
||||
assert.Equal(t, 0, len(resources.Nodes), "Should not have any nodes")
|
||||
|
||||
assert.Equal(t, 4, len(resources.Controllers), "Should have four controllers")
|
||||
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")
|
||||
@@ -57,12 +61,37 @@ 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(k8s, "test")
|
||||
k8s = test.SetupAddControllers(context.Background(), k8s, "test")
|
||||
// TODO find a way to mock out the dynamic client
|
||||
// and create fake pods in order to find all of the controllers.
|
||||
resources, err := CreateResourceProviderFromAPI(k8s, "test", &dynamicInterface)
|
||||
resources, err := CreateResourceProviderFromAPI(context.Background(), k8s, "test", &dynamicInterface)
|
||||
assert.Equal(t, nil, err, "Error should be nil")
|
||||
|
||||
assert.Equal(t, "Cluster", resources.SourceType, "Should have type Path")
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: test-service
|
||||
spec:
|
||||
ports:
|
||||
- port: 8080
|
||||
protocol: TCP
|
||||
targetPort: 8080
|
||||
selector:
|
||||
app: test
|
||||
+155
-68
@@ -1,93 +1,180 @@
|
||||
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
|
||||
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
|
||||
}
|
||||
|
||||
// NewGenericWorkloadFromPod builds a new workload for a given Pod without looking at parents
|
||||
func NewGenericWorkloadFromPod(originalResource kubeAPICoreV1.Pod) GenericWorkload {
|
||||
workload := GenericWorkload{}
|
||||
workload.PodSpec = originalResource.Spec
|
||||
workload.ObjectMeta = originalResource.ObjectMeta.GetObjectMeta()
|
||||
workload.Kind = "Pod"
|
||||
return workload
|
||||
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
|
||||
}
|
||||
|
||||
// NewGenericWorkload builds a new workload for a given Pod
|
||||
func NewGenericWorkload(originalResource kubeAPICoreV1.Pod, dynamicClientPointer *dynamic.Interface, restMapperPointer *meta.RESTMapper, objectCache map[string]kubeAPIMetaV1.Object) GenericWorkload {
|
||||
workload := NewGenericWorkloadFromPod(originalResource)
|
||||
func NewGenericWorkload(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
|
||||
}
|
||||
if len(workload.OriginalObjectJSON) == 0 {
|
||||
return NewGenericWorkloadFromPod(podResource, podResource)
|
||||
}
|
||||
return workload, err
|
||||
}
|
||||
|
||||
dynamicClient := *dynamicClientPointer
|
||||
restMapper := *restMapperPointer
|
||||
// If an owner exists then set the name to the workload.
|
||||
// This allows us to handle CRDs creating Workloads or DeploymentConfigs in OpenShift.
|
||||
owners := workload.ObjectMeta.GetOwnerReferences()
|
||||
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()
|
||||
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
|
||||
}
|
||||
|
||||
return workload
|
||||
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
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/fairwindsops/polaris/pkg/config"
|
||||
"github.com/fairwindsops/polaris/pkg/kube"
|
||||
|
||||
@@ -22,8 +24,8 @@ import (
|
||||
)
|
||||
|
||||
// ValidateContainer validates a single container from a given controller
|
||||
func ValidateContainer(conf *config.Configuration, controller kube.GenericWorkload, container *corev1.Container, isInit bool) (ContainerResult, error) {
|
||||
results, err := applyContainerSchemaChecks(conf, controller, container, isInit)
|
||||
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)
|
||||
if err != nil {
|
||||
return ContainerResult{}, err
|
||||
}
|
||||
@@ -37,18 +39,18 @@ func ValidateContainer(conf *config.Configuration, controller kube.GenericWorklo
|
||||
}
|
||||
|
||||
// ValidateAllContainers validates both init and regular containers
|
||||
func ValidateAllContainers(conf *config.Configuration, controller kube.GenericWorkload) ([]ContainerResult, error) {
|
||||
func ValidateAllContainers(ctx context.Context, conf *config.Configuration, controller kube.GenericWorkload) ([]ContainerResult, error) {
|
||||
results := []ContainerResult{}
|
||||
pod := controller.PodSpec
|
||||
for _, container := range pod.InitContainers {
|
||||
result, err := ValidateContainer(conf, controller, &container, true)
|
||||
result, err := ValidateContainer(ctx, conf, controller, &container, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
results = append(results, result)
|
||||
}
|
||||
for _, container := range pod.Containers {
|
||||
result, err := ValidateContainer(conf, controller, &container, false)
|
||||
result, err := ValidateContainer(ctx, conf, controller, &container, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
+120
-115
@@ -15,6 +15,7 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
@@ -30,16 +31,16 @@ var resourceConfMinimal = `---
|
||||
checks:
|
||||
cpuRequestsMissing: warning
|
||||
memoryRequestsMissing: warning
|
||||
cpuLimitsMissing: error
|
||||
memoryLimitsMissing: error
|
||||
cpuLimitsMissing: danger
|
||||
memoryLimitsMissing: danger
|
||||
`
|
||||
|
||||
var resourceConfExemptions = `---
|
||||
checks:
|
||||
cpuRequestsMissing: warning
|
||||
memoryRequestsMissing: warning
|
||||
cpuLimitsMissing: error
|
||||
memoryLimitsMissing: error
|
||||
cpuLimitsMissing: danger
|
||||
memoryLimitsMissing: danger
|
||||
exemptions:
|
||||
- rules:
|
||||
- cpuRequestsMissing
|
||||
@@ -50,24 +51,25 @@ exemptions:
|
||||
- foo
|
||||
`
|
||||
|
||||
func getEmptyWorkload(name string) kube.GenericWorkload {
|
||||
workload := kube.NewGenericWorkloadFromPod(corev1.Pod{
|
||||
func getEmptyWorkload(t *testing.T, name string) kube.GenericWorkload {
|
||||
workload, err := 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, expectedErrors []ResultMessage, expectedWarnings []ResultMessage, expectedSuccesses []ResultMessage) {
|
||||
testValidateWithWorkload(t, container, resourceConf, getEmptyWorkload(controllerName), expectedErrors, expectedWarnings, expectedSuccesses)
|
||||
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 testValidateWithWorkload(t *testing.T, container *corev1.Container, resourceConf *string, workload kube.GenericWorkload, expectedErrors []ResultMessage, expectedWarnings []ResultMessage, expectedSuccesses []ResultMessage) {
|
||||
func testValidateWithWorkload(t *testing.T, container *corev1.Container, resourceConf *string, workload kube.GenericWorkload, expectedDangers []ResultMessage, expectedWarnings []ResultMessage, expectedSuccesses []ResultMessage) {
|
||||
parsedConf, err := conf.Parse([]byte(*resourceConf))
|
||||
assert.NoError(t, err, "Expected no error when parsing config")
|
||||
|
||||
results, err := applyContainerSchemaChecks(&parsedConf, workload, container, false)
|
||||
results, err := applyContainerSchemaChecks(context.Background(), &parsedConf, workload, container, false)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -76,8 +78,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(expectedErrors)), summary.Errors)
|
||||
assert.ElementsMatch(t, expectedErrors, results.GetErrors())
|
||||
assert.Equal(t, uint(len(expectedDangers)), summary.Dangers)
|
||||
assert.ElementsMatch(t, expectedDangers, results.GetDangers())
|
||||
|
||||
assert.Equal(t, uint(len(expectedSuccesses)), summary.Successes)
|
||||
assert.ElementsMatch(t, expectedSuccesses, results.GetSuccesses())
|
||||
@@ -88,11 +90,11 @@ func TestValidateResourcesEmptyConfig(t *testing.T) {
|
||||
Name: "Empty",
|
||||
}
|
||||
|
||||
results, err := applyContainerSchemaChecks(&conf.Configuration{}, getEmptyWorkload(""), container, false)
|
||||
results, err := applyContainerSchemaChecks(context.Background(), &conf.Configuration{}, getEmptyWorkload(t, ""), container, false)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
assert.Equal(t, uint(0), results.GetSummary().Errors)
|
||||
assert.Equal(t, uint(0), results.GetSummary().Dangers)
|
||||
}
|
||||
|
||||
func TestValidateResourcesEmptyContainer(t *testing.T) {
|
||||
@@ -117,18 +119,18 @@ func TestValidateResourcesEmptyContainer(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
expectedErrors := []ResultMessage{
|
||||
expectedDangers := []ResultMessage{
|
||||
{
|
||||
ID: "cpuLimitsMissing",
|
||||
Success: false,
|
||||
Severity: "error",
|
||||
Severity: "danger",
|
||||
Message: "CPU limits should be set",
|
||||
Category: "Resources",
|
||||
},
|
||||
{
|
||||
ID: "memoryLimitsMissing",
|
||||
Success: false,
|
||||
Severity: "error",
|
||||
Severity: "danger",
|
||||
Message: "Memory limits should be set",
|
||||
Category: "Resources",
|
||||
},
|
||||
@@ -136,7 +138,7 @@ func TestValidateResourcesEmptyContainer(t *testing.T) {
|
||||
|
||||
expectedSuccesses := []ResultMessage{}
|
||||
|
||||
testValidate(t, &container, &resourceConfMinimal, "foo", expectedErrors, expectedWarnings, expectedSuccesses)
|
||||
testValidate(t, &container, &resourceConfMinimal, "foo", expectedDangers, expectedWarnings, expectedSuccesses)
|
||||
}
|
||||
|
||||
func TestValidateHealthChecks(t *testing.T) {
|
||||
@@ -148,7 +150,7 @@ func TestValidateHealthChecks(t *testing.T) {
|
||||
"livenessProbeMissing": conf.SeverityIgnore,
|
||||
}
|
||||
p3 := map[string]conf.Severity{
|
||||
"readinessProbeMissing": conf.SeverityError,
|
||||
"readinessProbeMissing": conf.SeverityDanger,
|
||||
"livenessProbeMissing": conf.SeverityWarning,
|
||||
}
|
||||
|
||||
@@ -161,7 +163,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: "error", Message: "Readiness probe should be configured", Category: "Health Checks"}
|
||||
r := ResultMessage{ID: "readinessProbeMissing", Success: false, Severity: "danger", Message: "Readiness probe should be configured", Category: "Health Checks"}
|
||||
f1 := []ResultMessage{}
|
||||
f2 := []ResultMessage{r}
|
||||
w1 := []ResultMessage{l}
|
||||
@@ -171,21 +173,21 @@ func TestValidateHealthChecks(t *testing.T) {
|
||||
probes map[string]conf.Severity
|
||||
container *corev1.Container
|
||||
isInit bool
|
||||
errors *[]ResultMessage
|
||||
dangers *[]ResultMessage
|
||||
warnings *[]ResultMessage
|
||||
}{
|
||||
{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},
|
||||
{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},
|
||||
}
|
||||
|
||||
for idx, tt := range testCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
controller := getEmptyWorkload("")
|
||||
results, err := applyContainerSchemaChecks(&conf.Configuration{Checks: tt.probes}, controller, tt.container, tt.isInit)
|
||||
controller := getEmptyWorkload(t, "")
|
||||
results, err := applyContainerSchemaChecks(context.Background(), &conf.Configuration{Checks: tt.probes}, controller, tt.container, tt.isInit)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -197,10 +199,10 @@ func TestValidateHealthChecks(t *testing.T) {
|
||||
assert.ElementsMatch(t, warnings, *tt.warnings, message)
|
||||
}
|
||||
|
||||
if tt.errors != nil {
|
||||
errors := results.GetErrors()
|
||||
assert.Len(t, errors, len(*tt.errors), message)
|
||||
assert.ElementsMatch(t, errors, *tt.errors, message)
|
||||
if tt.dangers != nil {
|
||||
dangers := results.GetDangers()
|
||||
assert.Len(t, dangers, len(*tt.dangers), message)
|
||||
assert.ElementsMatch(t, dangers, *tt.dangers, message)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -209,12 +211,12 @@ func TestValidateHealthChecks(t *testing.T) {
|
||||
func TestValidateImage(t *testing.T) {
|
||||
emptyConf := make(map[string]conf.Severity)
|
||||
standardConf := map[string]conf.Severity{
|
||||
"tagNotSpecified": conf.SeverityError,
|
||||
"tagNotSpecified": conf.SeverityDanger,
|
||||
"pullPolicyNotAlways": conf.SeverityIgnore,
|
||||
}
|
||||
strongConf := map[string]conf.Severity{
|
||||
"tagNotSpecified": conf.SeverityError,
|
||||
"pullPolicyNotAlways": conf.SeverityError,
|
||||
"tagNotSpecified": conf.SeverityDanger,
|
||||
"pullPolicyNotAlways": conf.SeverityDanger,
|
||||
}
|
||||
|
||||
emptyContainer := &corev1.Container{}
|
||||
@@ -242,7 +244,7 @@ func TestValidateImage(t *testing.T) {
|
||||
ID: "tagNotSpecified",
|
||||
Message: "Image tag should be specified",
|
||||
Success: false,
|
||||
Severity: "error",
|
||||
Severity: "danger",
|
||||
Category: "Images",
|
||||
}},
|
||||
},
|
||||
@@ -254,7 +256,7 @@ func TestValidateImage(t *testing.T) {
|
||||
ID: "tagNotSpecified",
|
||||
Message: "Image tag should be specified",
|
||||
Success: false,
|
||||
Severity: "error",
|
||||
Severity: "danger",
|
||||
Category: "Images",
|
||||
}},
|
||||
},
|
||||
@@ -266,7 +268,7 @@ func TestValidateImage(t *testing.T) {
|
||||
ID: "tagNotSpecified",
|
||||
Message: "Image tag should be specified",
|
||||
Success: false,
|
||||
Severity: "error",
|
||||
Severity: "danger",
|
||||
Category: "Images",
|
||||
}},
|
||||
},
|
||||
@@ -278,13 +280,13 @@ func TestValidateImage(t *testing.T) {
|
||||
ID: "pullPolicyNotAlways",
|
||||
Message: "Image pull policy should be \"Always\"",
|
||||
Success: false,
|
||||
Severity: "error",
|
||||
Severity: "danger",
|
||||
Category: "Images",
|
||||
}, {
|
||||
ID: "tagNotSpecified",
|
||||
Message: "Image tag should be specified",
|
||||
Success: false,
|
||||
Severity: "error",
|
||||
Severity: "danger",
|
||||
Category: "Images",
|
||||
}},
|
||||
},
|
||||
@@ -298,14 +300,14 @@ func TestValidateImage(t *testing.T) {
|
||||
|
||||
for _, tt := range testCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
controller := getEmptyWorkload("")
|
||||
results, err := applyContainerSchemaChecks(&conf.Configuration{Checks: tt.image}, controller, tt.container, false)
|
||||
controller := getEmptyWorkload(t, "")
|
||||
results, err := applyContainerSchemaChecks(context.Background(), &conf.Configuration{Checks: tt.image}, controller, tt.container, false)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
errors := results.GetErrors()
|
||||
assert.Len(t, errors, len(tt.expected))
|
||||
assert.ElementsMatch(t, errors, tt.expected)
|
||||
dangers := results.GetDangers()
|
||||
assert.Len(t, dangers, len(tt.expected))
|
||||
assert.ElementsMatch(t, dangers, tt.expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -317,7 +319,7 @@ func TestValidateNetworking(t *testing.T) {
|
||||
"hostPortSet": conf.SeverityWarning,
|
||||
}
|
||||
strongConf := map[string]conf.Severity{
|
||||
"hostPortSet": conf.SeverityError,
|
||||
"hostPortSet": conf.SeverityDanger,
|
||||
}
|
||||
|
||||
emptyContainer := &corev1.Container{Name: ""}
|
||||
@@ -407,7 +409,7 @@ func TestValidateNetworking(t *testing.T) {
|
||||
ID: "hostPortSet",
|
||||
Message: "Host port should not be configured",
|
||||
Success: false,
|
||||
Severity: "error",
|
||||
Severity: "danger",
|
||||
Category: "Networking",
|
||||
}},
|
||||
},
|
||||
@@ -415,8 +417,8 @@ func TestValidateNetworking(t *testing.T) {
|
||||
|
||||
for _, tt := range testCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
controller := getEmptyWorkload("")
|
||||
results, err := applyContainerSchemaChecks(&conf.Configuration{Checks: tt.networkConf}, controller, tt.container, false)
|
||||
controller := getEmptyWorkload(t, "")
|
||||
results, err := applyContainerSchemaChecks(context.Background(), &conf.Configuration{Checks: tt.networkConf}, controller, tt.container, false)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -438,19 +440,19 @@ func TestValidateSecurity(t *testing.T) {
|
||||
emptyConf := map[string]conf.Severity{}
|
||||
standardConf := map[string]conf.Severity{
|
||||
"runAsRootAllowed": conf.SeverityWarning,
|
||||
"runAsPrivileged": conf.SeverityError,
|
||||
"runAsPrivileged": conf.SeverityDanger,
|
||||
"notReadOnlyRootFilesystem": conf.SeverityWarning,
|
||||
"privilegeEscalationAllowed": conf.SeverityError,
|
||||
"dangerousCapabilities": conf.SeverityError,
|
||||
"privilegeEscalationAllowed": conf.SeverityDanger,
|
||||
"dangerousCapabilities": conf.SeverityDanger,
|
||||
"insecureCapabilities": conf.SeverityWarning,
|
||||
}
|
||||
strongConf := map[string]conf.Severity{
|
||||
"runAsRootAllowed": conf.SeverityError,
|
||||
"runAsPrivileged": conf.SeverityError,
|
||||
"notReadOnlyRootFilesystem": conf.SeverityError,
|
||||
"privilegeEscalationAllowed": conf.SeverityError,
|
||||
"dangerousCapabilities": conf.SeverityError,
|
||||
"insecureCapabilities": conf.SeverityError,
|
||||
"runAsRootAllowed": conf.SeverityDanger,
|
||||
"runAsPrivileged": conf.SeverityDanger,
|
||||
"notReadOnlyRootFilesystem": conf.SeverityDanger,
|
||||
"privilegeEscalationAllowed": conf.SeverityDanger,
|
||||
"dangerousCapabilities": conf.SeverityDanger,
|
||||
"insecureCapabilities": conf.SeverityDanger,
|
||||
}
|
||||
|
||||
emptyContainer := &corev1.Container{Name: ""}
|
||||
@@ -549,13 +551,13 @@ func TestValidateSecurity(t *testing.T) {
|
||||
ID: "runAsPrivileged",
|
||||
Message: "Not running as privileged",
|
||||
Success: true,
|
||||
Severity: "error",
|
||||
Severity: "danger",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "privilegeEscalationAllowed",
|
||||
Message: "Privilege escalation not allowed",
|
||||
Success: true,
|
||||
Severity: "error",
|
||||
Severity: "danger",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "insecureCapabilities",
|
||||
@@ -567,7 +569,7 @@ func TestValidateSecurity(t *testing.T) {
|
||||
ID: "dangerousCapabilities",
|
||||
Message: "Container does not have any dangerous capabilities",
|
||||
Success: true,
|
||||
Severity: "error",
|
||||
Severity: "danger",
|
||||
Category: "Security",
|
||||
}},
|
||||
},
|
||||
@@ -580,19 +582,19 @@ func TestValidateSecurity(t *testing.T) {
|
||||
ID: "dangerousCapabilities",
|
||||
Message: "Container should not have dangerous capabilities",
|
||||
Success: false,
|
||||
Severity: "error",
|
||||
Severity: "danger",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "privilegeEscalationAllowed",
|
||||
Message: "Privilege escalation should not be allowed",
|
||||
Success: false,
|
||||
Severity: "error",
|
||||
Severity: "danger",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "runAsPrivileged",
|
||||
Message: "Should not be running as privileged",
|
||||
Success: false,
|
||||
Severity: "error",
|
||||
Severity: "danger",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "insecureCapabilities",
|
||||
@@ -623,19 +625,19 @@ func TestValidateSecurity(t *testing.T) {
|
||||
ID: "dangerousCapabilities",
|
||||
Message: "Container should not have dangerous capabilities",
|
||||
Success: false,
|
||||
Severity: "error",
|
||||
Severity: "danger",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "privilegeEscalationAllowed",
|
||||
Message: "Privilege escalation should not be allowed",
|
||||
Success: false,
|
||||
Severity: "error",
|
||||
Severity: "danger",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "runAsPrivileged",
|
||||
Message: "Should not be running as privileged",
|
||||
Success: false,
|
||||
Severity: "error",
|
||||
Severity: "danger",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "insecureCapabilities",
|
||||
@@ -666,7 +668,7 @@ func TestValidateSecurity(t *testing.T) {
|
||||
ID: "dangerousCapabilities",
|
||||
Message: "Container should not have dangerous capabilities",
|
||||
Success: false,
|
||||
Severity: "error",
|
||||
Severity: "danger",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "insecureCapabilities",
|
||||
@@ -678,13 +680,13 @@ func TestValidateSecurity(t *testing.T) {
|
||||
ID: "privilegeEscalationAllowed",
|
||||
Message: "Privilege escalation should not be allowed",
|
||||
Success: false,
|
||||
Severity: "error",
|
||||
Severity: "danger",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "runAsPrivileged",
|
||||
Message: "Should not be running as privileged",
|
||||
Success: false,
|
||||
Severity: "error",
|
||||
Severity: "danger",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "runAsRootAllowed",
|
||||
@@ -721,19 +723,19 @@ func TestValidateSecurity(t *testing.T) {
|
||||
ID: "runAsPrivileged",
|
||||
Message: "Not running as privileged",
|
||||
Success: true,
|
||||
Severity: "error",
|
||||
Severity: "danger",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "privilegeEscalationAllowed",
|
||||
Message: "Privilege escalation not allowed",
|
||||
Success: true,
|
||||
Severity: "error",
|
||||
Severity: "danger",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "dangerousCapabilities",
|
||||
Message: "Container does not have any dangerous capabilities",
|
||||
Success: true,
|
||||
Severity: "error",
|
||||
Severity: "danger",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "insecureCapabilities",
|
||||
@@ -752,37 +754,37 @@ func TestValidateSecurity(t *testing.T) {
|
||||
ID: "dangerousCapabilities",
|
||||
Message: "Container does not have any dangerous capabilities",
|
||||
Success: true,
|
||||
Severity: "error",
|
||||
Severity: "danger",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "insecureCapabilities",
|
||||
Message: "Container does not have any insecure capabilities",
|
||||
Success: true,
|
||||
Severity: "error",
|
||||
Severity: "danger",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "runAsRootAllowed",
|
||||
Message: "Is not allowed to run as root",
|
||||
Success: true,
|
||||
Severity: "error",
|
||||
Severity: "danger",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "notReadOnlyRootFilesystem",
|
||||
Message: "Filesystem is read only",
|
||||
Success: true,
|
||||
Severity: "error",
|
||||
Severity: "danger",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "runAsPrivileged",
|
||||
Message: "Not running as privileged",
|
||||
Success: true,
|
||||
Severity: "error",
|
||||
Severity: "danger",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "privilegeEscalationAllowed",
|
||||
Message: "Privilege escalation not allowed",
|
||||
Success: true,
|
||||
Severity: "error",
|
||||
Severity: "danger",
|
||||
Category: "Security",
|
||||
}},
|
||||
},
|
||||
@@ -795,37 +797,37 @@ func TestValidateSecurity(t *testing.T) {
|
||||
ID: "runAsRootAllowed",
|
||||
Message: "Is not allowed to run as root",
|
||||
Success: true,
|
||||
Severity: "error",
|
||||
Severity: "danger",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "notReadOnlyRootFilesystem",
|
||||
Message: "Filesystem is read only",
|
||||
Success: true,
|
||||
Severity: "error",
|
||||
Severity: "danger",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "runAsPrivileged",
|
||||
Message: "Not running as privileged",
|
||||
Success: true,
|
||||
Severity: "error",
|
||||
Severity: "danger",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "privilegeEscalationAllowed",
|
||||
Message: "Privilege escalation not allowed",
|
||||
Success: true,
|
||||
Severity: "error",
|
||||
Severity: "danger",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "dangerousCapabilities",
|
||||
Message: "Container does not have any dangerous capabilities",
|
||||
Success: true,
|
||||
Severity: "error",
|
||||
Severity: "danger",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "insecureCapabilities",
|
||||
Message: "Container does not have any insecure capabilities",
|
||||
Success: true,
|
||||
Severity: "error",
|
||||
Severity: "danger",
|
||||
Category: "Security",
|
||||
}},
|
||||
},
|
||||
@@ -838,37 +840,37 @@ func TestValidateSecurity(t *testing.T) {
|
||||
ID: "runAsRootAllowed",
|
||||
Message: "Is not allowed to run as root",
|
||||
Success: true,
|
||||
Severity: "error",
|
||||
Severity: "danger",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "notReadOnlyRootFilesystem",
|
||||
Message: "Filesystem is read only",
|
||||
Success: true,
|
||||
Severity: "error",
|
||||
Severity: "danger",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "runAsPrivileged",
|
||||
Message: "Not running as privileged",
|
||||
Success: true,
|
||||
Severity: "error",
|
||||
Severity: "danger",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "privilegeEscalationAllowed",
|
||||
Message: "Privilege escalation not allowed",
|
||||
Success: true,
|
||||
Severity: "error",
|
||||
Severity: "danger",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "dangerousCapabilities",
|
||||
Message: "Container does not have any dangerous capabilities",
|
||||
Success: true,
|
||||
Severity: "error",
|
||||
Severity: "danger",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "insecureCapabilities",
|
||||
Message: "Container does not have any insecure capabilities",
|
||||
Success: true,
|
||||
Severity: "error",
|
||||
Severity: "danger",
|
||||
Category: "Security",
|
||||
}},
|
||||
},
|
||||
@@ -881,37 +883,37 @@ func TestValidateSecurity(t *testing.T) {
|
||||
ID: "runAsRootAllowed",
|
||||
Message: "Is not allowed to run as root",
|
||||
Success: true,
|
||||
Severity: "error",
|
||||
Severity: "danger",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "notReadOnlyRootFilesystem",
|
||||
Message: "Filesystem is read only",
|
||||
Success: true,
|
||||
Severity: "error",
|
||||
Severity: "danger",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "runAsPrivileged",
|
||||
Message: "Not running as privileged",
|
||||
Success: true,
|
||||
Severity: "error",
|
||||
Severity: "danger",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "privilegeEscalationAllowed",
|
||||
Message: "Privilege escalation not allowed",
|
||||
Success: true,
|
||||
Severity: "error",
|
||||
Severity: "danger",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "dangerousCapabilities",
|
||||
Message: "Container does not have any dangerous capabilities",
|
||||
Success: true,
|
||||
Severity: "error",
|
||||
Severity: "danger",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "insecureCapabilities",
|
||||
Message: "Container does not have any insecure capabilities",
|
||||
Success: true,
|
||||
Severity: "error",
|
||||
Severity: "danger",
|
||||
Category: "Security",
|
||||
}},
|
||||
},
|
||||
@@ -919,8 +921,9 @@ func TestValidateSecurity(t *testing.T) {
|
||||
|
||||
for _, tt := range testCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
workload := kube.NewGenericWorkloadFromPod(corev1.Pod{Spec: *tt.pod})
|
||||
results, err := applyContainerSchemaChecks(&conf.Configuration{Checks: tt.securityConf}, workload, tt.container, false)
|
||||
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)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -1063,8 +1066,9 @@ func TestValidateRunAsRoot(t *testing.T) {
|
||||
}
|
||||
for idx, tt := range testCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
workload := kube.NewGenericWorkloadFromPod(corev1.Pod{Spec: *tt.pod})
|
||||
results, err := applyContainerSchemaChecks(&config, workload, tt.container, false)
|
||||
workload, err := kube.NewGenericWorkloadFromPod(corev1.Pod{Spec: *tt.pod}, nil)
|
||||
assert.NoError(t, err)
|
||||
results, err := applyContainerSchemaChecks(context.Background(), &config, workload, tt.container, false)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -1086,10 +1090,10 @@ func TestValidateResourcesExemption(t *testing.T) {
|
||||
}
|
||||
|
||||
expectedWarnings := []ResultMessage{}
|
||||
expectedErrors := []ResultMessage{}
|
||||
expectedDangers := []ResultMessage{}
|
||||
expectedSuccesses := []ResultMessage{}
|
||||
|
||||
testValidate(t, &container, &resourceConfExemptions, "foo", expectedErrors, expectedWarnings, expectedSuccesses)
|
||||
testValidate(t, &container, &resourceConfExemptions, "foo", expectedDangers, expectedWarnings, expectedSuccesses)
|
||||
|
||||
expectedWarnings = []ResultMessage{
|
||||
{
|
||||
@@ -1108,18 +1112,18 @@ func TestValidateResourcesExemption(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
expectedErrors = []ResultMessage{
|
||||
expectedDangers = []ResultMessage{
|
||||
{
|
||||
ID: "cpuLimitsMissing",
|
||||
Success: false,
|
||||
Severity: "error",
|
||||
Severity: "danger",
|
||||
Message: "CPU limits should be set",
|
||||
Category: "Resources",
|
||||
},
|
||||
{
|
||||
ID: "memoryLimitsMissing",
|
||||
Success: false,
|
||||
Severity: "error",
|
||||
Severity: "danger",
|
||||
Message: "Memory limits should be set",
|
||||
Category: "Resources",
|
||||
},
|
||||
@@ -1127,7 +1131,7 @@ func TestValidateResourcesExemption(t *testing.T) {
|
||||
|
||||
disallowExemptionsConf := resourceConfExemptions + "\ndisallowExemptions: true"
|
||||
|
||||
testValidate(t, &container, &disallowExemptionsConf, "foo", expectedErrors, expectedWarnings, expectedSuccesses)
|
||||
testValidate(t, &container, &disallowExemptionsConf, "foo", expectedDangers, expectedWarnings, expectedSuccesses)
|
||||
}
|
||||
|
||||
func TestValidateResourcesEmptyContainerCPURequestsExempt(t *testing.T) {
|
||||
@@ -1145,18 +1149,18 @@ func TestValidateResourcesEmptyContainerCPURequestsExempt(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
expectedErrors := []ResultMessage{
|
||||
expectedDangers := []ResultMessage{
|
||||
{
|
||||
ID: "cpuLimitsMissing",
|
||||
Success: false,
|
||||
Severity: "error",
|
||||
Severity: "danger",
|
||||
Message: "CPU limits should be set",
|
||||
Category: "Resources",
|
||||
},
|
||||
{
|
||||
ID: "memoryLimitsMissing",
|
||||
Success: false,
|
||||
Severity: "error",
|
||||
Severity: "danger",
|
||||
Message: "Memory limits should be set",
|
||||
Category: "Resources",
|
||||
},
|
||||
@@ -1164,7 +1168,7 @@ func TestValidateResourcesEmptyContainerCPURequestsExempt(t *testing.T) {
|
||||
|
||||
expectedSuccesses := []ResultMessage{}
|
||||
|
||||
workload := kube.NewGenericWorkloadFromPod(corev1.Pod{
|
||||
workload, err := kube.NewGenericWorkloadFromPod(corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo",
|
||||
Annotations: map[string]string{
|
||||
@@ -1172,6 +1176,7 @@ func TestValidateResourcesEmptyContainerCPURequestsExempt(t *testing.T) {
|
||||
"polaris.fairwinds.com/memoryRequestsMissing-exempt": "truthy", // Don't actually exempt this controller from memoryRequestsMissing
|
||||
},
|
||||
},
|
||||
})
|
||||
testValidateWithWorkload(t, &container, &resourceConfMinimal, workload, expectedErrors, expectedWarnings, expectedSuccesses)
|
||||
}, nil)
|
||||
assert.NoError(t, err)
|
||||
testValidateWithWorkload(t, &container, &resourceConfMinimal, workload, expectedDangers, expectedWarnings, expectedSuccesses)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
@@ -26,16 +27,22 @@ import (
|
||||
const exemptionAnnotationKey = "polaris.fairwinds.com/exempt"
|
||||
|
||||
// ValidateController validates a single controller, returns a ControllerResult.
|
||||
func ValidateController(conf *conf.Configuration, controller kube.GenericWorkload) (ControllerResult, error) {
|
||||
podResult, err := ValidatePod(conf, controller)
|
||||
func ValidateController(ctx context.Context, conf *conf.Configuration, controller kube.GenericWorkload) (ControllerResult, error) {
|
||||
podResult, err := ValidatePod(ctx, 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: ResultSet{},
|
||||
Results: controllerResult,
|
||||
PodResult: podResult,
|
||||
}
|
||||
|
||||
@@ -44,7 +51,7 @@ func ValidateController(conf *conf.Configuration, controller kube.GenericWorkloa
|
||||
|
||||
// ValidateControllers validates that each deployment conforms to the Polaris config,
|
||||
// builds a list of ResourceResults organized by namespace.
|
||||
func ValidateControllers(config *conf.Configuration, kubeResources *kube.ResourceProvider) ([]ControllerResult, error) {
|
||||
func ValidateControllers(ctx context.Context, config *conf.Configuration, kubeResources *kube.ResourceProvider) ([]ControllerResult, error) {
|
||||
controllersToAudit := kubeResources.Controllers
|
||||
|
||||
results := []ControllerResult{}
|
||||
@@ -52,7 +59,7 @@ func ValidateControllers(config *conf.Configuration, kubeResources *kube.Resourc
|
||||
if !config.DisallowExemptions && hasExemptionAnnotation(controller) {
|
||||
continue
|
||||
}
|
||||
result, err := ValidateController(config, controller)
|
||||
result, err := ValidateController(ctx, config, controller)
|
||||
if err != nil {
|
||||
logrus.Warn("An error occured validating controller:", err)
|
||||
return nil, err
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -28,24 +29,25 @@ import (
|
||||
func TestValidateController(t *testing.T) {
|
||||
c := conf.Configuration{
|
||||
Checks: map[string]conf.Severity{
|
||||
"hostIPCSet": conf.SeverityError,
|
||||
"hostPIDSet": conf.SeverityError,
|
||||
"hostIPCSet": conf.SeverityDanger,
|
||||
"hostPIDSet": conf.SeverityDanger,
|
||||
},
|
||||
}
|
||||
deployment := kube.NewGenericWorkloadFromPod(test.MockPod())
|
||||
deployment, err := kube.NewGenericWorkloadFromPod(test.MockPod(), nil)
|
||||
assert.NoError(t, err)
|
||||
deployment.Kind = "Deployment"
|
||||
expectedSum := CountSummary{
|
||||
Successes: uint(2),
|
||||
Warnings: uint(0),
|
||||
Errors: uint(0),
|
||||
Dangers: uint(0),
|
||||
}
|
||||
|
||||
expectedResults := ResultSet{
|
||||
"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"},
|
||||
"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"},
|
||||
}
|
||||
|
||||
actualResult, err := ValidateController(&c, deployment)
|
||||
actualResult, err := ValidateController(context.Background(), &c, deployment)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -56,27 +58,66 @@ 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.SeverityError,
|
||||
"readinessProbeMissing": conf.SeverityDanger,
|
||||
"livenessProbeMissing": conf.SeverityWarning,
|
||||
},
|
||||
}
|
||||
pod := test.MockPod()
|
||||
pod.Spec.InitContainers = []corev1.Container{test.MockContainer("test")}
|
||||
deployment := kube.NewGenericWorkloadFromPod(pod)
|
||||
deployment, err := kube.NewGenericWorkloadFromPod(pod, nil)
|
||||
assert.NoError(t, err)
|
||||
deployment.Kind = "Deployment"
|
||||
expectedSum := CountSummary{
|
||||
Successes: uint(0),
|
||||
Warnings: uint(1),
|
||||
Errors: uint(1),
|
||||
Dangers: uint(1),
|
||||
}
|
||||
expectedResults := ResultSet{
|
||||
"readinessProbeMissing": {ID: "readinessProbeMissing", Message: "Readiness probe should be configured", Success: false, Severity: "error", Category: "Health Checks"},
|
||||
"readinessProbeMissing": {ID: "readinessProbeMissing", Message: "Readiness probe should be configured", Success: false, Severity: "danger", Category: "Health Checks"},
|
||||
"livenessProbeMissing": {ID: "livenessProbeMissing", Message: "Liveness probe should be configured", Success: false, Severity: "warning", Category: "Health Checks"},
|
||||
}
|
||||
actualResult, err := ValidateController(&c, deployment)
|
||||
actualResult, err := ValidateController(context.Background(), &c, deployment)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -86,15 +127,16 @@ func TestSkipHealthChecks(t *testing.T) {
|
||||
assert.EqualValues(t, ResultSet{}, actualResult.PodResult.ContainerResults[0].Results)
|
||||
assert.EqualValues(t, expectedResults, actualResult.PodResult.ContainerResults[1].Results)
|
||||
|
||||
job := kube.NewGenericWorkloadFromPod(test.MockPod())
|
||||
job, err := kube.NewGenericWorkloadFromPod(test.MockPod(), nil)
|
||||
assert.NoError(t, err)
|
||||
job.Kind = "Job"
|
||||
expectedSum = CountSummary{
|
||||
Successes: uint(0),
|
||||
Warnings: uint(0),
|
||||
Errors: uint(0),
|
||||
Dangers: uint(0),
|
||||
}
|
||||
expectedResults = ResultSet{}
|
||||
actualResult, err = ValidateController(&c, job)
|
||||
actualResult, err = ValidateController(context.Background(), &c, job)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -103,15 +145,16 @@ func TestSkipHealthChecks(t *testing.T) {
|
||||
assert.EqualValues(t, expectedSum, actualResult.GetSummary())
|
||||
assert.EqualValues(t, expectedResults, actualResult.PodResult.ContainerResults[0].Results)
|
||||
|
||||
cronjob := kube.NewGenericWorkloadFromPod(test.MockPod())
|
||||
cronjob, err := kube.NewGenericWorkloadFromPod(test.MockPod(), nil)
|
||||
assert.NoError(t, err)
|
||||
cronjob.Kind = "CronJob"
|
||||
expectedSum = CountSummary{
|
||||
Successes: uint(0),
|
||||
Warnings: uint(0),
|
||||
Errors: uint(0),
|
||||
Dangers: uint(0),
|
||||
}
|
||||
expectedResults = ResultSet{}
|
||||
actualResult, err = ValidateController(&c, cronjob)
|
||||
actualResult, err = ValidateController(context.Background(), &c, cronjob)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -124,12 +167,13 @@ func TestSkipHealthChecks(t *testing.T) {
|
||||
func TestControllerExemptions(t *testing.T) {
|
||||
c := conf.Configuration{
|
||||
Checks: map[string]conf.Severity{
|
||||
"readinessProbeMissing": conf.SeverityError,
|
||||
"readinessProbeMissing": conf.SeverityDanger,
|
||||
"livenessProbeMissing": conf.SeverityWarning,
|
||||
},
|
||||
}
|
||||
pod := test.MockPod()
|
||||
workload := kube.NewGenericWorkloadFromPod(pod)
|
||||
workload, err := kube.NewGenericWorkloadFromPod(pod, nil)
|
||||
assert.NoError(t, err)
|
||||
workload.Kind = "Deployment"
|
||||
resources := &kube.ResourceProvider{
|
||||
Controllers: []kube.GenericWorkload{workload},
|
||||
@@ -138,9 +182,9 @@ func TestControllerExemptions(t *testing.T) {
|
||||
expectedSum := CountSummary{
|
||||
Successes: uint(0),
|
||||
Warnings: uint(1),
|
||||
Errors: uint(1),
|
||||
Dangers: uint(1),
|
||||
}
|
||||
actualResults, err := ValidateControllers(&c, resources)
|
||||
actualResults, err := ValidateControllers(context.Background(), &c, resources)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -151,7 +195,7 @@ func TestControllerExemptions(t *testing.T) {
|
||||
resources.Controllers[0].ObjectMeta.SetAnnotations(map[string]string{
|
||||
exemptionAnnotationKey: "true",
|
||||
})
|
||||
actualResults, err = ValidateControllers(&c, resources)
|
||||
actualResults, err = ValidateControllers(context.Background(), &c, resources)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package validator
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
@@ -16,13 +17,13 @@ import (
|
||||
)
|
||||
|
||||
// RunAudit runs a full Polaris audit and returns an AuditData object
|
||||
func RunAudit(config conf.Configuration, kubeResources *kube.ResourceProvider) (AuditData, error) {
|
||||
func RunAudit(ctx context.Context, config conf.Configuration, kubeResources *kube.ResourceProvider) (AuditData, error) {
|
||||
displayName := config.DisplayName
|
||||
if displayName == "" {
|
||||
displayName = kubeResources.SourceName
|
||||
}
|
||||
|
||||
results, err := ValidateControllers(&config, kubeResources)
|
||||
results, err := ValidateControllers(ctx, &config, kubeResources)
|
||||
if err != nil {
|
||||
return AuditData{}, err
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
conf "github.com/fairwindsops/polaris/pkg/config"
|
||||
@@ -11,16 +12,16 @@ import (
|
||||
|
||||
func TestGetTemplateData(t *testing.T) {
|
||||
k8s, dynamicClient := test.SetupTestAPI()
|
||||
k8s = test.SetupAddControllers(k8s, "test")
|
||||
k8s = test.SetupAddExtraControllerVersions(k8s, "test-extra")
|
||||
k8s = test.SetupAddControllers(context.Background(), k8s, "test")
|
||||
k8s = test.SetupAddExtraControllerVersions(context.Background(), 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(k8s, "test", &dynamicClient)
|
||||
resources, err := kube.CreateResourceProviderFromAPI(context.Background(), k8s, "test", &dynamicClient)
|
||||
assert.Equal(t, err, nil, "error should be nil")
|
||||
|
||||
c := conf.Configuration{
|
||||
Checks: map[string]conf.Severity{
|
||||
"readinessProbeMissing": conf.SeverityError,
|
||||
"readinessProbeMissing": conf.SeverityDanger,
|
||||
"livenessProbeMissing": conf.SeverityWarning,
|
||||
},
|
||||
}
|
||||
@@ -28,10 +29,11 @@ func TestGetTemplateData(t *testing.T) {
|
||||
sum := CountSummary{
|
||||
Successes: uint(0),
|
||||
Warnings: uint(1),
|
||||
Errors: uint(1),
|
||||
Dangers: uint(1),
|
||||
}
|
||||
|
||||
actualAudit, err := RunAudit(c, resources)
|
||||
actualAudit, err := RunAudit(context.Background(), c, resources)
|
||||
|
||||
assert.Equal(t, err, nil, "error should be nil")
|
||||
|
||||
assert.EqualValues(t, sum, actualAudit.GetSummary())
|
||||
|
||||
@@ -15,13 +15,15 @@
|
||||
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(conf *config.Configuration, controller kube.GenericWorkload) (PodResult, error) {
|
||||
podResults, err := applyPodSchemaChecks(conf, controller)
|
||||
func ValidatePod(ctx context.Context, conf *config.Configuration, controller kube.GenericWorkload) (PodResult, error) {
|
||||
podResults, err := applyPodSchemaChecks(ctx, conf, controller)
|
||||
if err != nil {
|
||||
return PodResult{}, err
|
||||
}
|
||||
@@ -30,7 +32,7 @@ func ValidatePod(conf *config.Configuration, controller kube.GenericWorkload) (P
|
||||
ContainerResults: []ContainerResult{},
|
||||
}
|
||||
|
||||
pRes.ContainerResults, err = ValidateAllContainers(conf, controller)
|
||||
pRes.ContainerResults, err = ValidateAllContainers(ctx, conf, controller)
|
||||
if err != nil {
|
||||
return pRes, err
|
||||
}
|
||||
|
||||
+50
-49
@@ -15,6 +15,7 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -28,31 +29,31 @@ import (
|
||||
func TestValidatePod(t *testing.T) {
|
||||
c := conf.Configuration{
|
||||
Checks: map[string]conf.Severity{
|
||||
"hostIPCSet": conf.SeverityError,
|
||||
"hostPIDSet": conf.SeverityError,
|
||||
"hostIPCSet": conf.SeverityDanger,
|
||||
"hostPIDSet": conf.SeverityDanger,
|
||||
"hostNetworkSet": conf.SeverityWarning,
|
||||
"hostPortSet": conf.SeverityError,
|
||||
"hostPortSet": conf.SeverityDanger,
|
||||
},
|
||||
}
|
||||
|
||||
k8s, _ := test.SetupTestAPI()
|
||||
k8s = test.SetupAddControllers(k8s, "test")
|
||||
k8s = test.SetupAddControllers(context.Background(), k8s, "test")
|
||||
p := test.MockPod()
|
||||
deployment := kube.NewGenericWorkloadFromPod(p)
|
||||
|
||||
deployment, err := kube.NewGenericWorkloadFromPod(p, nil)
|
||||
assert.NoError(t, err)
|
||||
expectedSum := CountSummary{
|
||||
Successes: uint(4),
|
||||
Warnings: uint(0),
|
||||
Errors: uint(0),
|
||||
Dangers: uint(0),
|
||||
}
|
||||
|
||||
expectedResults := ResultSet{
|
||||
"hostIPCSet": {ID: "hostIPCSet", Message: "Host IPC is not configured", Success: true, Severity: "error", Category: "Security"},
|
||||
"hostIPCSet": {ID: "hostIPCSet", Message: "Host IPC is not configured", Success: true, Severity: "danger", 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: "error", Category: "Security"},
|
||||
"hostPIDSet": {ID: "hostPIDSet", Message: "Host PID is not configured", Success: true, Severity: "danger", Category: "Security"},
|
||||
}
|
||||
|
||||
actualPodResult, err := ValidatePod(&c, deployment)
|
||||
actualPodResult, err := ValidatePod(context.Background(), &c, deployment)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -65,31 +66,31 @@ func TestValidatePod(t *testing.T) {
|
||||
func TestInvalidIPCPod(t *testing.T) {
|
||||
c := conf.Configuration{
|
||||
Checks: map[string]conf.Severity{
|
||||
"hostIPCSet": conf.SeverityError,
|
||||
"hostPIDSet": conf.SeverityError,
|
||||
"hostIPCSet": conf.SeverityDanger,
|
||||
"hostPIDSet": conf.SeverityDanger,
|
||||
"hostNetworkSet": conf.SeverityWarning,
|
||||
"hostPortSet": conf.SeverityError,
|
||||
"hostPortSet": conf.SeverityDanger,
|
||||
},
|
||||
}
|
||||
|
||||
k8s, _ := test.SetupTestAPI()
|
||||
k8s = test.SetupAddControllers(k8s, "test")
|
||||
k8s = test.SetupAddControllers(context.Background(), k8s, "test")
|
||||
p := test.MockPod()
|
||||
p.Spec.HostIPC = true
|
||||
workload := kube.NewGenericWorkloadFromPod(p)
|
||||
|
||||
workload, err := kube.NewGenericWorkloadFromPod(p, nil)
|
||||
assert.NoError(t, err)
|
||||
expectedSum := CountSummary{
|
||||
Successes: uint(3),
|
||||
Warnings: uint(0),
|
||||
Errors: uint(1),
|
||||
Dangers: uint(1),
|
||||
}
|
||||
expectedResults := ResultSet{
|
||||
"hostIPCSet": {ID: "hostIPCSet", Message: "Host IPC should not be configured", Success: false, Severity: "error", Category: "Security"},
|
||||
"hostIPCSet": {ID: "hostIPCSet", Message: "Host IPC should not be configured", Success: false, Severity: "danger", 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: "error", Category: "Security"},
|
||||
"hostPIDSet": {ID: "hostPIDSet", Message: "Host PID is not configured", Success: true, Severity: "danger", Category: "Security"},
|
||||
}
|
||||
|
||||
actualPodResult, err := ValidatePod(&c, workload)
|
||||
actualPodResult, err := ValidatePod(context.Background(), &c, workload)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -103,31 +104,31 @@ func TestInvalidNeworkPod(t *testing.T) {
|
||||
c := conf.Configuration{
|
||||
Checks: map[string]conf.Severity{
|
||||
"hostNetworkSet": conf.SeverityWarning,
|
||||
"hostPortSet": conf.SeverityError,
|
||||
"hostIPCSet": conf.SeverityError,
|
||||
"hostPIDSet": conf.SeverityError,
|
||||
"hostPortSet": conf.SeverityDanger,
|
||||
"hostIPCSet": conf.SeverityDanger,
|
||||
"hostPIDSet": conf.SeverityDanger,
|
||||
},
|
||||
}
|
||||
|
||||
k8s, _ := test.SetupTestAPI()
|
||||
k8s = test.SetupAddControllers(k8s, "test")
|
||||
k8s = test.SetupAddControllers(context.Background(), k8s, "test")
|
||||
p := test.MockPod()
|
||||
p.Spec.HostNetwork = true
|
||||
workload := kube.NewGenericWorkloadFromPod(p)
|
||||
|
||||
workload, err := kube.NewGenericWorkloadFromPod(p, nil)
|
||||
assert.NoError(t, err)
|
||||
expectedSum := CountSummary{
|
||||
Successes: uint(3),
|
||||
Warnings: uint(1),
|
||||
Errors: uint(0),
|
||||
Dangers: 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: "error", Category: "Security"},
|
||||
"hostPIDSet": {ID: "hostPIDSet", Message: "Host PID is not configured", Success: true, Severity: "error", Category: "Security"},
|
||||
"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"},
|
||||
}
|
||||
|
||||
actualPodResult, err := ValidatePod(&c, workload)
|
||||
actualPodResult, err := ValidatePod(context.Background(), &c, workload)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -140,32 +141,32 @@ func TestInvalidNeworkPod(t *testing.T) {
|
||||
func TestInvalidPIDPod(t *testing.T) {
|
||||
c := conf.Configuration{
|
||||
Checks: map[string]conf.Severity{
|
||||
"hostIPCSet": conf.SeverityError,
|
||||
"hostPIDSet": conf.SeverityError,
|
||||
"hostIPCSet": conf.SeverityDanger,
|
||||
"hostPIDSet": conf.SeverityDanger,
|
||||
"hostNetworkSet": conf.SeverityWarning,
|
||||
"hostPortSet": conf.SeverityError,
|
||||
"hostPortSet": conf.SeverityDanger,
|
||||
},
|
||||
}
|
||||
|
||||
k8s, _ := test.SetupTestAPI()
|
||||
k8s = test.SetupAddControllers(k8s, "test")
|
||||
k8s = test.SetupAddControllers(context.Background(), k8s, "test")
|
||||
p := test.MockPod()
|
||||
p.Spec.HostPID = true
|
||||
workload := kube.NewGenericWorkloadFromPod(p)
|
||||
|
||||
workload, err := kube.NewGenericWorkloadFromPod(p, nil)
|
||||
assert.NoError(t, err)
|
||||
expectedSum := CountSummary{
|
||||
Successes: uint(3),
|
||||
Warnings: uint(0),
|
||||
Errors: uint(1),
|
||||
Dangers: uint(1),
|
||||
}
|
||||
|
||||
expectedResults := ResultSet{
|
||||
"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"},
|
||||
"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"},
|
||||
"hostNetworkSet": {ID: "hostNetworkSet", Message: "Host network is not configured", Success: true, Severity: "warning", Category: "Networking"},
|
||||
}
|
||||
|
||||
actualPodResult, err := ValidatePod(&c, workload)
|
||||
actualPodResult, err := ValidatePod(context.Background(), &c, workload)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -178,10 +179,10 @@ func TestInvalidPIDPod(t *testing.T) {
|
||||
func TestExemption(t *testing.T) {
|
||||
c := conf.Configuration{
|
||||
Checks: map[string]conf.Severity{
|
||||
"hostIPCSet": conf.SeverityError,
|
||||
"hostIPCSet": conf.SeverityDanger,
|
||||
"hostNetworkSet": conf.SeverityWarning,
|
||||
"hostPIDSet": conf.SeverityError,
|
||||
"hostPortSet": conf.SeverityError,
|
||||
"hostPIDSet": conf.SeverityDanger,
|
||||
"hostPortSet": conf.SeverityDanger,
|
||||
},
|
||||
Exemptions: []conf.Exemption{
|
||||
conf.Exemption{
|
||||
@@ -192,25 +193,25 @@ func TestExemption(t *testing.T) {
|
||||
}
|
||||
|
||||
k8s, _ := test.SetupTestAPI()
|
||||
k8s = test.SetupAddControllers(k8s, "test")
|
||||
k8s = test.SetupAddControllers(context.Background(), k8s, "test")
|
||||
p := test.MockPod()
|
||||
p.Spec.HostIPC = true
|
||||
p.ObjectMeta = metav1.ObjectMeta{
|
||||
Name: "foo",
|
||||
}
|
||||
workload := kube.NewGenericWorkloadFromPod(p)
|
||||
|
||||
workload, err := kube.NewGenericWorkloadFromPod(p, nil)
|
||||
assert.NoError(t, err)
|
||||
expectedSum := CountSummary{
|
||||
Successes: uint(3),
|
||||
Warnings: uint(0),
|
||||
Errors: uint(0),
|
||||
Dangers: 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: "error", Category: "Security"},
|
||||
"hostPIDSet": {ID: "hostPIDSet", Message: "Host PID is not configured", Success: true, Severity: "danger", Category: "Security"},
|
||||
}
|
||||
|
||||
actualPodResult, err := ValidatePod(&c, workload)
|
||||
actualPodResult, err := ValidatePod(context.Background(), &c, workload)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
+31
-2
@@ -2,6 +2,7 @@ package validator
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
@@ -21,6 +22,8 @@ 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",
|
||||
@@ -41,6 +44,7 @@ var (
|
||||
"privilegeEscalationAllowed",
|
||||
"dangerousCapabilities",
|
||||
"insecureCapabilities",
|
||||
"priorityClassNotSet",
|
||||
}
|
||||
)
|
||||
|
||||
@@ -110,7 +114,7 @@ func getExemptKey(checkID string) string {
|
||||
return fmt.Sprintf("polaris.fairwinds.com/%s-exempt", checkID)
|
||||
}
|
||||
|
||||
func applyPodSchemaChecks(conf *config.Configuration, controller kube.GenericWorkload) (ResultSet, error) {
|
||||
func applyPodSchemaChecks(ctx context.Context, conf *config.Configuration, controller kube.GenericWorkload) (ResultSet, error) {
|
||||
results := ResultSet{}
|
||||
checkIDs := getSortedKeys(conf.Checks)
|
||||
objectAnnotations := controller.ObjectMeta.GetAnnotations()
|
||||
@@ -135,7 +139,32 @@ func applyPodSchemaChecks(conf *config.Configuration, controller kube.GenericWor
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func applyContainerSchemaChecks(conf *config.Configuration, controller kube.GenericWorkload, container *corev1.Container, isInit bool) (ResultSet, error) {
|
||||
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) {
|
||||
results := ResultSet{}
|
||||
checkIDs := getSortedKeys(conf.Checks)
|
||||
objectAnnotations := controller.ObjectMeta.GetAnnotations()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
conf "github.com/fairwindsops/polaris/pkg/config"
|
||||
@@ -12,7 +13,7 @@ import (
|
||||
|
||||
var customCheckExemptions = `
|
||||
checks:
|
||||
foo: error
|
||||
foo: danger
|
||||
customChecks:
|
||||
foo:
|
||||
successMessage: success!
|
||||
@@ -32,7 +33,7 @@ exemptions:
|
||||
|
||||
var resourceConfRanges = `
|
||||
checks:
|
||||
memoryRequestsRange: error
|
||||
memoryRequestsRange: danger
|
||||
memoryLimitsRange: warning
|
||||
customChecks:
|
||||
memoryLimitsRange:
|
||||
@@ -121,11 +122,11 @@ func TestValidateResourcesPartiallyValid(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
expectedErrors := []ResultMessage{
|
||||
expectedDangers := []ResultMessage{
|
||||
{
|
||||
ID: "memoryRequestsRange",
|
||||
Success: false,
|
||||
Severity: "error",
|
||||
Severity: "danger",
|
||||
Message: "Memory requests should be within the required range",
|
||||
Category: "Resources",
|
||||
},
|
||||
@@ -133,28 +134,28 @@ func TestValidateResourcesPartiallyValid(t *testing.T) {
|
||||
|
||||
expectedSuccesses := []ResultMessage{}
|
||||
|
||||
testValidate(t, &container, &resourceConfRanges, "foo", expectedErrors, expectedWarnings, expectedSuccesses)
|
||||
testValidate(t, &container, &resourceConfRanges, "foo", expectedDangers, expectedWarnings, expectedSuccesses)
|
||||
}
|
||||
|
||||
func TestValidateResourcesInit(t *testing.T) {
|
||||
emptyContainer := &corev1.Container{}
|
||||
controller := getEmptyWorkload("")
|
||||
controller := getEmptyWorkload(t, "")
|
||||
|
||||
parsedConf, err := conf.Parse([]byte(resourceConfRanges))
|
||||
assert.NoError(t, err, "Expected no error when parsing config")
|
||||
|
||||
results, err := applyContainerSchemaChecks(&parsedConf, controller, emptyContainer, false)
|
||||
results, err := applyContainerSchemaChecks(context.Background(), &parsedConf, controller, emptyContainer, false)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
assert.Equal(t, uint(1), results.GetSummary().Errors)
|
||||
assert.Equal(t, uint(1), results.GetSummary().Dangers)
|
||||
assert.Equal(t, uint(1), results.GetSummary().Warnings)
|
||||
|
||||
results, err = applyContainerSchemaChecks(&parsedConf, controller, emptyContainer, true)
|
||||
results, err = applyContainerSchemaChecks(context.Background(), &parsedConf, controller, emptyContainer, true)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
assert.Equal(t, uint(0), results.GetSummary().Errors)
|
||||
assert.Equal(t, uint(0), results.GetSummary().Dangers)
|
||||
assert.Equal(t, uint(0), results.GetSummary().Warnings)
|
||||
}
|
||||
|
||||
@@ -189,7 +190,7 @@ func TestValidateResourcesFullyValid(t *testing.T) {
|
||||
{
|
||||
ID: "memoryRequestsRange",
|
||||
Success: true,
|
||||
Severity: "error",
|
||||
Severity: "danger",
|
||||
Message: "Memory requests are within the required range",
|
||||
Category: "Resources",
|
||||
},
|
||||
@@ -222,14 +223,14 @@ func TestValidateResourcesFullyValid(t *testing.T) {
|
||||
{
|
||||
ID: "cpuLimitsMissing",
|
||||
Success: true,
|
||||
Severity: "error",
|
||||
Severity: "danger",
|
||||
Message: "CPU limits are set",
|
||||
Category: "Resources",
|
||||
},
|
||||
{
|
||||
ID: "memoryLimitsMissing",
|
||||
Success: true,
|
||||
Severity: "error",
|
||||
Severity: "danger",
|
||||
Message: "Memory limits are set",
|
||||
Category: "Resources",
|
||||
},
|
||||
@@ -245,18 +246,18 @@ func TestValidateCustomCheckExemptions(t *testing.T) {
|
||||
}
|
||||
|
||||
expectedWarnings := []ResultMessage{}
|
||||
expectedErrors := []ResultMessage{}
|
||||
expectedDangers := []ResultMessage{}
|
||||
expectedSuccesses := []ResultMessage{}
|
||||
testValidate(t, &container, &customCheckExemptions, "exempt", expectedErrors, expectedWarnings, expectedSuccesses)
|
||||
testValidate(t, &container, &customCheckExemptions, "exempt", expectedDangers, expectedWarnings, expectedSuccesses)
|
||||
|
||||
expectedErrors = []ResultMessage{
|
||||
expectedDangers = []ResultMessage{
|
||||
{
|
||||
ID: "foo",
|
||||
Success: false,
|
||||
Severity: "error",
|
||||
Severity: "danger",
|
||||
Message: "fail!",
|
||||
Category: "Security",
|
||||
},
|
||||
}
|
||||
testValidate(t, &container, &customCheckExemptions, "notexempt", expectedErrors, expectedWarnings, expectedSuccesses)
|
||||
testValidate(t, &container, &customCheckExemptions, "notexempt", expectedDangers, expectedWarnings, expectedSuccesses)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/fairwindsops/polaris/pkg/config"
|
||||
)
|
||||
|
||||
@@ -8,7 +11,7 @@ import (
|
||||
type CountSummary struct {
|
||||
Successes uint
|
||||
Warnings uint
|
||||
Errors uint
|
||||
Dangers uint
|
||||
}
|
||||
|
||||
// CountSummaryByCategory is a map from category to CountSummary
|
||||
@@ -16,7 +19,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.Errors * 2)
|
||||
total := (cs.Successes * 2) + cs.Warnings + (cs.Dangers * 2)
|
||||
if total == 0 {
|
||||
return 0 // Prevent divide by 0.
|
||||
}
|
||||
@@ -27,7 +30,7 @@ func (cs CountSummary) GetScore() uint {
|
||||
func (cs *CountSummary) AddSummary(other CountSummary) {
|
||||
cs.Successes += other.Successes
|
||||
cs.Warnings += other.Warnings
|
||||
cs.Errors += other.Errors
|
||||
cs.Dangers += other.Dangers
|
||||
}
|
||||
|
||||
// AddResult adds a single result to the summary
|
||||
@@ -36,7 +39,7 @@ func (cs *CountSummary) AddResult(result ResultMessage) {
|
||||
if result.Severity == config.SeverityWarning {
|
||||
cs.Warnings++
|
||||
} else {
|
||||
cs.Errors++
|
||||
cs.Dangers++
|
||||
}
|
||||
} else {
|
||||
cs.Successes++
|
||||
@@ -143,6 +146,19 @@ 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
|
||||
}
|
||||
|
||||
@@ -168,11 +184,11 @@ func (rs ResultSet) GetWarnings() []ResultMessage {
|
||||
return warnings
|
||||
}
|
||||
|
||||
// GetErrors returns the error messages in a result set
|
||||
func (rs ResultSet) GetErrors() []ResultMessage {
|
||||
// GetDangers returns the error messages in a result set
|
||||
func (rs ResultSet) GetDangers() []ResultMessage {
|
||||
errors := []ResultMessage{}
|
||||
for _, msg := range rs {
|
||||
if msg.Success == false && msg.Severity == config.SeverityError {
|
||||
if msg.Success == false && msg.Severity == config.SeverityDanger {
|
||||
errors = append(errors, msg)
|
||||
}
|
||||
}
|
||||
@@ -182,7 +198,7 @@ func (rs ResultSet) GetErrors() []ResultMessage {
|
||||
// GetSortedResults returns messages sorted as errors, then warnings, then successes
|
||||
func (rs ResultSet) GetSortedResults() []ResultMessage {
|
||||
messages := []ResultMessage{}
|
||||
messages = append(messages, rs.GetErrors()...)
|
||||
messages = append(messages, rs.GetDangers()...)
|
||||
messages = append(messages, rs.GetWarnings()...)
|
||||
messages = append(messages, rs.GetSuccesses()...)
|
||||
return messages
|
||||
|
||||
+62
-60
@@ -17,6 +17,7 @@ package webhook
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
@@ -24,37 +25,25 @@ 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/runtime/inject"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook"
|
||||
"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 types.Decoder
|
||||
Client client.Client
|
||||
decoder *admission.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 types.Decoder) error {
|
||||
func (v *Validator) InjectDecoder(d *admission.Decoder) error {
|
||||
logrus.Info("Injecting decoder")
|
||||
v.decoder = d
|
||||
return nil
|
||||
}
|
||||
@@ -62,51 +51,61 @@ func (v *Validator) InjectDecoder(d types.Decoder) error {
|
||||
var _ admission.Handler = &Validator{}
|
||||
|
||||
// NewWebhook creates a validating admission webhook for the apiType.
|
||||
func NewWebhook(name string, mgr manager.Manager, validator Validator, apiType runtime.Object) (*admission.Webhook, error) {
|
||||
name = fmt.Sprintf("%s.k8s.io", name)
|
||||
path := fmt.Sprintf("/validating-%s", name)
|
||||
func NewWebhook(mgr manager.Manager, validator Validator) {
|
||||
path := "/validate"
|
||||
|
||||
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
|
||||
mgr.GetWebhookServer().Register(path, &webhook.Admission{Handler: &validator})
|
||||
}
|
||||
|
||||
func (v *Validator) handleInternal(ctx context.Context, req types.Request) (*validator.PodResult, error) {
|
||||
// GetObjectFromRawRequest returns the pod object and the controller's object from the raw json bytes.
|
||||
func GetObjectFromRawRequest(raw []byte) (corev1.Pod, interface{}, 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 {
|
||||
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
|
||||
}
|
||||
pod, originalObject, err = GetObjectFromRawRequest(req.Object.Raw)
|
||||
}
|
||||
controller, err := kube.NewGenericWorkloadFromPod(pod, originalObject)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
controller := kube.NewGenericWorkloadFromPod(pod)
|
||||
controller.Kind = req.AdmissionRequest.Kind.Kind
|
||||
controllerResult, err := validator.ValidateController(&v.Config, controller)
|
||||
controllerResult, err := validator.ValidateController(ctx, &v.Config, controller)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -114,20 +113,23 @@ func (v *Validator) handleInternal(ctx context.Context, req types.Request) (*val
|
||||
}
|
||||
|
||||
// Handle for Validator to run validation checks.
|
||||
func (v *Validator) Handle(ctx context.Context, req types.Request) types.Response {
|
||||
func (v *Validator) Handle(ctx context.Context, req admission.Request) admission.Response {
|
||||
logrus.Info("Starting request")
|
||||
podResult, err := v.handleInternal(ctx, req)
|
||||
if err != nil {
|
||||
logrus.Errorf("Error validating request: %v", err)
|
||||
return admission.ErrorResponse(http.StatusBadRequest, err)
|
||||
return admission.Errored(http.StatusBadRequest, err)
|
||||
}
|
||||
allowed := true
|
||||
reason := ""
|
||||
numErrors := podResult.GetSummary().Errors
|
||||
if numErrors > 0 {
|
||||
allowed = false
|
||||
reason = getFailureReason(*podResult)
|
||||
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)
|
||||
}
|
||||
logrus.Infof("%d validation errors found when validating %s", numErrors, podResult.Name)
|
||||
return admission.ValidationResponse(allowed, reason)
|
||||
}
|
||||
|
||||
@@ -135,14 +137,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.SeverityError {
|
||||
if !message.Success && message.Severity == config.SeverityDanger {
|
||||
reason += fmt.Sprintf("- Pod: %s\n", message.Message)
|
||||
}
|
||||
}
|
||||
|
||||
for _, containerResult := range podResult.ContainerResults {
|
||||
for _, message := range containerResult.Results {
|
||||
if !message.Success && message.Severity == config.SeverityError {
|
||||
if !message.Success && message.Severity == config.SeverityDanger {
|
||||
reason += fmt.Sprintf("- Container %s: %s\n", containerResult.Name, message.Message)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
# /bin/bash
|
||||
|
||||
set -eo pipefail
|
||||
set -e
|
||||
|
||||
helm template $CHARTS_DIR/stable/polaris/ \
|
||||
--name polaris --namespace polaris \
|
||||
helm template polaris $CHARTS_DIR/stable/polaris/ \
|
||||
--namespace polaris \
|
||||
--set templateOnly=true \
|
||||
--set config="$(cat ./examples/config.yaml)" \
|
||||
> deploy/dashboard.yaml
|
||||
|
||||
helm template $CHARTS_DIR/stable/polaris/ \
|
||||
--name polaris --namespace polaris \
|
||||
helm template polaris $CHARTS_DIR/stable/polaris/ \
|
||||
--namespace polaris \
|
||||
--set templateOnly=true \
|
||||
--set webhook.enable=true \
|
||||
--set dashboard.enable=false \
|
||||
--set config="$(cat ./examples/config.yaml)" \
|
||||
> deploy/webhook.yaml
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: nginx
|
||||
labels:
|
||||
env: test
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx
|
||||
imagePullPolicy: IfNotPresent
|
||||
@@ -0,0 +1,12 @@
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: nginx
|
||||
labels:
|
||||
env: test
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx
|
||||
imagePullPolicy: IfNotPresent
|
||||
priorityClassName: high-priority
|
||||
@@ -0,0 +1,73 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+17
-14
@@ -1,12 +1,15 @@
|
||||
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"
|
||||
@@ -116,39 +119,39 @@ func SetupTestAPI() (kubernetes.Interface, dynamic.Interface) {
|
||||
}
|
||||
|
||||
// SetupAddControllers creates mock controllers and adds them to the test clientset.
|
||||
func SetupAddControllers(k kubernetes.Interface, namespace string) kubernetes.Interface {
|
||||
func SetupAddControllers(ctx context.Context, k kubernetes.Interface, namespace string) kubernetes.Interface {
|
||||
d1 := MockDeploy()
|
||||
if _, err := k.AppsV1().Deployments(namespace).Create(&d1); err != nil {
|
||||
if _, err := k.AppsV1().Deployments(namespace).Create(ctx, &d1, metav1.CreateOptions{}); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
s1 := MockStatefulSet()
|
||||
if _, err := k.AppsV1().StatefulSets(namespace).Create(&s1); err != nil {
|
||||
if _, err := k.AppsV1().StatefulSets(namespace).Create(ctx, &s1, metav1.CreateOptions{}); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
ds1 := MockDaemonSet()
|
||||
if _, err := k.AppsV1().DaemonSets(namespace).Create(&ds1); err != nil {
|
||||
if _, err := k.AppsV1().DaemonSets(namespace).Create(ctx, &ds1, metav1.CreateOptions{}); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
j1 := MockJob()
|
||||
if _, err := k.BatchV1().Jobs(namespace).Create(&j1); err != nil {
|
||||
if _, err := k.BatchV1().Jobs(namespace).Create(ctx, &j1, metav1.CreateOptions{}); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
cj1 := MockCronJob()
|
||||
if _, err := k.BatchV1beta1().CronJobs(namespace).Create(&cj1); err != nil {
|
||||
if _, err := k.BatchV1beta1().CronJobs(namespace).Create(ctx, &cj1, metav1.CreateOptions{}); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
rc1 := MockReplicationController()
|
||||
if _, err := k.CoreV1().ReplicationControllers(namespace).Create(&rc1); err != nil {
|
||||
if _, err := k.CoreV1().ReplicationControllers(namespace).Create(ctx, &rc1, metav1.CreateOptions{}); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
p1 := MockNakedPod()
|
||||
if _, err := k.CoreV1().Pods(namespace).Create(&p1); err != nil {
|
||||
if _, err := k.CoreV1().Pods(namespace).Create(ctx, &p1, metav1.CreateOptions{}); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
@@ -156,7 +159,7 @@ func SetupAddControllers(k kubernetes.Interface, namespace string) kubernetes.In
|
||||
}
|
||||
|
||||
// SetupAddExtraControllerVersions creates mock controllers and adds them to the test clientset.
|
||||
func SetupAddExtraControllerVersions(k kubernetes.Interface, namespace string) kubernetes.Interface {
|
||||
func SetupAddExtraControllerVersions(ctx context.Context, k kubernetes.Interface, namespace string) kubernetes.Interface {
|
||||
p := MockPod()
|
||||
|
||||
dv1b1 := appsv1beta1.Deployment{
|
||||
@@ -164,7 +167,7 @@ func SetupAddExtraControllerVersions(k kubernetes.Interface, namespace string) k
|
||||
Template: corev1.PodTemplateSpec{Spec: p.Spec},
|
||||
},
|
||||
}
|
||||
if _, err := k.AppsV1beta1().Deployments(namespace).Create(&dv1b1); err != nil {
|
||||
if _, err := k.AppsV1beta1().Deployments(namespace).Create(ctx, &dv1b1, metav1.CreateOptions{}); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
@@ -173,7 +176,7 @@ func SetupAddExtraControllerVersions(k kubernetes.Interface, namespace string) k
|
||||
Template: corev1.PodTemplateSpec{Spec: p.Spec},
|
||||
},
|
||||
}
|
||||
if _, err := k.AppsV1beta2().Deployments(namespace).Create(&dv1b2); err != nil {
|
||||
if _, err := k.AppsV1beta2().Deployments(namespace).Create(ctx, &dv1b2, metav1.CreateOptions{}); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
@@ -182,7 +185,7 @@ func SetupAddExtraControllerVersions(k kubernetes.Interface, namespace string) k
|
||||
Template: corev1.PodTemplateSpec{Spec: p.Spec},
|
||||
},
|
||||
}
|
||||
if _, err := k.AppsV1beta1().StatefulSets(namespace).Create(&ssv1b1); err != nil {
|
||||
if _, err := k.AppsV1beta1().StatefulSets(namespace).Create(ctx, &ssv1b1, metav1.CreateOptions{}); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
@@ -191,7 +194,7 @@ func SetupAddExtraControllerVersions(k kubernetes.Interface, namespace string) k
|
||||
Template: corev1.PodTemplateSpec{Spec: p.Spec},
|
||||
},
|
||||
}
|
||||
if _, err := k.AppsV1beta2().StatefulSets(namespace).Create(&ssv1b2); err != nil {
|
||||
if _, err := k.AppsV1beta2().StatefulSets(namespace).Create(ctx, &ssv1b2, metav1.CreateOptions{}); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
@@ -200,7 +203,7 @@ func SetupAddExtraControllerVersions(k kubernetes.Interface, namespace string) k
|
||||
Template: corev1.PodTemplateSpec{Spec: p.Spec},
|
||||
},
|
||||
}
|
||||
if _, err := k.AppsV1beta2().DaemonSets(namespace).Create(&dsv1b2); err != nil {
|
||||
if _, err := k.AppsV1beta2().DaemonSets(namespace).Create(ctx, &dsv1b2, metav1.CreateOptions{}); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return k
|
||||
|
||||
@@ -5,7 +5,7 @@ metadata:
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
replicas: 3
|
||||
replicas: 1
|
||||
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
|
||||
|
||||
+25
-6
@@ -2,9 +2,7 @@
|
||||
set -e
|
||||
|
||||
#sed is replacing the polaris version with this commit sha so we are testing exactly this verison.
|
||||
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
|
||||
sed -r "s|'(quay.io/fairwinds/polaris:).+'|'\1${CIRCLE_SHA1}'|" ./deploy/webhook.yaml > ./deploy/webhook-test.yaml
|
||||
|
||||
# Testing to ensure that the webhook starts up, allows a correct deployment to pass,
|
||||
# and prevents a incorrectly formatted deployment.
|
||||
@@ -12,8 +10,13 @@ function check_webhook_is_ready() {
|
||||
# Get the epoch time in one minute from now
|
||||
local timeout_epoch
|
||||
|
||||
# Reset another 2 minutes to wait for webhook
|
||||
timeout_epoch=$(date -d "+2 minutes" +%s)
|
||||
# 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
|
||||
|
||||
# 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"
|
||||
@@ -22,6 +25,8 @@ function check_webhook_is_ready() {
|
||||
echo -n "."
|
||||
done
|
||||
|
||||
check_timeout "${timeout_epoch}"
|
||||
|
||||
echo "Webhook started!"
|
||||
}
|
||||
|
||||
@@ -52,11 +57,16 @@ 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.yaml &> /dev/null
|
||||
kubectl apply -f ./deploy/webhook-test.yaml &> /dev/null
|
||||
|
||||
|
||||
# wait for the webhook to come online
|
||||
@@ -86,6 +96,15 @@ 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.
|
||||
|
||||
Reference in New Issue
Block a user