mirror of
https://github.com/FairwindsOps/polaris.git
synced 2026-08-23 22:26:34 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91378c3d92 | ||
|
|
207297c11d | ||
|
|
0d86096f09 | ||
|
|
67ab987f7e | ||
|
|
97457d71c0 | ||
|
|
1159a380ba | ||
|
|
ca6aa76729 | ||
|
|
7ce71f1632 | ||
|
|
98b47e0aeb | ||
|
|
159c165468 | ||
|
|
4eeabb2c7f | ||
|
|
7e35b03452 | ||
|
|
c8e4f766a2 | ||
|
|
0ee39a4470 | ||
|
|
fc7c913122 | ||
|
|
3e15586597 | ||
|
|
3d07ce8004 | ||
|
|
22ab851681 | ||
|
|
152c876067 | ||
|
|
c10b2e0e55 | ||
|
|
2b15f11d57 | ||
|
|
b172f61c5f | ||
|
|
150b812371 | ||
|
|
434b1f604f | ||
|
|
6e3ab067d7 | ||
|
|
2e44d151fe | ||
|
|
c91a85a08a | ||
|
|
f8bb171209 | ||
|
|
f860c34d62 | ||
|
|
b8422a93b7 | ||
|
|
15b14555a6 | ||
|
|
467ab945cd | ||
|
|
dddeeb3332 | ||
|
|
f03f97ab07 | ||
|
|
75f70352ba | ||
|
|
4aa360a933 | ||
|
|
dce24a0a34 | ||
|
|
fae7a9fc17 | ||
|
|
79b0a00d5c | ||
|
|
e4fe5ab085 | ||
|
|
2049698c8e | ||
|
|
20bd32afb6 | ||
|
|
c132f7ee51 | ||
|
|
10025764cd | ||
|
|
c8c0e07a22 | ||
|
|
b55f4717b1 | ||
|
|
f493c380fc | ||
|
|
baa652197a | ||
|
|
a6525fc807 | ||
|
|
e01c39230a | ||
|
|
7b8b5fee4d | ||
|
|
819b77ae1a | ||
|
|
edccf88ccd | ||
|
|
7f42841572 | ||
|
|
b5a180d3de | ||
|
|
69efa2a0a3 | ||
|
|
7f4029386d | ||
|
|
9f7ed8214e | ||
|
|
5e67812d5a | ||
|
|
821de5fa8f | ||
|
|
20bf86ec12 | ||
|
|
87370a4fbc | ||
|
|
7b0e0e7f15 | ||
|
|
962494e47a | ||
|
|
c816e60441 | ||
|
|
d46fd6215a | ||
|
|
7bcd01e6be | ||
|
|
20a6028145 | ||
|
|
f784c48217 | ||
|
|
9b755b8b93 | ||
|
|
29261ed8bf | ||
|
|
1b5557d6ed | ||
|
|
3fd3c04429 | ||
|
|
5b9257b4ce |
@@ -1,7 +1,7 @@
|
||||
DOCKERFILE='Dockerfile'
|
||||
|
||||
EXTERNAL_REGISTRY_BASE_DOMAIN=quay.io
|
||||
REPOSITORY_NAME=reactiveops/polaris
|
||||
REPOSITORY_NAME=fairwinds/polaris
|
||||
DOCKERTAG=${EXTERNAL_REGISTRY_BASE_DOMAIN}/${REPOSITORY_NAME}
|
||||
if [[ -n $CI_TAG ]]; then
|
||||
ADDITIONAL_DOCKER_TAG_VERSIONS=()
|
||||
|
||||
+76
-27
@@ -1,4 +1,9 @@
|
||||
version: 2
|
||||
version: 2.1
|
||||
|
||||
executors:
|
||||
vm:
|
||||
machine:
|
||||
enabled: true
|
||||
|
||||
references:
|
||||
set_environment_variables: &set_environment_variables
|
||||
@@ -10,18 +15,34 @@ references:
|
||||
echo 'export CI_BUILD_NUM=$CIRCLE_BUILD_NUM' >> ${BASH_ENV}
|
||||
echo 'export CI_TAG=$CIRCLE_TAG' >> ${BASH_ENV}
|
||||
echo 'export PUSH_ALL_VERSION_TAGS=true' >> ${BASH_ENV}
|
||||
docker_build_and_push: &docker_build_and_push
|
||||
install_k8s: &install_k8s
|
||||
run:
|
||||
name: Docker login, build, and push
|
||||
name: Install K8s
|
||||
command: |
|
||||
docker-pull -f .circleci/build.config
|
||||
docker-build -f .circleci/build.config
|
||||
if [[ -z $CIRCLE_PR_NUMBER ]]; then
|
||||
docker login quay.io -u="reactiveops+circleci" -p="${quay_token}"
|
||||
docker-push -f .circleci/build.config
|
||||
else
|
||||
echo "Skipping docker push for forked PR"
|
||||
fi
|
||||
sudo apt-get update
|
||||
echo "Installing git and jq"
|
||||
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
|
||||
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
|
||||
chmod 0755 kubectl
|
||||
sudo mv kubectl /usr/local/bin/
|
||||
kubectl version --client
|
||||
|
||||
|
||||
echo "Creating Kubernetes Cluster with Kind"
|
||||
kind create cluster --wait=90s
|
||||
docker ps -a
|
||||
|
||||
echo "Setting up kubecfg"
|
||||
cp $(kind get kubeconfig-path --name=kind) ~/.kube/config
|
||||
kubectl version
|
||||
|
||||
# Test scripts
|
||||
update_coverage: &update_coverage
|
||||
@@ -29,23 +50,32 @@ references:
|
||||
name: Update Coverage
|
||||
command: |
|
||||
if [[ -z $CIRCLE_PR_NUMBER ]]; then
|
||||
go test ./pkg/... -coverprofile=coverage.txt -covermode=count
|
||||
bash <(curl -s https://codecov.io/bash)
|
||||
else
|
||||
echo "Skipping coverage for forked PR"
|
||||
fi
|
||||
test_dashboard: &test_dashboard
|
||||
test_binary_dashboard: &test_binary_dashboard
|
||||
run:
|
||||
name: Test Dashboard
|
||||
command: |
|
||||
go run main.go --dashboard --dashboard-port 3000 --audit-path ./examples &
|
||||
sleep 5
|
||||
sleep 30
|
||||
curl -f http://localhost:3000 > /dev/null
|
||||
curl -f http://localhost:3000/health > /dev/null
|
||||
curl -f http://localhost:3000/favicon.ico > /dev/null
|
||||
curl -f http://localhost:3000/static/css/main.css > /dev/null
|
||||
curl -f http://localhost:3000/results.json > /dev/null
|
||||
curl -f http://localhost:3000/details/security > /dev/null
|
||||
test_k8s: &test_k8s
|
||||
run:
|
||||
name: Test Kubernetes Deployments
|
||||
command: |
|
||||
if [[ -z $CIRCLE_PR_NUMBER ]]; then
|
||||
./test/webhook_test.sh
|
||||
./test/kube_dashboard_test.sh
|
||||
else
|
||||
echo "Skipping Kubernetes tests for forked PR"
|
||||
fi
|
||||
|
||||
# Release scripts
|
||||
install_goreleaser: &install_goreleaser
|
||||
@@ -56,20 +86,26 @@ references:
|
||||
echo "8dbad6683d6fc9367e637e6eed8e01a0d63c9660 goreleaser.deb" | sha1sum -c
|
||||
sudo dpkg -i goreleaser.deb
|
||||
rm goreleaser.deb
|
||||
docker_build_and_push: &docker_build_and_push
|
||||
run:
|
||||
name: Docker login, build, and push
|
||||
command: |
|
||||
docker-pull -f .circleci/build.config
|
||||
docker-build -f .circleci/build.config
|
||||
if [[ -z $CIRCLE_PR_NUMBER ]]; then
|
||||
docker login quay.io -u="${fairwinds_quay_user}" -p="${fairwinds_quay_token}"
|
||||
docker-push -f .circleci/build.config
|
||||
else
|
||||
echo "Skipping docker push for forked PR"
|
||||
fi
|
||||
release_deploy_configs: &release_deploy_configs
|
||||
run:
|
||||
name: Release deploy configs
|
||||
command: |
|
||||
git clone --branch polaris-latest https://github.com/reactiveops/charts
|
||||
mkdir deploy
|
||||
helm template ./charts/stable/polaris --name polaris --namespace polaris --set templateOnly=true > deploy/dashboard.yaml
|
||||
helm template ./charts/stable/polaris --name polaris --namespace polaris --set templateOnly=true --set webhook.enable=true --set dashboard.enable=false > deploy/webhook.yaml
|
||||
upload_url=$(curl --silent https://api.github.com/repos/reactiveops/polaris/releases/latest | grep upload_url)
|
||||
upload_url=$(curl --silent https://api.github.com/repos/FairwindsOps/polaris/releases/latest | grep upload_url)
|
||||
upload_url=$(echo $upload_url | sed -e 's/.*\(https.*\){.*$/\1/')
|
||||
curl -X POST "$upload_url?name=dashboard.yaml" --data-binary "@./deploy/dashboard.yaml" -H "Authorization: Bearer $GITHUB_TOKEN" -H "Content-Type: application/x-yaml"
|
||||
curl -X POST "$upload_url?name=webhook.yaml" --data-binary "@./deploy/webhook.yaml" -H "Authorization: Bearer $GITHUB_TOKEN" -H "Content-Type: application/x-yaml"
|
||||
rm -rf ./charts
|
||||
rm -rf ./deploy
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -81,21 +117,31 @@ jobs:
|
||||
- *set_environment_variables
|
||||
- *docker_build_and_push
|
||||
|
||||
test_k8s:
|
||||
working_directory: ~/polaris
|
||||
resource_class: medium
|
||||
executor: vm
|
||||
steps:
|
||||
- checkout
|
||||
- *install_k8s
|
||||
- *test_k8s
|
||||
|
||||
test:
|
||||
working_directory: /go/src/github.com/reactiveops/polaris/
|
||||
working_directory: /go/src/github.com/fairwindsops/polaris/
|
||||
docker:
|
||||
- image: circleci/golang:1.12
|
||||
|
||||
steps:
|
||||
- checkout
|
||||
- run: go get -u golang.org/x/lint/golint
|
||||
- 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-path ./deploy --set-exit-code-below-score 100 --set-exit-code-on-error
|
||||
- *update_coverage
|
||||
- *test_dashboard
|
||||
- *test_binary_dashboard
|
||||
|
||||
release_binary:
|
||||
working_directory: /go/src/github.com/reactiveops/polaris/
|
||||
working_directory: /go/src/github.com/fairwindsops/polaris/
|
||||
docker:
|
||||
- image: circleci/golang:1.12
|
||||
steps:
|
||||
@@ -106,9 +152,10 @@ jobs:
|
||||
- run: go get -u github.com/gobuffalo/packr/v2/packr2
|
||||
- run: packr2
|
||||
- run: goreleaser
|
||||
- *release_deploy_configs
|
||||
|
||||
release_images:
|
||||
working_directory: /go/src/github.com/reactiveops/polaris/
|
||||
working_directory: /go/src/github.com/fairwindsops/polaris/
|
||||
docker:
|
||||
- image: quay.io/reactiveops/ci-images:v8.0-stretch
|
||||
steps:
|
||||
@@ -116,7 +163,6 @@ jobs:
|
||||
- setup_remote_docker
|
||||
- *set_environment_variables
|
||||
- *docker_build_and_push
|
||||
- *release_deploy_configs
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
@@ -131,6 +177,9 @@ workflows:
|
||||
filters:
|
||||
tags:
|
||||
only: /^testing-.*/
|
||||
- test_k8s:
|
||||
requires:
|
||||
- build
|
||||
release:
|
||||
jobs:
|
||||
- release_binary:
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
name: Bug report
|
||||
about: Let us know about an unexpected error, a crash, or an incorrect behavior.
|
||||
|
||||
---
|
||||
### Installation Process
|
||||
<!---
|
||||
Did you install polaris on your local machine, or in a kubernetes cluster? Did you use kubectl, helm, brew, a GitHub release, or the source code? Let us know below.
|
||||
-->
|
||||
|
||||
### Polaris Version
|
||||
<!---
|
||||
If you are using polaris locally, run `polaris --version` to show the version, and paste the result between the ``` marks below.
|
||||
|
||||
If you are not running the latest version of Polaris, please try upgrading because your issue may have already been fixed.
|
||||
-->
|
||||
|
||||
```
|
||||
...
|
||||
```
|
||||
|
||||
### Expected Behavior
|
||||
<!--
|
||||
What should have happened?
|
||||
-->
|
||||
|
||||
### Actual Behavior
|
||||
<!--
|
||||
What actually happened?
|
||||
-->
|
||||
|
||||
### Steps to Reproduce
|
||||
<!--
|
||||
Please list the full steps required to reproduce the issue, for example:
|
||||
1. `polaris --audit --output-format score`
|
||||
-->
|
||||
|
||||
### Additional Context
|
||||
<!--
|
||||
Are there anything atypical about your situation that we should know?
|
||||
-->
|
||||
|
||||
### References
|
||||
<!--
|
||||
Are there any other GitHub issues (open or closed) or Pull Requests that should be linked here? For example:
|
||||
|
||||
- #6017
|
||||
|
||||
-->
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
name: Other
|
||||
about: For misc. tasks like research or continued conversation
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ changelog:
|
||||
- '^test:'
|
||||
brew:
|
||||
github:
|
||||
owner: reactiveops
|
||||
owner: FairwindsOps
|
||||
name: homebrew-tap
|
||||
folder: Formula
|
||||
description: Open Source Best Practices for Kubernetes
|
||||
|
||||
+36
-8
@@ -1,8 +1,36 @@
|
||||
# x.x.x (next release)
|
||||
|
||||
# 0.6.0
|
||||
* Fixed webhook support in Kubernetes 1.16
|
||||
* this also removes support for 1.8
|
||||
* Added support for exemptions via controller annotations
|
||||
|
||||
# 0.5.2
|
||||
* Fixed missing success messages for resource requests/limits
|
||||
|
||||
# 0.5.1
|
||||
* Added a few more exemptions
|
||||
* Started checking exemptions based on controller name prefix
|
||||
* `runAsUser != 0` now passes the `runAsNonRoot` check
|
||||
|
||||
# 0.5.0
|
||||
* Added `--load-audit-file` flag to run the dashboard from an existing audit
|
||||
* Added an `ID` field to each check in the output
|
||||
* Skip health checks for jobs, cronjobs, initcontainers
|
||||
* Added support for exemptions
|
||||
* Fixed dashboard base path option
|
||||
|
||||
# 0.4.0
|
||||
* Added additional Pod Controllers to scan PodSpec (`jobs`, `cronjobs`, `daemonsets`, `replicationcontrollers`)
|
||||
|
||||
# 0.3.1
|
||||
* Changed dashboard branding to refer to new org name Fairwinds
|
||||
|
||||
# 0.3.0
|
||||
* Added `--set-exit-code-on-error` and `--set-exit-code-below-score` flags to better support CI/CD
|
||||
|
||||
# 0.2.1
|
||||
* [Fix](https://github.com/reactiveops/polaris/issues/146): Fixed logic on RunAsNonRoot check to incorporate settings in podSpec
|
||||
* [Fix](https://github.com/FairwindsOps/polaris/issues/146): Fixed logic on RunAsNonRoot check to incorporate settings in podSpec
|
||||
|
||||
# 0.2.0
|
||||
* Added `--output-format` flag for better CI/CD support
|
||||
@@ -11,23 +39,23 @@
|
||||
* Show error message if no kubeconfig is set
|
||||
|
||||
# 0.1.5
|
||||
* [Fix](https://github.com/reactiveops/polaris/issues/125): ignore limits/requests for initContainers
|
||||
* [Fix](https://github.com/reactiveops/polaris/issues/132): support custom base path
|
||||
* [Fix](https://github.com/FairwindsOps/polaris/issues/125): ignore limits/requests for initContainers
|
||||
* [Fix](https://github.com/FairwindsOps/polaris/issues/132): support custom base path
|
||||
|
||||
# 0.1.4
|
||||
* [Fix](https://github.com/reactiveops/polaris/issues/116): details pages getting template errors
|
||||
* [Fix](https://github.com/reactiveops/polaris/issues/114): support all auth providers
|
||||
* [Fix](https://github.com/reactiveops/polaris/issues/112): Ignore readiness probe for initContainers
|
||||
* [Fix](https://github.com/FairwindsOps/polaris/issues/116): details pages getting template errors
|
||||
* [Fix](https://github.com/FairwindsOps/polaris/issues/114): support all auth providers
|
||||
* [Fix](https://github.com/FairwindsOps/polaris/issues/112): Ignore readiness probe for initContainers
|
||||
|
||||
# 0.1.3
|
||||
* [Fix](https://github.com/reactiveops/polaris/issues/109): dashboard not updating when running persistently
|
||||
* [Fix](https://github.com/FairwindsOps/polaris/issues/109): dashboard not updating when running persistently
|
||||
|
||||
# 0.1.2
|
||||
* Stored all third-party assets (e.g. Charts.js) to local files to support offline dashboard viewing
|
||||
* Fix: custom configs in `ConfigMap` not respected
|
||||
|
||||
# 0.1.1
|
||||
* [Fix](https://github.com/reactiveops/polaris/issues/93): missing `config.yaml` and dashboard assets in binary releases
|
||||
* [Fix](https://github.com/FairwindsOps/polaris/issues/93): missing `config.yaml` and dashboard assets in binary releases
|
||||
* Added some tests and better error handling
|
||||
|
||||
# 0.1.0
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
* @bobby-brennan @endzyme @kimschles @robscott
|
||||
* @rbren @makoscafee @jordandoig
|
||||
|
||||
+44
-28
@@ -7,7 +7,7 @@ Issues, whether bugs, tasks, or feature requests are essential for keeping Polar
|
||||
This project adheres to a [code of conduct](CODE_OF_CONDUCT.md). Please review this document before contributing to this project.
|
||||
|
||||
## Sign the CLA
|
||||
Before you can contribute, you will need to sign the [Contributor License Agreement](https://cla-assistant.io/reactiveops/polaris).
|
||||
Before you can contribute, you will need to sign the [Contributor License Agreement](https://cla-assistant.io/fairwinds/polaris).
|
||||
|
||||
## Project Structure
|
||||
|
||||
@@ -15,7 +15,7 @@ Polaris is built on top of [controller-runtime](https://github.com/kubernetes-si
|
||||
|
||||
## Getting Started
|
||||
|
||||
We label issues with the ["good first issue" tag](https://github.com/reactiveops/polaris/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) if we believe they'll be a good starting point for new contributors. If you're interested in working on an issue, please start a conversation on that issue, and we can help answer any questions as they come up.
|
||||
We label issues with the ["good first issue" tag](https://github.com/FairwindsOps/polaris/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) if we believe they'll be a good starting point for new contributors. If you're interested in working on an issue, please start a conversation on that issue, and we can help answer any questions as they come up.
|
||||
|
||||
## Setting Up Your Development Environment
|
||||
### Prerequisites
|
||||
@@ -23,8 +23,8 @@ We label issues with the ["good first issue" tag](https://github.com/reactiveops
|
||||
* If you want to see the local changes you make on a Polaris dashboard, you will need access to a Kubernetes cluster defined in `~/.kube/config`
|
||||
|
||||
### Installation
|
||||
* Install the project with `go get github.com/reactiveops/polaris`
|
||||
* Change into the polaris directory which is installed at `$GOPATH/src/github.com/reactiveops/polaris`
|
||||
* Install the project with `go get github.com/fairwindsops/polaris`
|
||||
* Change into the polaris directory which is installed at `$GOPATH/src/github.com/fairwindsops/polaris`
|
||||
* See the dashboard with `go run main.go --dashboard`, then open http://localhost:8080/
|
||||
* See the audit data `go run main.go --audit`. This command shows the audit information on the command line.
|
||||
|
||||
@@ -40,11 +40,8 @@ go test ./pkg/... -v -coverprofile cover.out
|
||||
|
||||
## Creating a New Issue
|
||||
|
||||
If you've encountered an issue that is not already reported, please create an issue that contains the following:
|
||||
If you've encountered an issue that is not already reported, please create a [new issue](https://github.com/FairwindsOps/polaris/issues), choose `Bug Report`, `Feature Request` or `Misc.` and follow the instructions in the template.
|
||||
|
||||
- Clear description of the issue
|
||||
- Steps to reproduce it
|
||||
- Appropriate labels
|
||||
|
||||
## Creating a Pull Request
|
||||
|
||||
@@ -58,35 +55,54 @@ Each new pull request should:
|
||||
|
||||
## Creating a new release
|
||||
|
||||
### Minor/patch releases
|
||||
Minor and patch releases only need to change this repo. The Helm chart and deploy scripts
|
||||
### Patch releases
|
||||
Patch releases only need to change this repo. The Helm chart and deploy scripts
|
||||
will automatically pull in the latest changes.
|
||||
|
||||
To deploy a minor or patch release, follow steps 2 and 3 from "Major releases" below.
|
||||
If the release involves changes to anything in the `deploy/` folder (e.g. new RBAC permissions),
|
||||
it needs to be a minor or major release in order to prevent breaking the Helm chart.
|
||||
|
||||
### Major releases
|
||||
Major releases need to change both this repository and the
|
||||
[Helm chart repo](https://github.com/reactiveops/charts/).
|
||||
|
||||
The steps are:
|
||||
1. Create a PR in the [charts repo](https://github.com/reactiveops/charts/)
|
||||
1. Use a branch named `polaris-latest`
|
||||
2. Bump the version number in:
|
||||
1. stable/polaris/README.md
|
||||
2. stable/polaris/Chart.yaml
|
||||
3. stable/polaris/values.yaml
|
||||
3. **Don't merge yet!**
|
||||
2. Create a PR for this repo
|
||||
1. Create a PR for this repo
|
||||
1. Bump the version number in:
|
||||
1. main.go
|
||||
2. README.md
|
||||
2. Update CHANGELOG.md
|
||||
3. Merge your PR
|
||||
2. Tag the latest branch for this repo
|
||||
1. Pull the latest commit for the `master` branch (which you just merged in your PR)
|
||||
2. Run `git tag $VERSION && git push --tags`
|
||||
3. Make sure CircleCI runs successfully for the new tag - this will push images to quay.io and create a release in GitHub
|
||||
1. If CircleCI fails, check with Codeowners ASAP
|
||||
|
||||
### Minor/Major releases
|
||||
Minor and major releases need to change both this repository and the
|
||||
[Helm chart repo](https://github.com/FairwindsOps/charts/).
|
||||
|
||||
The steps are:
|
||||
1. Modify the [Helm chart](https://github.com/FairwindsOps/charts/stable/polaris)
|
||||
1. Clone the helm charts repo
|
||||
1. `git clone https://github.com/FairwindsOps/charts`
|
||||
2. `git checkout -b yourname/update-polaris`
|
||||
1. Bump the version number in:
|
||||
1. stable/polaris/README.md
|
||||
2. stable/polaris/Chart.yaml
|
||||
3. stable/polaris/values.yaml
|
||||
2. Make any necessary changes to the chart to support the new version of Polaris (e.g. new RBAC permissions)
|
||||
3. **Don't merge yet!**
|
||||
2. Create a PR for this repo
|
||||
1. Create a new branch named `yourname/update-version`
|
||||
2. Bump the version number in:
|
||||
1. main.go
|
||||
2. README.md
|
||||
3. Regenerate the deployment files. Assuming you've cloned the charts repo to `./charts`:
|
||||
1. `helm template ./charts/stable/polaris/ --name polaris --namespace polaris --set templateOnly=true > deploy/dashboard.yaml`
|
||||
2. `helm template ./charts/stable/polaris/ --name polaris --namespace polaris --set templateOnly=true --set webhook.enable=true --set dashboard.enable=false > deploy/webhook.yaml`
|
||||
4. Update CHANGELOG.md
|
||||
5. Merge your PR
|
||||
3. Tag the latest branch for this repo
|
||||
1. Pull the latest for the `master` branch
|
||||
2. Run `git tag $VERSION && git push --tags`
|
||||
3. Wait for CircleCI to finish the build for the tag, which will:
|
||||
1. Create the proper image tag in quay.io
|
||||
2. Add an entry to the releases page on GitHub
|
||||
4. Merge the PR for the charts repo you created in step 1.
|
||||
3. Make sure CircleCI runs successfully for the new tag - this will push images to quay.io and create a release in GitHub
|
||||
1. If CircleCI fails, check with Codeowners ASAP
|
||||
4. Create and merge a PR for your changes to the Helm chart
|
||||
|
||||
|
||||
+4
-4
@@ -1,17 +1,17 @@
|
||||
FROM golang:1.12.4 AS build-env
|
||||
WORKDIR /go/src/github.com/reactiveops/polaris/
|
||||
FROM golang:1.12.9 AS build-env
|
||||
WORKDIR /go/src/github.com/fairwindsops/polaris/
|
||||
|
||||
COPY . .
|
||||
RUN go get -u github.com/gobuffalo/packr/v2/packr2
|
||||
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 packr2 build -a -o polaris *.go
|
||||
|
||||
FROM alpine:3.9
|
||||
FROM alpine:3.10
|
||||
WORKDIR /usr/local/bin
|
||||
RUN apk --no-cache add ca-certificates
|
||||
|
||||
RUN addgroup -S polaris && adduser -u 1200 -S polaris -G polaris
|
||||
USER 1200
|
||||
COPY --from=build-env /go/src/github.com/reactiveops/polaris/polaris .
|
||||
COPY --from=build-env /go/src/github.com/fairwindsops/polaris/polaris .
|
||||
|
||||
WORKDIR /opt/app
|
||||
|
||||
|
||||
@@ -186,7 +186,7 @@
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2019 ReactiveOps
|
||||
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.
|
||||
|
||||
@@ -5,33 +5,42 @@
|
||||
[![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.2.1&color=239922
|
||||
[version-link]: https://github.com/reactiveops/polaris
|
||||
[version-image]: https://img.shields.io/static/v1.svg?label=Version&message=0.6.0&color=239922
|
||||
[version-link]: https://github.com/FairwindsOps/polaris
|
||||
|
||||
[goreport-image]: https://goreportcard.com/badge/github.com/reactiveops/polaris
|
||||
[goreport-link]: https://goreportcard.com/report/github.com/reactiveops/polaris
|
||||
[goreport-image]: https://goreportcard.com/badge/github.com/FairwindsOps/polaris
|
||||
[goreport-link]: https://goreportcard.com/report/github.com/FairwindsOps/polaris
|
||||
|
||||
[circleci-image]: https://circleci.com/gh/reactiveops/polaris.svg?style=svg
|
||||
[circleci-link]: https://circleci.com/gh/reactiveops/polaris.svg
|
||||
[circleci-image]: https://circleci.com/gh/FairwindsOps/polaris.svg?style=svg
|
||||
[circleci-link]: https://circleci.com/gh/FairwindsOps/polaris.svg
|
||||
|
||||
Polaris helps keep your cluster healthy. It runs a variety of checks to ensure that Kubernetes deployments are configured using best practices that will avoid potential problems in the future. The project includes two primary components:
|
||||
Fairwinds' Polaris keeps your clusters sailing smoothly. It runs a variety of checks to ensure that
|
||||
Kubernetes pods and controllers are configured using best practices, helping you avoid
|
||||
problems in the future. Polaris can be run in a few different modes:
|
||||
|
||||
- A dashboard that provides an overview of how well current deployments are configured within a cluster.
|
||||
- An experimental validating webhook that can prevent any future deployments that do not live up to a configured standard.
|
||||
Polaris can be run in three different modes:
|
||||
* As a [dashboard](#dashboard), so you can audit what's running inside your cluster.
|
||||
* As a [validating webhook](#webhook), so you can automatically reject workloads that don't adhere to your organization's policies.
|
||||
* As a [command-line tool](#cli), so you can test local YAML files, e.g. as part of a CI/CD process.
|
||||
|
||||
**Want to learn more?** ReactiveOps holds [office hours on Zoom](https://zoom.us/j/242508205) the first Friday of every month, at 12pm Eastern. You can also reach out via email at `opensource@reactiveops.com`
|
||||
**Want to learn more?** Fairwinds holds [office hours on Zoom](https://zoom.us/j/242508205) the first Friday of every month, at 12pm Eastern. You can also reach out via email at `opensource@fairwinds.com`
|
||||
|
||||
## Quickstart
|
||||
# Dashboard Quickstart
|
||||
|
||||
```
|
||||
kubectl apply -f https://github.com/reactiveops/polaris/releases/latest/download/dashboard.yaml
|
||||
```bash
|
||||
kubectl apply -f https://github.com/FairwindsOps/polaris/releases/latest/download/dashboard.yaml
|
||||
kubectl port-forward --namespace polaris svc/polaris-dashboard 8080:80
|
||||
```
|
||||
With the port forwarding in place, you can open http://localhost:8080 in your browser to view the dashboard.
|
||||
|
||||
## Dashboard
|
||||
* * *
|
||||
|
||||
The Polaris dashboard is a way to get a simple visual overview of the current state of your Kubernetes deployments 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 deployment.
|
||||
# Components
|
||||
|
||||
## Dashboard
|
||||
> [View installation instructions](docs/usage.md#dashboard)
|
||||
|
||||
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" />
|
||||
@@ -39,126 +48,34 @@ The Polaris dashboard is a way to get a simple visual overview of the current st
|
||||
|
||||
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.
|
||||
|
||||
|
||||
## Webhook
|
||||
> [View installation instructions](docs/usage.md#webhook)
|
||||
|
||||
Polaris includes experimental support for an optional validating webhook. This accepts the same configuration as the dashboard, and can run the same validations. This webhook will reject any deployments 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. *Although we are working towards greater stability and better test coverage, we do not currently consider this webhook component production ready.*
|
||||
Polaris includes an optional 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.
|
||||
|
||||
Unfortunately we have not found a way to display warnings as part of `kubectl` output unless we are rejecting a deployment 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.
|
||||
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.
|
||||
|
||||
## Installation and Usage
|
||||
Polaris can be installed on your cluster using kubectl or Helm. It can also
|
||||
be run as a local binary, which will use your kubeconfig to connect to the cluster
|
||||
or run against local YAML files.
|
||||
## CLI
|
||||
> [View installation instructions](docs/usage.md#cli)
|
||||
|
||||
### kubectl
|
||||
#### Dashboard
|
||||
```
|
||||
kubectl apply -f https://github.com/reactiveops/polaris/releases/latest/download/dashboard.yaml
|
||||
kubectl port-forward --namespace polaris svc/polaris-dashboard 8080:80
|
||||
```
|
||||
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.
|
||||
|
||||
#### Webhook
|
||||
```
|
||||
kubectl apply -f https://github.com/reactiveops/polaris/releases/latest/download/webhook.yaml
|
||||
```
|
||||
# Installation and Usage
|
||||
See the [Usage Guide](/docs/usage.md) for details on different methods for installing and using Polaris.
|
||||
|
||||
### Helm
|
||||
Start by adding the ReactiveOps Helm repo:
|
||||
```
|
||||
helm repo add reactiveops-stable https://charts.reactiveops.com/stable
|
||||
```
|
||||
|
||||
#### Dashboard
|
||||
```
|
||||
helm upgrade --install polaris reactiveops-stable/polaris --namespace polaris
|
||||
kubectl port-forward --namespace polaris svc/polaris-dashboard 8080:80
|
||||
```
|
||||
|
||||
#### Webhook
|
||||
```
|
||||
helm upgrade --install polaris reactiveops-stable/polaris --namespace polaris \
|
||||
--set webhook.enable=true --set dashboard.enable=false
|
||||
```
|
||||
|
||||
### Local Binary
|
||||
#### Installation
|
||||
Binary releases are available on the [releases page](https://github.com/reactiveops/polaris/releases) or can be installed with [Homebrew](https://brew.sh/):
|
||||
```
|
||||
brew tap reactiveops/tap
|
||||
brew install reactiveops/tap/polaris
|
||||
polaris --version
|
||||
```
|
||||
|
||||
You can run `polaris --help` to see a full list of options.
|
||||
|
||||
#### Dashboard
|
||||
The dashboard can be run on your local machine, without installing anything on the cluster.
|
||||
Polaris will use your local kubeconfig to connect to the cluster.
|
||||
|
||||
```
|
||||
polaris --dashboard --dashboard-port 8080
|
||||
```
|
||||
|
||||
#### Audits
|
||||
You can also run audits on the command line and see the output as JSON, YAML, or a raw score:
|
||||
```
|
||||
polaris --audit --output-format yaml > report.yaml
|
||||
polaris --audit --output-format score
|
||||
# 92
|
||||
```
|
||||
|
||||
Both the dashboard and audits can run against a local directory or YAML file
|
||||
rather than a cluster:
|
||||
```
|
||||
polaris --audit --audit-path ./deploy/
|
||||
```
|
||||
|
||||
##### Running with CI/CD
|
||||
You can integrate Polaris into CI/CD for repositories containing infrastructure-as-code.
|
||||
For example, to fail whenever the Polaris score drops below 90%:
|
||||
```bash
|
||||
score=`polaris --audit --audit-path ./deploy/ --output-format score`
|
||||
if [[ $score -lt 90 ]]; then
|
||||
exit 1
|
||||
else
|
||||
exit 0
|
||||
fi
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Polaris supports a wide range of validations covering a number of Kubernetes best practices. Here's a sample configuration file that includes all currently supported checks. The [default configuration](https://github.com/reactiveops/polaris/blob/master/examples/config.yaml) contains a number of those checks. This repository also includes a sample [full configuration file](https://github.com/reactiveops/polaris/blob/master/examples/config-full.yaml) that enables all available 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.
|
||||
|
||||
Polaris validation checks fall into several different categories:
|
||||
|
||||
- [Health Checks](docs/health-checks.md)
|
||||
- [Images](docs/images.md)
|
||||
- [Networking](docs/networking.md)
|
||||
- [Resources](docs/resources.md)
|
||||
- [Security](docs/security.md)
|
||||
|
||||
## CLI Options
|
||||
|
||||
* `config`: Specify a location for the Polaris config
|
||||
* `dashboard`: Runs the webserver for Polaris dashboard.
|
||||
* `dashboard-port`: Port for the dashboard webserver (default `8080`)
|
||||
* `dashboard-base-path`: Path on which the dashboard is being served (default `/`)
|
||||
* `webhook`: Runs the webhook webserver.
|
||||
* `webhook-port`: Port for the webhook webserver (default `9876`)
|
||||
* `disable-webhook-config-installer`: disable the installer in the webhook server, so it won't install webhook configuration resources during bootstrapping
|
||||
* `kubeconfig`: Paths to a kubeconfig. Only required if out-of-cluster.
|
||||
|
||||
## Contributing
|
||||
# Contributing
|
||||
PRs welcome! Check out the [Contributing Guidelines](CONTRIBUTING.md),
|
||||
[Code of Conduct](CODE_OF_CONDUCT.md), and [Roadmap](ROADMAP.md) for more information.
|
||||
|
||||
## Further Information
|
||||
# Further Information
|
||||
A history of changes to this project can be viewed in the [Changelog](CHANGELOG.md)
|
||||
|
||||
If you'd like to learn more about Polaris, or if you'd like to speak with
|
||||
a Kubernetes expert, you can contact `info@reactiveops.com` or [visit our website](https://reactiveops.com)
|
||||
a Kubernetes expert, you can contact `info@fairwinds.com` or [visit our website](https://fairwinds.com)
|
||||
|
||||
## License
|
||||
# License
|
||||
Apache License 2.0
|
||||
|
||||
+14
-8
@@ -5,7 +5,16 @@ 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.
|
||||
|
||||
## Features
|
||||
## 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.
|
||||
@@ -21,7 +30,7 @@ when we feel it's ready.
|
||||
* Pass `warning`-level messages back to `kubectl` if possible (may require changes
|
||||
to core k8s or `kubectl`)
|
||||
|
||||
## Ability to override checks
|
||||
### 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.
|
||||
@@ -30,12 +39,9 @@ This is important, as certain workloads have good reason to skip particular Pola
|
||||
checks - for instance, the `dns-controller` deployment in `kube-system` needs to have
|
||||
the host network configured.
|
||||
|
||||
## Checks
|
||||
These are checks that we plan to implement. If you have requests or ideas,
|
||||
let us know! You can submit a pull request for this file, or open an issue in GitHub.
|
||||
|
||||
### Images
|
||||
* List of allowed/disallowed registries
|
||||
### 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:
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
---
|
||||
# Source: polaris/templates/0-namespace.yaml
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: polaris
|
||||
---
|
||||
# Source: polaris/templates/configmap.yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: polaris
|
||||
namespace: polaris
|
||||
labels:
|
||||
app: polaris
|
||||
data:
|
||||
config.yaml: |
|
||||
resources:
|
||||
cpuRequestsMissing: warning
|
||||
cpuLimitsMissing: warning
|
||||
memoryRequestsMissing: warning
|
||||
memoryLimitsMissing: warning
|
||||
images:
|
||||
tagNotSpecified: error
|
||||
healthChecks:
|
||||
readinessProbeMissing: warning
|
||||
livenessProbeMissing: warning
|
||||
networking:
|
||||
hostNetworkSet: warning
|
||||
hostPortSet: warning
|
||||
security:
|
||||
hostIPCSet: error
|
||||
hostPIDSet: error
|
||||
notReadOnlyRootFileSystem: warning
|
||||
privilegeEscalationAllowed: error
|
||||
runAsRootAllowed: warning
|
||||
runAsPrivileged: error
|
||||
capabilities:
|
||||
error:
|
||||
ifAnyAdded:
|
||||
- SYS_ADMIN
|
||||
- NET_ADMIN
|
||||
- ALL
|
||||
warning:
|
||||
ifAnyAddedBeyond:
|
||||
- CHOWN
|
||||
- DAC_OVERRIDE
|
||||
- FSETID
|
||||
- FOWNER
|
||||
- MKNOD
|
||||
- NET_RAW
|
||||
- SETGID
|
||||
- SETUID
|
||||
- SETFCAP
|
||||
- SETPCAP
|
||||
- NET_BIND_SERVICE
|
||||
- SYS_CHROOT
|
||||
- KILL
|
||||
- AUDIT_WRITE
|
||||
controllers_to_scan:
|
||||
- Deployments
|
||||
- StatefulSets
|
||||
- DaemonSets
|
||||
- Jobs
|
||||
- CronJobs
|
||||
- ReplicationControllers
|
||||
|
||||
---
|
||||
# Source: polaris/templates/dashboard.rbac.yaml
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: polaris-dashboard
|
||||
namespace: polaris
|
||||
labels:
|
||||
app: polaris
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: polaris-dashboard
|
||||
labels:
|
||||
app: polaris
|
||||
rules:
|
||||
- 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'
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: polaris-dashboard
|
||||
labels:
|
||||
app: polaris
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: polaris-dashboard
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: polaris-dashboard
|
||||
namespace: polaris
|
||||
---
|
||||
# Source: polaris/templates/dashboard.service.yaml
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: polaris-dashboard
|
||||
namespace: polaris
|
||||
labels:
|
||||
app: polaris
|
||||
annotations:
|
||||
spec:
|
||||
ports:
|
||||
- name: dashboard
|
||||
port: 80
|
||||
protocol: TCP
|
||||
targetPort: 8080
|
||||
selector:
|
||||
app: polaris
|
||||
component: dashboard
|
||||
type: ClusterIP
|
||||
---
|
||||
# Source: polaris/templates/dashboard.deployment.yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: polaris-dashboard
|
||||
namespace: polaris
|
||||
labels:
|
||||
app: polaris
|
||||
component: dashboard
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: polaris
|
||||
component: dashboard
|
||||
template:
|
||||
metadata:
|
||||
annotations:
|
||||
checksum/config: '8aa5a565fba7a2db98d46752087de8c1dcc83b70cd762c5829d5ba01270d54a2'
|
||||
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'
|
||||
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
|
||||
volumeMounts:
|
||||
- name: config
|
||||
mountPath: /opt/app/config.yaml
|
||||
subPath: config.yaml
|
||||
readOnly: true
|
||||
serviceAccountName: polaris-dashboard
|
||||
nodeSelector:
|
||||
tolerations:
|
||||
---
|
||||
# Source: polaris/templates/audit.job.yaml
|
||||
|
||||
---
|
||||
# Source: polaris/templates/audit.rbac.yaml
|
||||
|
||||
---
|
||||
# Source: polaris/templates/ingress.yaml
|
||||
|
||||
---
|
||||
# Source: polaris/templates/webhook.deployment.yaml
|
||||
|
||||
---
|
||||
# Source: polaris/templates/webhook.rbac.yaml
|
||||
|
||||
---
|
||||
# Source: polaris/templates/webhook.secret.yaml
|
||||
|
||||
---
|
||||
# Source: polaris/templates/webhook.service.yaml
|
||||
|
||||
@@ -0,0 +1,320 @@
|
||||
---
|
||||
# Source: polaris/templates/0-namespace.yaml
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: polaris
|
||||
---
|
||||
# Source: polaris/templates/webhook.secret.yaml
|
||||
# The name of this secret is static as it is populated by the webhook pod.
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: polaris-webhook
|
||||
namespace: polaris
|
||||
labels:
|
||||
app: polaris
|
||||
type: Opaque
|
||||
stringData:
|
||||
cert.pem: ''
|
||||
---
|
||||
# Source: polaris/templates/configmap.yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: polaris
|
||||
namespace: polaris
|
||||
labels:
|
||||
app: polaris
|
||||
data:
|
||||
config.yaml: |
|
||||
resources:
|
||||
cpuRequestsMissing: warning
|
||||
cpuLimitsMissing: warning
|
||||
memoryRequestsMissing: warning
|
||||
memoryLimitsMissing: warning
|
||||
images:
|
||||
tagNotSpecified: error
|
||||
healthChecks:
|
||||
readinessProbeMissing: warning
|
||||
livenessProbeMissing: warning
|
||||
networking:
|
||||
hostNetworkSet: warning
|
||||
hostPortSet: warning
|
||||
security:
|
||||
hostIPCSet: error
|
||||
hostPIDSet: error
|
||||
notReadOnlyRootFileSystem: warning
|
||||
privilegeEscalationAllowed: error
|
||||
runAsRootAllowed: warning
|
||||
runAsPrivileged: error
|
||||
capabilities:
|
||||
error:
|
||||
ifAnyAdded:
|
||||
- SYS_ADMIN
|
||||
- NET_ADMIN
|
||||
- ALL
|
||||
warning:
|
||||
ifAnyAddedBeyond:
|
||||
- CHOWN
|
||||
- DAC_OVERRIDE
|
||||
- FSETID
|
||||
- FOWNER
|
||||
- MKNOD
|
||||
- NET_RAW
|
||||
- SETGID
|
||||
- SETUID
|
||||
- SETFCAP
|
||||
- SETPCAP
|
||||
- NET_BIND_SERVICE
|
||||
- SYS_CHROOT
|
||||
- KILL
|
||||
- AUDIT_WRITE
|
||||
controllers_to_scan:
|
||||
- Deployments
|
||||
- StatefulSets
|
||||
- DaemonSets
|
||||
- Jobs
|
||||
- CronJobs
|
||||
- ReplicationControllers
|
||||
|
||||
---
|
||||
# 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:
|
||||
name: polaris-webhook
|
||||
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:
|
||||
- 'secrets'
|
||||
- 'services'
|
||||
verbs:
|
||||
- 'get'
|
||||
- 'list'
|
||||
- 'watch'
|
||||
- apiGroups:
|
||||
- 'admissionregistration.k8s.io'
|
||||
resources:
|
||||
- 'validatingwebhookconfigurations'
|
||||
verbs:
|
||||
- '*'
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: polaris-webhook
|
||||
labels:
|
||||
app: polaris
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: polaris-webhook
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: polaris-webhook
|
||||
namespace: polaris
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: polaris-webhook
|
||||
namespace: polaris
|
||||
labels:
|
||||
app: polaris
|
||||
rules:
|
||||
# required for current controller-runtime bootstrap method
|
||||
- apiGroups:
|
||||
- ''
|
||||
resources:
|
||||
- 'secrets'
|
||||
- 'services'
|
||||
verbs:
|
||||
- '*'
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: polaris-webhook
|
||||
namespace: polaris
|
||||
labels:
|
||||
app: polaris
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: Role
|
||||
name: polaris-webhook
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: polaris-webhook
|
||||
namespace: polaris
|
||||
---
|
||||
# Source: polaris/templates/webhook.service.yaml
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: polaris-webhook
|
||||
namespace: polaris
|
||||
labels:
|
||||
app: polaris
|
||||
spec:
|
||||
ports:
|
||||
- name: webhook
|
||||
port: 443
|
||||
protocol: TCP
|
||||
targetPort: 9876
|
||||
selector:
|
||||
app: polaris
|
||||
component: webhook
|
||||
type: ClusterIP
|
||||
---
|
||||
# Source: polaris/templates/webhook.deployment.yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: polaris-webhook
|
||||
namespace: polaris
|
||||
labels:
|
||||
app: polaris
|
||||
component: webhook
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: polaris
|
||||
component: webhook
|
||||
template:
|
||||
metadata:
|
||||
annotations:
|
||||
checksum/config: '8aa5a565fba7a2db98d46752087de8c1dcc83b70cd762c5829d5ba01270d54a2'
|
||||
labels:
|
||||
app: polaris
|
||||
component: webhook
|
||||
spec:
|
||||
containers:
|
||||
- name: webhook
|
||||
command:
|
||||
- polaris
|
||||
- --webhook
|
||||
- --config
|
||||
- /opt/app/config.yaml
|
||||
image: 'quay.io/fairwinds/polaris:0.6'
|
||||
imagePullPolicy: 'Always'
|
||||
ports:
|
||||
- containerPort: 9876
|
||||
# These are fairly useless readiness/liveness probes for now
|
||||
# Follow this issue for potential improvements:
|
||||
# https://github.com/kubernetes-sigs/controller-runtime/issues/356
|
||||
livenessProbe:
|
||||
exec:
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- ps -ef | grep polaris
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
readinessProbe:
|
||||
exec:
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- ps -ef | grep polaris
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
resources:
|
||||
limits:
|
||||
cpu: 100m
|
||||
memory: 128Mi
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 128Mi
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
privileged: false
|
||||
readOnlyRootFilesystem: true
|
||||
runAsNonRoot: true
|
||||
capabilities:
|
||||
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
|
||||
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/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
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# Polaris Exit Codes for Audit Runs
|
||||
<dl>
|
||||
<dt>Exit 0</dt>
|
||||
<dd>Successful exit code</dd>
|
||||
<dt>Exit 1</dt>
|
||||
<dd>Could not run audit, or application had a failure while running.</dd>
|
||||
<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>
|
||||
<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>
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
# Installation and Usage
|
||||
Polaris can be installed on your cluster using kubectl or Helm. It can also
|
||||
be run as a local binary, which will use your kubeconfig to connect to the cluster
|
||||
or run against local YAML files.
|
||||
|
||||
## Configuration
|
||||
Polaris supports a wide range of validations covering a number of Kubernetes best practices.
|
||||
Here's a [sample configuration file](/examples/config-full.yaml) that includes all currently supported checks.
|
||||
The [default configuration](/examples/config.yaml) contains a number of those checks.
|
||||
|
||||
|
||||
### 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.
|
||||
|
||||
Polaris validation checks fall into several different categories:
|
||||
|
||||
- [Health Checks](check-documentation/health-checks.md)
|
||||
- [Images](check-documentation/images.md)
|
||||
- [Networking](check-documentation/networking.md)
|
||||
- [Resources](check-documentation/resources.md)
|
||||
- [Security](check-documentation/security.md)
|
||||
|
||||
### Exemptions
|
||||
Exemptions can be added two ways: by annotating a controller, or editing the Polaris config.
|
||||
|
||||
To exempt a controller via annotations, use the annotation `polaris.fairwinds.com/exempt=true`, e.g.
|
||||
```
|
||||
kubectl annotate deployment my-deployment polaris.fairwinds.com/exempt=true
|
||||
```
|
||||
|
||||
To exempt a controller via the config, you have to specify a list of controller names and a list of rules, e.g.
|
||||
```yaml
|
||||
exemptions:
|
||||
- controllerNames:
|
||||
- dns-controller
|
||||
rules:
|
||||
- hostNetworkSet
|
||||
```
|
||||
|
||||
# Installing
|
||||
There are several ways to install and use Polaris. Below outline ways to install using `kubectl`, `helm` and `local binary`.
|
||||
|
||||
## Dashboard
|
||||
The dashboard can be installed on a cluster using kubectl or Helm. It can also be run locally,
|
||||
connecting to your cluster using the credentials stored in your `KUBECONFIG`.
|
||||
|
||||
### kubectl
|
||||
```bash
|
||||
kubectl apply -f https://github.com/fairwindsops/polaris/releases/latest/download/dashboard.yaml
|
||||
kubectl port-forward --namespace polaris svc/polaris-dashboard 8080:80
|
||||
```
|
||||
### Helm
|
||||
```bash
|
||||
helm repo add reactiveops-stable https://charts.reactiveops.com/stable
|
||||
helm upgrade --install polaris reactiveops-stable/polaris --namespace polaris
|
||||
kubectl port-forward --namespace polaris svc/polaris-dashboard 8080:80
|
||||
```
|
||||
|
||||
### Local Binary
|
||||
You'll need a valid `KUBECONFIG` set up for the dashboard to connect to your cluster.
|
||||
|
||||
Binary releases can be dowloaded from the [releases page](https://github.com/fairwindsops/polaris/releases)
|
||||
or can be installed with [Homebrew](https://brew.sh/):
|
||||
```bash
|
||||
brew tap reactiveops/tap
|
||||
brew install reactiveops/tap/polaris
|
||||
polaris --dashboard --dashboard-port 8080
|
||||
```
|
||||
|
||||
## Webhook
|
||||
### kubectl
|
||||
```bash
|
||||
kubectl apply -f https://github.com/fairwindsops/polaris/releases/latest/download/webhook.yaml
|
||||
```
|
||||
|
||||
### Helm
|
||||
```bash
|
||||
helm repo add reactiveops-stable https://charts.reactiveops.com/stable
|
||||
helm upgrade --install polaris reactiveops-stable/polaris --namespace polaris \
|
||||
--set webhook.enable=true --set dashboard.enable=false
|
||||
```
|
||||
|
||||
## CLI
|
||||
### Installation
|
||||
Binary releases can be downloaded from the [releases page](https://github.com/fairwindsops/polaris/releases)
|
||||
or can be installed with [Homebrew](https://brew.sh/):
|
||||
```bash
|
||||
brew tap reactiveops/tap
|
||||
brew install reactiveops/tap/polaris
|
||||
polaris --version
|
||||
```
|
||||
|
||||
You can run audits on the command line and see the output as JSON, YAML, or a raw score:
|
||||
```bash
|
||||
polaris --audit --output-format yaml > report.yaml
|
||||
polaris --audit --output-format score
|
||||
# 92
|
||||
```
|
||||
|
||||
Both the dashboard and audits can run against a local directory or YAML file
|
||||
rather than a cluster:
|
||||
```bash
|
||||
polaris --audit --audit-path ./deploy/
|
||||
```
|
||||
|
||||
#### 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%:
|
||||
```bash
|
||||
polaris --audit --audit-path ./deploy/ \
|
||||
--set-exit-code-on-error \
|
||||
--set-exit-code-below-score 90
|
||||
```
|
||||
|
||||
For more on exit code meanings, see [exit-code docs](exit-codes.md).
|
||||
|
||||
#### CLI Options
|
||||
|
||||
```
|
||||
# high-level flags
|
||||
-version
|
||||
Prints the version of Polaris
|
||||
-config string
|
||||
Location of Polaris configuration file
|
||||
-kubeconfig string
|
||||
Path to a kubeconfig. Only required if out-of-cluster.
|
||||
-log-level string
|
||||
Logrus log level (default "info")
|
||||
-master string
|
||||
The address of the Kubernetes API server. Overrides any value in kubeconfig. Only required if out-of-cluster.
|
||||
|
||||
# dashboard flags
|
||||
-dashboard
|
||||
Runs the webserver for Polaris dashboard.
|
||||
-dashboard-base-path string
|
||||
Path on which the dashboard is served (default "/")
|
||||
-dashboard-port int
|
||||
Port for the dashboard webserver (default 8080)
|
||||
-display-name string
|
||||
An optional identifier for the audit
|
||||
|
||||
# audit flags
|
||||
-audit
|
||||
Runs a one-time audit.
|
||||
-audit-path string
|
||||
If specified, audits one or more YAML files instead of a cluster
|
||||
-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
|
||||
When running with --audit, set an exit code of 4 when the score is below this threshold (1-100)
|
||||
-set-exit-code-on-error
|
||||
When running with --audit, set an exit code of 3 when the audit contains error-level issues.
|
||||
|
||||
# webhook flags
|
||||
-webhook
|
||||
Runs the webhook webserver.
|
||||
-webhook-port int
|
||||
Port for the webhook webserver (default 9876)
|
||||
-disable-webhook-config-installer
|
||||
disable the installer in the webhook server, so it won't install webhook configuration resources during bootstrapping
|
||||
```
|
||||
|
||||
@@ -63,3 +63,80 @@ security:
|
||||
warning:
|
||||
ifAnyAddedBeyond:
|
||||
- NONE
|
||||
controllers_to_scan:
|
||||
- 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
|
||||
rules:
|
||||
- cpuRequestsMissing
|
||||
- cpuLimitsMissing
|
||||
- memoryRequestsMissing
|
||||
- memoryLimitsMissing
|
||||
- controllerNames:
|
||||
- kube2iam
|
||||
rules:
|
||||
- runAsPrivileged
|
||||
|
||||
@@ -41,3 +41,103 @@ security:
|
||||
- SYS_CHROOT
|
||||
- KILL
|
||||
- AUDIT_WRITE
|
||||
controllers_to_scan:
|
||||
- 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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019 ReactiveOps
|
||||
// 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.
|
||||
@@ -22,15 +22,14 @@ import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
conf "github.com/reactiveops/polaris/pkg/config"
|
||||
"github.com/reactiveops/polaris/pkg/dashboard"
|
||||
"github.com/reactiveops/polaris/pkg/kube"
|
||||
"github.com/reactiveops/polaris/pkg/validator"
|
||||
fwebhook "github.com/reactiveops/polaris/pkg/webhook"
|
||||
conf "github.com/fairwindsops/polaris/pkg/config"
|
||||
"github.com/fairwindsops/polaris/pkg/dashboard"
|
||||
"github.com/fairwindsops/polaris/pkg/kube"
|
||||
"github.com/fairwindsops/polaris/pkg/validator"
|
||||
fwebhook "github.com/fairwindsops/polaris/pkg/webhook"
|
||||
"github.com/sirupsen/logrus"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
|
||||
apitypes "k8s.io/apimachinery/pkg/types"
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth" // Required for other auth providers like GKE.
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/config"
|
||||
@@ -42,22 +41,28 @@ import (
|
||||
|
||||
const (
|
||||
// Version represents the current release version of Polaris
|
||||
Version = "0.2.1"
|
||||
Version = "0.6.0"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Load CLI Flags
|
||||
// TODO: Split up global flags vs dashboard/webhook/audit specific flags
|
||||
dashboard := flag.Bool("dashboard", false, "Runs the webserver for Polaris dashboard.")
|
||||
webhook := flag.Bool("webhook", false, "Runs the webhook webserver.")
|
||||
audit := flag.Bool("audit", false, "Runs a one-time audit.")
|
||||
auditPath := flag.String("audit-path", "", "If specified, audits one or more YAML files instead of a cluster")
|
||||
setExitCode := flag.Bool("set-exit-code-on-error", false, "When running with --audit, set an exit code of 3 when the audit contains error-level issues.")
|
||||
minScore := flag.Int("set-exit-code-below-score", 0, "When running with --audit, set an exit code of 4 when the score is below this threshold (1-100)")
|
||||
dashboardPort := flag.Int("dashboard-port", 8080, "Port for the dashboard webserver")
|
||||
dashboardBasePath := flag.String("dashboard-base-path", "/", "Path on which the dashboard is served")
|
||||
webhookPort := flag.Int("webhook-port", 9876, "Port for the webhook webserver")
|
||||
auditOutputURL := flag.String("output-url", "", "Destination URL to send audit results")
|
||||
auditOutputFile := flag.String("output-file", "", "Destination file for audit results")
|
||||
auditOutputFormat := flag.String("output-format", "json", "Output format for results - json, yaml, or score")
|
||||
loadAuditFile := flag.String("load-audit-file", "", "Runs the dashboard with data saved from a past audit.")
|
||||
displayName := flag.String("display-name", "", "An optional identifier for the audit")
|
||||
configPath := flag.String("config", "", "Location of Polaris configuration file")
|
||||
disallowExemptions := flag.Bool("disallow-exemptions", false, "Location of Polaris configuration file")
|
||||
logLevel := flag.String("log-level", logrus.InfoLevel.String(), "Logrus log level")
|
||||
version := flag.Bool("version", false, "Prints the version of Polaris")
|
||||
disableWebhookConfigInstaller := flag.Bool("disable-webhook-config-installer", false,
|
||||
@@ -78,14 +83,19 @@ func main() {
|
||||
}
|
||||
|
||||
c, err := conf.ParseFile(*configPath)
|
||||
if *displayName != "" {
|
||||
c.DisplayName = *displayName
|
||||
}
|
||||
if err != nil {
|
||||
logrus.Errorf("Error parsing config at %s: %v", *configPath, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if *displayName != "" {
|
||||
c.DisplayName = *displayName
|
||||
}
|
||||
|
||||
if *disallowExemptions {
|
||||
c.DisallowExemptions = true
|
||||
}
|
||||
|
||||
if !*dashboard && !*webhook && !*audit {
|
||||
*audit = true
|
||||
}
|
||||
@@ -93,14 +103,27 @@ func main() {
|
||||
if *webhook {
|
||||
startWebhookServer(c, *disableWebhookConfigInstaller, *webhookPort)
|
||||
} else if *dashboard {
|
||||
startDashboardServer(c, *auditPath, *dashboardPort, *dashboardBasePath)
|
||||
startDashboardServer(c, *auditPath, *loadAuditFile, *dashboardPort, *dashboardBasePath)
|
||||
} else if *audit {
|
||||
runAudit(c, *auditPath, *auditOutputFile, *auditOutputURL, *auditOutputFormat)
|
||||
auditData := runAndReportAudit(c, *auditPath, *auditOutputFile, *auditOutputURL, *auditOutputFormat)
|
||||
|
||||
if *setExitCode && auditData.ClusterSummary.Results.Totals.Errors > 0 {
|
||||
logrus.Infof("%d errors found in audit", auditData.ClusterSummary.Results.Totals.Errors)
|
||||
os.Exit(3)
|
||||
} else if *minScore != 0 && auditData.ClusterSummary.Score < uint(*minScore) {
|
||||
logrus.Infof("Audit score of %d is less than the provided minimum of %d", auditData.ClusterSummary.Score, *minScore)
|
||||
os.Exit(4)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func startDashboardServer(c conf.Configuration, auditPath string, port int, basePath string) {
|
||||
router := dashboard.GetRouter(c, auditPath, port, basePath)
|
||||
func startDashboardServer(c conf.Configuration, auditPath string, loadAuditFile string, port int, basePath string) {
|
||||
var auditDataPtr *validator.AuditData
|
||||
if loadAuditFile != "" {
|
||||
auditData := validator.ReadAuditFromFile(loadAuditFile)
|
||||
auditDataPtr = &auditData
|
||||
}
|
||||
router := dashboard.GetRouter(c, auditPath, port, basePath, auditDataPtr)
|
||||
router.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte("OK"))
|
||||
})
|
||||
@@ -165,10 +188,19 @@ func startWebhookServer(c conf.Configuration, disableWebhookConfigInstaller bool
|
||||
|
||||
logrus.Infof("Polaris webhook server listening on port %d", port)
|
||||
|
||||
d1 := fwebhook.NewWebhook("deployments", mgr, fwebhook.Validator{Config: c}, &appsv1.Deployment{})
|
||||
d2 := fwebhook.NewWebhook("deployments-ext", mgr, fwebhook.Validator{Config: c}, &extensionsv1beta1.Deployment{})
|
||||
// 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")
|
||||
if err = as.Register(d1, d2); err != nil {
|
||||
var webhooks []webhook.Webhook
|
||||
for index, controllerToScan := range c.ControllersToScan {
|
||||
for innerIndex, supportedAPIType := range controllerToScan.ListSupportedAPIVersions() {
|
||||
webhookName := strings.ToLower(fmt.Sprintf("%s-%d-%d", controllerToScan, index, innerIndex))
|
||||
hook := fwebhook.NewWebhook(webhookName, mgr, fwebhook.Validator{Config: c}, supportedAPIType)
|
||||
webhooks = append(webhooks, hook)
|
||||
}
|
||||
}
|
||||
|
||||
if err = as.Register(webhooks...); err != nil {
|
||||
logrus.Debugf("Unable to register webhooks in the admission server: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -180,7 +212,8 @@ func startWebhookServer(c conf.Configuration, disableWebhookConfigInstaller bool
|
||||
}
|
||||
}
|
||||
|
||||
func runAudit(c conf.Configuration, auditPath string, outputFile string, outputURL string, outputFormat string) {
|
||||
func runAndReportAudit(c conf.Configuration, auditPath string, outputFile string, outputURL string, outputFormat string) validator.AuditData {
|
||||
// Create a kubernetes client resource provider
|
||||
k, err := kube.CreateResourceProvider(auditPath)
|
||||
if err != nil {
|
||||
logrus.Errorf("Error fetching Kubernetes resources %v", err)
|
||||
@@ -189,12 +222,13 @@ func runAudit(c conf.Configuration, auditPath string, outputFile string, outputU
|
||||
auditData, err := validator.RunAudit(c, k)
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
logrus.Errorf("Error while running audit on resources: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var outputBytes []byte
|
||||
if outputFormat == "score" {
|
||||
outputBytes = []byte(fmt.Sprint(auditData.ClusterSummary.Score))
|
||||
outputBytes = []byte(fmt.Sprintf("%d\n", auditData.ClusterSummary.Score))
|
||||
} else if outputFormat == "yaml" {
|
||||
jsonBytes, err := json.Marshal(auditData)
|
||||
if err == nil {
|
||||
@@ -253,4 +287,5 @@ func runAudit(c conf.Configuration, auditPath string, outputFile string, outputU
|
||||
}
|
||||
}
|
||||
}
|
||||
return auditData
|
||||
}
|
||||
|
||||
+28
-9
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019 ReactiveOps
|
||||
// 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.
|
||||
@@ -19,6 +19,8 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
packr "github.com/gobuffalo/packr/v2"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
@@ -28,12 +30,21 @@ import (
|
||||
|
||||
// Configuration contains all of the config for the validation checks.
|
||||
type Configuration struct {
|
||||
DisplayName string `json:"displayName"`
|
||||
Resources Resources `json:"resources"`
|
||||
HealthChecks HealthChecks `json:"healthChecks"`
|
||||
Images Images `json:"images"`
|
||||
Networking Networking `json:"networking"`
|
||||
Security Security `json:"security"`
|
||||
DisplayName string `json:"displayName"`
|
||||
Resources Resources `json:"resources"`
|
||||
HealthChecks HealthChecks `json:"healthChecks"`
|
||||
Images Images `json:"images"`
|
||||
Networking Networking `json:"networking"`
|
||||
Security Security `json:"security"`
|
||||
ControllersToScan []SupportedController `json:"controllers_to_scan"`
|
||||
Exemptions []Exemption `json:"exemptions"`
|
||||
DisallowExemptions bool `json:"disallowExemptions"`
|
||||
}
|
||||
|
||||
// Exemption represents an exemption to normal rules
|
||||
type Exemption struct {
|
||||
Rules []string `json:"rules"`
|
||||
ControllerNames []string `json:"controllerNames"`
|
||||
}
|
||||
|
||||
// Resources contains config for resource requests and limits.
|
||||
@@ -91,7 +102,7 @@ type Security struct {
|
||||
HostIPCSet Severity `json:"hostIPCSet"`
|
||||
HostPIDSet Severity `json:"hostPIDSet"`
|
||||
RunAsRootAllowed Severity `json:"runAsRootAllowed"`
|
||||
RunAsPrivileged Severity `json:"RunAsPrivileged"`
|
||||
RunAsPrivileged Severity `json:"runAsPrivileged"`
|
||||
NotReadOnlyRootFileSystem Severity `json:"notReadOnlyRootFileSystem"`
|
||||
PrivilegeEscalationAllowed Severity `json:"privilegeEscalationAllowed"`
|
||||
Capabilities SecurityCapabilities `json:"capabilities"`
|
||||
@@ -112,12 +123,20 @@ type SecurityCapabilityLists struct {
|
||||
|
||||
// ParseFile parses config from a file.
|
||||
func ParseFile(path string) (Configuration, error) {
|
||||
configBox := packr.New("Config", "../../examples")
|
||||
var rawBytes []byte
|
||||
var err error
|
||||
if path == "" {
|
||||
configBox := packr.New("Config", "../../examples")
|
||||
rawBytes, err = configBox.Find("config.yaml")
|
||||
} else if strings.HasPrefix(path, "https://") || strings.HasPrefix(path, "http://") {
|
||||
//path is a url
|
||||
response, err2 := http.Get(path)
|
||||
if err2 != nil {
|
||||
return Configuration{}, err2
|
||||
}
|
||||
rawBytes, err = ioutil.ReadAll(response.Body)
|
||||
} else {
|
||||
//path is local
|
||||
rawBytes, err = ioutil.ReadFile(path)
|
||||
}
|
||||
if err != nil {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019 ReactiveOps
|
||||
// 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.
|
||||
@@ -15,7 +15,13 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
@@ -53,6 +59,13 @@ resources:
|
||||
warning:
|
||||
below: 300M
|
||||
above: 4G
|
||||
controllers_to_scan:
|
||||
- Deployments
|
||||
- StatefulSets
|
||||
- Jobs
|
||||
- CronJobs
|
||||
- DaemonSets
|
||||
- ReplicationControllers
|
||||
`
|
||||
|
||||
var resourceConfJSON1 = `{
|
||||
@@ -97,7 +110,8 @@ var resourceConfJSON1 = `{
|
||||
"above": "4G"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"controllers_to_scan": ["Deployments", "StatefulSets", "Jobs", "CronJobs", "DaemonSets", "ReplicationControllers"]
|
||||
}`
|
||||
|
||||
func TestParseError(t *testing.T) {
|
||||
@@ -120,6 +134,37 @@ func TestParseJson(t *testing.T) {
|
||||
testParsedConfig(t, &parsedConf)
|
||||
}
|
||||
|
||||
func TestConfigFromURL(t *testing.T) {
|
||||
var err error
|
||||
var parsedConf Configuration
|
||||
srv := &http.Server{Addr: ":8081"}
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
io.WriteString(w, resourceConfYAML1)
|
||||
})
|
||||
|
||||
go func() {
|
||||
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
|
||||
log.Fatalf("ListenAndServe(): %s", err)
|
||||
}
|
||||
}()
|
||||
time.Sleep(time.Second)
|
||||
|
||||
parsedConf, err = ParseFile("http://localhost:8081/exampleURL")
|
||||
assert.NoError(t, err, "Expected no error when parsing YAML from URL")
|
||||
if err := srv.Shutdown(context.TODO()); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
testParsedConfig(t, &parsedConf)
|
||||
|
||||
}
|
||||
|
||||
func TestConfigNoServerError(t *testing.T) {
|
||||
var err error
|
||||
_, err = ParseFile("http://localhost:8081/exampleURL")
|
||||
assert.Error(t, err)
|
||||
assert.Regexp(t, regexp.MustCompile("connection refused"), err.Error())
|
||||
}
|
||||
|
||||
func testParsedConfig(t *testing.T, config *Configuration) {
|
||||
cpuRequests := config.Resources.CPURequestRanges
|
||||
assert.Equal(t, int64(100), cpuRequests.Error.Below.ScaledValue(resource.Milli))
|
||||
@@ -144,4 +189,7 @@ func testParsedConfig(t *testing.T, config *Configuration) {
|
||||
assert.Equal(t, int64(6000), memLimits.Error.Above.ScaledValue(resource.Mega))
|
||||
assert.Equal(t, int64(300), memLimits.Warning.Below.ScaledValue(resource.Mega))
|
||||
assert.Equal(t, int64(4000), memLimits.Warning.Above.ScaledValue(resource.Mega))
|
||||
|
||||
controllersToScan := config.ControllersToScan
|
||||
assert.ElementsMatch(t, []SupportedController{Deployments, StatefulSets, Jobs, CronJobs, DaemonSets, ReplicationControllers}, controllersToScan)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// IsActionable determines whether a check is actionable given the current configuration
|
||||
func (conf *Configuration) IsActionable(subConf interface{}, ruleName, controllerName string) bool {
|
||||
ruleID := GetIDFromField(subConf, ruleName)
|
||||
subConfRef := reflect.ValueOf(subConf)
|
||||
fieldVal := reflect.Indirect(subConfRef).FieldByName(ruleName).Interface()
|
||||
if severity, ok := fieldVal.(Severity); ok && !severity.IsActionable() {
|
||||
return false
|
||||
}
|
||||
if ranges, ok := fieldVal.(ResourceRanges); ok {
|
||||
if ranges.Warning.Above == nil && ranges.Warning.Below == nil &&
|
||||
ranges.Error.Above == nil && ranges.Error.Below == nil {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if conf.DisallowExemptions {
|
||||
return true
|
||||
}
|
||||
for _, example := range conf.Exemptions {
|
||||
for _, rule := range example.Rules {
|
||||
if rule != ruleID {
|
||||
continue
|
||||
}
|
||||
for _, controller := range example.ControllerNames {
|
||||
if strings.HasPrefix(controllerName, controller) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// GetIDFromField returns the JSON key associated with a particular field, which serves as the check ID.
|
||||
func GetIDFromField(config interface{}, name string) string {
|
||||
t := reflect.TypeOf(config)
|
||||
field, ok := t.FieldByName(name)
|
||||
if !ok {
|
||||
panic("No JSON annotation for field " + name)
|
||||
}
|
||||
id, ok := field.Tag.Lookup("json")
|
||||
if !ok {
|
||||
panic("No JSON tag for field " + name)
|
||||
}
|
||||
return id
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019 ReactiveOps
|
||||
// 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.
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
batchv1 "k8s.io/api/batch/v1"
|
||||
batchv1beta1 "k8s.io/api/batch/v1beta1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
const (
|
||||
// Unsupported is the default enum for non-defined controller types
|
||||
Unsupported SupportedController = iota
|
||||
// Deployments are a supported controller for scanning pod specs
|
||||
Deployments
|
||||
// StatefulSets are a supported controller for scanning pod specs
|
||||
StatefulSets
|
||||
// DaemonSets are a supported controller for scanning pod specs
|
||||
DaemonSets
|
||||
// Jobs are a supported controller for scanning pod specs
|
||||
Jobs
|
||||
// CronJobs are a supported controller for scanning pod specs
|
||||
CronJobs
|
||||
// ReplicationControllers are supported controllers for scanning pod specs
|
||||
ReplicationControllers
|
||||
)
|
||||
|
||||
// ControllerStrings are strongly ordered to match the SupportedController enum
|
||||
var ControllerStrings = []string{
|
||||
"Unsupported",
|
||||
"Deployments",
|
||||
"StatefulSets",
|
||||
"DaemonSets",
|
||||
"Jobs",
|
||||
"CronJobs",
|
||||
"ReplicationController",
|
||||
}
|
||||
|
||||
// stringLookupForSupportedControllers is the list of lowercase singular and plural strings for string to enum lookup
|
||||
var stringLookupForSupportedControllers = map[string]SupportedController{
|
||||
"deployment": Deployments,
|
||||
"deployments": Deployments,
|
||||
"statefulset": StatefulSets,
|
||||
"statefulsets": StatefulSets,
|
||||
"daemonset": DaemonSets,
|
||||
"daemonsets": DaemonSets,
|
||||
"job": Jobs,
|
||||
"jobs": Jobs,
|
||||
"cronjob": CronJobs,
|
||||
"cronjobs": CronJobs,
|
||||
"replicationcontroller": ReplicationControllers,
|
||||
"replicationcontrollers": ReplicationControllers,
|
||||
}
|
||||
|
||||
// SupportedController is a constant item of a controller that is supported for scanning pod specs
|
||||
type SupportedController int
|
||||
|
||||
// String returns the string name for a given SupportedController enum
|
||||
func (s SupportedController) String() string {
|
||||
return ControllerStrings[s]
|
||||
}
|
||||
|
||||
// MarshalJSON manages writing the enum into json data or error on unsupported value
|
||||
func (s SupportedController) MarshalJSON() ([]byte, error) {
|
||||
if s == Unsupported {
|
||||
return []byte{}, fmt.Errorf("Unsupported is not a valid Supported Controller")
|
||||
}
|
||||
buffer := bytes.NewBufferString(`"`)
|
||||
buffer.WriteString(s.String())
|
||||
buffer.WriteString(`"`)
|
||||
return buffer.Bytes(), nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON handles reading json data into enum
|
||||
func (s *SupportedController) UnmarshalJSON(b []byte) error {
|
||||
var j string
|
||||
err := json.Unmarshal(b, &j)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*s, err = GetSupportedControllerFromString(j)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListSupportedAPIVersions for SupportedController returns all the apimachinery object type supported
|
||||
func (s SupportedController) ListSupportedAPIVersions() []runtime.Object {
|
||||
var supportedVersions []runtime.Object
|
||||
switch s {
|
||||
case Deployments:
|
||||
supportedVersions = []runtime.Object{
|
||||
&appsv1.Deployment{},
|
||||
}
|
||||
case StatefulSets:
|
||||
supportedVersions = []runtime.Object{
|
||||
&appsv1.StatefulSet{},
|
||||
}
|
||||
case DaemonSets:
|
||||
supportedVersions = []runtime.Object{
|
||||
&appsv1.DaemonSet{},
|
||||
}
|
||||
case Jobs:
|
||||
supportedVersions = []runtime.Object{
|
||||
&batchv1.Job{},
|
||||
}
|
||||
case CronJobs:
|
||||
supportedVersions = []runtime.Object{
|
||||
&batchv1beta1.CronJob{},
|
||||
}
|
||||
case ReplicationControllers:
|
||||
supportedVersions = []runtime.Object{
|
||||
&corev1.ReplicationController{},
|
||||
}
|
||||
}
|
||||
return supportedVersions
|
||||
}
|
||||
|
||||
// GetSupportedControllerFromString fuzzy matches a string with a SupportedController Enum
|
||||
func GetSupportedControllerFromString(str string) (SupportedController, error) {
|
||||
lowerStr := strings.ToLower(str)
|
||||
controller, keyFound := stringLookupForSupportedControllers[lowerStr]
|
||||
if !keyFound || controller == Unsupported {
|
||||
return 0, fmt.Errorf("Value ('%v') in configuration was not found in Supported Controllers: (%v)", str, strings.Join(ControllerStrings, ","))
|
||||
}
|
||||
return controller, nil
|
||||
}
|
||||
|
||||
// CheckIfKindIsConfiguredForValidation takes a kind (in string format) and checks if Polaris is configured to scan this type of controller
|
||||
func (c Configuration) CheckIfKindIsConfiguredForValidation(kind string) bool {
|
||||
controller, err := GetSupportedControllerFromString(kind)
|
||||
// if no errors then we found the kind in supported controller types
|
||||
if err == nil {
|
||||
// see if the kind exists in the controllers to scan config
|
||||
for _, controllerToScan := range c.ControllersToScan {
|
||||
if controller == controllerToScan {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
// Copyright 2019 FairwindsOps Inc
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type checkMarshal struct {
|
||||
Controllers []SupportedController `json:"controllers"`
|
||||
}
|
||||
|
||||
func TestUnmarshalSupportedControllers(t *testing.T) {
|
||||
for idx, controllerString := range ControllerStrings {
|
||||
// Check taking all strings and convert them into enums
|
||||
object := checkMarshal{}
|
||||
jsonBytes := []byte(fmt.Sprintf(`{"controllers":["%v"]}`, controllerString))
|
||||
err := json.Unmarshal(jsonBytes, &object)
|
||||
if idx == 0 {
|
||||
if err == nil {
|
||||
// Assure the first element always should NOT unmarshal
|
||||
t.Errorf("Expected the first element (%s) to fail json unmarshal. First element in this array should always be 'Unsupported'", controllerString)
|
||||
}
|
||||
} else if err != nil {
|
||||
t.Errorf("Could not unmarshal json (%s) to a Supported controller; Received (%v)", jsonBytes, err)
|
||||
}
|
||||
}
|
||||
|
||||
badJSON := []byte(`{"controllers":[{"not":"valid_structure"}]}`)
|
||||
err := json.Unmarshal(badJSON, &checkMarshal{})
|
||||
if err == nil {
|
||||
t.Error("expected invalid schema json to fail unmarshal")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarshalSupportedControllers(t *testing.T) {
|
||||
for idx, controllerString := range ControllerStrings {
|
||||
controllerType, err := GetSupportedControllerFromString(controllerString)
|
||||
if idx == 0 {
|
||||
if err == nil {
|
||||
t.Errorf("Expected first element (%s) to fail as a non-valid supported controller. Reserved for 'Unsupported'", controllerString)
|
||||
}
|
||||
} else if err != nil {
|
||||
t.Errorf("Unable to take the configured string (%s) and convert into Enum; Error: (%s)", controllerString, err)
|
||||
}
|
||||
|
||||
object := checkMarshal{
|
||||
Controllers: []SupportedController{controllerType},
|
||||
}
|
||||
_, err = json.Marshal(object)
|
||||
if idx == 0 {
|
||||
if err == nil {
|
||||
t.Errorf("Expected (%s) to throw an error. Reserving the first element in the enum to be an invalid config", controllerString)
|
||||
}
|
||||
} else if err != nil {
|
||||
t.Errorf("Could not write json output for element (%s); Received Error: (%s)", controllerString, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckIfControllerKindIsConfiguredForValidation(t *testing.T) {
|
||||
config := Configuration{}
|
||||
for _, controllerString := range ControllerStrings[1:] {
|
||||
controllerEnum, err := GetSupportedControllerFromString(controllerString)
|
||||
if err != nil {
|
||||
t.Errorf("Expected controller string (%s) to be convertable into enum: (%s)", controllerString, err)
|
||||
}
|
||||
config.ControllersToScan = append(config.ControllersToScan, controllerEnum)
|
||||
}
|
||||
|
||||
validControllerKinds := []string{
|
||||
"deployment",
|
||||
"statefulset",
|
||||
}
|
||||
|
||||
invalidControllerKinds := []string{
|
||||
"nonExistent",
|
||||
}
|
||||
|
||||
for _, kind := range validControllerKinds {
|
||||
if ok := config.CheckIfKindIsConfiguredForValidation(kind); !ok {
|
||||
t.Errorf("Kind (%s) expected to be valid for configuration.", kind)
|
||||
}
|
||||
}
|
||||
|
||||
for _, kind := range invalidControllerKinds {
|
||||
if ok := config.CheckIfKindIsConfiguredForValidation(kind); ok {
|
||||
t.Errorf("Kind (%s) should not be a valid controller to check", kind)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSupportedControllerFromString(t *testing.T) {
|
||||
fixture := map[string]SupportedController{
|
||||
"": Unsupported,
|
||||
"asdfasdf": Unsupported,
|
||||
"\000": Unsupported,
|
||||
"deployMENTS": Deployments,
|
||||
"JOB": Jobs,
|
||||
}
|
||||
|
||||
for inputString, expectedType := range fixture {
|
||||
resolvedType, err := GetSupportedControllerFromString(inputString)
|
||||
if expectedType == Unsupported && err == nil {
|
||||
t.Errorf("Expected (%s) to resolve to an unsupported type and throw an error.", inputString)
|
||||
}
|
||||
assert.Equal(t, expectedType, resolvedType, fmt.Sprintf("Expected (%s) to return (%s) controller type.", inputString, expectedType))
|
||||
}
|
||||
}
|
||||
@@ -22,24 +22,17 @@ body {
|
||||
}
|
||||
|
||||
.navbar .logo {
|
||||
width: 250px;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.navbar span.oss-text {
|
||||
color: #23103A;
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
margin-bottom: 3px;
|
||||
.navbar .fw-logo {
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
.navbar a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.navbar .ro-logo {
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.card {
|
||||
margin: 25px 20px;
|
||||
padding: 20px;
|
||||
@@ -64,3 +57,13 @@ body {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.exemption-alert {
|
||||
margin-top: 15px;
|
||||
padding: 15px;
|
||||
border: 1px solid #f26c21;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.exemption-alert .fa-exclamation {
|
||||
margin-right: 10px;
|
||||
color: #f26c21;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 486.5 92.3" style="enable-background:new 0 0 486.5 92.3;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#23103A;}
|
||||
</style>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st0" d="M72.5,24.3H0.4V0.4h84.4V12C84.8,18.8,79.3,24.3,72.5,24.3z"/>
|
||||
<path class="st0" d="M41.8,58H0.4V34.1h53.7v11.6C54.1,52.5,48.6,58,41.8,58z"/>
|
||||
<path class="st0" d="M12,91.7H0.4V67.8h23.9v11.6C24.2,86.2,18.7,91.7,12,91.7z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="st0" d="M121.7,77V16.9h36.9v4.2c0,3.7-3,6.8-6.8,6.8h-18.4v13.5H150v3.5c0,4.1-3.3,7.5-7.5,7.5h-9.1v19.1
|
||||
c0,3-2.5,5.5-5.5,5.5H121.7z"/>
|
||||
<path class="st0" d="M161.1,64.7c0-2,0.3-3.7,1-5.1c0.7-1.5,1.6-2.7,2.9-3.6c1.3-1,2.7-1.7,4.2-2.3c1.6-0.6,3.3-1,5.4-1.3l11.3-2
|
||||
v-1.5c0-2-0.5-3.5-1.6-4.6c-1-1.1-2.6-1.6-4.5-1.6s-3.4,0.5-4.6,1.4c-1.2,1-1.8,2.2-1.8,3.8h-10.8c0.3-4.5,1.9-8.2,5.1-11
|
||||
c3.1-2.8,7.2-4.3,12.3-4.3c5.3,0,9.5,1.3,12.6,4s4.7,6.6,4.7,11.8V77h-4.7c-2.8,0-5.1-2.1-5.5-4.8l-0.1-1.1
|
||||
c-2.7,4.6-6.9,6.8-12.4,6.8c-3.9,0-7.1-1.2-9.6-3.6C162.4,72,161.1,68.8,161.1,64.7z M172.3,64c0,1.2,0.4,2.1,1.3,2.8
|
||||
c0.9,0.7,2.1,1,3.7,1c1.4,0,2.7-0.3,3.9-0.9c1.2-0.6,2.2-1.5,3-2.7c1-1.4,1.5-3.2,1.5-5.3v-1.2l-8.7,1.8c-1.6,0.3-2.8,0.8-3.6,1.5
|
||||
C172.7,61.8,172.3,62.7,172.3,64z"/>
|
||||
<path class="st0" d="M207,20.9c0-1.8,0.7-3.6,2-4.9c1.3-1.2,2.8-1.9,4.6-1.9c1.8,0,3.6,0.6,4.9,1.9c1.4,1.3,2.1,2.9,2.1,4.8
|
||||
s-0.7,3.4-2,4.7c-1.3,1.3-2.9,1.9-4.8,1.9s-3.5-0.6-4.8-1.9C207.7,24.2,207.1,22.7,207,20.9z M208.2,77V33.5h11.2v38
|
||||
c0,3-2.5,5.5-5.5,5.5H208.2z"/>
|
||||
<path class="st0" d="M230.4,77V33.5h9.6l1.1,6.8c0.8-2.1,2.3-3.9,4.4-5.2c2.1-1.4,4.9-2.1,8.3-2.1v11c-3.5,0-6.4,0.9-8.6,2.6
|
||||
s-3.4,4.4-3.5,8.1v16.7c0,3-2.5,5.5-5.5,5.5L230.4,77L230.4,77z"/>
|
||||
<path class="st0" d="M259.6,33.5h11.9l6.6,26.1l0.8,4.6l1-4.6l7.4-26.1h9.9l7.4,26.1l0.9,4.6l0.8-4.6l6.7-26.1h11.9L313,72.9
|
||||
c-0.7,2.4-3,4.1-5.5,4.1H300l-7.9-25.6L285,73c-0.8,2.3-3,3.9-5.4,3.9h-7L259.6,33.5z"/>
|
||||
<path class="st0" d="M330.5,20.7c0-1.8,0.7-3.4,2-4.7c1.3-1.3,2.9-1.9,4.8-1.9c1.8,0,3.4,0.6,4.8,1.9c1.3,1.3,2,2.8,2,4.7
|
||||
c0,1.9-0.7,3.4-2,4.7c-1.3,1.3-2.9,1.9-4.8,1.9s-3.5-0.6-4.8-1.9C331.2,24.2,330.5,22.6,330.5,20.7z M331.7,77V33.5h11.2v38
|
||||
c0,3-2.5,5.5-5.5,5.5H331.7z"/>
|
||||
<path class="st0" d="M354.8,77V33.5h9.6l0.9,5.8c1.1-2,2.6-3.6,4.6-4.9c2-1.2,4.4-1.9,7.2-1.9c4.5,0,8,1.4,10.4,4.3
|
||||
s3.6,6.8,3.6,11.9v22.5c0,3.2-2.6,5.7-5.7,5.7h-5.5V51.4c0-2.5-0.5-4.5-1.6-5.8c-1-1.3-2.6-2-4.6-2c-2.3,0-4.1,0.9-5.5,2.7
|
||||
c-1.4,1.8-2.2,4.4-2.2,7.9v17.1c0,3.2-2.6,5.7-5.7,5.7L354.8,77L354.8,77z"/>
|
||||
<path class="st0" d="M399.1,55.2c0-6.4,1.9-11.8,5.7-16.1c3.8-4.3,8.7-6.5,14.5-6.5c2.9,0,5.5,0.5,7.8,1.6s4,2.4,5.2,4.1V14.4
|
||||
h11.2V77h-4.8c-2.7,0-5-2-5.4-4.7l-0.2-1.2c-3.1,4.5-7.7,6.8-13.9,6.8c-3.8,0-7.3-1-10.4-3s-5.5-4.7-7.2-8.2
|
||||
C399.9,63.3,399.1,59.4,399.1,55.2z M410.5,55.2c0,3.7,1,6.6,3.1,8.9c2.1,2.3,4.7,3.4,7.8,3.4c2.9,0,5.5-1.1,7.7-3.2
|
||||
c2.2-2.2,3.3-5.1,3.3-8.8V55c0-2.5-0.6-4.6-1.7-6.5s-2.5-3.2-4.1-4.1s-3.4-1.3-5.2-1.3c-3.1,0-5.7,1.1-7.8,3.4
|
||||
S410.5,51.6,410.5,55.2z"/>
|
||||
<path class="st0" d="M452.2,63.8h6.9c2,0,3.7,1.2,4.8,2.9c0.2,0.4,0.6,0.7,0.9,1c1.1,0.9,2.6,1.4,4.5,1.4c1.7,0,3-0.4,4-1.1
|
||||
c0.9-0.7,1.4-1.8,1.4-3c0-1.4-0.5-2.5-1.6-3.3s-3.2-1.5-6.2-2.1c-4.8-0.8-8.3-2.3-10.5-4.4c-2.3-2.1-3.4-4.9-3.4-8.5
|
||||
c0-4,1.4-7.4,4.2-10.1c2.8-2.7,6.8-4.1,11.9-4.1c4.6,0,8.4,1.3,11.3,3.8c2.9,2.5,4.5,5.8,4.7,9.9H474c-0.1-3.3-1.7-5-5-5
|
||||
c-1.5,0-2.7,0.4-3.5,1.2c-0.8,0.8-1.3,1.8-1.3,3.1c0,1.4,0.5,2.4,1.6,3.1s3.1,1.3,6,1.8c5.1,0.9,8.8,2.4,10.9,4.5
|
||||
c2.2,2.1,3.2,5,3.2,8.7c0,2.4-0.4,4.5-1.3,6.4c-0.9,1.8-2.1,3.3-3.7,4.4c-1.6,1.1-3.3,2-5.3,2.5c-2,0.6-4.1,0.8-6.5,0.8
|
||||
c-5.3,0-9.4-1.3-12.3-3.8C453.9,71.6,452.3,68.2,452.2,63.8z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 3.8 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
+70
-36
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019 ReactiveOps
|
||||
// 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.
|
||||
@@ -19,13 +19,15 @@ import (
|
||||
"encoding/json"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/fairwindsops/polaris/pkg/config"
|
||||
"github.com/fairwindsops/polaris/pkg/kube"
|
||||
"github.com/fairwindsops/polaris/pkg/validator"
|
||||
packr "github.com/gobuffalo/packr/v2"
|
||||
"github.com/gorilla/mux"
|
||||
conf "github.com/reactiveops/polaris/pkg/config"
|
||||
"github.com/reactiveops/polaris/pkg/kube"
|
||||
"github.com/reactiveops/polaris/pkg/validator"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gitlab.com/golang-commonmark/markdown"
|
||||
)
|
||||
@@ -72,7 +74,7 @@ func GetTemplateBox() *packr.Box {
|
||||
// GetMarkdownBox returns a binary-friendly set of markdown files with error details
|
||||
func GetMarkdownBox() *packr.Box {
|
||||
if markdownBox == (*packr.Box)(nil) {
|
||||
markdownBox = packr.New("Markdown", "../../docs")
|
||||
markdownBox = packr.New("Markdown", "../../docs/check-documentation")
|
||||
}
|
||||
return markdownBox
|
||||
}
|
||||
@@ -80,6 +82,7 @@ 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
|
||||
}
|
||||
@@ -135,12 +138,28 @@ func writeTemplate(tmpl *template.Template, data *templateData, w http.ResponseW
|
||||
buf.WriteTo(w)
|
||||
}
|
||||
|
||||
func getConfigForQuery(base config.Configuration, query url.Values) config.Configuration {
|
||||
c := base
|
||||
exemptions := query.Get("disallowExemptions")
|
||||
if exemptions == "false" {
|
||||
c.DisallowExemptions = false
|
||||
}
|
||||
if exemptions == "true" {
|
||||
c.DisallowExemptions = true
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// GetRouter returns a mux router serving all routes necessary for the dashboard
|
||||
func GetRouter(c conf.Configuration, auditPath string, port int, basePath string) *mux.Router {
|
||||
router := mux.NewRouter()
|
||||
func GetRouter(c config.Configuration, auditPath string, port int, basePath string, auditData *validator.AuditData) *mux.Router {
|
||||
router := mux.NewRouter().PathPrefix(basePath).Subrouter()
|
||||
fileServer := http.FileServer(GetAssetBox())
|
||||
router.PathPrefix("/static/").Handler(http.StripPrefix(path.Join(basePath, "/static/"), fileServer))
|
||||
|
||||
router.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte("OK"))
|
||||
})
|
||||
|
||||
router.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
|
||||
favicon, err := GetAssetBox().Find("favicon-32x32.png")
|
||||
if err != nil {
|
||||
@@ -150,47 +169,67 @@ func GetRouter(c conf.Configuration, auditPath string, port int, basePath string
|
||||
}
|
||||
w.Write(favicon)
|
||||
})
|
||||
|
||||
router.HandleFunc("/results.json", func(w http.ResponseWriter, r *http.Request) {
|
||||
k, err := kube.CreateResourceProvider(auditPath)
|
||||
if err != nil {
|
||||
logrus.Errorf("Error fetching Kubernetes resources %v", err)
|
||||
http.Error(w, "Error fetching Kubernetes resources", http.StatusInternalServerError)
|
||||
return
|
||||
adjustedConf := getConfigForQuery(c, r.URL.Query())
|
||||
if auditData == nil {
|
||||
k, err := kube.CreateResourceProvider(auditPath)
|
||||
if err != nil {
|
||||
logrus.Errorf("Error fetching Kubernetes resources %v", err)
|
||||
http.Error(w, "Error fetching Kubernetes resources", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
auditDataObj, err := validator.RunAudit(adjustedConf, k)
|
||||
if err != nil {
|
||||
http.Error(w, "Error Fetching Deployments", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
auditData = &auditDataObj
|
||||
}
|
||||
JSONHandler(w, r, c, k)
|
||||
|
||||
JSONHandler(w, r, auditData)
|
||||
})
|
||||
|
||||
router.HandleFunc("/details/{category}", func(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
category := vars["category"]
|
||||
category = strings.Replace(category, ".md", "", -1)
|
||||
DetailsHandler(w, r, category, basePath)
|
||||
})
|
||||
fileServer := http.FileServer(GetAssetBox())
|
||||
router.PathPrefix("/static/").Handler(http.StripPrefix("/static/", fileServer))
|
||||
|
||||
router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/" {
|
||||
if r.URL.Path != "/" && r.URL.Path != basePath {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
k, err := kube.CreateResourceProvider(auditPath)
|
||||
if err != nil {
|
||||
logrus.Errorf("Error fetching Kubernetes resources %v", err)
|
||||
http.Error(w, "Error fetching Kubernetes resources", http.StatusInternalServerError)
|
||||
return
|
||||
adjustedConf := getConfigForQuery(c, r.URL.Query())
|
||||
|
||||
if auditData == nil {
|
||||
k, err := kube.CreateResourceProvider(auditPath)
|
||||
if err != nil {
|
||||
logrus.Errorf("Error fetching Kubernetes resources %v", err)
|
||||
http.Error(w, "Error fetching Kubernetes resources", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
auditData, err := validator.RunAudit(adjustedConf, k)
|
||||
if err != nil {
|
||||
logrus.Errorf("Error getting audit data: %v", err)
|
||||
http.Error(w, "Error running audit", 500)
|
||||
return
|
||||
}
|
||||
MainHandler(w, r, adjustedConf, auditData, basePath)
|
||||
} else {
|
||||
MainHandler(w, r, adjustedConf, *auditData, basePath)
|
||||
}
|
||||
auditData, err := validator.RunAudit(c, k)
|
||||
if err != nil {
|
||||
logrus.Errorf("Error getting audit data: %v", err)
|
||||
http.Error(w, "Error running audit", 500)
|
||||
return
|
||||
}
|
||||
MainHandler(w, r, auditData, basePath)
|
||||
|
||||
})
|
||||
return router
|
||||
}
|
||||
|
||||
// MainHandler gets template data and renders the dashboard with it.
|
||||
func MainHandler(w http.ResponseWriter, r *http.Request, auditData validator.AuditData, basePath string) {
|
||||
func MainHandler(w http.ResponseWriter, r *http.Request, c config.Configuration, auditData validator.AuditData, basePath string) {
|
||||
jsonData, err := json.Marshal(auditData)
|
||||
|
||||
if err != nil {
|
||||
@@ -202,6 +241,7 @@ func MainHandler(w http.ResponseWriter, r *http.Request, auditData validator.Aud
|
||||
BasePath: basePath,
|
||||
AuditData: auditData,
|
||||
JSON: template.JS(jsonData),
|
||||
Config: c,
|
||||
}
|
||||
tmpl, err := GetBaseTemplate("main")
|
||||
if err != nil {
|
||||
@@ -213,13 +253,7 @@ func MainHandler(w http.ResponseWriter, r *http.Request, auditData validator.Aud
|
||||
}
|
||||
|
||||
// JSONHandler gets template data and renders json with it.
|
||||
func JSONHandler(w http.ResponseWriter, r *http.Request, c conf.Configuration, kubeResources *kube.ResourceProvider) {
|
||||
auditData, err := validator.RunAudit(c, kubeResources)
|
||||
if err != nil {
|
||||
http.Error(w, "Error Fetching Deployments", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
func JSONHandler(w http.ResponseWriter, r *http.Request, auditData *validator.AuditData) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(auditData)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019 ReactiveOps
|
||||
// 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.
|
||||
@@ -16,15 +16,13 @@ package dashboard
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/reactiveops/polaris/pkg/validator"
|
||||
"strings"
|
||||
|
||||
"github.com/fairwindsops/polaris/pkg/validator"
|
||||
)
|
||||
|
||||
func getAllControllerResults(nr validator.NamespaceResult) []validator.ControllerResult {
|
||||
results := []validator.ControllerResult{}
|
||||
results = append(results, nr.DeploymentResults...)
|
||||
results = append(results, nr.StatefulSetResults...)
|
||||
return results
|
||||
return nr.GetAllControllerResults()
|
||||
}
|
||||
|
||||
func getWarningWidth(counts validator.CountSummary, fullWidth int) uint {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{{define "footer"}}
|
||||
<div class="footer">
|
||||
<a href="https://reactiveops.com?source=polaris" target="_blank">©2019 ReactiveOps Inc.</a>
|
||||
<a href="https://fairwinds.com?source=polaris" target="_blank">©2019 FairwindsOps Inc.</a>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<base href="{{ .BasePath }}">
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||
<title>ReactiveOps Polaris</title>
|
||||
<title>Fairwinds Polaris</title>
|
||||
<meta name="description" content="">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
|
||||
@@ -5,11 +5,20 @@
|
||||
<img class="logo" src="static/images/polaris-logo.png" alt="Polaris" />
|
||||
</a>
|
||||
<div class="navbar-right">
|
||||
<a href="https://reactiveops.com?source=polaris" target="_blank">
|
||||
<span class="oss-text">An Open Source Project By</span>
|
||||
<img class="ro-logo" src="static/images/ro-logo.png" alt="ReactiveOps" />
|
||||
<a href="https://fairwinds.com?source=polaris" target="_blank">
|
||||
<img class="fw-logo" src="static/images/oss-logo.png" alt="Fairwinds" />
|
||||
</a>
|
||||
</div>
|
||||
{{ if and (not .Config.DisallowExemptions) (gt (len .Config.Exemptions) 0) }}
|
||||
<div class="exemption-alert">
|
||||
<i class="fas fa-exclamation"></i>
|
||||
<span>
|
||||
Some checks were skipped based on configured exemptions.
|
||||
<a href="?disallowExemptions=true">Click here</a>
|
||||
to view the report with these checks included.
|
||||
</span>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
+84
-35
@@ -11,6 +11,8 @@ import (
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
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"
|
||||
k8sYaml "k8s.io/apimachinery/pkg/util/yaml"
|
||||
@@ -21,15 +23,19 @@ import (
|
||||
|
||||
// ResourceProvider contains k8s resources to be audited
|
||||
type ResourceProvider struct {
|
||||
ServerVersion string
|
||||
CreationTime time.Time
|
||||
SourceName string
|
||||
SourceType string
|
||||
Nodes []corev1.Node
|
||||
Deployments []appsv1.Deployment
|
||||
StatefulSets []appsv1.StatefulSet
|
||||
Namespaces []corev1.Namespace
|
||||
Pods []corev1.Pod
|
||||
ServerVersion string
|
||||
CreationTime time.Time
|
||||
SourceName string
|
||||
SourceType string
|
||||
Nodes []corev1.Node
|
||||
Deployments []appsv1.Deployment
|
||||
StatefulSets []appsv1.StatefulSet
|
||||
DaemonSets []appsv1.DaemonSet
|
||||
Jobs []batchv1.Job
|
||||
CronJobs []batchv1beta1.CronJob
|
||||
ReplicationControllers []corev1.ReplicationController
|
||||
Namespaces []corev1.Namespace
|
||||
Pods []corev1.Pod
|
||||
}
|
||||
|
||||
type k8sResource struct {
|
||||
@@ -47,14 +53,18 @@ func CreateResourceProvider(directory string) (*ResourceProvider, error) {
|
||||
// CreateResourceProviderFromPath returns a new ResourceProvider using the YAML files in a directory
|
||||
func CreateResourceProviderFromPath(directory string) (*ResourceProvider, error) {
|
||||
resources := ResourceProvider{
|
||||
ServerVersion: "unknown",
|
||||
SourceType: "Path",
|
||||
SourceName: directory,
|
||||
Nodes: []corev1.Node{},
|
||||
Deployments: []appsv1.Deployment{},
|
||||
StatefulSets: []appsv1.StatefulSet{},
|
||||
Namespaces: []corev1.Namespace{},
|
||||
Pods: []corev1.Pod{},
|
||||
ServerVersion: "unknown",
|
||||
SourceType: "Path",
|
||||
SourceName: directory,
|
||||
Nodes: []corev1.Node{},
|
||||
Deployments: []appsv1.Deployment{},
|
||||
StatefulSets: []appsv1.StatefulSet{},
|
||||
DaemonSets: []appsv1.DaemonSet{},
|
||||
Jobs: []batchv1.Job{},
|
||||
CronJobs: []batchv1beta1.CronJob{},
|
||||
ReplicationControllers: []corev1.ReplicationController{},
|
||||
Namespaces: []corev1.Namespace{},
|
||||
Pods: []corev1.Pod{},
|
||||
}
|
||||
|
||||
addYaml := func(contents string) error {
|
||||
@@ -77,7 +87,7 @@ func CreateResourceProviderFromPath(directory string) (*ResourceProvider, error)
|
||||
}
|
||||
err = addYaml(spec)
|
||||
if err != nil {
|
||||
logrus.Errorf("Error parsing YAML %v", err)
|
||||
logrus.Errorf("Error parsing YAML: (%v)", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -124,6 +134,26 @@ func CreateResourceProviderFromAPI(kube kubernetes.Interface, clusterName string
|
||||
logrus.Errorf("Error fetching StatefulSets%v", err)
|
||||
return nil, err
|
||||
}
|
||||
daemonSets, err := kube.AppsV1().DaemonSets("").List(listOpts)
|
||||
if err != nil {
|
||||
logrus.Errorf("Error fetching DaemonSets %v", err)
|
||||
return nil, err
|
||||
}
|
||||
jobs, err := kube.BatchV1().Jobs("").List(listOpts)
|
||||
if err != nil {
|
||||
logrus.Errorf("Error fetching Jobs %v", err)
|
||||
return nil, err
|
||||
}
|
||||
cronJobs, err := kube.BatchV1beta1().CronJobs("").List(listOpts)
|
||||
if err != nil {
|
||||
logrus.Errorf("Error fetching CronJobs %v", err)
|
||||
return nil, err
|
||||
}
|
||||
replicationControllers, err := kube.CoreV1().ReplicationControllers("").List(listOpts)
|
||||
if err != nil {
|
||||
logrus.Errorf("Error fetching ReplicationControllers %v", err)
|
||||
return nil, err
|
||||
}
|
||||
nodes, err := kube.CoreV1().Nodes().List(listOpts)
|
||||
if err != nil {
|
||||
logrus.Errorf("Error fetching Nodes %v", err)
|
||||
@@ -141,15 +171,19 @@ func CreateResourceProviderFromAPI(kube kubernetes.Interface, clusterName string
|
||||
}
|
||||
|
||||
api := ResourceProvider{
|
||||
ServerVersion: serverVersion.Major + "." + serverVersion.Minor,
|
||||
SourceType: "Cluster",
|
||||
SourceName: clusterName,
|
||||
CreationTime: time.Now(),
|
||||
Deployments: deploys.Items,
|
||||
StatefulSets: statefulSets.Items,
|
||||
Nodes: nodes.Items,
|
||||
Namespaces: namespaces.Items,
|
||||
Pods: pods.Items,
|
||||
ServerVersion: serverVersion.Major + "." + serverVersion.Minor,
|
||||
SourceType: "Cluster",
|
||||
SourceName: clusterName,
|
||||
CreationTime: time.Now(),
|
||||
Deployments: deploys.Items,
|
||||
StatefulSets: statefulSets.Items,
|
||||
DaemonSets: daemonSets.Items,
|
||||
Jobs: jobs.Items,
|
||||
CronJobs: cronJobs.Items,
|
||||
ReplicationControllers: replicationControllers.Items,
|
||||
Nodes: nodes.Items,
|
||||
Namespaces: namespaces.Items,
|
||||
Pods: pods.Items,
|
||||
}
|
||||
return &api, nil
|
||||
}
|
||||
@@ -160,19 +194,34 @@ func addResourceFromString(contents string, resources *ResourceProvider) error {
|
||||
resource := k8sResource{}
|
||||
err := decoder.Decode(&resource)
|
||||
if err != nil {
|
||||
// TODO: should we panic if the YAML is bad?
|
||||
logrus.Errorf("Invalid YAML: %s", string(contents))
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
decoder = k8sYaml.NewYAMLOrJSONDecoder(bytes.NewReader(contentBytes), 1000)
|
||||
if resource.Kind == "Deployment" {
|
||||
dep := appsv1.Deployment{}
|
||||
err = decoder.Decode(&dep)
|
||||
resources.Deployments = append(resources.Deployments, dep)
|
||||
controller := appsv1.Deployment{}
|
||||
err = decoder.Decode(&controller)
|
||||
resources.Deployments = append(resources.Deployments, controller)
|
||||
} else if resource.Kind == "StatefulSet" {
|
||||
dep := appsv1.StatefulSet{}
|
||||
err = decoder.Decode(&dep)
|
||||
resources.StatefulSets = append(resources.StatefulSets, dep)
|
||||
controller := appsv1.StatefulSet{}
|
||||
err = decoder.Decode(&controller)
|
||||
resources.StatefulSets = append(resources.StatefulSets, controller)
|
||||
} else if resource.Kind == "DaemonSet" {
|
||||
controller := appsv1.DaemonSet{}
|
||||
err = decoder.Decode(&controller)
|
||||
resources.DaemonSets = append(resources.DaemonSets, controller)
|
||||
} else if resource.Kind == "Job" {
|
||||
controller := batchv1.Job{}
|
||||
err = decoder.Decode(&controller)
|
||||
resources.Jobs = append(resources.Jobs, controller)
|
||||
} else if resource.Kind == "CronJob" {
|
||||
controller := batchv1beta1.CronJob{}
|
||||
err = decoder.Decode(&controller)
|
||||
resources.CronJobs = append(resources.CronJobs, controller)
|
||||
} else if resource.Kind == "ReplicationController" {
|
||||
controller := corev1.ReplicationController{}
|
||||
err = decoder.Decode(&controller)
|
||||
resources.ReplicationControllers = append(resources.ReplicationControllers, controller)
|
||||
} else if resource.Kind == "Namespace" {
|
||||
ns := corev1.Namespace{}
|
||||
err = decoder.Decode(&ns)
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package kube
|
||||
|
||||
import (
|
||||
"github.com/reactiveops/polaris/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/fairwindsops/polaris/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetResourcesFromPath(t *testing.T) {
|
||||
@@ -53,6 +54,11 @@ func TestGetMultipleResourceFromSingleFile(t *testing.T) {
|
||||
assert.Equal(t, "polaris-2", resources.Namespaces[1].ObjectMeta.Name)
|
||||
}
|
||||
|
||||
func TestGetMultipleResourceFromBadFile(t *testing.T) {
|
||||
_, err := CreateResourceProviderFromPath("./test_files/test_3")
|
||||
assert.NotEqual(t, nil, err, "CreateResource From Path should fail with bad yaml")
|
||||
}
|
||||
|
||||
func TestGetResourceFromAPI(t *testing.T) {
|
||||
k8s := test.SetupTestAPI()
|
||||
k8s = test.SetupAddControllers(k8s, "test")
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
apiVersion: batch/v1beta1
|
||||
kind: CronJob
|
||||
metadata:
|
||||
name: test
|
||||
spec:
|
||||
schedule: "*/1 * * * *"
|
||||
jobTemplate:
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: test
|
||||
image: busybox
|
||||
args:
|
||||
- whoami
|
||||
restartPolicy: OnFailure
|
||||
@@ -0,0 +1,20 @@
|
||||
apiVersion: apps/v1
|
||||
kind: DaemonSet
|
||||
metadata:
|
||||
name: test
|
||||
labels:
|
||||
k8s-app: test
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
name: test
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
name: test
|
||||
spec:
|
||||
containers:
|
||||
- name: test
|
||||
image: busybox
|
||||
command: ["whoami"]
|
||||
resources: {}
|
||||
@@ -0,0 +1,12 @@
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: test
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: test
|
||||
image: alpine
|
||||
command: ["whoami"]
|
||||
restartPolicy: Never
|
||||
@@ -0,0 +1,19 @@
|
||||
apiVersion: v1
|
||||
kind: ReplicationController
|
||||
metadata:
|
||||
name: test
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
app: test
|
||||
template:
|
||||
metadata:
|
||||
name: test
|
||||
labels:
|
||||
app: test
|
||||
spec:
|
||||
containers:
|
||||
- name: test
|
||||
image: busybox
|
||||
command: ["tail"]
|
||||
args: ["-f", "/dev/null"]
|
||||
@@ -0,0 +1,5 @@
|
||||
--
|
||||
# Source: polaris/templates/dashboard.deployment.yaml
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Deployment
|
||||
::::
|
||||
+176
-119
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019 ReactiveOps
|
||||
// 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.
|
||||
@@ -18,8 +18,8 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
conf "github.com/reactiveops/polaris/pkg/config"
|
||||
"github.com/reactiveops/polaris/pkg/validator/messages"
|
||||
"github.com/fairwindsops/polaris/pkg/config"
|
||||
"github.com/fairwindsops/polaris/pkg/validator/messages"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
)
|
||||
@@ -38,7 +38,7 @@ type ContainerValidation struct {
|
||||
// relevant podSpec in order to check certain aspects of a containerSpec.
|
||||
// Perhaps there is a more ideal solution instead of attaching a parent
|
||||
// podSpec to every container Validation struct...
|
||||
func ValidateContainer(container *corev1.Container, parentPodResult *PodResult, cnConf *conf.Configuration, isInit bool) ContainerResult {
|
||||
func ValidateContainer(container *corev1.Container, parentPodResult *PodResult, conf *config.Configuration, controllerName string, controllerType config.SupportedController, isInit bool) ContainerResult {
|
||||
cv := ContainerValidation{
|
||||
Container: container,
|
||||
ResourceValidation: &ResourceValidation{},
|
||||
@@ -57,11 +57,13 @@ func ValidateContainer(container *corev1.Container, parentPodResult *PodResult,
|
||||
cv.parentPodSpec = parentPodResult.podSpec
|
||||
}
|
||||
|
||||
cv.validateResources(&cnConf.Resources)
|
||||
cv.validateHealthChecks(&cnConf.HealthChecks)
|
||||
cv.validateImage(&cnConf.Images)
|
||||
cv.validateNetworking(&cnConf.Networking)
|
||||
cv.validateSecurity(&cnConf.Security)
|
||||
cv.validateResources(conf, controllerName)
|
||||
if !isInit && controllerType != config.Jobs && controllerType != config.CronJobs {
|
||||
cv.validateHealthChecks(conf, controllerName)
|
||||
}
|
||||
cv.validateImage(conf, controllerName)
|
||||
cv.validateNetworking(conf, controllerName)
|
||||
cv.validateSecurity(conf, controllerName)
|
||||
|
||||
cRes := ContainerResult{
|
||||
Name: container.Name,
|
||||
@@ -72,7 +74,7 @@ func ValidateContainer(container *corev1.Container, parentPodResult *PodResult,
|
||||
return cRes
|
||||
}
|
||||
|
||||
func (cv *ContainerValidation) validateResources(resConf *conf.Resources) {
|
||||
func (cv *ContainerValidation) validateResources(conf *config.Configuration, controllerName string) {
|
||||
// Only validate resources for primary containers. Although it can
|
||||
// be helpful to set these in certain cases, it usually isn't
|
||||
if cv.IsInitContainer {
|
||||
@@ -82,32 +84,56 @@ func (cv *ContainerValidation) validateResources(resConf *conf.Resources) {
|
||||
category := messages.CategoryResources
|
||||
res := cv.Container.Resources
|
||||
|
||||
if resConf.CPURequestsMissing.IsActionable() && res.Requests.Cpu().MilliValue() == 0 {
|
||||
cv.addFailure(messages.CPURequestsFailure, resConf.CPURequestsMissing, category)
|
||||
} else {
|
||||
cv.validateResourceRange(messages.CPURequestsLabel, &resConf.CPURequestRanges, res.Requests.Cpu())
|
||||
missingName := "CPURequestsMissing"
|
||||
rangeName := "CPURequestRanges"
|
||||
id := config.GetIDFromField(conf.Resources, missingName)
|
||||
if conf.IsActionable(conf.Resources, missingName, controllerName) && res.Requests.Cpu().MilliValue() == 0 {
|
||||
cv.addFailure(messages.CPURequestsFailure, conf.Resources.CPURequestsMissing, category, id)
|
||||
} else if conf.IsActionable(conf.Resources, rangeName, controllerName) {
|
||||
id := config.GetIDFromField(conf.Resources, rangeName)
|
||||
cv.validateResourceRange(id, messages.CPURequestsLabel, &conf.Resources.CPURequestRanges, res.Requests.Cpu())
|
||||
} else if conf.IsActionable(conf.Resources, missingName, controllerName) {
|
||||
cv.addSuccess(fmt.Sprintf(messages.ResourcePresentSuccess, messages.CPURequestsLabel), category, id)
|
||||
}
|
||||
|
||||
if resConf.CPULimitsMissing.IsActionable() && res.Limits.Cpu().MilliValue() == 0 {
|
||||
cv.addFailure(messages.CPULimitsFailure, resConf.CPULimitsMissing, category)
|
||||
} else {
|
||||
cv.validateResourceRange(messages.CPULimitsLabel, &resConf.CPULimitRanges, res.Requests.Cpu())
|
||||
missingName = "CPULimitsMissing"
|
||||
rangeName = "CPULimitRanges"
|
||||
id = config.GetIDFromField(conf.Resources, missingName)
|
||||
if conf.IsActionable(conf.Resources, missingName, controllerName) && res.Limits.Cpu().MilliValue() == 0 {
|
||||
cv.addFailure(messages.CPULimitsFailure, conf.Resources.CPULimitsMissing, category, id)
|
||||
} else if conf.IsActionable(conf.Resources, rangeName, controllerName) {
|
||||
id := config.GetIDFromField(conf.Resources, rangeName)
|
||||
cv.validateResourceRange(id, messages.CPULimitsLabel, &conf.Resources.CPULimitRanges, res.Requests.Cpu())
|
||||
} else if conf.IsActionable(conf.Resources, missingName, controllerName) {
|
||||
cv.addSuccess(fmt.Sprintf(messages.ResourcePresentSuccess, messages.CPULimitsLabel), category, id)
|
||||
}
|
||||
|
||||
if resConf.MemoryRequestsMissing.IsActionable() && res.Requests.Memory().MilliValue() == 0 {
|
||||
cv.addFailure(messages.MemoryRequestsFailure, resConf.MemoryRequestsMissing, category)
|
||||
} else {
|
||||
cv.validateResourceRange(messages.MemoryRequestsLabel, &resConf.MemoryRequestRanges, res.Requests.Memory())
|
||||
missingName = "MemoryRequestsMissing"
|
||||
rangeName = "MemoryRequestRanges"
|
||||
id = config.GetIDFromField(conf.Resources, missingName)
|
||||
if conf.IsActionable(conf.Resources, missingName, controllerName) && res.Requests.Memory().MilliValue() == 0 {
|
||||
cv.addFailure(messages.MemoryRequestsFailure, conf.Resources.MemoryRequestsMissing, category, id)
|
||||
} else if conf.IsActionable(conf.Resources, rangeName, controllerName) {
|
||||
id := config.GetIDFromField(conf.Resources, rangeName)
|
||||
cv.validateResourceRange(id, messages.MemoryRequestsLabel, &conf.Resources.MemoryRequestRanges, res.Requests.Memory())
|
||||
} else if conf.IsActionable(conf.Resources, missingName, controllerName) {
|
||||
cv.addSuccess(fmt.Sprintf(messages.ResourcePresentSuccess, messages.MemoryRequestsLabel), category, id)
|
||||
}
|
||||
|
||||
if resConf.MemoryLimitsMissing.IsActionable() && res.Limits.Memory().MilliValue() == 0 {
|
||||
cv.addFailure(messages.MemoryLimitsFailure, resConf.MemoryLimitsMissing, category)
|
||||
} else {
|
||||
cv.validateResourceRange(messages.MemoryLimitsLabel, &resConf.MemoryLimitRanges, res.Limits.Memory())
|
||||
missingName = "MemoryLimitsMissing"
|
||||
rangeName = "MemoryLimitRanges"
|
||||
id = config.GetIDFromField(conf.Resources, missingName)
|
||||
if conf.IsActionable(conf.Resources, missingName, controllerName) && res.Limits.Memory().MilliValue() == 0 {
|
||||
cv.addFailure(messages.MemoryLimitsFailure, conf.Resources.MemoryLimitsMissing, category, id)
|
||||
} else if conf.IsActionable(conf.Resources, rangeName, controllerName) {
|
||||
id := config.GetIDFromField(conf.Resources, rangeName)
|
||||
cv.validateResourceRange(id, messages.MemoryLimitsLabel, &conf.Resources.MemoryLimitRanges, res.Limits.Memory())
|
||||
} else if conf.IsActionable(conf.Resources, missingName, controllerName) {
|
||||
cv.addSuccess(fmt.Sprintf(messages.ResourcePresentSuccess, messages.MemoryLimitsLabel), category, id)
|
||||
}
|
||||
}
|
||||
|
||||
func (cv *ContainerValidation) validateResourceRange(resourceName string, rangeConf *conf.ResourceRanges, res *resource.Quantity) {
|
||||
func (cv *ContainerValidation) validateResourceRange(id, resourceName string, rangeConf *config.ResourceRanges, res *resource.Quantity) {
|
||||
warnAbove := rangeConf.Warning.Above
|
||||
warnBelow := rangeConf.Warning.Below
|
||||
errorAbove := rangeConf.Error.Above
|
||||
@@ -115,66 +141,73 @@ func (cv *ContainerValidation) validateResourceRange(resourceName string, rangeC
|
||||
category := messages.CategoryResources
|
||||
|
||||
if errorAbove != nil && errorAbove.MilliValue() < res.MilliValue() {
|
||||
cv.addError(fmt.Sprintf(messages.ResourceAmountTooHighFailure, resourceName, errorAbove.String()), category)
|
||||
cv.addError(fmt.Sprintf(messages.ResourceAmountTooHighFailure, resourceName, errorAbove.String()), category, id)
|
||||
} else if warnAbove != nil && warnAbove.MilliValue() < res.MilliValue() {
|
||||
cv.addWarning(fmt.Sprintf(messages.ResourceAmountTooHighFailure, resourceName, warnAbove.String()), category)
|
||||
cv.addWarning(fmt.Sprintf(messages.ResourceAmountTooHighFailure, resourceName, warnAbove.String()), category, id)
|
||||
} else if errorBelow != nil && errorBelow.MilliValue() > res.MilliValue() {
|
||||
cv.addError(fmt.Sprintf(messages.ResourceAmountTooLowFailure, resourceName, errorBelow.String()), category)
|
||||
cv.addError(fmt.Sprintf(messages.ResourceAmountTooLowFailure, resourceName, errorBelow.String()), category, id)
|
||||
} else if warnBelow != nil && warnBelow.MilliValue() > res.MilliValue() {
|
||||
cv.addWarning(fmt.Sprintf(messages.ResourceAmountTooLowFailure, resourceName, warnBelow.String()), category)
|
||||
} else {
|
||||
if warnAbove != nil || warnBelow != nil || errorAbove != nil || errorBelow != nil {
|
||||
cv.addSuccess(fmt.Sprintf(messages.ResourceAmountSuccess, resourceName), category)
|
||||
} else {
|
||||
cv.addSuccess(fmt.Sprintf(messages.ResourcePresentSuccess, resourceName), category)
|
||||
}
|
||||
cv.addWarning(fmt.Sprintf(messages.ResourceAmountTooLowFailure, resourceName, warnBelow.String()), category, id)
|
||||
} else if errorAbove != nil && warnAbove != nil && errorBelow != nil && warnBelow != nil {
|
||||
cv.addSuccess(fmt.Sprintf(messages.ResourceAmountSuccess, resourceName), category, id)
|
||||
}
|
||||
}
|
||||
|
||||
func (cv *ContainerValidation) validateHealthChecks(conf *conf.HealthChecks) {
|
||||
func (cv *ContainerValidation) validateHealthChecks(conf *config.Configuration, controllerName string) {
|
||||
category := messages.CategoryHealthChecks
|
||||
|
||||
name := "ReadinessProbeMissing"
|
||||
// Don't validate readiness probes on init containers
|
||||
if !cv.IsInitContainer && conf.ReadinessProbeMissing.IsActionable() {
|
||||
if !cv.IsInitContainer && conf.IsActionable(conf.HealthChecks, name, controllerName) {
|
||||
id := config.GetIDFromField(conf.HealthChecks, name)
|
||||
if cv.Container.ReadinessProbe == nil {
|
||||
cv.addFailure(messages.ReadinessProbeFailure, conf.ReadinessProbeMissing, category)
|
||||
cv.addFailure(messages.ReadinessProbeFailure, conf.HealthChecks.ReadinessProbeMissing, category, id)
|
||||
} else {
|
||||
cv.addSuccess(messages.ReadinessProbeSuccess, category)
|
||||
cv.addSuccess(messages.ReadinessProbeSuccess, category, id)
|
||||
}
|
||||
}
|
||||
|
||||
if conf.LivenessProbeMissing.IsActionable() {
|
||||
name = "LivenessProbeMissing"
|
||||
if conf.IsActionable(conf.HealthChecks, name, controllerName) {
|
||||
id := config.GetIDFromField(conf.HealthChecks, "LivenessProbeMissing")
|
||||
if cv.Container.LivenessProbe == nil {
|
||||
cv.addFailure(messages.LivenessProbeFailure, conf.LivenessProbeMissing, category)
|
||||
cv.addFailure(messages.LivenessProbeFailure, conf.HealthChecks.LivenessProbeMissing, category, id)
|
||||
} else {
|
||||
cv.addSuccess(messages.LivenessProbeSuccess, category)
|
||||
cv.addSuccess(messages.LivenessProbeSuccess, category, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (cv *ContainerValidation) validateImage(imageConf *conf.Images) {
|
||||
func (cv *ContainerValidation) validateImage(conf *config.Configuration, controllerName string) {
|
||||
category := messages.CategoryImages
|
||||
if imageConf.PullPolicyNotAlways.IsActionable() {
|
||||
|
||||
name := "PullPolicyNotAlways"
|
||||
if conf.IsActionable(conf.Images, name, controllerName) {
|
||||
id := config.GetIDFromField(conf.Images, name)
|
||||
if cv.Container.ImagePullPolicy != corev1.PullAlways {
|
||||
cv.addFailure(messages.ImagePullPolicyFailure, imageConf.PullPolicyNotAlways, category)
|
||||
cv.addFailure(messages.ImagePullPolicyFailure, conf.Images.PullPolicyNotAlways, category, id)
|
||||
} else {
|
||||
cv.addSuccess(messages.ImagePullPolicySuccess, category)
|
||||
cv.addSuccess(messages.ImagePullPolicySuccess, category, id)
|
||||
}
|
||||
}
|
||||
|
||||
if imageConf.TagNotSpecified.IsActionable() {
|
||||
name = "TagNotSpecified"
|
||||
if conf.IsActionable(conf.Images, name, controllerName) {
|
||||
id := config.GetIDFromField(conf.Images, name)
|
||||
img := strings.Split(cv.Container.Image, ":")
|
||||
if len(img) == 1 || img[1] == "latest" {
|
||||
cv.addFailure(messages.ImageTagFailure, imageConf.TagNotSpecified, category)
|
||||
cv.addFailure(messages.ImageTagFailure, conf.Images.TagNotSpecified, category, id)
|
||||
} else {
|
||||
cv.addSuccess(messages.ImageTagSuccess, category)
|
||||
cv.addSuccess(messages.ImageTagSuccess, category, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (cv *ContainerValidation) validateNetworking(networkConf *conf.Networking) {
|
||||
func (cv *ContainerValidation) validateNetworking(conf *config.Configuration, controllerName string) {
|
||||
category := messages.CategoryNetworking
|
||||
if networkConf.HostPortSet.IsActionable() {
|
||||
|
||||
name := "HostPortSet"
|
||||
if conf.IsActionable(conf.Networking, name, controllerName) {
|
||||
hostPortSet := false
|
||||
for _, port := range cv.Container.Ports {
|
||||
if port.HostPort != 0 {
|
||||
@@ -183,15 +216,16 @@ func (cv *ContainerValidation) validateNetworking(networkConf *conf.Networking)
|
||||
}
|
||||
}
|
||||
|
||||
id := config.GetIDFromField(conf.Networking, name)
|
||||
if hostPortSet {
|
||||
cv.addFailure(messages.HostPortFailure, networkConf.HostPortSet, category)
|
||||
cv.addFailure(messages.HostPortFailure, conf.Networking.HostPortSet, category, id)
|
||||
} else {
|
||||
cv.addSuccess(messages.HostPortSuccess, category)
|
||||
cv.addSuccess(messages.HostPortSuccess, category, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (cv *ContainerValidation) validateSecurity(securityConf *conf.Security) {
|
||||
func (cv *ContainerValidation) validateSecurity(conf *config.Configuration, controllerName string) {
|
||||
category := messages.CategorySecurity
|
||||
securityContext := cv.Container.SecurityContext
|
||||
podSecurityContext := cv.parentPodSpec.SecurityContext
|
||||
@@ -206,107 +240,130 @@ func (cv *ContainerValidation) validateSecurity(securityConf *conf.Security) {
|
||||
podSecurityContext = &corev1.PodSecurityContext{}
|
||||
}
|
||||
|
||||
if securityConf.RunAsRootAllowed.IsActionable() {
|
||||
if getBoolValue(securityContext.RunAsNonRoot) {
|
||||
name := "RunAsRootAllowed"
|
||||
if conf.IsActionable(conf.Security, name, controllerName) {
|
||||
id := config.GetIDFromField(conf.Security, name)
|
||||
runAsRootSuccess := false
|
||||
if getBoolValue(securityContext.RunAsNonRoot) || (securityContext.RunAsUser != nil && *securityContext.RunAsUser > 0) {
|
||||
// Check if the container is explicitly set to True (pass)
|
||||
cv.addSuccess(messages.RunAsRootSuccess, category)
|
||||
} else if securityContext.RunAsNonRoot == nil {
|
||||
// Check if the value in the container spec if nil (thus defaulting to the podspec)
|
||||
// Check if the container value is not set
|
||||
if getBoolValue(podSecurityContext.RunAsNonRoot) {
|
||||
// if the pod spec default for containers is true, then pass
|
||||
cv.addSuccess(messages.RunAsRootSuccess, category)
|
||||
} else {
|
||||
// else fail as RunAsNonRoot defaults to false
|
||||
cv.addFailure(messages.RunAsRootFailure, securityConf.RunAsRootAllowed, category)
|
||||
}
|
||||
runAsRootSuccess = true
|
||||
} else if securityContext.RunAsNonRoot == nil && securityContext.RunAsUser == nil {
|
||||
// Or if the container values are unset, check the pod values
|
||||
runAsRootSuccess = getBoolValue(podSecurityContext.RunAsNonRoot) || (podSecurityContext.RunAsUser != nil && *podSecurityContext.RunAsUser > 0)
|
||||
}
|
||||
if runAsRootSuccess {
|
||||
cv.addSuccess(messages.RunAsRootSuccess, category, id)
|
||||
} else {
|
||||
cv.addFailure(messages.RunAsRootFailure, securityConf.RunAsRootAllowed, category)
|
||||
cv.addFailure(messages.RunAsRootFailure, conf.Security.RunAsRootAllowed, category, id)
|
||||
}
|
||||
}
|
||||
|
||||
if securityConf.RunAsPrivileged.IsActionable() {
|
||||
name = "RunAsPrivileged"
|
||||
if conf.IsActionable(conf.Security, name, controllerName) {
|
||||
id := config.GetIDFromField(conf.Security, name)
|
||||
if getBoolValue(securityContext.Privileged) {
|
||||
cv.addFailure(messages.RunAsPrivilegedFailure, securityConf.RunAsPrivileged, category)
|
||||
cv.addFailure(messages.RunAsPrivilegedFailure, conf.Security.RunAsPrivileged, category, id)
|
||||
} else {
|
||||
cv.addSuccess(messages.RunAsPrivilegedSuccess, category)
|
||||
cv.addSuccess(messages.RunAsPrivilegedSuccess, category, id)
|
||||
}
|
||||
}
|
||||
|
||||
if securityConf.NotReadOnlyRootFileSystem.IsActionable() {
|
||||
name = "NotReadOnlyRootFileSystem"
|
||||
if conf.IsActionable(conf.Security, name, controllerName) {
|
||||
id := config.GetIDFromField(conf.Security, name)
|
||||
if getBoolValue(securityContext.ReadOnlyRootFilesystem) {
|
||||
cv.addSuccess(messages.ReadOnlyFilesystemSuccess, category)
|
||||
cv.addSuccess(messages.ReadOnlyFilesystemSuccess, category, id)
|
||||
} else {
|
||||
cv.addFailure(messages.ReadOnlyFilesystemFailure, securityConf.NotReadOnlyRootFileSystem, category)
|
||||
cv.addFailure(messages.ReadOnlyFilesystemFailure, conf.Security.NotReadOnlyRootFileSystem, category, id)
|
||||
}
|
||||
}
|
||||
|
||||
if securityConf.PrivilegeEscalationAllowed.IsActionable() {
|
||||
name = "PrivilegeEscalationAllowed"
|
||||
if conf.IsActionable(conf.Security, name, controllerName) {
|
||||
id := config.GetIDFromField(conf.Security, name)
|
||||
if getBoolValue(securityContext.AllowPrivilegeEscalation) {
|
||||
cv.addFailure(messages.PrivilegeEscalationFailure, securityConf.PrivilegeEscalationAllowed, category)
|
||||
cv.addFailure(messages.PrivilegeEscalationFailure, conf.Security.PrivilegeEscalationAllowed, category, id)
|
||||
} else {
|
||||
cv.addSuccess(messages.PrivilegeEscalationSuccess, category)
|
||||
cv.addSuccess(messages.PrivilegeEscalationSuccess, category, id)
|
||||
}
|
||||
}
|
||||
|
||||
hasSecurityError :=
|
||||
!cv.validateCapabilities(securityConf.Capabilities.Error, conf.SeverityError)
|
||||
hasSecurityWarning :=
|
||||
!cv.validateCapabilities(securityConf.Capabilities.Warning, conf.SeverityWarning)
|
||||
hasSecurityCheck := func(confLists conf.SecurityCapabilityLists) bool {
|
||||
return len(confLists.IfAnyAdded) > 0 ||
|
||||
len(confLists.IfAnyAddedBeyond) > 0 ||
|
||||
len(confLists.IfAnyNotDropped) > 0
|
||||
}
|
||||
if !hasSecurityError && !hasSecurityWarning &&
|
||||
(hasSecurityCheck(securityConf.Capabilities.Error) ||
|
||||
hasSecurityCheck(securityConf.Capabilities.Warning)) {
|
||||
cv.addSuccess(messages.SecurityCapabilitiesSuccess, category)
|
||||
name = "Capabilities"
|
||||
if conf.IsActionable(conf.Security, name, controllerName) {
|
||||
cv.validateCapabilities(&conf.Security.Capabilities.Warning, &conf.Security.Capabilities.Error)
|
||||
}
|
||||
}
|
||||
|
||||
func (cv *ContainerValidation) validateCapabilities(confLists conf.SecurityCapabilityLists, severity conf.Severity) bool {
|
||||
func (cv *ContainerValidation) validateCapabilities(warningLists *config.SecurityCapabilityLists, errorLists *config.SecurityCapabilityLists) {
|
||||
category := messages.CategorySecurity
|
||||
capabilities := &corev1.Capabilities{}
|
||||
if cv.Container.SecurityContext != nil && cv.Container.SecurityContext.Capabilities != nil {
|
||||
capabilities = cv.Container.SecurityContext.Capabilities
|
||||
}
|
||||
allLists := []*config.SecurityCapabilityLists{warningLists, errorLists}
|
||||
|
||||
everythingOK := true
|
||||
if len(confLists.IfAnyAdded) > 0 {
|
||||
intersectAdds := capIntersection(capabilities.Add, confLists.IfAnyAdded)
|
||||
if len(intersectAdds) > 0 {
|
||||
capsString := commaSeparatedCapabilities(intersectAdds)
|
||||
cv.addFailure(fmt.Sprintf(messages.SecurityCapabilitiesAddedFailure, capsString), severity, category)
|
||||
everythingOK = false
|
||||
} else if capContains(capabilities.Add, "ALL") {
|
||||
cv.addFailure(fmt.Sprintf(messages.SecurityCapabilitiesAddedFailure, "ALL"), severity, category)
|
||||
everythingOK = false
|
||||
addID := "capabilitiesAdded"
|
||||
hasAddFailure := false
|
||||
hasAddCheck := false
|
||||
for _, confLists := range allLists {
|
||||
if len(confLists.IfAnyAdded) == 0 && len(confLists.IfAnyAddedBeyond) == 0 {
|
||||
continue
|
||||
}
|
||||
hasAddCheck = true
|
||||
var severity config.Severity
|
||||
if confLists == warningLists {
|
||||
severity = config.SeverityWarning
|
||||
} else {
|
||||
severity = config.SeverityError
|
||||
}
|
||||
badAdds := make([]corev1.Capability, 0)
|
||||
if len(confLists.IfAnyAdded) > 0 {
|
||||
intersectAdds := capIntersection(capabilities.Add, confLists.IfAnyAdded)
|
||||
badAdds = append(badAdds, intersectAdds...)
|
||||
}
|
||||
if len(confLists.IfAnyAddedBeyond) > 0 {
|
||||
differentAdds := capDifference(capabilities.Add, confLists.IfAnyAddedBeyond)
|
||||
differentAdds = capDifference(differentAdds, badAdds)
|
||||
badAdds = append(badAdds, differentAdds...)
|
||||
}
|
||||
if capContains(capabilities.Add, "ALL") && !capContains(badAdds, "ALL") {
|
||||
badAdds = append(badAdds, "ALL")
|
||||
}
|
||||
if len(badAdds) > 0 {
|
||||
hasAddFailure = true
|
||||
capsString := commaSeparatedCapabilities(badAdds)
|
||||
cv.addFailure(fmt.Sprintf(messages.SecurityCapabilitiesAddedFailure, capsString), severity, category, addID)
|
||||
}
|
||||
}
|
||||
|
||||
if len(confLists.IfAnyAddedBeyond) > 0 {
|
||||
differentAdds := capDifference(capabilities.Add, confLists.IfAnyAddedBeyond)
|
||||
if len(differentAdds) > 0 {
|
||||
capsString := commaSeparatedCapabilities(differentAdds)
|
||||
cv.addFailure(fmt.Sprintf(messages.SecurityCapabilitiesAddedFailure, capsString), severity, category)
|
||||
everythingOK = false
|
||||
} else if capContains(capabilities.Add, "ALL") {
|
||||
cv.addFailure(fmt.Sprintf(messages.SecurityCapabilitiesAddedFailure, "ALL"), severity, category)
|
||||
everythingOK = false
|
||||
}
|
||||
if hasAddCheck && !hasAddFailure {
|
||||
cv.addSuccess(messages.SecurityCapabilitiesAddedSuccess, category, addID)
|
||||
}
|
||||
|
||||
if len(confLists.IfAnyNotDropped) > 0 {
|
||||
dropID := "capabilitiesDropped"
|
||||
hasDropCheck := false
|
||||
hasDropFailure := false
|
||||
for _, confLists := range allLists {
|
||||
if len(confLists.IfAnyNotDropped) == 0 {
|
||||
continue
|
||||
}
|
||||
hasDropCheck = true
|
||||
var severity config.Severity
|
||||
if confLists == warningLists {
|
||||
severity = config.SeverityWarning
|
||||
} else {
|
||||
severity = config.SeverityError
|
||||
}
|
||||
missingDrops := capDifference(confLists.IfAnyNotDropped, capabilities.Drop)
|
||||
id := "capabilitiesNotDropped"
|
||||
if len(missingDrops) > 0 && !capContains(capabilities.Drop, "ALL") {
|
||||
hasDropFailure = true
|
||||
capsString := commaSeparatedCapabilities(missingDrops)
|
||||
cv.addFailure(fmt.Sprintf(messages.SecurityCapabilitiesNotDroppedFailure, capsString), severity, category)
|
||||
everythingOK = false
|
||||
cv.addFailure(fmt.Sprintf(messages.SecurityCapabilitiesNotDroppedFailure, capsString), severity, category, id)
|
||||
}
|
||||
}
|
||||
|
||||
return everythingOK
|
||||
if hasDropCheck && !hasDropFailure {
|
||||
cv.addSuccess(messages.SecurityCapabilitiesNotDroppedSuccess, category, dropID)
|
||||
}
|
||||
}
|
||||
|
||||
func commaSeparatedCapabilities(caps []corev1.Capability) string {
|
||||
|
||||
+426
-34
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019 ReactiveOps
|
||||
// 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.
|
||||
@@ -15,9 +15,10 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
conf "github.com/reactiveops/polaris/pkg/config"
|
||||
conf "github.com/fairwindsops/polaris/pkg/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
@@ -63,6 +64,83 @@ resources:
|
||||
memoryLimitsMissing: error
|
||||
`
|
||||
|
||||
var resourceConfExemptions = `---
|
||||
resources:
|
||||
cpuRequestsMissing: warning
|
||||
memoryRequestsMissing: warning
|
||||
cpuLimitsMissing: error
|
||||
memoryLimitsMissing: error
|
||||
exemptions:
|
||||
- rules:
|
||||
- cpuRequestsMissing
|
||||
- memoryRequestsMissing
|
||||
- cpuLimitsMissing
|
||||
- memoryLimitsMissing
|
||||
controllerNames:
|
||||
- foo
|
||||
`
|
||||
|
||||
var resourceConfRangeExemptions = `---
|
||||
resources:
|
||||
cpuRequestRanges:
|
||||
error:
|
||||
below: 100m
|
||||
above: 1
|
||||
warning:
|
||||
below: 200m
|
||||
above: 800m
|
||||
memoryRequestRanges:
|
||||
error:
|
||||
below: 100M
|
||||
above: 3G
|
||||
warning:
|
||||
below: 200M
|
||||
above: 2G
|
||||
cpuLimitRanges:
|
||||
error:
|
||||
below: 100m
|
||||
above: 2
|
||||
warning:
|
||||
below: 300m
|
||||
above: 1800m
|
||||
memoryLimitRanges:
|
||||
error:
|
||||
below: 200M
|
||||
above: 6G
|
||||
warning:
|
||||
below: 300M
|
||||
above: 4G
|
||||
exemptions:
|
||||
- rules:
|
||||
- cpuRequestRanges
|
||||
- memoryRequestRanges
|
||||
- cpuLimitRanges
|
||||
- memoryLimitRanges
|
||||
controllerNames:
|
||||
- foo
|
||||
`
|
||||
|
||||
func testValidateResources(t *testing.T, container *corev1.Container, resourceConf *string, controllerName string, expectedErrors []*ResultMessage, expectedWarnings []*ResultMessage, expectedSuccesses []*ResultMessage) {
|
||||
cv := ContainerValidation{
|
||||
Container: container,
|
||||
ResourceValidation: &ResourceValidation{},
|
||||
}
|
||||
|
||||
parsedConf, err := conf.Parse([]byte(*resourceConf))
|
||||
assert.NoError(t, err, "Expected no error when parsing config")
|
||||
|
||||
cv.validateResources(&parsedConf, controllerName)
|
||||
|
||||
assert.Len(t, cv.Warnings, len(expectedWarnings))
|
||||
assert.ElementsMatch(t, expectedWarnings, cv.Warnings)
|
||||
|
||||
assert.Len(t, cv.Errors, len(expectedErrors))
|
||||
assert.ElementsMatch(t, expectedErrors, cv.Errors)
|
||||
|
||||
assert.Len(t, cv.Successes, len(expectedSuccesses))
|
||||
assert.ElementsMatch(t, expectedSuccesses, cv.Successes)
|
||||
}
|
||||
|
||||
func TestValidateResourcesEmptyConfig(t *testing.T) {
|
||||
container := corev1.Container{
|
||||
Name: "Empty",
|
||||
@@ -73,9 +151,7 @@ func TestValidateResourcesEmptyConfig(t *testing.T) {
|
||||
ResourceValidation: &ResourceValidation{},
|
||||
}
|
||||
|
||||
expected := conf.Resources{}
|
||||
|
||||
cv.validateResources(&expected)
|
||||
cv.validateResources(&conf.Configuration{}, "")
|
||||
assert.Len(t, cv.Errors, 0)
|
||||
}
|
||||
|
||||
@@ -86,11 +162,13 @@ func TestValidateResourcesEmptyContainer(t *testing.T) {
|
||||
|
||||
expectedWarnings := []*ResultMessage{
|
||||
{
|
||||
ID: "cpuRequestsMissing",
|
||||
Type: "warning",
|
||||
Message: "CPU requests should be set",
|
||||
Category: "Resources",
|
||||
},
|
||||
{
|
||||
ID: "memoryRequestsMissing",
|
||||
Type: "warning",
|
||||
Message: "Memory requests should be set",
|
||||
Category: "Resources",
|
||||
@@ -99,18 +177,22 @@ func TestValidateResourcesEmptyContainer(t *testing.T) {
|
||||
|
||||
expectedErrors := []*ResultMessage{
|
||||
{
|
||||
ID: "cpuLimitsMissing",
|
||||
Type: "error",
|
||||
Message: "CPU limits should be set",
|
||||
Category: "Resources",
|
||||
},
|
||||
{
|
||||
ID: "memoryLimitsMissing",
|
||||
Type: "error",
|
||||
Message: "Memory limits should be set",
|
||||
Category: "Resources",
|
||||
},
|
||||
}
|
||||
|
||||
testValidateResources(t, &container, &resourceConf2, &expectedErrors, &expectedWarnings)
|
||||
expectedSuccesses := []*ResultMessage{}
|
||||
|
||||
testValidateResources(t, &container, &resourceConf2, "foo", expectedErrors, expectedWarnings, expectedSuccesses)
|
||||
}
|
||||
|
||||
func TestValidateResourcesPartiallyValid(t *testing.T) {
|
||||
@@ -134,11 +216,13 @@ func TestValidateResourcesPartiallyValid(t *testing.T) {
|
||||
|
||||
expectedWarnings := []*ResultMessage{
|
||||
{
|
||||
ID: "cpuRequestRanges",
|
||||
Type: "warning",
|
||||
Message: "CPU requests should be higher than 200m",
|
||||
Category: "Resources",
|
||||
},
|
||||
{
|
||||
ID: "cpuLimitRanges",
|
||||
Type: "warning",
|
||||
Message: "CPU limits should be higher than 300m",
|
||||
Category: "Resources",
|
||||
@@ -147,18 +231,22 @@ func TestValidateResourcesPartiallyValid(t *testing.T) {
|
||||
|
||||
expectedErrors := []*ResultMessage{
|
||||
{
|
||||
ID: "memoryRequestRanges",
|
||||
Type: "error",
|
||||
Message: "Memory requests should be higher than 100M",
|
||||
Category: "Resources",
|
||||
},
|
||||
{
|
||||
ID: "memoryLimitRanges",
|
||||
Type: "error",
|
||||
Message: "Memory limits should be higher than 200M",
|
||||
Category: "Resources",
|
||||
},
|
||||
}
|
||||
|
||||
testValidateResources(t, &container, &resourceConf1, &expectedErrors, &expectedWarnings)
|
||||
expectedSuccesses := []*ResultMessage{}
|
||||
|
||||
testValidateResources(t, &container, &resourceConf1, "foo", expectedErrors, expectedWarnings, expectedSuccesses)
|
||||
}
|
||||
|
||||
func TestValidateResourcesInit(t *testing.T) {
|
||||
@@ -175,10 +263,10 @@ func TestValidateResourcesInit(t *testing.T) {
|
||||
parsedConf, err := conf.Parse([]byte(resourceConf1))
|
||||
assert.NoError(t, err, "Expected no error when parsing config")
|
||||
|
||||
cvEmpty.validateResources(&parsedConf.Resources)
|
||||
cvEmpty.validateResources(&parsedConf, "")
|
||||
assert.Len(t, cvEmpty.Errors, 4)
|
||||
|
||||
cvInit.validateResources(&parsedConf.Resources)
|
||||
cvInit.validateResources(&parsedConf, "")
|
||||
assert.Len(t, cvInit.Errors, 0)
|
||||
}
|
||||
|
||||
@@ -209,24 +297,63 @@ func TestValidateResourcesFullyValid(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
testValidateResources(t, &container, &resourceConf1, &[]*ResultMessage{}, &[]*ResultMessage{})
|
||||
}
|
||||
|
||||
func testValidateResources(t *testing.T, container *corev1.Container, resourceConf *string, expectedErrors *[]*ResultMessage, expectedWarnings *[]*ResultMessage) {
|
||||
cv := ContainerValidation{
|
||||
Container: container,
|
||||
ResourceValidation: &ResourceValidation{},
|
||||
expectedSuccesses := []*ResultMessage{
|
||||
{
|
||||
ID: "cpuRequestRanges",
|
||||
Type: "success",
|
||||
Message: "CPU requests are within the expected range",
|
||||
Category: "Resources",
|
||||
},
|
||||
{
|
||||
ID: "memoryRequestRanges",
|
||||
Type: "success",
|
||||
Message: "Memory requests are within the expected range",
|
||||
Category: "Resources",
|
||||
},
|
||||
{
|
||||
ID: "cpuLimitRanges",
|
||||
Type: "success",
|
||||
Message: "CPU limits are within the expected range",
|
||||
Category: "Resources",
|
||||
},
|
||||
{
|
||||
ID: "memoryLimitRanges",
|
||||
Type: "success",
|
||||
Message: "Memory limits are within the expected range",
|
||||
Category: "Resources",
|
||||
},
|
||||
}
|
||||
|
||||
parsedConf, err := conf.Parse([]byte(*resourceConf))
|
||||
assert.NoError(t, err, "Expected no error when parsing config")
|
||||
testValidateResources(t, &container, &resourceConf1, "foo", []*ResultMessage{}, []*ResultMessage{}, expectedSuccesses)
|
||||
|
||||
cv.validateResources(&parsedConf.Resources)
|
||||
assert.Len(t, cv.Warnings, len(*expectedWarnings))
|
||||
assert.ElementsMatch(t, cv.Warnings, *expectedWarnings)
|
||||
expectedSuccesses = []*ResultMessage{
|
||||
{
|
||||
ID: "cpuRequestsMissing",
|
||||
Type: "success",
|
||||
Message: "CPU requests are set",
|
||||
Category: "Resources",
|
||||
},
|
||||
{
|
||||
ID: "memoryRequestsMissing",
|
||||
Type: "success",
|
||||
Message: "Memory requests are set",
|
||||
Category: "Resources",
|
||||
},
|
||||
{
|
||||
ID: "cpuLimitsMissing",
|
||||
Type: "success",
|
||||
Message: "CPU limits are set",
|
||||
Category: "Resources",
|
||||
},
|
||||
{
|
||||
ID: "memoryLimitsMissing",
|
||||
Type: "success",
|
||||
Message: "Memory limits are set",
|
||||
Category: "Resources",
|
||||
},
|
||||
}
|
||||
|
||||
assert.Len(t, cv.Errors, len(*expectedErrors))
|
||||
assert.ElementsMatch(t, cv.Errors, *expectedErrors)
|
||||
testValidateResources(t, &container, &resourceConf2, "foo", []*ResultMessage{}, []*ResultMessage{}, expectedSuccesses)
|
||||
}
|
||||
|
||||
func TestValidateHealthChecks(t *testing.T) {
|
||||
@@ -261,8 +388,8 @@ func TestValidateHealthChecks(t *testing.T) {
|
||||
ResourceValidation: &ResourceValidation{},
|
||||
}
|
||||
|
||||
l := &ResultMessage{Type: "warning", Message: "Liveness probe should be configured", Category: "Health Checks"}
|
||||
r := &ResultMessage{Type: "error", Message: "Readiness probe should be configured", Category: "Health Checks"}
|
||||
l := &ResultMessage{ID: "livenessProbeMissing", Type: "warning", Message: "Liveness probe should be configured", Category: "Health Checks"}
|
||||
r := &ResultMessage{ID: "readinessProbeMissing", Type: "error", Message: "Readiness probe should be configured", Category: "Health Checks"}
|
||||
f1 := []*ResultMessage{}
|
||||
f2 := []*ResultMessage{r}
|
||||
w1 := []*ResultMessage{l}
|
||||
@@ -284,7 +411,7 @@ func TestValidateHealthChecks(t *testing.T) {
|
||||
|
||||
for _, tt := range testCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tt.cv.validateHealthChecks(&tt.probes)
|
||||
tt.cv.validateHealthChecks(&conf.Configuration{HealthChecks: tt.probes}, "")
|
||||
|
||||
if tt.warnings != nil {
|
||||
assert.Len(t, tt.cv.Warnings, len(*tt.warnings))
|
||||
@@ -342,6 +469,7 @@ func TestValidateImage(t *testing.T) {
|
||||
image: standardConf,
|
||||
cv: emptyCV,
|
||||
expected: []*ResultMessage{{
|
||||
ID: "tagNotSpecified",
|
||||
Message: "Image tag should be specified",
|
||||
Type: "error",
|
||||
Category: "Images",
|
||||
@@ -352,6 +480,7 @@ func TestValidateImage(t *testing.T) {
|
||||
image: standardConf,
|
||||
cv: badCV,
|
||||
expected: []*ResultMessage{{
|
||||
ID: "tagNotSpecified",
|
||||
Message: "Image tag should be specified",
|
||||
Type: "error",
|
||||
Category: "Images",
|
||||
@@ -362,6 +491,7 @@ func TestValidateImage(t *testing.T) {
|
||||
image: standardConf,
|
||||
cv: lessBadCV,
|
||||
expected: []*ResultMessage{{
|
||||
ID: "tagNotSpecified",
|
||||
Message: "Image tag should be specified",
|
||||
Type: "error",
|
||||
Category: "Images",
|
||||
@@ -372,10 +502,12 @@ func TestValidateImage(t *testing.T) {
|
||||
image: strongConf,
|
||||
cv: badCV,
|
||||
expected: []*ResultMessage{{
|
||||
ID: "pullPolicyNotAlways",
|
||||
Message: "Image pull policy should be \"Always\"",
|
||||
Type: "error",
|
||||
Category: "Images",
|
||||
}, {
|
||||
ID: "tagNotSpecified",
|
||||
Message: "Image tag should be specified",
|
||||
Type: "error",
|
||||
Category: "Images",
|
||||
@@ -392,7 +524,7 @@ func TestValidateImage(t *testing.T) {
|
||||
for _, tt := range testCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tt.cv = resetCV(tt.cv)
|
||||
tt.cv.validateImage(&tt.image)
|
||||
tt.cv.validateImage(&conf.Configuration{Images: tt.image}, "")
|
||||
assert.Len(t, tt.cv.Errors, len(tt.expected))
|
||||
assert.ElementsMatch(t, tt.cv.Errors, tt.expected)
|
||||
})
|
||||
@@ -450,6 +582,7 @@ func TestValidateNetworking(t *testing.T) {
|
||||
networkConf: standardConf,
|
||||
cv: emptyCV,
|
||||
expectedMessages: []*ResultMessage{{
|
||||
ID: "hostPortSet",
|
||||
Message: "Host port is not configured",
|
||||
Type: "success",
|
||||
Category: "Networking",
|
||||
@@ -460,6 +593,7 @@ func TestValidateNetworking(t *testing.T) {
|
||||
networkConf: standardConf,
|
||||
cv: emptyCV,
|
||||
expectedMessages: []*ResultMessage{{
|
||||
ID: "hostPortSet",
|
||||
Message: "Host port is not configured",
|
||||
Type: "success",
|
||||
Category: "Networking",
|
||||
@@ -476,6 +610,7 @@ func TestValidateNetworking(t *testing.T) {
|
||||
networkConf: standardConf,
|
||||
cv: badCV,
|
||||
expectedMessages: []*ResultMessage{{
|
||||
ID: "hostPortSet",
|
||||
Message: "Host port should not be configured",
|
||||
Type: "warning",
|
||||
Category: "Networking",
|
||||
@@ -486,6 +621,7 @@ func TestValidateNetworking(t *testing.T) {
|
||||
networkConf: standardConf,
|
||||
cv: goodCV,
|
||||
expectedMessages: []*ResultMessage{{
|
||||
ID: "hostPortSet",
|
||||
Message: "Host port is not configured",
|
||||
Type: "success",
|
||||
Category: "Networking",
|
||||
@@ -496,6 +632,7 @@ func TestValidateNetworking(t *testing.T) {
|
||||
networkConf: strongConf,
|
||||
cv: badCV,
|
||||
expectedMessages: []*ResultMessage{{
|
||||
ID: "hostPortSet",
|
||||
Message: "Host port should not be configured",
|
||||
Type: "error",
|
||||
Category: "Networking",
|
||||
@@ -506,7 +643,7 @@ func TestValidateNetworking(t *testing.T) {
|
||||
for _, tt := range testCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tt.cv = resetCV(tt.cv)
|
||||
tt.cv.validateNetworking(&tt.networkConf)
|
||||
tt.cv.validateNetworking(&conf.Configuration{Networking: tt.networkConf}, "")
|
||||
assert.Len(t, tt.cv.messages(), len(tt.expectedMessages))
|
||||
assert.ElementsMatch(t, tt.cv.messages(), tt.expectedMessages)
|
||||
})
|
||||
@@ -682,23 +819,28 @@ func TestValidateSecurity(t *testing.T) {
|
||||
securityConf: standardConf,
|
||||
cv: emptyCV,
|
||||
expectedMessages: []*ResultMessage{{
|
||||
ID: "runAsRootAllowed",
|
||||
Message: "Should not be allowed to run as root",
|
||||
Type: "warning",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "notReadOnlyRootFileSystem",
|
||||
Message: "Filesystem should be read only",
|
||||
Type: "warning",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "runAsPrivileged",
|
||||
Message: "Not running as privileged",
|
||||
Type: "success",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "privilegeEscalationAllowed",
|
||||
Message: "Privilege escalation not allowed",
|
||||
Type: "success",
|
||||
Category: "Security",
|
||||
}, {
|
||||
Message: "Security capabilities are within the configured limits",
|
||||
ID: "capabilitiesAdded",
|
||||
Message: "Disallowed security capabilities have not been added",
|
||||
Type: "success",
|
||||
Category: "Security",
|
||||
}},
|
||||
@@ -708,26 +850,32 @@ func TestValidateSecurity(t *testing.T) {
|
||||
securityConf: standardConf,
|
||||
cv: badCV,
|
||||
expectedMessages: []*ResultMessage{{
|
||||
ID: "capabilitiesAdded",
|
||||
Message: "The following security capabilities should not be added: SYS_ADMIN, NET_ADMIN",
|
||||
Type: "error",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "privilegeEscalationAllowed",
|
||||
Message: "Privilege escalation should not be allowed",
|
||||
Type: "error",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "runAsPrivileged",
|
||||
Message: "Should not be running as privileged",
|
||||
Type: "error",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "capabilitiesAdded",
|
||||
Message: "The following security capabilities should not be added: AUDIT_CONTROL, SYS_ADMIN, NET_ADMIN",
|
||||
Type: "warning",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "runAsRootAllowed",
|
||||
Message: "Should not be allowed to run as root",
|
||||
Type: "warning",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "notReadOnlyRootFileSystem",
|
||||
Message: "Filesystem should be read only",
|
||||
Type: "warning",
|
||||
Category: "Security",
|
||||
@@ -738,26 +886,32 @@ func TestValidateSecurity(t *testing.T) {
|
||||
securityConf: standardConf,
|
||||
cv: badCVWithGoodPodSpec,
|
||||
expectedMessages: []*ResultMessage{{
|
||||
ID: "capabilitiesAdded",
|
||||
Message: "The following security capabilities should not be added: SYS_ADMIN, NET_ADMIN",
|
||||
Type: "error",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "privilegeEscalationAllowed",
|
||||
Message: "Privilege escalation should not be allowed",
|
||||
Type: "error",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "runAsPrivileged",
|
||||
Message: "Should not be running as privileged",
|
||||
Type: "error",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "capabilitiesAdded",
|
||||
Message: "The following security capabilities should not be added: AUDIT_CONTROL, SYS_ADMIN, NET_ADMIN",
|
||||
Type: "warning",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "runAsRootAllowed",
|
||||
Message: "Should not be allowed to run as root",
|
||||
Type: "warning",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "notReadOnlyRootFileSystem",
|
||||
Message: "Filesystem should be read only",
|
||||
Type: "warning",
|
||||
Category: "Security",
|
||||
@@ -768,26 +922,32 @@ func TestValidateSecurity(t *testing.T) {
|
||||
securityConf: standardConf,
|
||||
cv: badCVWithBadPodSpec,
|
||||
expectedMessages: []*ResultMessage{{
|
||||
ID: "capabilitiesAdded",
|
||||
Message: "The following security capabilities should not be added: SYS_ADMIN, NET_ADMIN",
|
||||
Type: "error",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "privilegeEscalationAllowed",
|
||||
Message: "Privilege escalation should not be allowed",
|
||||
Type: "error",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "runAsPrivileged",
|
||||
Message: "Should not be running as privileged",
|
||||
Type: "error",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "capabilitiesAdded",
|
||||
Message: "The following security capabilities should not be added: AUDIT_CONTROL, SYS_ADMIN, NET_ADMIN",
|
||||
Type: "warning",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "runAsRootAllowed",
|
||||
Message: "Should not be allowed to run as root",
|
||||
Type: "warning",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "notReadOnlyRootFileSystem",
|
||||
Message: "Filesystem should be read only",
|
||||
Type: "warning",
|
||||
Category: "Security",
|
||||
@@ -798,23 +958,28 @@ func TestValidateSecurity(t *testing.T) {
|
||||
securityConf: standardConf,
|
||||
cv: goodCV,
|
||||
expectedMessages: []*ResultMessage{{
|
||||
ID: "runAsRootAllowed",
|
||||
Message: "Is not allowed to run as root",
|
||||
Type: "success",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "notReadOnlyRootFileSystem",
|
||||
Message: "Filesystem is read only",
|
||||
Type: "success",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "runAsPrivileged",
|
||||
Message: "Not running as privileged",
|
||||
Type: "success",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "privilegeEscalationAllowed",
|
||||
Message: "Privilege escalation not allowed",
|
||||
Type: "success",
|
||||
Category: "Security",
|
||||
}, {
|
||||
Message: "Security capabilities are within the configured limits",
|
||||
ID: "capabilitiesAdded",
|
||||
Message: "Disallowed security capabilities have not been added",
|
||||
Type: "success",
|
||||
Category: "Security",
|
||||
}},
|
||||
@@ -824,22 +989,32 @@ func TestValidateSecurity(t *testing.T) {
|
||||
securityConf: strongConf,
|
||||
cv: goodCV,
|
||||
expectedMessages: []*ResultMessage{{
|
||||
ID: "capabilitiesNotDropped",
|
||||
Message: "The following security capabilities should be dropped: DAC_OVERRIDE, SYS_CHROOT",
|
||||
Type: "error",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "capabilitiesAdded",
|
||||
Message: "Disallowed security capabilities have not been added",
|
||||
Type: "success",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "runAsRootAllowed",
|
||||
Message: "Is not allowed to run as root",
|
||||
Type: "success",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "notReadOnlyRootFileSystem",
|
||||
Message: "Filesystem is read only",
|
||||
Type: "success",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "runAsPrivileged",
|
||||
Message: "Not running as privileged",
|
||||
Type: "success",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "privilegeEscalationAllowed",
|
||||
Message: "Privilege escalation not allowed",
|
||||
Type: "success",
|
||||
Category: "Security",
|
||||
@@ -850,23 +1025,33 @@ func TestValidateSecurity(t *testing.T) {
|
||||
securityConf: strongConf,
|
||||
cv: strongCV,
|
||||
expectedMessages: []*ResultMessage{{
|
||||
ID: "runAsRootAllowed",
|
||||
Message: "Is not allowed to run as root",
|
||||
Type: "success",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "notReadOnlyRootFileSystem",
|
||||
Message: "Filesystem is read only",
|
||||
Type: "success",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "runAsPrivileged",
|
||||
Message: "Not running as privileged",
|
||||
Type: "success",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "privilegeEscalationAllowed",
|
||||
Message: "Privilege escalation not allowed",
|
||||
Type: "success",
|
||||
Category: "Security",
|
||||
}, {
|
||||
Message: "Security capabilities are within the configured limits",
|
||||
ID: "capabilitiesAdded",
|
||||
Message: "Disallowed security capabilities have not been added",
|
||||
Type: "success",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "capabilitiesDropped",
|
||||
Message: "All disallowed security capabilities have been dropped",
|
||||
Type: "success",
|
||||
Category: "Security",
|
||||
}},
|
||||
@@ -876,23 +1061,33 @@ func TestValidateSecurity(t *testing.T) {
|
||||
securityConf: strongConf,
|
||||
cv: strongCVWithPodSpecSecurityContext,
|
||||
expectedMessages: []*ResultMessage{{
|
||||
ID: "runAsRootAllowed",
|
||||
Message: "Is not allowed to run as root",
|
||||
Type: "success",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "notReadOnlyRootFileSystem",
|
||||
Message: "Filesystem is read only",
|
||||
Type: "success",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "runAsPrivileged",
|
||||
Message: "Not running as privileged",
|
||||
Type: "success",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "privilegeEscalationAllowed",
|
||||
Message: "Privilege escalation not allowed",
|
||||
Type: "success",
|
||||
Category: "Security",
|
||||
}, {
|
||||
Message: "Security capabilities are within the configured limits",
|
||||
ID: "capabilitiesAdded",
|
||||
Message: "Disallowed security capabilities have not been added",
|
||||
Type: "success",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "capabilitiesDropped",
|
||||
Message: "All disallowed security capabilities have been dropped",
|
||||
Type: "success",
|
||||
Category: "Security",
|
||||
}},
|
||||
@@ -902,23 +1097,33 @@ func TestValidateSecurity(t *testing.T) {
|
||||
securityConf: strongConf,
|
||||
cv: strongCVWithBadPodSpecSecurityContext,
|
||||
expectedMessages: []*ResultMessage{{
|
||||
ID: "runAsRootAllowed",
|
||||
Message: "Is not allowed to run as root",
|
||||
Type: "success",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "notReadOnlyRootFileSystem",
|
||||
Message: "Filesystem is read only",
|
||||
Type: "success",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "runAsPrivileged",
|
||||
Message: "Not running as privileged",
|
||||
Type: "success",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "privilegeEscalationAllowed",
|
||||
Message: "Privilege escalation not allowed",
|
||||
Type: "success",
|
||||
Category: "Security",
|
||||
}, {
|
||||
Message: "Security capabilities are within the configured limits",
|
||||
ID: "capabilitiesAdded",
|
||||
Message: "Disallowed security capabilities have not been added",
|
||||
Type: "success",
|
||||
Category: "Security",
|
||||
}, {
|
||||
ID: "capabilitiesDropped",
|
||||
Message: "All disallowed security capabilities have been dropped",
|
||||
Type: "success",
|
||||
Category: "Security",
|
||||
}},
|
||||
@@ -928,13 +1133,200 @@ func TestValidateSecurity(t *testing.T) {
|
||||
for _, tt := range testCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tt.cv = resetCV(tt.cv)
|
||||
tt.cv.validateSecurity(&tt.securityConf)
|
||||
tt.cv.validateSecurity(&conf.Configuration{Security: tt.securityConf}, "")
|
||||
assert.Len(t, tt.cv.messages(), len(tt.expectedMessages))
|
||||
assert.ElementsMatch(t, tt.cv.messages(), tt.expectedMessages)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRunAsRoot(t *testing.T) {
|
||||
falseVar := false
|
||||
trueVar := true
|
||||
nonRootUser := int64(1000)
|
||||
rootUser := int64(0)
|
||||
config := conf.Configuration{
|
||||
Security: conf.Security{
|
||||
RunAsRootAllowed: conf.SeverityWarning,
|
||||
},
|
||||
}
|
||||
testCases := []struct {
|
||||
cv ContainerValidation
|
||||
message ResultMessage
|
||||
}{
|
||||
{
|
||||
cv: ContainerValidation{
|
||||
ResourceValidation: &ResourceValidation{},
|
||||
Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{
|
||||
RunAsNonRoot: nil,
|
||||
}},
|
||||
parentPodSpec: corev1.PodSpec{
|
||||
SecurityContext: &corev1.PodSecurityContext{
|
||||
RunAsNonRoot: &falseVar,
|
||||
},
|
||||
},
|
||||
},
|
||||
message: ResultMessage{
|
||||
ID: "runAsRootAllowed",
|
||||
Message: "Should not be allowed to run as root",
|
||||
Type: "warning",
|
||||
Category: "Security",
|
||||
},
|
||||
},
|
||||
{
|
||||
cv: ContainerValidation{
|
||||
ResourceValidation: &ResourceValidation{},
|
||||
Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{
|
||||
RunAsNonRoot: &trueVar,
|
||||
}},
|
||||
parentPodSpec: corev1.PodSpec{
|
||||
SecurityContext: &corev1.PodSecurityContext{
|
||||
RunAsNonRoot: &falseVar,
|
||||
},
|
||||
},
|
||||
},
|
||||
message: ResultMessage{
|
||||
ID: "runAsRootAllowed",
|
||||
Message: "Is not allowed to run as root",
|
||||
Type: "success",
|
||||
Category: "Security",
|
||||
},
|
||||
},
|
||||
{
|
||||
cv: ContainerValidation{
|
||||
ResourceValidation: &ResourceValidation{},
|
||||
Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{
|
||||
RunAsUser: &nonRootUser,
|
||||
}},
|
||||
},
|
||||
message: ResultMessage{
|
||||
ID: "runAsRootAllowed",
|
||||
Message: "Is not allowed to run as root",
|
||||
Type: "success",
|
||||
Category: "Security",
|
||||
},
|
||||
},
|
||||
{
|
||||
cv: ContainerValidation{
|
||||
ResourceValidation: &ResourceValidation{},
|
||||
Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{}},
|
||||
parentPodSpec: corev1.PodSpec{
|
||||
SecurityContext: &corev1.PodSecurityContext{
|
||||
RunAsUser: &nonRootUser,
|
||||
},
|
||||
},
|
||||
},
|
||||
message: ResultMessage{
|
||||
ID: "runAsRootAllowed",
|
||||
Message: "Is not allowed to run as root",
|
||||
Type: "success",
|
||||
Category: "Security",
|
||||
},
|
||||
},
|
||||
{
|
||||
cv: ContainerValidation{
|
||||
ResourceValidation: &ResourceValidation{},
|
||||
Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{
|
||||
RunAsUser: &rootUser,
|
||||
}},
|
||||
parentPodSpec: corev1.PodSpec{
|
||||
SecurityContext: &corev1.PodSecurityContext{
|
||||
RunAsUser: &nonRootUser,
|
||||
},
|
||||
},
|
||||
},
|
||||
message: ResultMessage{
|
||||
ID: "runAsRootAllowed",
|
||||
Message: "Should not be allowed to run as root",
|
||||
Type: "warning",
|
||||
Category: "Security",
|
||||
},
|
||||
},
|
||||
{
|
||||
cv: ContainerValidation{
|
||||
ResourceValidation: &ResourceValidation{},
|
||||
Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{
|
||||
RunAsNonRoot: &falseVar,
|
||||
}},
|
||||
parentPodSpec: corev1.PodSpec{
|
||||
SecurityContext: &corev1.PodSecurityContext{
|
||||
RunAsUser: &nonRootUser,
|
||||
},
|
||||
},
|
||||
},
|
||||
message: ResultMessage{
|
||||
ID: "runAsRootAllowed",
|
||||
Message: "Should not be allowed to run as root",
|
||||
Type: "warning",
|
||||
Category: "Security",
|
||||
},
|
||||
},
|
||||
}
|
||||
for idx, tt := range testCases {
|
||||
tt.cv.validateSecurity(&config, "")
|
||||
assert.Len(t, tt.cv.messages(), 1)
|
||||
assert.Equal(t, &tt.message, tt.cv.messages()[0], fmt.Sprintf("Test case %d failed", idx))
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateResourcesExemption(t *testing.T) {
|
||||
container := corev1.Container{
|
||||
Name: "Empty",
|
||||
}
|
||||
|
||||
expectedWarnings := []*ResultMessage{}
|
||||
expectedErrors := []*ResultMessage{}
|
||||
expectedSuccesses := []*ResultMessage{}
|
||||
|
||||
testValidateResources(t, &container, &resourceConfExemptions, "foo", expectedErrors, expectedWarnings, expectedSuccesses)
|
||||
|
||||
expectedWarnings = []*ResultMessage{
|
||||
{
|
||||
ID: "cpuRequestsMissing",
|
||||
Type: "warning",
|
||||
Message: "CPU requests should be set",
|
||||
Category: "Resources",
|
||||
},
|
||||
{
|
||||
ID: "memoryRequestsMissing",
|
||||
Type: "warning",
|
||||
Message: "Memory requests should be set",
|
||||
Category: "Resources",
|
||||
},
|
||||
}
|
||||
|
||||
expectedErrors = []*ResultMessage{
|
||||
{
|
||||
ID: "cpuLimitsMissing",
|
||||
Type: "error",
|
||||
Message: "CPU limits should be set",
|
||||
Category: "Resources",
|
||||
},
|
||||
{
|
||||
ID: "memoryLimitsMissing",
|
||||
Type: "error",
|
||||
Message: "Memory limits should be set",
|
||||
Category: "Resources",
|
||||
},
|
||||
}
|
||||
|
||||
disallowExemptionsConf := resourceConfExemptions + "\ndisallowExemptions: true"
|
||||
|
||||
testValidateResources(t, &container, &disallowExemptionsConf, "foo", expectedErrors, expectedWarnings, expectedSuccesses)
|
||||
}
|
||||
|
||||
func TestValidateResourceRangeExemption(t *testing.T) {
|
||||
container := corev1.Container{
|
||||
Name: "Empty",
|
||||
}
|
||||
|
||||
expectedWarnings := []*ResultMessage{}
|
||||
expectedErrors := []*ResultMessage{}
|
||||
expectedSuccesses := []*ResultMessage{}
|
||||
|
||||
testValidateResources(t, &container, &resourceConfRangeExemptions, "foo", expectedErrors, expectedWarnings, expectedSuccesses)
|
||||
}
|
||||
|
||||
func resetCV(cv ContainerValidation) ContainerValidation {
|
||||
cv.Errors = []*ResultMessage{}
|
||||
cv.Successes = []*ResultMessage{}
|
||||
|
||||
+32
-61
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019 ReactiveOps
|
||||
// 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.
|
||||
@@ -15,82 +15,53 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
conf "github.com/reactiveops/polaris/pkg/config"
|
||||
"github.com/reactiveops/polaris/pkg/kube"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"strings"
|
||||
|
||||
conf "github.com/fairwindsops/polaris/pkg/config"
|
||||
"github.com/fairwindsops/polaris/pkg/kube"
|
||||
"github.com/fairwindsops/polaris/pkg/validator/controllers"
|
||||
controller "github.com/fairwindsops/polaris/pkg/validator/controllers"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// ControllerSpec is a generic type for k8s controller specs
|
||||
type ControllerSpec struct {
|
||||
Template corev1.PodTemplateSpec
|
||||
}
|
||||
|
||||
// Controller is a generic type for k8s controllers (e.g. Deployments and StatefulSets)
|
||||
type Controller struct {
|
||||
Type string
|
||||
Name string
|
||||
Namespace string
|
||||
Spec ControllerSpec
|
||||
}
|
||||
const exemptionAnnotationKey = "polaris.fairwinds.com/exempt"
|
||||
|
||||
// ValidateController validates a single controller, returns a ControllerResult.
|
||||
func ValidateController(conf conf.Configuration, controller Controller) ControllerResult {
|
||||
pod := controller.Spec.Template.Spec
|
||||
podResult := ValidatePod(conf, &pod)
|
||||
func ValidateController(conf conf.Configuration, controller controller.Interface) ControllerResult {
|
||||
controllerType := controller.GetType()
|
||||
pod := controller.GetPodSpec()
|
||||
podResult := ValidatePod(conf, pod, controller.GetName(), controllerType)
|
||||
return ControllerResult{
|
||||
Type: controller.Type,
|
||||
Name: controller.Name,
|
||||
Type: controllerType.String(),
|
||||
Name: controller.GetName(),
|
||||
PodResult: podResult,
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateControllers validates that each deployment conforms to the Polaris config,
|
||||
// returns a list of ResourceResults organized by namespace.
|
||||
// builds a list of ResourceResults organized by namespace.
|
||||
func ValidateControllers(config conf.Configuration, kubeResources *kube.ResourceProvider, nsResults *NamespacedResults) {
|
||||
controllers := []Controller{}
|
||||
for _, deploy := range kubeResources.Deployments {
|
||||
controllers = append(controllers, ControllerFromDeployment(deploy))
|
||||
var controllersToAudit []controller.Interface
|
||||
for _, supportedControllers := range config.ControllersToScan {
|
||||
loadedControllers, _ := controllers.LoadControllersByType(supportedControllers, kubeResources)
|
||||
controllersToAudit = append(controllersToAudit, loadedControllers...)
|
||||
}
|
||||
for _, deploy := range kubeResources.StatefulSets {
|
||||
controllers = append(controllers, ControllerFromStatefulSet(deploy))
|
||||
}
|
||||
for _, controller := range controllers {
|
||||
|
||||
for _, controller := range controllersToAudit {
|
||||
if !config.DisallowExemptions && hasExemptionAnnotation(controller) {
|
||||
continue
|
||||
}
|
||||
controllerResult := ValidateController(config, controller)
|
||||
nsResult := nsResults.getNamespaceResult(controller.Namespace)
|
||||
nsResult := nsResults.getNamespaceResult(controller.GetNamespace())
|
||||
nsResult.Summary.appendResults(*controllerResult.PodResult.Summary)
|
||||
if controller.Type == "Deployment" {
|
||||
nsResult.DeploymentResults = append(nsResult.DeploymentResults, controllerResult)
|
||||
} else if controller.Type == "StatefulSet" {
|
||||
nsResult.StatefulSetResults = append(nsResult.StatefulSetResults, controllerResult)
|
||||
if err := nsResult.AddResult(controller.GetType(), controllerResult); err != nil {
|
||||
logrus.Errorf("Internal Error: Failed to add a grouped result: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ControllerFrom* functions are 100% boilerplate
|
||||
|
||||
// ControllerFromDeployment creates a controller
|
||||
func ControllerFromDeployment(c appsv1.Deployment) Controller {
|
||||
spec := ControllerSpec{
|
||||
Template: c.Spec.Template,
|
||||
}
|
||||
return Controller{
|
||||
Type: "Deployment",
|
||||
Name: c.Name,
|
||||
Namespace: c.Namespace,
|
||||
Spec: spec,
|
||||
}
|
||||
}
|
||||
|
||||
// ControllerFromStatefulSet creates a controller
|
||||
func ControllerFromStatefulSet(c appsv1.StatefulSet) Controller {
|
||||
spec := ControllerSpec{
|
||||
Template: c.Spec.Template,
|
||||
}
|
||||
return Controller{
|
||||
Type: "StatefulSet",
|
||||
Name: c.Name,
|
||||
Namespace: c.Namespace,
|
||||
Spec: spec,
|
||||
}
|
||||
func hasExemptionAnnotation(ctrl controller.Interface) bool {
|
||||
annot := ctrl.GetAnnotations()
|
||||
val := annot[exemptionAnnotationKey]
|
||||
return strings.ToLower(val) == "true"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
// 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 validator
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
|
||||
conf "github.com/fairwindsops/polaris/pkg/config"
|
||||
"github.com/fairwindsops/polaris/pkg/kube"
|
||||
controller "github.com/fairwindsops/polaris/pkg/validator/controllers"
|
||||
"github.com/fairwindsops/polaris/test"
|
||||
)
|
||||
|
||||
func TestValidateController(t *testing.T) {
|
||||
c := conf.Configuration{
|
||||
Security: conf.Security{
|
||||
HostIPCSet: conf.SeverityError,
|
||||
HostPIDSet: conf.SeverityError,
|
||||
},
|
||||
}
|
||||
deployment := controller.NewDeploymentController(test.MockDeploy())
|
||||
expectedSum := ResultSummary{
|
||||
Totals: CountSummary{
|
||||
Successes: uint(2),
|
||||
Warnings: uint(0),
|
||||
Errors: uint(0),
|
||||
},
|
||||
ByCategory: make(map[string]*CountSummary),
|
||||
}
|
||||
expectedSum.ByCategory["Security"] = &CountSummary{
|
||||
Successes: uint(2),
|
||||
Warnings: uint(0),
|
||||
Errors: uint(0),
|
||||
}
|
||||
|
||||
expectedMessages := []*ResultMessage{
|
||||
{ID: "hostIPCSet", Message: "Host IPC is not configured", Type: "success", Category: "Security"},
|
||||
{ID: "hostPIDSet", Message: "Host PID is not configured", Type: "success", Category: "Security"},
|
||||
}
|
||||
|
||||
actualResult := ValidateController(c, deployment)
|
||||
|
||||
assert.Equal(t, "Deployments", actualResult.Type)
|
||||
assert.Equal(t, 1, len(actualResult.PodResult.ContainerResults), "should be equal")
|
||||
assert.EqualValues(t, &expectedSum, actualResult.PodResult.Summary)
|
||||
assert.EqualValues(t, expectedMessages, actualResult.PodResult.Messages)
|
||||
}
|
||||
|
||||
func TestSkipHealthChecks(t *testing.T) {
|
||||
c := conf.Configuration{
|
||||
HealthChecks: conf.HealthChecks{
|
||||
ReadinessProbeMissing: conf.SeverityError,
|
||||
LivenessProbeMissing: conf.SeverityWarning,
|
||||
},
|
||||
ControllersToScan: []conf.SupportedController{
|
||||
conf.Deployments,
|
||||
conf.StatefulSets,
|
||||
conf.DaemonSets,
|
||||
conf.Jobs,
|
||||
conf.CronJobs,
|
||||
conf.ReplicationControllers,
|
||||
},
|
||||
}
|
||||
deploymentBase := test.MockDeploy()
|
||||
deploymentBase.Spec.Template.Spec.InitContainers = []corev1.Container{test.MockContainer("test")}
|
||||
deployment := controller.NewDeploymentController(deploymentBase)
|
||||
expectedSum := ResultSummary{
|
||||
Totals: CountSummary{
|
||||
Successes: uint(0),
|
||||
Warnings: uint(1),
|
||||
Errors: uint(1),
|
||||
},
|
||||
ByCategory: make(map[string]*CountSummary),
|
||||
}
|
||||
expectedSum.ByCategory["Health Checks"] = &CountSummary{
|
||||
Successes: uint(0),
|
||||
Warnings: uint(1),
|
||||
Errors: uint(1),
|
||||
}
|
||||
expectedMessages := []*ResultMessage{
|
||||
{ID: "readinessProbeMissing", Message: "Readiness probe should be configured", Type: "error", Category: "Health Checks"},
|
||||
{ID: "livenessProbeMissing", Message: "Liveness probe should be configured", Type: "warning", Category: "Health Checks"},
|
||||
}
|
||||
actualResult := ValidateController(c, deployment)
|
||||
assert.Equal(t, "Deployments", actualResult.Type)
|
||||
assert.Equal(t, 2, len(actualResult.PodResult.ContainerResults), "should be equal")
|
||||
assert.EqualValues(t, &expectedSum, actualResult.PodResult.Summary)
|
||||
assert.EqualValues(t, []*ResultMessage{}, actualResult.PodResult.ContainerResults[0].Messages)
|
||||
assert.EqualValues(t, expectedMessages, actualResult.PodResult.ContainerResults[1].Messages)
|
||||
|
||||
job := controller.NewJobController(test.MockJob())
|
||||
expectedSum = ResultSummary{
|
||||
Totals: CountSummary{
|
||||
Successes: uint(0),
|
||||
Warnings: uint(0),
|
||||
Errors: uint(0),
|
||||
},
|
||||
ByCategory: make(map[string]*CountSummary),
|
||||
}
|
||||
expectedMessages = []*ResultMessage{}
|
||||
actualResult = ValidateController(c, job)
|
||||
assert.Equal(t, "Jobs", actualResult.Type)
|
||||
assert.Equal(t, 1, len(actualResult.PodResult.ContainerResults), "should be equal")
|
||||
assert.EqualValues(t, &expectedSum, actualResult.PodResult.Summary)
|
||||
assert.EqualValues(t, expectedMessages, actualResult.PodResult.ContainerResults[0].Messages)
|
||||
|
||||
cronjob := controller.NewCronJobController(test.MockCronJob())
|
||||
expectedSum = ResultSummary{
|
||||
Totals: CountSummary{
|
||||
Successes: uint(0),
|
||||
Warnings: uint(0),
|
||||
Errors: uint(0),
|
||||
},
|
||||
ByCategory: make(map[string]*CountSummary),
|
||||
}
|
||||
expectedMessages = []*ResultMessage{}
|
||||
actualResult = ValidateController(c, cronjob)
|
||||
assert.Equal(t, "CronJobs", actualResult.Type)
|
||||
assert.Equal(t, 1, len(actualResult.PodResult.ContainerResults), "should be equal")
|
||||
assert.EqualValues(t, &expectedSum, actualResult.PodResult.Summary)
|
||||
assert.EqualValues(t, expectedMessages, actualResult.PodResult.ContainerResults[0].Messages)
|
||||
}
|
||||
|
||||
func TestControllerExemptions(t *testing.T) {
|
||||
c := conf.Configuration{
|
||||
HealthChecks: conf.HealthChecks{
|
||||
ReadinessProbeMissing: conf.SeverityError,
|
||||
LivenessProbeMissing: conf.SeverityWarning,
|
||||
},
|
||||
ControllersToScan: []conf.SupportedController{
|
||||
conf.Deployments,
|
||||
},
|
||||
}
|
||||
resources := &kube.ResourceProvider{
|
||||
Deployments: []appsv1.Deployment{test.MockDeploy()},
|
||||
}
|
||||
|
||||
expectedSum := ResultSummary{
|
||||
Totals: CountSummary{
|
||||
Successes: uint(0),
|
||||
Warnings: uint(1),
|
||||
Errors: uint(1),
|
||||
},
|
||||
ByCategory: make(map[string]*CountSummary),
|
||||
}
|
||||
expectedSum.ByCategory["Health Checks"] = &CountSummary{
|
||||
Successes: uint(0),
|
||||
Warnings: uint(1),
|
||||
Errors: uint(1),
|
||||
}
|
||||
nsResults := NamespacedResults{}
|
||||
ValidateControllers(c, resources, &nsResults)
|
||||
actualResult := nsResults[""].DeploymentResults[0]
|
||||
assert.Equal(t, "Deployments", actualResult.Type)
|
||||
assert.EqualValues(t, &expectedSum, actualResult.PodResult.Summary)
|
||||
|
||||
resources.Deployments[0].ObjectMeta.Annotations = map[string]string{
|
||||
exemptionAnnotationKey: "true",
|
||||
}
|
||||
nsResults = NamespacedResults{}
|
||||
ValidateControllers(c, resources, &nsResults)
|
||||
assert.Equal(t, (*NamespaceResult)(nil), nsResults[""])
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"github.com/fairwindsops/polaris/pkg/config"
|
||||
kubeAPIBatchV1beta1 "k8s.io/api/batch/v1beta1"
|
||||
kubeAPICoreV1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
// CronJobController is an implementation of controller for deployments
|
||||
type CronJobController struct {
|
||||
GenericController
|
||||
K8SResource kubeAPIBatchV1beta1.CronJob
|
||||
}
|
||||
|
||||
// GetPodTemplate returns the original template spec
|
||||
func (c CronJobController) GetPodTemplate() *kubeAPICoreV1.PodTemplateSpec {
|
||||
return &c.K8SResource.Spec.JobTemplate.Spec.Template
|
||||
}
|
||||
|
||||
// GetPodSpec returns the original kubernetes template pod spec
|
||||
func (c CronJobController) GetPodSpec() *kubeAPICoreV1.PodSpec {
|
||||
return &c.K8SResource.Spec.JobTemplate.Spec.Template.Spec
|
||||
}
|
||||
|
||||
// GetType returns the supportedcontroller enum type
|
||||
func (c CronJobController) GetType() config.SupportedController {
|
||||
return config.CronJobs
|
||||
}
|
||||
|
||||
// GetAnnotations returns the controller's annotations
|
||||
func (c CronJobController) GetAnnotations() map[string]string {
|
||||
return c.K8SResource.ObjectMeta.Annotations
|
||||
}
|
||||
|
||||
// NewCronJobController builds a new controller interface for Deployments
|
||||
func NewCronJobController(originalDeploymentResource kubeAPIBatchV1beta1.CronJob) Interface {
|
||||
controller := CronJobController{}
|
||||
controller.Name = originalDeploymentResource.Name
|
||||
controller.Namespace = originalDeploymentResource.Namespace
|
||||
controller.K8SResource = originalDeploymentResource
|
||||
return controller
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"github.com/fairwindsops/polaris/pkg/config"
|
||||
kubeAPIAppsV1 "k8s.io/api/apps/v1"
|
||||
kubeAPICoreV1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
// DaemonSetController is an implementation of controller for deployments
|
||||
type DaemonSetController struct {
|
||||
GenericController
|
||||
K8SResource kubeAPIAppsV1.DaemonSet
|
||||
}
|
||||
|
||||
// GetPodTemplate returns the original template spec
|
||||
func (d DaemonSetController) GetPodTemplate() *kubeAPICoreV1.PodTemplateSpec {
|
||||
return &d.K8SResource.Spec.Template
|
||||
}
|
||||
|
||||
// GetPodSpec returns the original kubernetes template pod spec
|
||||
func (d DaemonSetController) GetPodSpec() *kubeAPICoreV1.PodSpec {
|
||||
return &d.K8SResource.Spec.Template.Spec
|
||||
}
|
||||
|
||||
// GetAnnotations returns the controller's annotations
|
||||
func (d DaemonSetController) GetAnnotations() map[string]string {
|
||||
return d.K8SResource.ObjectMeta.Annotations
|
||||
}
|
||||
|
||||
// GetType returns the supportedcontroller enum type
|
||||
func (d DaemonSetController) GetType() config.SupportedController {
|
||||
return config.DaemonSets
|
||||
}
|
||||
|
||||
// NewDaemonSetController builds a new controller interface for Deployments
|
||||
func NewDaemonSetController(originalResource kubeAPIAppsV1.DaemonSet) Interface {
|
||||
controller := DaemonSetController{}
|
||||
controller.Name = originalResource.Name
|
||||
controller.Namespace = originalResource.Namespace
|
||||
controller.K8SResource = originalResource
|
||||
return controller
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"github.com/fairwindsops/polaris/pkg/config"
|
||||
kubeAPIAppsV1 "k8s.io/api/apps/v1"
|
||||
kubeAPICoreV1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
// DeploymentController is an implementation of controller for deployments
|
||||
type DeploymentController struct {
|
||||
GenericController
|
||||
K8SResource kubeAPIAppsV1.Deployment
|
||||
}
|
||||
|
||||
// GetPodTemplate returns the original template spec
|
||||
func (d DeploymentController) GetPodTemplate() *kubeAPICoreV1.PodTemplateSpec {
|
||||
return &d.K8SResource.Spec.Template
|
||||
}
|
||||
|
||||
// GetPodSpec returns the original kubernetes template pod spec
|
||||
func (d DeploymentController) GetPodSpec() *kubeAPICoreV1.PodSpec {
|
||||
return &d.K8SResource.Spec.Template.Spec
|
||||
}
|
||||
|
||||
// GetAnnotations returns the controller's annotations
|
||||
func (d DeploymentController) GetAnnotations() map[string]string {
|
||||
return d.K8SResource.ObjectMeta.Annotations
|
||||
}
|
||||
|
||||
// GetType returns the supportedcontroller enum type
|
||||
func (d DeploymentController) GetType() config.SupportedController {
|
||||
return config.Deployments
|
||||
}
|
||||
|
||||
// NewDeploymentController builds a new controller interface for Deployments
|
||||
func NewDeploymentController(originalDeploymentResource kubeAPIAppsV1.Deployment) Interface {
|
||||
controller := DeploymentController{}
|
||||
controller.Name = originalDeploymentResource.Name
|
||||
controller.Namespace = originalDeploymentResource.Namespace
|
||||
controller.K8SResource = originalDeploymentResource
|
||||
return controller
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/fairwindsops/polaris/pkg/config"
|
||||
"github.com/fairwindsops/polaris/pkg/kube"
|
||||
kubeAPICoreV1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
// Interface is an interface for k8s controllers (e.g. Deployments and StatefulSets)
|
||||
type Interface interface {
|
||||
GetName() string
|
||||
GetNamespace() string
|
||||
GetPodTemplate() *kubeAPICoreV1.PodTemplateSpec
|
||||
GetPodSpec() *kubeAPICoreV1.PodSpec
|
||||
GetType() config.SupportedController
|
||||
GetAnnotations() map[string]string
|
||||
}
|
||||
|
||||
// GenericController is a base implementation with some free methods for inherited structs
|
||||
type GenericController struct {
|
||||
Name string
|
||||
Namespace string
|
||||
}
|
||||
|
||||
// GetName is inherited by all controllers using generic controller to get the name of the controller
|
||||
func (g GenericController) GetName() string {
|
||||
return g.Name
|
||||
}
|
||||
|
||||
// GetNamespace is inherited by all controllers using generic controller to get the namespace of the controller
|
||||
func (g GenericController) GetNamespace() string {
|
||||
return g.Namespace
|
||||
}
|
||||
|
||||
// LoadControllersByType loads a list of controllers from the kubeResources by detecting their type
|
||||
func LoadControllersByType(controllerType config.SupportedController, kubeResources *kube.ResourceProvider) ([]Interface, error) {
|
||||
interfaces := []Interface{}
|
||||
switch controllerType {
|
||||
case config.Deployments:
|
||||
for _, deploy := range kubeResources.Deployments {
|
||||
interfaces = append(interfaces, NewDeploymentController(deploy))
|
||||
}
|
||||
case config.StatefulSets:
|
||||
for _, statefulSet := range kubeResources.StatefulSets {
|
||||
interfaces = append(interfaces, NewStatefulSetController(statefulSet))
|
||||
}
|
||||
case config.DaemonSets:
|
||||
for _, daemonSet := range kubeResources.DaemonSets {
|
||||
interfaces = append(interfaces, NewDaemonSetController(daemonSet))
|
||||
}
|
||||
case config.Jobs:
|
||||
for _, job := range kubeResources.Jobs {
|
||||
interfaces = append(interfaces, NewJobController(job))
|
||||
}
|
||||
case config.CronJobs:
|
||||
for _, cronJob := range kubeResources.CronJobs {
|
||||
interfaces = append(interfaces, NewCronJobController(cronJob))
|
||||
}
|
||||
case config.ReplicationControllers:
|
||||
for _, replicationController := range kubeResources.ReplicationControllers {
|
||||
interfaces = append(interfaces, NewReplicationControllerController(replicationController))
|
||||
}
|
||||
}
|
||||
if len(interfaces) > 0 {
|
||||
return interfaces, nil
|
||||
}
|
||||
return nil, fmt.Errorf("Controller type (%s) does not have a generator", controllerType)
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"github.com/fairwindsops/polaris/pkg/config"
|
||||
kubeAPIBatchV1 "k8s.io/api/batch/v1"
|
||||
kubeAPICoreV1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
// JobController is an implementation of controller for deployments
|
||||
type JobController struct {
|
||||
GenericController
|
||||
K8SResource kubeAPIBatchV1.Job
|
||||
}
|
||||
|
||||
// GetPodTemplate returns the original template spec
|
||||
func (j JobController) GetPodTemplate() *kubeAPICoreV1.PodTemplateSpec {
|
||||
return &j.K8SResource.Spec.Template
|
||||
}
|
||||
|
||||
// GetPodSpec returns the original kubernetes template pod spec
|
||||
func (j JobController) GetPodSpec() *kubeAPICoreV1.PodSpec {
|
||||
return &j.K8SResource.Spec.Template.Spec
|
||||
}
|
||||
|
||||
// GetAnnotations returns the controller's annotations
|
||||
func (j JobController) GetAnnotations() map[string]string {
|
||||
return j.K8SResource.ObjectMeta.Annotations
|
||||
}
|
||||
|
||||
// GetType returns the supportedcontroller enum type
|
||||
func (j JobController) GetType() config.SupportedController {
|
||||
return config.Jobs
|
||||
}
|
||||
|
||||
// NewJobController builds a new controller interface for Deployments
|
||||
func NewJobController(originalResource kubeAPIBatchV1.Job) Interface {
|
||||
controller := JobController{}
|
||||
controller.Name = originalResource.Name
|
||||
controller.Namespace = originalResource.Namespace
|
||||
controller.K8SResource = originalResource
|
||||
return controller
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"github.com/fairwindsops/polaris/pkg/config"
|
||||
kubeAPICoreV1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
// NOTE: Maybe this name of ReplicationController is duplicative but it's more explicit since
|
||||
// that's how kubernetes refers the the object.
|
||||
|
||||
// ReplicationControllerController is an implementation of controller for deployments
|
||||
type ReplicationControllerController struct {
|
||||
GenericController
|
||||
K8SResource kubeAPICoreV1.ReplicationController
|
||||
}
|
||||
|
||||
// GetPodTemplate returns the original template spec
|
||||
func (r ReplicationControllerController) GetPodTemplate() *kubeAPICoreV1.PodTemplateSpec {
|
||||
return r.K8SResource.Spec.Template
|
||||
}
|
||||
|
||||
// GetPodSpec returns the original kubernetes template pod spec
|
||||
func (r ReplicationControllerController) GetPodSpec() *kubeAPICoreV1.PodSpec {
|
||||
return &r.K8SResource.Spec.Template.Spec
|
||||
}
|
||||
|
||||
// GetAnnotations returns the controller's annotations
|
||||
func (r ReplicationControllerController) GetAnnotations() map[string]string {
|
||||
return r.K8SResource.ObjectMeta.Annotations
|
||||
}
|
||||
|
||||
// GetType returns the supportedcontroller enum type
|
||||
func (r ReplicationControllerController) GetType() config.SupportedController {
|
||||
return config.ReplicationControllers
|
||||
}
|
||||
|
||||
// NewReplicationControllerController builds a new controller interface for Deployments
|
||||
func NewReplicationControllerController(originalResource kubeAPICoreV1.ReplicationController) Interface {
|
||||
controller := ReplicationControllerController{}
|
||||
controller.Name = originalResource.Name
|
||||
controller.Namespace = originalResource.Namespace
|
||||
controller.K8SResource = originalResource
|
||||
return controller
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"github.com/fairwindsops/polaris/pkg/config"
|
||||
kubeAPIAppsV1 "k8s.io/api/apps/v1"
|
||||
kubeAPICoreV1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
// StatefulSetController is an implementation of controller for deployments
|
||||
type StatefulSetController struct {
|
||||
GenericController
|
||||
K8SResource kubeAPIAppsV1.StatefulSet
|
||||
}
|
||||
|
||||
// GetPodTemplate returns the kubernetes template spec
|
||||
func (s StatefulSetController) GetPodTemplate() *kubeAPICoreV1.PodTemplateSpec {
|
||||
return &s.K8SResource.Spec.Template
|
||||
}
|
||||
|
||||
// GetPodSpec returns the podspec from the original kubernetes resource
|
||||
func (s StatefulSetController) GetPodSpec() *kubeAPICoreV1.PodSpec {
|
||||
return &s.K8SResource.Spec.Template.Spec
|
||||
}
|
||||
|
||||
// GetAnnotations returns the controller's annotations
|
||||
func (s StatefulSetController) GetAnnotations() map[string]string {
|
||||
return s.K8SResource.ObjectMeta.Annotations
|
||||
}
|
||||
|
||||
// GetType returns the supportedcontroller enum type
|
||||
func (s StatefulSetController) GetType() config.SupportedController {
|
||||
return config.StatefulSets
|
||||
}
|
||||
|
||||
// NewStatefulSetController builds a statefulset controller
|
||||
func NewStatefulSetController(originalResource kubeAPIAppsV1.StatefulSet) Interface {
|
||||
controller := StatefulSetController{}
|
||||
controller.Name = originalResource.Name
|
||||
controller.Namespace = originalResource.Namespace
|
||||
controller.K8SResource = originalResource
|
||||
return controller
|
||||
}
|
||||
+16
-45
@@ -3,38 +3,10 @@ package validator
|
||||
import (
|
||||
"time"
|
||||
|
||||
conf "github.com/reactiveops/polaris/pkg/config"
|
||||
"github.com/reactiveops/polaris/pkg/kube"
|
||||
conf "github.com/fairwindsops/polaris/pkg/config"
|
||||
"github.com/fairwindsops/polaris/pkg/kube"
|
||||
)
|
||||
|
||||
const (
|
||||
// PolarisOutputVersion is the version of the current output structure
|
||||
PolarisOutputVersion = "0.2"
|
||||
)
|
||||
|
||||
// ClusterSummary contains Polaris results as well as some high-level stats
|
||||
type ClusterSummary struct {
|
||||
Results ResultSummary
|
||||
Version string
|
||||
Nodes int
|
||||
Pods int
|
||||
Namespaces int
|
||||
Deployments int
|
||||
StatefulSets int
|
||||
Score uint
|
||||
}
|
||||
|
||||
// AuditData contains all the data from a full Polaris audit
|
||||
type AuditData struct {
|
||||
PolarisOutputVersion string
|
||||
AuditTime string
|
||||
SourceType string
|
||||
SourceName string
|
||||
DisplayName string
|
||||
ClusterSummary ClusterSummary
|
||||
NamespacedResults NamespacedResults
|
||||
}
|
||||
|
||||
// RunAudit runs a full Polaris audit and returns an AuditData object
|
||||
func RunAudit(config conf.Configuration, kubeResources *kube.ResourceProvider) (AuditData, error) {
|
||||
nsResults := NamespacedResults{}
|
||||
@@ -43,13 +15,8 @@ func RunAudit(config conf.Configuration, kubeResources *kube.ResourceProvider) (
|
||||
clusterResults := ResultSummary{}
|
||||
|
||||
// Aggregate all summary counts to get a clusterwide count.
|
||||
for _, nsRes := range nsResults {
|
||||
for _, dr := range nsRes.DeploymentResults {
|
||||
clusterResults.appendResults(*dr.PodResult.Summary)
|
||||
}
|
||||
for _, dr := range nsRes.StatefulSetResults {
|
||||
clusterResults.appendResults(*dr.PodResult.Summary)
|
||||
}
|
||||
for _, result := range nsResults.GetAllControllerResults() {
|
||||
clusterResults.appendResults(*result.PodResult.Summary)
|
||||
}
|
||||
|
||||
displayName := config.DisplayName
|
||||
@@ -64,14 +31,18 @@ func RunAudit(config conf.Configuration, kubeResources *kube.ResourceProvider) (
|
||||
SourceName: kubeResources.SourceName,
|
||||
DisplayName: displayName,
|
||||
ClusterSummary: ClusterSummary{
|
||||
Version: kubeResources.ServerVersion,
|
||||
Nodes: len(kubeResources.Nodes),
|
||||
Pods: len(kubeResources.Pods),
|
||||
Namespaces: len(kubeResources.Namespaces),
|
||||
Deployments: len(kubeResources.Deployments),
|
||||
StatefulSets: len(kubeResources.StatefulSets),
|
||||
Results: clusterResults,
|
||||
Score: clusterResults.Totals.GetScore(),
|
||||
Version: kubeResources.ServerVersion,
|
||||
Nodes: len(kubeResources.Nodes),
|
||||
Pods: len(kubeResources.Pods),
|
||||
Namespaces: len(kubeResources.Namespaces),
|
||||
Deployments: len(kubeResources.Deployments),
|
||||
StatefulSets: len(kubeResources.StatefulSets),
|
||||
DaemonSets: len(kubeResources.DaemonSets),
|
||||
Jobs: len(kubeResources.Jobs),
|
||||
CronJobs: len(kubeResources.CronJobs),
|
||||
ReplicationControllers: len(kubeResources.ReplicationControllers),
|
||||
Results: clusterResults,
|
||||
Score: clusterResults.Totals.GetScore(),
|
||||
},
|
||||
NamespacedResults: nsResults,
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@ package validator
|
||||
import (
|
||||
"testing"
|
||||
|
||||
conf "github.com/reactiveops/polaris/pkg/config"
|
||||
"github.com/reactiveops/polaris/pkg/kube"
|
||||
"github.com/reactiveops/polaris/test"
|
||||
conf "github.com/fairwindsops/polaris/pkg/config"
|
||||
"github.com/fairwindsops/polaris/pkg/kube"
|
||||
"github.com/fairwindsops/polaris/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -20,25 +20,29 @@ func TestGetTemplateData(t *testing.T) {
|
||||
ReadinessProbeMissing: conf.SeverityError,
|
||||
LivenessProbeMissing: conf.SeverityWarning,
|
||||
},
|
||||
ControllersToScan: []conf.SupportedController{
|
||||
conf.Deployments,
|
||||
conf.StatefulSets,
|
||||
conf.DaemonSets,
|
||||
conf.Jobs,
|
||||
conf.CronJobs,
|
||||
conf.ReplicationControllers,
|
||||
},
|
||||
}
|
||||
|
||||
// TODO: split out the logic for calculating summaries into another set of tests
|
||||
sum := ResultSummary{
|
||||
Totals: CountSummary{
|
||||
Successes: uint(8),
|
||||
Warnings: uint(2),
|
||||
Errors: uint(2),
|
||||
Successes: uint(0),
|
||||
Warnings: uint(4),
|
||||
Errors: uint(4),
|
||||
},
|
||||
ByCategory: CategorySummary{},
|
||||
}
|
||||
sum.ByCategory["Health Checks"] = &CountSummary{
|
||||
Successes: uint(0),
|
||||
Warnings: uint(2),
|
||||
Errors: uint(2),
|
||||
}
|
||||
sum.ByCategory["Resources"] = &CountSummary{
|
||||
Successes: uint(8),
|
||||
Warnings: uint(0),
|
||||
Errors: uint(0),
|
||||
Warnings: uint(4),
|
||||
Errors: uint(4),
|
||||
}
|
||||
|
||||
actualAudit, err := RunAudit(c, resources)
|
||||
@@ -51,10 +55,10 @@ func TestGetTemplateData(t *testing.T) {
|
||||
assert.Equal(t, 1, len(actualAudit.NamespacedResults["test"].DeploymentResults), "should be equal")
|
||||
assert.Equal(t, 1, len(actualAudit.NamespacedResults["test"].DeploymentResults), "should be equal")
|
||||
assert.Equal(t, 1, len(actualAudit.NamespacedResults["test"].DeploymentResults[0].PodResult.ContainerResults), "should be equal")
|
||||
assert.Equal(t, 6, len(actualAudit.NamespacedResults["test"].DeploymentResults[0].PodResult.ContainerResults[0].Messages), "should be equal")
|
||||
assert.Equal(t, 2, len(actualAudit.NamespacedResults["test"].DeploymentResults[0].PodResult.ContainerResults[0].Messages), "should be equal")
|
||||
|
||||
assert.Equal(t, 1, len(actualAudit.NamespacedResults["test"].StatefulSetResults), "should be equal")
|
||||
assert.Equal(t, 1, len(actualAudit.NamespacedResults["test"].StatefulSetResults), "should be equal")
|
||||
assert.Equal(t, 1, len(actualAudit.NamespacedResults["test"].StatefulSetResults[0].PodResult.ContainerResults), "should be equal")
|
||||
assert.Equal(t, 6, len(actualAudit.NamespacedResults["test"].StatefulSetResults[0].PodResult.ContainerResults[0].Messages), "should be equal")
|
||||
assert.Equal(t, 2, len(actualAudit.NamespacedResults["test"].StatefulSetResults[0].PodResult.ContainerResults[0].Messages), "should be equal")
|
||||
}
|
||||
|
||||
@@ -73,12 +73,14 @@ const (
|
||||
PrivilegeEscalationFailure = "Privilege escalation should not be allowed"
|
||||
// PrivilegeEscalationSuccess message
|
||||
PrivilegeEscalationSuccess = "Privilege escalation not allowed"
|
||||
// SecurityCapabilitiesAddedSuccess message
|
||||
SecurityCapabilitiesAddedSuccess = "Disallowed security capabilities have not been added"
|
||||
// SecurityCapabilitiesAddedFailure message
|
||||
SecurityCapabilitiesAddedFailure = "The following security capabilities should not be added: %v"
|
||||
// SecurityCapabilitiesNotDroppedSuccess message
|
||||
SecurityCapabilitiesNotDroppedSuccess = "All disallowed security capabilities have been dropped"
|
||||
// SecurityCapabilitiesNotDroppedFailure message
|
||||
SecurityCapabilitiesNotDroppedFailure = "The following security capabilities should be dropped: %v"
|
||||
// SecurityCapabilitiesSuccess message
|
||||
SecurityCapabilitiesSuccess = "Security capabilities are within the configured limits"
|
||||
|
||||
// HostAliasFailure message
|
||||
HostAliasFailure = "Host alias should not be configured"
|
||||
|
||||
+27
-21
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019 ReactiveOps
|
||||
// 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.
|
||||
@@ -15,8 +15,8 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
conf "github.com/reactiveops/polaris/pkg/config"
|
||||
"github.com/reactiveops/polaris/pkg/validator/messages"
|
||||
"github.com/fairwindsops/polaris/pkg/config"
|
||||
"github.com/fairwindsops/polaris/pkg/validator/messages"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
@@ -27,14 +27,14 @@ type PodValidation struct {
|
||||
}
|
||||
|
||||
// ValidatePod validates that each pod conforms to the Polaris config, returns a ResourceResult.
|
||||
func ValidatePod(podConf conf.Configuration, pod *corev1.PodSpec) PodResult {
|
||||
func ValidatePod(conf config.Configuration, pod *corev1.PodSpec, controllerName string, controllerType config.SupportedController) PodResult {
|
||||
pv := PodValidation{
|
||||
Pod: pod,
|
||||
ResourceValidation: &ResourceValidation{},
|
||||
}
|
||||
|
||||
pv.validateSecurity(&podConf.Security)
|
||||
pv.validateNetworking(&podConf.Networking)
|
||||
pv.validateSecurity(&conf, controllerName)
|
||||
pv.validateNetworking(&conf, controllerName)
|
||||
|
||||
pRes := PodResult{
|
||||
Messages: pv.messages(),
|
||||
@@ -43,8 +43,8 @@ func ValidatePod(podConf conf.Configuration, pod *corev1.PodSpec) PodResult {
|
||||
podSpec: *pod,
|
||||
}
|
||||
|
||||
pv.validateContainers(pod.InitContainers, &pRes, &podConf, true)
|
||||
pv.validateContainers(pod.Containers, &pRes, &podConf, false)
|
||||
pv.validateContainers(pod.InitContainers, &pRes, &conf, controllerName, controllerType, true)
|
||||
pv.validateContainers(pod.Containers, &pRes, &conf, controllerName, controllerType, false)
|
||||
|
||||
for _, cRes := range pRes.ContainerResults {
|
||||
pRes.Summary.appendResults(*cRes.Summary)
|
||||
@@ -53,41 +53,47 @@ func ValidatePod(podConf conf.Configuration, pod *corev1.PodSpec) PodResult {
|
||||
return pRes
|
||||
}
|
||||
|
||||
func (pv *PodValidation) validateContainers(containers []corev1.Container, pRes *PodResult, podConf *conf.Configuration, isInit bool) {
|
||||
func (pv *PodValidation) validateContainers(containers []corev1.Container, pRes *PodResult, conf *config.Configuration, controllerName string, controllerType config.SupportedController, isInit bool) {
|
||||
for _, container := range containers {
|
||||
cRes := ValidateContainer(&container, pRes, podConf, isInit)
|
||||
cRes := ValidateContainer(&container, pRes, conf, controllerName, controllerType, isInit)
|
||||
pRes.ContainerResults = append(pRes.ContainerResults, cRes)
|
||||
}
|
||||
}
|
||||
|
||||
func (pv *PodValidation) validateSecurity(securityConf *conf.Security) {
|
||||
func (pv *PodValidation) validateSecurity(conf *config.Configuration, controllerName string) {
|
||||
category := messages.CategorySecurity
|
||||
|
||||
if securityConf.HostIPCSet.IsActionable() {
|
||||
name := "HostIPCSet"
|
||||
if conf.IsActionable(conf.Security, name, controllerName) {
|
||||
id := config.GetIDFromField(conf.Security, name)
|
||||
if pv.Pod.HostIPC {
|
||||
pv.addFailure(messages.HostIPCFailure, securityConf.HostIPCSet, category)
|
||||
pv.addFailure(messages.HostIPCFailure, conf.Security.HostIPCSet, category, id)
|
||||
} else {
|
||||
pv.addSuccess(messages.HostIPCSuccess, category)
|
||||
pv.addSuccess(messages.HostIPCSuccess, category, id)
|
||||
}
|
||||
}
|
||||
|
||||
if securityConf.HostPIDSet.IsActionable() {
|
||||
name = "HostPIDSet"
|
||||
if conf.IsActionable(conf.Security, name, controllerName) {
|
||||
id := config.GetIDFromField(conf.Security, name)
|
||||
if pv.Pod.HostPID {
|
||||
pv.addFailure(messages.HostPIDFailure, securityConf.HostPIDSet, category)
|
||||
pv.addFailure(messages.HostPIDFailure, conf.Security.HostPIDSet, category, id)
|
||||
} else {
|
||||
pv.addSuccess(messages.HostPIDSuccess, category)
|
||||
pv.addSuccess(messages.HostPIDSuccess, category, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (pv *PodValidation) validateNetworking(networkConf *conf.Networking) {
|
||||
func (pv *PodValidation) validateNetworking(conf *config.Configuration, controllerName string) {
|
||||
category := messages.CategoryNetworking
|
||||
|
||||
if networkConf.HostNetworkSet.IsActionable() {
|
||||
name := "HostNetworkSet"
|
||||
if conf.IsActionable(conf.Networking, name, controllerName) {
|
||||
id := config.GetIDFromField(conf.Networking, name)
|
||||
if pv.Pod.HostNetwork {
|
||||
pv.addFailure(messages.HostNetworkFailure, networkConf.HostNetworkSet, category)
|
||||
pv.addFailure(messages.HostNetworkFailure, conf.Networking.HostNetworkSet, category, id)
|
||||
} else {
|
||||
pv.addSuccess(messages.HostNetworkSuccess, category)
|
||||
pv.addSuccess(messages.HostNetworkSuccess, category, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+211
-16
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019 ReactiveOps
|
||||
// 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.
|
||||
@@ -17,8 +17,8 @@ package validator
|
||||
import (
|
||||
"testing"
|
||||
|
||||
conf "github.com/reactiveops/polaris/pkg/config"
|
||||
"github.com/reactiveops/polaris/test"
|
||||
conf "github.com/fairwindsops/polaris/pkg/config"
|
||||
"github.com/fairwindsops/polaris/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -40,7 +40,7 @@ func TestValidatePod(t *testing.T) {
|
||||
|
||||
expectedSum := ResultSummary{
|
||||
Totals: CountSummary{
|
||||
Successes: uint(8),
|
||||
Successes: uint(4),
|
||||
Warnings: uint(0),
|
||||
Errors: uint(0),
|
||||
},
|
||||
@@ -51,11 +51,6 @@ func TestValidatePod(t *testing.T) {
|
||||
Warnings: uint(0),
|
||||
Errors: uint(0),
|
||||
}
|
||||
expectedSum.ByCategory["Resources"] = &CountSummary{
|
||||
Successes: uint(4),
|
||||
Warnings: uint(0),
|
||||
Errors: uint(0),
|
||||
}
|
||||
expectedSum.ByCategory["Security"] = &CountSummary{
|
||||
Successes: uint(2),
|
||||
Warnings: uint(0),
|
||||
@@ -63,14 +58,214 @@ func TestValidatePod(t *testing.T) {
|
||||
}
|
||||
|
||||
expectedMessages := []*ResultMessage{
|
||||
{Message: "Host IPC is not configured", Type: "success", Category: "Security"},
|
||||
{Message: "Host PID is not configured", Type: "success", Category: "Security"},
|
||||
{Message: "Host network is not configured", Type: "success", Category: "Networking"},
|
||||
{ID: "hostIPCSet", Message: "Host IPC is not configured", Type: "success", Category: "Security"},
|
||||
{ID: "hostPIDSet", Message: "Host PID is not configured", Type: "success", Category: "Security"},
|
||||
{ID: "hostNetworkSet", Message: "Host network is not configured", Type: "success", Category: "Networking"},
|
||||
}
|
||||
|
||||
actualPodResult := ValidatePod(c, &pod.Spec)
|
||||
actualPodResult := ValidatePod(c, &pod.Spec, "", conf.Deployments)
|
||||
|
||||
assert.Equal(t, len(actualPodResult.ContainerResults), 1, "should be equal")
|
||||
assert.EqualValues(t, actualPodResult.Summary, &expectedSum)
|
||||
assert.EqualValues(t, actualPodResult.Messages, expectedMessages)
|
||||
assert.Equal(t, 1, len(actualPodResult.ContainerResults), "should be equal")
|
||||
assert.EqualValues(t, &expectedSum, actualPodResult.Summary)
|
||||
assert.EqualValues(t, expectedMessages, actualPodResult.Messages)
|
||||
}
|
||||
|
||||
func TestInvalidIPCPod(t *testing.T) {
|
||||
c := conf.Configuration{
|
||||
Security: conf.Security{
|
||||
HostIPCSet: conf.SeverityError,
|
||||
HostPIDSet: conf.SeverityError,
|
||||
},
|
||||
Networking: conf.Networking{
|
||||
HostNetworkSet: conf.SeverityWarning,
|
||||
HostPortSet: conf.SeverityError,
|
||||
},
|
||||
}
|
||||
|
||||
k8s := test.SetupTestAPI()
|
||||
k8s = test.SetupAddControllers(k8s, "test")
|
||||
pod := test.MockPod()
|
||||
pod.Spec.HostIPC = true
|
||||
|
||||
expectedSum := ResultSummary{
|
||||
Totals: CountSummary{
|
||||
Successes: uint(3),
|
||||
Warnings: uint(0),
|
||||
Errors: uint(1),
|
||||
},
|
||||
ByCategory: make(map[string]*CountSummary),
|
||||
}
|
||||
expectedSum.ByCategory["Networking"] = &CountSummary{
|
||||
Successes: uint(2),
|
||||
Warnings: uint(0),
|
||||
Errors: uint(0),
|
||||
}
|
||||
expectedSum.ByCategory["Security"] = &CountSummary{
|
||||
Successes: uint(1),
|
||||
Warnings: uint(0),
|
||||
Errors: uint(1),
|
||||
}
|
||||
expectedMessages := []*ResultMessage{
|
||||
{ID: "hostIPCSet", Message: "Host IPC should not be configured", Type: "error", Category: "Security"},
|
||||
{ID: "hostPIDSet", Message: "Host PID is not configured", Type: "success", Category: "Security"},
|
||||
{ID: "hostNetworkSet", Message: "Host network is not configured", Type: "success", Category: "Networking"},
|
||||
}
|
||||
|
||||
actualPodResult := ValidatePod(c, &pod.Spec, "", conf.Deployments)
|
||||
|
||||
assert.Equal(t, 1, len(actualPodResult.ContainerResults), "should be equal")
|
||||
assert.EqualValues(t, &expectedSum, actualPodResult.Summary)
|
||||
assert.EqualValues(t, expectedMessages, actualPodResult.Messages)
|
||||
}
|
||||
|
||||
func TestInvalidNeworkPod(t *testing.T) {
|
||||
c := conf.Configuration{
|
||||
Networking: conf.Networking{
|
||||
HostNetworkSet: conf.SeverityWarning,
|
||||
HostPortSet: conf.SeverityError,
|
||||
},
|
||||
Security: conf.Security{
|
||||
HostIPCSet: conf.SeverityError,
|
||||
HostPIDSet: conf.SeverityError,
|
||||
},
|
||||
}
|
||||
|
||||
k8s := test.SetupTestAPI()
|
||||
k8s = test.SetupAddControllers(k8s, "test")
|
||||
pod := test.MockPod()
|
||||
pod.Spec.HostNetwork = true
|
||||
|
||||
expectedSum := ResultSummary{
|
||||
Totals: CountSummary{
|
||||
Successes: uint(3),
|
||||
Warnings: uint(1),
|
||||
Errors: uint(0),
|
||||
},
|
||||
ByCategory: make(map[string]*CountSummary),
|
||||
}
|
||||
expectedSum.ByCategory["Networking"] = &CountSummary{
|
||||
Successes: uint(1),
|
||||
Warnings: uint(1),
|
||||
Errors: uint(0),
|
||||
}
|
||||
|
||||
expectedSum.ByCategory["Security"] = &CountSummary{
|
||||
Successes: uint(2),
|
||||
Warnings: uint(0),
|
||||
Errors: uint(0),
|
||||
}
|
||||
|
||||
expectedMessages := []*ResultMessage{
|
||||
{ID: "hostNetworkSet", Message: "Host network should not be configured", Type: "warning", Category: "Networking"},
|
||||
{ID: "hostIPCSet", Message: "Host IPC is not configured", Type: "success", Category: "Security"},
|
||||
{ID: "hostPIDSet", Message: "Host PID is not configured", Type: "success", Category: "Security"},
|
||||
}
|
||||
|
||||
actualPodResult := ValidatePod(c, &pod.Spec, "", conf.Deployments)
|
||||
|
||||
assert.Equal(t, 1, len(actualPodResult.ContainerResults), "should be equal")
|
||||
assert.EqualValues(t, &expectedSum, actualPodResult.Summary)
|
||||
assert.EqualValues(t, expectedMessages, actualPodResult.Messages)
|
||||
}
|
||||
|
||||
func TestInvalidPIDPod(t *testing.T) {
|
||||
c := conf.Configuration{
|
||||
Security: conf.Security{
|
||||
HostIPCSet: conf.SeverityError,
|
||||
HostPIDSet: conf.SeverityError,
|
||||
},
|
||||
Networking: conf.Networking{
|
||||
HostNetworkSet: conf.SeverityWarning,
|
||||
HostPortSet: conf.SeverityError,
|
||||
},
|
||||
}
|
||||
|
||||
k8s := test.SetupTestAPI()
|
||||
k8s = test.SetupAddControllers(k8s, "test")
|
||||
pod := test.MockPod()
|
||||
pod.Spec.HostPID = true
|
||||
|
||||
expectedSum := ResultSummary{
|
||||
Totals: CountSummary{
|
||||
Successes: uint(3),
|
||||
Warnings: uint(0),
|
||||
Errors: uint(1),
|
||||
},
|
||||
ByCategory: make(map[string]*CountSummary),
|
||||
}
|
||||
expectedSum.ByCategory["Networking"] = &CountSummary{
|
||||
Successes: uint(2),
|
||||
Warnings: uint(0),
|
||||
Errors: uint(0),
|
||||
}
|
||||
expectedSum.ByCategory["Security"] = &CountSummary{
|
||||
Successes: uint(1),
|
||||
Warnings: uint(0),
|
||||
Errors: uint(1),
|
||||
}
|
||||
|
||||
expectedMessages := []*ResultMessage{
|
||||
{ID: "hostPIDSet", Message: "Host PID should not be configured", Type: "error", Category: "Security"},
|
||||
{ID: "hostIPCSet", Message: "Host IPC is not configured", Type: "success", Category: "Security"},
|
||||
{ID: "hostNetworkSet", Message: "Host network is not configured", Type: "success", Category: "Networking"},
|
||||
}
|
||||
|
||||
actualPodResult := ValidatePod(c, &pod.Spec, "", conf.Deployments)
|
||||
|
||||
assert.Equal(t, 1, len(actualPodResult.ContainerResults), "should be equal")
|
||||
assert.EqualValues(t, &expectedSum, actualPodResult.Summary)
|
||||
assert.EqualValues(t, expectedMessages, actualPodResult.Messages)
|
||||
}
|
||||
|
||||
func TestExemption(t *testing.T) {
|
||||
c := conf.Configuration{
|
||||
Security: conf.Security{
|
||||
HostIPCSet: conf.SeverityError,
|
||||
HostPIDSet: conf.SeverityError,
|
||||
},
|
||||
Networking: conf.Networking{
|
||||
HostNetworkSet: conf.SeverityWarning,
|
||||
HostPortSet: conf.SeverityError,
|
||||
},
|
||||
Exemptions: []conf.Exemption{
|
||||
conf.Exemption{
|
||||
Rules: []string{"hostIPCSet"},
|
||||
ControllerNames: []string{"foo"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
k8s := test.SetupTestAPI()
|
||||
k8s = test.SetupAddControllers(k8s, "test")
|
||||
pod := test.MockPod()
|
||||
pod.Spec.HostIPC = true
|
||||
|
||||
expectedSum := ResultSummary{
|
||||
Totals: CountSummary{
|
||||
Successes: uint(3),
|
||||
Warnings: uint(0),
|
||||
Errors: uint(0),
|
||||
},
|
||||
ByCategory: make(map[string]*CountSummary),
|
||||
}
|
||||
expectedSum.ByCategory["Networking"] = &CountSummary{
|
||||
Successes: uint(2),
|
||||
Warnings: uint(0),
|
||||
Errors: uint(0),
|
||||
}
|
||||
expectedSum.ByCategory["Security"] = &CountSummary{
|
||||
Successes: uint(1),
|
||||
Warnings: uint(0),
|
||||
Errors: uint(0),
|
||||
}
|
||||
expectedMessages := []*ResultMessage{
|
||||
{ID: "hostPIDSet", Message: "Host PID is not configured", Type: "success", Category: "Security"},
|
||||
{ID: "hostNetworkSet", Message: "Host network is not configured", Type: "success", Category: "Networking"},
|
||||
}
|
||||
|
||||
actualPodResult := ValidatePod(c, &pod.Spec, "foo", conf.Deployments)
|
||||
|
||||
assert.Equal(t, 1, len(actualPodResult.ContainerResults), "should be equal")
|
||||
assert.EqualValues(t, &expectedSum, actualPodResult.Summary)
|
||||
assert.EqualValues(t, expectedMessages, actualPodResult.Messages)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019 ReactiveOps
|
||||
// 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.
|
||||
@@ -15,7 +15,7 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
conf "github.com/reactiveops/polaris/pkg/config"
|
||||
conf "github.com/fairwindsops/polaris/pkg/config"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
@@ -71,34 +71,37 @@ func (rv *ResourceValidation) addMessage(message ResultMessage) {
|
||||
}
|
||||
}
|
||||
|
||||
func (rv *ResourceValidation) addFailure(message string, severity conf.Severity, category string) {
|
||||
func (rv *ResourceValidation) addFailure(message string, severity conf.Severity, category string, id string) {
|
||||
if severity == conf.SeverityError {
|
||||
rv.addError(message, category)
|
||||
rv.addError(message, category, id)
|
||||
} else if severity == conf.SeverityWarning {
|
||||
rv.addWarning(message, category)
|
||||
rv.addWarning(message, category, id)
|
||||
} else {
|
||||
logrus.Errorf("Invalid severity: %s", severity)
|
||||
}
|
||||
}
|
||||
|
||||
func (rv *ResourceValidation) addError(message string, category string) {
|
||||
func (rv *ResourceValidation) addError(message string, category string, id string) {
|
||||
rv.Errors = append(rv.Errors, &ResultMessage{
|
||||
ID: id,
|
||||
Message: message,
|
||||
Type: MessageTypeError,
|
||||
Category: category,
|
||||
})
|
||||
}
|
||||
|
||||
func (rv *ResourceValidation) addWarning(message string, category string) {
|
||||
func (rv *ResourceValidation) addWarning(message string, category string, id string) {
|
||||
rv.Warnings = append(rv.Warnings, &ResultMessage{
|
||||
ID: id,
|
||||
Message: message,
|
||||
Type: MessageTypeWarning,
|
||||
Category: category,
|
||||
})
|
||||
}
|
||||
|
||||
func (rv *ResourceValidation) addSuccess(message string, category string) {
|
||||
func (rv *ResourceValidation) addSuccess(message string, category string, id string) {
|
||||
rv.Successes = append(rv.Successes, &ResultMessage{
|
||||
ID: id,
|
||||
Message: message,
|
||||
Type: MessageTypeSuccess,
|
||||
Category: category,
|
||||
|
||||
+150
-9
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019 ReactiveOps
|
||||
// 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.
|
||||
@@ -14,7 +14,52 @@
|
||||
|
||||
package validator
|
||||
|
||||
import corev1 "k8s.io/api/core/v1"
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/fairwindsops/polaris/pkg/config"
|
||||
conf "github.com/fairwindsops/polaris/pkg/config"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
apiMachineryYAML "k8s.io/apimachinery/pkg/util/yaml"
|
||||
)
|
||||
|
||||
const (
|
||||
// PolarisOutputVersion is the version of the current output structure
|
||||
PolarisOutputVersion = "0.3"
|
||||
)
|
||||
|
||||
// AuditData contains all the data from a full Polaris audit
|
||||
type AuditData struct {
|
||||
PolarisOutputVersion string
|
||||
AuditTime string
|
||||
SourceType string
|
||||
SourceName string
|
||||
DisplayName string
|
||||
ClusterSummary ClusterSummary
|
||||
NamespacedResults NamespacedResults
|
||||
}
|
||||
|
||||
// ClusterSummary contains Polaris results as well as some high-level stats
|
||||
type ClusterSummary struct {
|
||||
Results ResultSummary
|
||||
Version string
|
||||
Nodes int
|
||||
Pods int
|
||||
Namespaces int
|
||||
Deployments int
|
||||
StatefulSets int
|
||||
DaemonSets int
|
||||
Jobs int
|
||||
CronJobs int
|
||||
ReplicationControllers int
|
||||
Score uint
|
||||
}
|
||||
|
||||
// MessageType represents the type of Message
|
||||
type MessageType string
|
||||
@@ -32,23 +77,87 @@ const (
|
||||
|
||||
// NamespaceResult groups container results by parent resource.
|
||||
type NamespaceResult struct {
|
||||
Name string
|
||||
Summary *ResultSummary
|
||||
DeploymentResults []ControllerResult
|
||||
StatefulSetResults []ControllerResult
|
||||
Name string
|
||||
Summary *ResultSummary
|
||||
|
||||
// TODO: This struct could use some love to reorganize it as just having "results"
|
||||
// and then having methods to return filtered results by type
|
||||
// (deploy, daemonset, etc)
|
||||
// The way this is structured right now makes it difficult to add
|
||||
// additional result types and potentially miss things in the metrics
|
||||
// summary.
|
||||
DeploymentResults []ControllerResult
|
||||
StatefulSetResults []ControllerResult
|
||||
DaemonSetResults []ControllerResult
|
||||
JobResults []ControllerResult
|
||||
CronJobResults []ControllerResult
|
||||
ReplicationControllerResults []ControllerResult
|
||||
}
|
||||
|
||||
// AddResult adds a result to the result sets by leveraging the types supported by NamespaceResult
|
||||
func (n *NamespaceResult) AddResult(resourceType config.SupportedController, result ControllerResult) error {
|
||||
// Iterate all the resource types supported in this struct
|
||||
var results *[]ControllerResult
|
||||
switch resourceType {
|
||||
case conf.Deployments:
|
||||
results = &n.DeploymentResults
|
||||
case conf.StatefulSets:
|
||||
results = &n.StatefulSetResults
|
||||
case conf.DaemonSets:
|
||||
results = &n.DaemonSetResults
|
||||
case conf.Jobs:
|
||||
results = &n.JobResults
|
||||
case conf.CronJobs:
|
||||
results = &n.CronJobResults
|
||||
case conf.ReplicationControllers:
|
||||
results = &n.ReplicationControllerResults
|
||||
default:
|
||||
return fmt.Errorf("Unknown Resource Type: (%s) Missing Implementation in NamespacedResult", resourceType)
|
||||
}
|
||||
|
||||
// Append the new result to the results pointer loaded from the supported values
|
||||
*results = append(*results, result)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAllControllerResults grabs all the different types of controller results from the namespaced result as a single list for easier iteration
|
||||
func (n NamespaceResult) GetAllControllerResults() []ControllerResult {
|
||||
all := []ControllerResult{}
|
||||
all = append(all, n.DeploymentResults...)
|
||||
all = append(all, n.StatefulSetResults...)
|
||||
all = append(all, n.DaemonSetResults...)
|
||||
all = append(all, n.JobResults...)
|
||||
all = append(all, n.CronJobResults...)
|
||||
all = append(all, n.ReplicationControllerResults...)
|
||||
|
||||
return all
|
||||
}
|
||||
|
||||
// NamespacedResults is a mapping of namespace name to the validation results.
|
||||
type NamespacedResults map[string]*NamespaceResult
|
||||
|
||||
// GetAllControllerResults aggregates all the namespaced results in the set together
|
||||
func (nsResults NamespacedResults) GetAllControllerResults() []ControllerResult {
|
||||
all := []ControllerResult{}
|
||||
for _, nsResult := range nsResults {
|
||||
all = append(all, nsResult.GetAllControllerResults()...)
|
||||
}
|
||||
return all
|
||||
}
|
||||
|
||||
func (nsResults NamespacedResults) getNamespaceResult(nsName string) *NamespaceResult {
|
||||
nsResult := &NamespaceResult{}
|
||||
switch nsResults[nsName] {
|
||||
case nil:
|
||||
nsResult = &NamespaceResult{
|
||||
Summary: &ResultSummary{},
|
||||
DeploymentResults: []ControllerResult{},
|
||||
StatefulSetResults: []ControllerResult{},
|
||||
Summary: &ResultSummary{},
|
||||
DeploymentResults: []ControllerResult{},
|
||||
StatefulSetResults: []ControllerResult{},
|
||||
DaemonSetResults: []ControllerResult{},
|
||||
JobResults: []ControllerResult{},
|
||||
CronJobResults: []ControllerResult{},
|
||||
ReplicationControllerResults: []ControllerResult{},
|
||||
}
|
||||
nsResults[nsName] = nsResult
|
||||
default:
|
||||
@@ -123,7 +232,39 @@ type PodResult struct {
|
||||
|
||||
// ResultMessage contains a message and a type indicator (success, warning, or error).
|
||||
type ResultMessage struct {
|
||||
ID string
|
||||
Message string
|
||||
Type MessageType
|
||||
Category string
|
||||
}
|
||||
|
||||
// ReadAuditFromFile reads the data from a past audit stored in a JSON or YAML file.
|
||||
func ReadAuditFromFile(fileName string) AuditData {
|
||||
auditData := AuditData{}
|
||||
oldFileBytes, err := ioutil.ReadFile(fileName)
|
||||
if err != nil {
|
||||
logrus.Errorf("Unable to read contents of loaded file: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
auditData, err = ParseAudit(oldFileBytes)
|
||||
if err != nil {
|
||||
logrus.Errorf("Error parsing file contents into auditData: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
return auditData
|
||||
}
|
||||
|
||||
// ParseAudit decodes either a YAML or JSON file and returns AuditData.
|
||||
func ParseAudit(oldFileBytes []byte) (AuditData, error) {
|
||||
reader := bytes.NewReader(oldFileBytes)
|
||||
conf := AuditData{}
|
||||
d := apiMachineryYAML.NewYAMLOrJSONDecoder(reader, 4096)
|
||||
for {
|
||||
if err := d.Decode(&conf); err != nil {
|
||||
if err == io.EOF {
|
||||
return conf, nil
|
||||
}
|
||||
return conf, fmt.Errorf("Decoding config failed: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+47
-12
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019 ReactiveOps
|
||||
// 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.
|
||||
@@ -20,11 +20,14 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
conf "github.com/reactiveops/polaris/pkg/config"
|
||||
validator "github.com/reactiveops/polaris/pkg/validator"
|
||||
"github.com/fairwindsops/polaris/pkg/config"
|
||||
validator "github.com/fairwindsops/polaris/pkg/validator"
|
||||
"github.com/fairwindsops/polaris/pkg/validator/controllers"
|
||||
"github.com/sirupsen/logrus"
|
||||
admissionregistrationv1beta1 "k8s.io/api/admissionregistration/v1beta1"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
batchv1 "k8s.io/api/batch/v1"
|
||||
batchv1beta1 "k8s.io/api/batch/v1beta1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
@@ -39,7 +42,7 @@ import (
|
||||
type Validator struct {
|
||||
client client.Client
|
||||
decoder types.Decoder
|
||||
Config conf.Configuration
|
||||
Config config.Configuration
|
||||
}
|
||||
|
||||
var _ inject.Client = &Validator{}
|
||||
@@ -77,8 +80,9 @@ func NewWebhook(name string, mgr manager.Manager, validator Validator, apiType r
|
||||
if err != nil {
|
||||
logrus.Errorf("Error building webhook: %v", err)
|
||||
os.Exit(1)
|
||||
} else {
|
||||
logrus.Info(name + " webhook started")
|
||||
}
|
||||
|
||||
return webhook
|
||||
}
|
||||
|
||||
@@ -90,18 +94,49 @@ func (v *Validator) Handle(ctx context.Context, req types.Request) types.Respons
|
||||
if req.AdmissionRequest.Kind.Kind == "Pod" {
|
||||
pod := corev1.Pod{}
|
||||
err = v.decoder.Decode(req, &pod)
|
||||
podResult = validator.ValidatePod(v.Config, &pod.Spec)
|
||||
podResult = validator.ValidatePod(v.Config, &pod.Spec, "", config.Unsupported)
|
||||
} else {
|
||||
var controller validator.Controller
|
||||
switch req.AdmissionRequest.Kind.Kind {
|
||||
case "Deployment":
|
||||
var controller controllers.Interface
|
||||
if yes := v.Config.CheckIfKindIsConfiguredForValidation(req.AdmissionRequest.Kind.Kind); !yes {
|
||||
logrus.Warnf("Skipping, kind (%s) isn't something we are configured to scan", req.AdmissionRequest.Kind.Kind)
|
||||
return admission.ValidationResponse(true, fmt.Sprintf("Skipping: (%s) isn't something we're configured to scan.", req.AdmissionRequest.Kind.Kind))
|
||||
}
|
||||
|
||||
// We should never hit this case unless something is misconfiured in CheckIfKindIsConfiguredForValidation
|
||||
controllerType, err := config.GetSupportedControllerFromString(req.AdmissionRequest.Kind.Kind)
|
||||
if err != nil {
|
||||
msg := fmt.Errorf("Unexpected error occurred. Expected Kind to be a supported type (%s)", req.AdmissionRequest.Kind.Kind)
|
||||
logrus.Error(msg)
|
||||
return admission.ErrorResponse(http.StatusInternalServerError, err)
|
||||
}
|
||||
|
||||
// For each type, perform the scan
|
||||
// TODO: This isn't really that elegant due to the decoder and NewXXXController setup :( could use love
|
||||
switch controllerType {
|
||||
case config.Deployments:
|
||||
deploy := appsv1.Deployment{}
|
||||
err = v.decoder.Decode(req, &deploy)
|
||||
controller = validator.ControllerFromDeployment(deploy)
|
||||
case "StatefulSet":
|
||||
controller = controllers.NewDeploymentController(deploy)
|
||||
case config.StatefulSets:
|
||||
statefulSet := appsv1.StatefulSet{}
|
||||
err = v.decoder.Decode(req, &statefulSet)
|
||||
controller = validator.ControllerFromStatefulSet(statefulSet)
|
||||
controller = controllers.NewStatefulSetController(statefulSet)
|
||||
case config.DaemonSets:
|
||||
daemonSet := appsv1.DaemonSet{}
|
||||
err = v.decoder.Decode(req, &daemonSet)
|
||||
controller = controllers.NewDaemonSetController(daemonSet)
|
||||
case config.Jobs:
|
||||
job := batchv1.Job{}
|
||||
err = v.decoder.Decode(req, &job)
|
||||
controller = controllers.NewJobController(job)
|
||||
case config.CronJobs:
|
||||
cronJob := batchv1beta1.CronJob{}
|
||||
err = v.decoder.Decode(req, &cronJob)
|
||||
controller = controllers.NewCronJobController(cronJob)
|
||||
case config.ReplicationControllers:
|
||||
replicationController := corev1.ReplicationController{}
|
||||
err = v.decoder.Decode(req, &replicationController)
|
||||
controller = controllers.NewReplicationControllerController(replicationController)
|
||||
}
|
||||
controllerResult := validator.ValidateController(v.Config, controller)
|
||||
podResult = controllerResult.PodResult
|
||||
|
||||
+76
-10
@@ -4,12 +4,15 @@ import (
|
||||
"fmt"
|
||||
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
batchv1 "k8s.io/api/batch/v1"
|
||||
batchv1beta1 "k8s.io/api/batch/v1beta1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
)
|
||||
|
||||
func mockContainer(name string) corev1.Container {
|
||||
// MockContainer creates a container object
|
||||
func MockContainer(name string) corev1.Container {
|
||||
c := corev1.Container{
|
||||
Name: name,
|
||||
}
|
||||
@@ -18,7 +21,7 @@ func mockContainer(name string) corev1.Container {
|
||||
|
||||
// MockPod creates a pod object.
|
||||
func MockPod() corev1.PodTemplateSpec {
|
||||
c1 := mockContainer("test")
|
||||
c1 := MockContainer("test")
|
||||
p := corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
@@ -29,7 +32,8 @@ func MockPod() corev1.PodTemplateSpec {
|
||||
return p
|
||||
}
|
||||
|
||||
func mockDeploy() appsv1.Deployment {
|
||||
// MockDeploy creates a Deployment object.
|
||||
func MockDeploy() appsv1.Deployment {
|
||||
p := MockPod()
|
||||
d := appsv1.Deployment{
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
@@ -39,7 +43,8 @@ func mockDeploy() appsv1.Deployment {
|
||||
return d
|
||||
}
|
||||
|
||||
func mockStatefulSet() appsv1.StatefulSet {
|
||||
// MockStatefulSet creates a StatefulSet object.
|
||||
func MockStatefulSet() appsv1.StatefulSet {
|
||||
p := MockPod()
|
||||
s := appsv1.StatefulSet{
|
||||
Spec: appsv1.StatefulSetSpec{
|
||||
@@ -49,6 +54,47 @@ func mockStatefulSet() appsv1.StatefulSet {
|
||||
return s
|
||||
}
|
||||
|
||||
// MockDaemonSet creates a DaemonSet object.
|
||||
func MockDaemonSet() appsv1.DaemonSet {
|
||||
return appsv1.DaemonSet{
|
||||
Spec: appsv1.DaemonSetSpec{
|
||||
Template: MockPod(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// MockJob creates a Job object.
|
||||
func MockJob() batchv1.Job {
|
||||
return batchv1.Job{
|
||||
Spec: batchv1.JobSpec{
|
||||
Template: MockPod(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// MockCronJob creates a CronJob object.
|
||||
func MockCronJob() batchv1beta1.CronJob {
|
||||
return batchv1beta1.CronJob{
|
||||
Spec: batchv1beta1.CronJobSpec{
|
||||
JobTemplate: batchv1beta1.JobTemplateSpec{
|
||||
Spec: batchv1.JobSpec{
|
||||
Template: MockPod(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// MockReplicationController creates a ReplicationController object.
|
||||
func MockReplicationController() corev1.ReplicationController {
|
||||
p := MockPod()
|
||||
return corev1.ReplicationController{
|
||||
Spec: corev1.ReplicationControllerSpec{
|
||||
Template: &p,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// SetupTestAPI creates a test kube API struct.
|
||||
func SetupTestAPI() kubernetes.Interface {
|
||||
return fake.NewSimpleClientset()
|
||||
@@ -56,15 +102,35 @@ func SetupTestAPI() kubernetes.Interface {
|
||||
|
||||
// SetupAddControllers creates mock controllers and adds them to the test clientset.
|
||||
func SetupAddControllers(k kubernetes.Interface, namespace string) kubernetes.Interface {
|
||||
d1 := mockDeploy()
|
||||
_, err := k.AppsV1().Deployments(namespace).Create(&d1)
|
||||
if err != nil {
|
||||
d1 := MockDeploy()
|
||||
if _, err := k.AppsV1().Deployments(namespace).Create(&d1); err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
s1 := mockStatefulSet()
|
||||
_, err = k.AppsV1().StatefulSets(namespace).Create(&s1)
|
||||
if err != nil {
|
||||
|
||||
s1 := MockStatefulSet()
|
||||
if _, err := k.AppsV1().StatefulSets(namespace).Create(&s1); err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
ds1 := MockDaemonSet()
|
||||
if _, err := k.AppsV1().DaemonSets(namespace).Create(&ds1); err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
j1 := MockJob()
|
||||
if _, err := k.BatchV1().Jobs(namespace).Create(&j1); err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
cj1 := MockCronJob()
|
||||
if _, err := k.BatchV1beta1().CronJobs(namespace).Create(&cj1); err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
rc1 := MockReplicationController()
|
||||
if _, err := k.CoreV1().ReplicationControllers(namespace).Create(&rc1); err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
return k
|
||||
}
|
||||
|
||||
Executable
+37
@@ -0,0 +1,37 @@
|
||||
sed -ri "s|'(quay.io/fairwinds/polaris:).+'|'\1${CIRCLE_SHA1}'|" ./deploy/dashboard.yaml
|
||||
|
||||
|
||||
function check_dashboard_is_ready() {
|
||||
local timeout_epoch
|
||||
timeout_epoch=$(date -d "+2 minutes" +%s)
|
||||
echo "Waiting for dashboard to be ready"
|
||||
while ! kubectl get pods -n polaris | grep -E "dashboard.*1/1.*Running"; do
|
||||
check_timeout "${timeout_epoch}"
|
||||
echo -n "."
|
||||
done
|
||||
|
||||
echo "Dashboard Running!"
|
||||
}
|
||||
|
||||
function check_timeout() {
|
||||
local timeout_epoch="${1}"
|
||||
if [[ "$(date +%s)" -ge "${timeout_epoch}" ]]; then
|
||||
echo -e "Timeout hit waiting for readiness: exiting"
|
||||
grab_logs
|
||||
clean_up
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
kubectl apply -f ./deploy/dashboard.yaml &>/dev/null
|
||||
|
||||
check_dashboard_is_ready
|
||||
|
||||
kubectl port-forward --namespace polaris svc/polaris-dashboard 3000:80 &
|
||||
sleep 30
|
||||
curl -f http://localhost:3000 > /dev/null
|
||||
curl -f http://localhost:3000/health > /dev/null
|
||||
curl -f http://localhost:3000/favicon.ico > /dev/null
|
||||
curl -f http://localhost:3000/static/css/main.css > /dev/null
|
||||
curl -f http://localhost:3000/results.json > /dev/null
|
||||
curl -f http://localhost:3000/details/security > /dev/null
|
||||
@@ -0,0 +1,24 @@
|
||||
apiVersion: batch/v1beta1
|
||||
kind: CronJob
|
||||
metadata:
|
||||
name: test
|
||||
spec:
|
||||
schedule: "*/1 * * * *"
|
||||
jobTemplate:
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: test
|
||||
image: busybox:uclibc
|
||||
args:
|
||||
- whoami
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: true
|
||||
privileged: false
|
||||
readOnlyRootFilesystem: true
|
||||
runAsNonRoot: true
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
restartPolicy: OnFailure
|
||||
@@ -0,0 +1,48 @@
|
||||
apiVersion: apps/v1
|
||||
kind: DaemonSet
|
||||
metadata:
|
||||
name: fluentd-elasticsearch
|
||||
namespace: kube-system
|
||||
labels:
|
||||
k8s-app: fluentd-logging
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
name: fluentd-elasticsearch
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
name: fluentd-elasticsearch
|
||||
spec:
|
||||
tolerations:
|
||||
- key: node-role.kubernetes.io/master
|
||||
effect:
|
||||
containers:
|
||||
- name: fluentd-elasticsearch
|
||||
image: gcr.io/fluentd-elasticsearch/fluentd:v2.5.1
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
volumeMounts:
|
||||
- name: varlog
|
||||
mountPath: /var/log
|
||||
- name: varlibdockercontainers
|
||||
mountPath: /var/lib/docker/containers
|
||||
readOnly: true
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: true
|
||||
privileged: false
|
||||
readOnlyRootFilesystem: true
|
||||
runAsNonRoot: true
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
terminationGracePeriodSeconds: 30
|
||||
volumes:
|
||||
- name: varlog
|
||||
hostPath:
|
||||
path: /var/log
|
||||
- name: varlibdockercontainers
|
||||
hostPath:
|
||||
path: /var/lib/docker/containers
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: nginx-deployment
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
replicas: 3
|
||||
selector:
|
||||
matchLabels:
|
||||
app: nginx
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx:1.7.9
|
||||
ports:
|
||||
- containerPort: 80
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: true
|
||||
privileged: false
|
||||
readOnlyRootFilesystem: true
|
||||
runAsNonRoot: true
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
@@ -0,0 +1,21 @@
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: pi-with-ttl-2
|
||||
spec:
|
||||
ttlSecondsAfterFinished: 100
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: pi
|
||||
image: perl:5.3
|
||||
command: ["perl", "-Mbignum=bpi", "-wle", "print bpi(2000)"]
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: true
|
||||
privileged: false
|
||||
readOnlyRootFilesystem: true
|
||||
runAsNonRoot: true
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
restartPolicy: Never
|
||||
@@ -0,0 +1,27 @@
|
||||
apiVersion: v1
|
||||
kind: ReplicationController
|
||||
metadata:
|
||||
name: nginx
|
||||
spec:
|
||||
replicas: 3
|
||||
selector:
|
||||
app: nginx
|
||||
template:
|
||||
metadata:
|
||||
name: nginx
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx:1.7.9
|
||||
ports:
|
||||
- containerPort: 80
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: true
|
||||
privileged: false
|
||||
readOnlyRootFilesystem: true
|
||||
runAsNonRoot: true
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
@@ -0,0 +1,56 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: nginx
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
ports:
|
||||
- port: 80
|
||||
name: web
|
||||
clusterIP: None
|
||||
selector:
|
||||
app: nginx
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: web
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: nginx # has to match .spec.template.metadata.labels
|
||||
serviceName: "nginx"
|
||||
replicas: 3 # by default is 1
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: nginx # has to match .spec.selector.matchLabels
|
||||
spec:
|
||||
terminationGracePeriodSeconds: 10
|
||||
containers:
|
||||
- name: nginx
|
||||
image: k8s.gcr.io/nginx-slim:0.8
|
||||
ports:
|
||||
- containerPort: 80
|
||||
name: web
|
||||
volumeMounts:
|
||||
- name: www
|
||||
mountPath: /usr/share/nginx/html
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: true
|
||||
privileged: false
|
||||
readOnlyRootFilesystem: true
|
||||
runAsNonRoot: true
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
volumeClaimTemplates:
|
||||
- metadata:
|
||||
name: www
|
||||
spec:
|
||||
accessModes: [ "ReadWriteOnce" ]
|
||||
storageClassName: "my-storage-class"
|
||||
resources:
|
||||
requests:
|
||||
storage: 1Gi
|
||||
@@ -0,0 +1,24 @@
|
||||
apiVersion: batch/v1beta1
|
||||
kind: CronJob
|
||||
metadata:
|
||||
name: test
|
||||
spec:
|
||||
schedule: "*/1 * * * *"
|
||||
jobTemplate:
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: test
|
||||
image: busybox:uclibc
|
||||
args:
|
||||
- whoami
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
privileged: false
|
||||
readOnlyRootFilesystem: true
|
||||
runAsNonRoot: true
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
restartPolicy: OnFailure
|
||||
@@ -0,0 +1,48 @@
|
||||
apiVersion: apps/v1
|
||||
kind: DaemonSet
|
||||
metadata:
|
||||
name: fluentd-elasticsearch
|
||||
namespace: kube-system
|
||||
labels:
|
||||
k8s-app: fluentd-logging
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
name: fluentd-elasticsearch
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
name: fluentd-elasticsearch
|
||||
spec:
|
||||
tolerations:
|
||||
- key: node-role.kubernetes.io/master
|
||||
effect:
|
||||
containers:
|
||||
- name: fluentd-elasticsearch
|
||||
image: gcr.io/fluentd-elasticsearch/fluentd:v2.5.1
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
volumeMounts:
|
||||
- name: varlog
|
||||
mountPath: /var/log
|
||||
- name: varlibdockercontainers
|
||||
mountPath: /var/lib/docker/containers
|
||||
readOnly: true
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
privileged: false
|
||||
readOnlyRootFilesystem: true
|
||||
runAsNonRoot: true
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
terminationGracePeriodSeconds: 30
|
||||
volumes:
|
||||
- name: varlog
|
||||
hostPath:
|
||||
path: /var/log
|
||||
- name: varlibdockercontainers
|
||||
hostPath:
|
||||
path: /var/lib/docker/containers
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: nginx-deployment
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
replicas: 3
|
||||
selector:
|
||||
matchLabels:
|
||||
app: nginx
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx:1.7.9
|
||||
ports:
|
||||
- containerPort: 80
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
privileged: false
|
||||
readOnlyRootFilesystem: true
|
||||
runAsNonRoot: true
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
@@ -0,0 +1,21 @@
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: pi-with-ttl
|
||||
spec:
|
||||
ttlSecondsAfterFinished: 100
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: pi
|
||||
image: perl:5.3
|
||||
command: ["perl", "-Mbignum=bpi", "-wle", "print bpi(2000)"]
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
privileged: false
|
||||
readOnlyRootFilesystem: true
|
||||
runAsNonRoot: true
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
restartPolicy: Never
|
||||
@@ -0,0 +1,27 @@
|
||||
apiVersion: v1
|
||||
kind: ReplicationController
|
||||
metadata:
|
||||
name: nginx
|
||||
spec:
|
||||
replicas: 3
|
||||
selector:
|
||||
app: nginx
|
||||
template:
|
||||
metadata:
|
||||
name: nginx
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx:1.7.9
|
||||
ports:
|
||||
- containerPort: 80
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
privileged: false
|
||||
readOnlyRootFilesystem: true
|
||||
runAsNonRoot: true
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
@@ -0,0 +1,56 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: nginx
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
ports:
|
||||
- port: 80
|
||||
name: web
|
||||
clusterIP: None
|
||||
selector:
|
||||
app: nginx
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: web
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: nginx # has to match .spec.template.metadata.labels
|
||||
serviceName: "nginx"
|
||||
replicas: 3 # by default is 1
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: nginx # has to match .spec.selector.matchLabels
|
||||
spec:
|
||||
terminationGracePeriodSeconds: 10
|
||||
containers:
|
||||
- name: nginx
|
||||
image: k8s.gcr.io/nginx-slim:0.8
|
||||
ports:
|
||||
- containerPort: 80
|
||||
name: web
|
||||
volumeMounts:
|
||||
- name: www
|
||||
mountPath: /usr/share/nginx/html
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
privileged: false
|
||||
readOnlyRootFilesystem: true
|
||||
runAsNonRoot: true
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
volumeClaimTemplates:
|
||||
- metadata:
|
||||
name: www
|
||||
spec:
|
||||
accessModes: [ "ReadWriteOnce" ]
|
||||
storageClassName: "my-storage-class"
|
||||
resources:
|
||||
requests:
|
||||
storage: 1Gi
|
||||
Executable
+93
@@ -0,0 +1,93 @@
|
||||
#!/bin/bash
|
||||
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
|
||||
|
||||
# Testing to ensure that the webhook starts up, allows a correct deployment to pass,
|
||||
# and prevents a incorrectly formatted deployment.
|
||||
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)
|
||||
|
||||
# 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"
|
||||
while ! kubectl get pods -n polaris | grep -E "webhook.*1/1.*Running"; do
|
||||
check_timeout "${timeout_epoch}"
|
||||
echo -n "."
|
||||
done
|
||||
|
||||
echo "Webhook started!"
|
||||
}
|
||||
|
||||
# Check if timeout is hit and exit if it is
|
||||
function check_timeout() {
|
||||
local timeout_epoch="${1}"
|
||||
if [[ "$(date +%s)" -ge "${timeout_epoch}" ]]; then
|
||||
echo -e "Timeout hit waiting for readiness: exiting"
|
||||
grab_logs
|
||||
clean_up
|
||||
exit 1
|
||||
fi
|
||||
|
||||
}
|
||||
|
||||
# Clean up all your stuff
|
||||
function clean_up() {
|
||||
# Clean up files you've installed (helps with local testing)
|
||||
for filename in test/webhook_cases/*.yaml; do
|
||||
# || true to avoid issues when we cannot delete
|
||||
kubectl delete -f $filename &>/dev/null ||true
|
||||
done
|
||||
# Uninstall webhook and webhook config
|
||||
kubectl delete validatingwebhookconfigurations polaris-webhook --wait=false &>/dev/null
|
||||
kubectl -n polaris delete deploy -l app=polaris --wait=false &>/dev/null
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
# Install the webhook
|
||||
kubectl apply -f ./deploy/webhook.yaml &> /dev/null
|
||||
|
||||
|
||||
# wait for the webhook to come online
|
||||
check_webhook_is_ready
|
||||
sleep 30
|
||||
|
||||
# Webhook started, setting all tests as passed initially.
|
||||
ALL_TESTS_PASSED=1
|
||||
|
||||
# Run tests against correctly configured objects
|
||||
for filename in test/webhook_cases/passing_test.*.yaml; do
|
||||
echo $filename
|
||||
if ! kubectl apply -f $filename &> /dev/null; then
|
||||
ALL_TESTS_PASSED=0
|
||||
echo "Test Failed: Polaris prevented a deployment with no configuration issues."
|
||||
fi
|
||||
done
|
||||
|
||||
# Run tests against incorrectly configured objects
|
||||
for filename in test/webhook_cases/failing_test.*.yaml; do
|
||||
echo $filename
|
||||
if kubectl apply -f $filename &> /dev/null; then
|
||||
ALL_TESTS_PASSED=0
|
||||
echo "Test Failed: Polaris should have prevented this deployment due to configuration issues."
|
||||
fi
|
||||
done
|
||||
|
||||
clean_up
|
||||
|
||||
#Verify that all the tests passed.
|
||||
if [ $ALL_TESTS_PASSED -eq 1 ]; then
|
||||
echo "Tests Passed."
|
||||
else
|
||||
echo "Tests Failed."
|
||||
exit 1
|
||||
fi
|
||||
Reference in New Issue
Block a user