mirror of
https://github.com/FairwindsOps/polaris.git
synced 2026-08-29 22:17:16 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8d0783f29 | ||
|
|
4c7429efbc | ||
|
|
73727bd9d8 | ||
|
|
41e64f7405 | ||
|
|
50d32b9b4c | ||
|
|
ebfb4ea9a1 | ||
|
|
e030b61705 | ||
|
|
b32a7afa77 | ||
|
|
30ff6e6667 | ||
|
|
6fa27eed76 | ||
|
|
538b1fba1c | ||
|
|
d569605d0d | ||
|
|
ca4b45451f | ||
|
|
1d6248180e | ||
|
|
7cfa13f285 | ||
|
|
0bd8f8f507 | ||
|
|
62b914d743 | ||
|
|
65c0f07284 | ||
|
|
e3c0d53df5 | ||
|
|
8ad223f6f7 | ||
|
|
ac44416d0f | ||
|
|
3c72515946 |
@@ -0,0 +1,10 @@
|
||||
DOCKERFILE='Dockerfile'
|
||||
|
||||
EXTERNAL_REGISTRY_BASE_DOMAIN=quay.io
|
||||
REPOSITORY_NAME=reactiveops/polaris
|
||||
DOCKERTAG=${EXTERNAL_REGISTRY_BASE_DOMAIN}/${REPOSITORY_NAME}
|
||||
if [[ -n $CI_TAG ]]; then
|
||||
ADDITIONAL_DOCKER_TAG_VERSIONS=()
|
||||
ADDITIONAL_DOCKER_TAG_VERSIONS+=(`echo $CI_TAG | sed -e 's/\(\w\+\)\..*$/\1/'`)
|
||||
ADDITIONAL_DOCKER_TAG_VERSIONS+=(`echo $CI_TAG | sed -e 's/\(\w\+\.\w\+\)\..*$/\1/'`)
|
||||
fi
|
||||
+82
-66
@@ -1,6 +1,53 @@
|
||||
version: 2
|
||||
|
||||
references:
|
||||
set_environment_variables: &set_environment_variables
|
||||
run:
|
||||
name: Set Environment Variables
|
||||
command: |
|
||||
echo 'export CI_SHA1=$CIRCLE_SHA1' >> ${BASH_ENV}
|
||||
echo 'export CI_BRANCH=$CIRCLE_BRANCH' >> ${BASH_ENV}
|
||||
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
|
||||
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="reactiveops+circleci" -p="${quay_token}"
|
||||
docker-push -f .circleci/build.config
|
||||
else
|
||||
echo "Skipping docker push for forked PR"
|
||||
fi
|
||||
|
||||
# Test scripts
|
||||
update_coverage: &update_coverage
|
||||
run:
|
||||
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
|
||||
run:
|
||||
name: Test Dashboard
|
||||
command: |
|
||||
go run main.go --dashboard --dashboard-port 3000 --audit-path ./examples &
|
||||
sleep 5
|
||||
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
|
||||
|
||||
# Release scripts
|
||||
install_goreleaser: &install_goreleaser
|
||||
run:
|
||||
name: Install GoReleaser
|
||||
@@ -9,46 +56,20 @@ references:
|
||||
echo "8dbad6683d6fc9367e637e6eed8e01a0d63c9660 goreleaser.deb" | sha1sum -c
|
||||
sudo dpkg -i goreleaser.deb
|
||||
rm goreleaser.deb
|
||||
set_environment_variables: &set_environment_variables
|
||||
release_deploy_configs: &release_deploy_configs
|
||||
run:
|
||||
name: Set Environment Variables
|
||||
name: Release deploy configs
|
||||
command: |
|
||||
echo 'export REPO=quay.io/reactiveops/polaris' >> ${BASH_ENV}
|
||||
docker_build_and_push: &docker_build_and_push
|
||||
run:
|
||||
name: Docker login, build, and push
|
||||
command: |
|
||||
docker login quay.io -u="reactiveops+circleci" -p="${quay_token}"
|
||||
docker build -t $REPO:dev-$CIRCLE_SHA1 .
|
||||
docker push $REPO:dev-$CIRCLE_SHA1
|
||||
docker_tag_release: &docker_tag_release
|
||||
run:
|
||||
name: Docker tag release
|
||||
command: |
|
||||
docker login quay.io -u="reactiveops+circleci" -p="${quay_token}"
|
||||
docker pull $REPO:dev-$CIRCLE_SHA1
|
||||
docker tag $REPO:dev-$CIRCLE_SHA1 $REPO:$CIRCLE_TAG
|
||||
docker push $REPO:$CIRCLE_TAG
|
||||
|
||||
update_coverage: &update_coverage
|
||||
run:
|
||||
name: Update Coverage
|
||||
command: |
|
||||
go test ./pkg/... -coverprofile=coverage.txt -covermode=count
|
||||
bash <(curl -s https://codecov.io/bash)
|
||||
|
||||
test_dashboard: &test_dashboard
|
||||
run:
|
||||
name: Test Dashboard
|
||||
command: |
|
||||
go run main.go --dashboard --dashboard-port 3000 --audit-path ./deploy/dashboard.yaml &
|
||||
sleep 5
|
||||
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
|
||||
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=$(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:
|
||||
@@ -73,30 +94,7 @@ jobs:
|
||||
- *update_coverage
|
||||
- *test_dashboard
|
||||
|
||||
test-deploy:
|
||||
docker:
|
||||
- image: quay.io/reactiveops/ci-images:v8.0-stretch
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: Verify helm chart synced with dashboard.yaml
|
||||
command: |
|
||||
diff \
|
||||
<(helm template deploy/helm/polaris/ --name polaris --namespace polaris --set templateOnly=true) \
|
||||
deploy/dashboard.yaml || (echo "
|
||||
Make sure to regenerate k8s deploy config with make helm-to-k8s" &&
|
||||
exit 1)
|
||||
|
||||
- run:
|
||||
name: Verify helm chart synced with webhook.yaml
|
||||
command: |
|
||||
diff \
|
||||
<(helm template deploy/helm/polaris/ --name polaris --namespace polaris --set templateOnly=true --set webhook.enable=true --set dashboard.enable=false) \
|
||||
deploy/webhook.yaml || (echo "
|
||||
Make sure to regenerate k8s deploy config with make helm-to-k8s" &&
|
||||
exit 1)
|
||||
|
||||
release:
|
||||
release_binary:
|
||||
working_directory: /go/src/github.com/reactiveops/polaris/
|
||||
docker:
|
||||
- image: circleci/golang:1.12
|
||||
@@ -104,22 +102,30 @@ jobs:
|
||||
- checkout
|
||||
- setup_remote_docker
|
||||
- *set_environment_variables
|
||||
- *docker_tag_release
|
||||
- *install_goreleaser
|
||||
- run: go get -u github.com/gobuffalo/packr/v2/packr2
|
||||
- run: packr2
|
||||
- run: goreleaser
|
||||
|
||||
release_images:
|
||||
working_directory: /go/src/github.com/reactiveops/polaris/
|
||||
docker:
|
||||
- image: quay.io/reactiveops/ci-images:v8.0-stretch
|
||||
steps:
|
||||
- checkout
|
||||
- setup_remote_docker
|
||||
- *set_environment_variables
|
||||
- *docker_build_and_push
|
||||
- *release_deploy_configs
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
build:
|
||||
jobs:
|
||||
- test
|
||||
- test-deploy
|
||||
- build:
|
||||
requires:
|
||||
- test
|
||||
- test-deploy
|
||||
context: org-global
|
||||
# Allow using testing tags for testing circle test + build steps
|
||||
filters:
|
||||
@@ -127,7 +133,17 @@ workflows:
|
||||
only: /^testing-.*/
|
||||
release:
|
||||
jobs:
|
||||
- release:
|
||||
- release_binary:
|
||||
context: org-global
|
||||
filters:
|
||||
branches:
|
||||
ignore: /.*/
|
||||
# Testing tags are reserved for testing circle test + build steps
|
||||
tags:
|
||||
ignore: /^testing-.*/
|
||||
- release_images:
|
||||
requires:
|
||||
- release_binary
|
||||
context: org-global
|
||||
filters:
|
||||
branches:
|
||||
|
||||
+32
-20
@@ -1,3 +1,35 @@
|
||||
# x.x.x (next release)
|
||||
|
||||
|
||||
# 0.2.1
|
||||
* [Fix](https://github.com/reactiveops/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
|
||||
* Added `--display-name` flag
|
||||
* Added support for StatefulSets
|
||||
* 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
|
||||
|
||||
# 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
|
||||
|
||||
# 0.1.3
|
||||
* [Fix](https://github.com/reactiveops/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
|
||||
* Added some tests and better error handling
|
||||
|
||||
# 0.1.0
|
||||
* Dashboard fully functional
|
||||
* Validating webhook functional, but still considered beta
|
||||
@@ -23,23 +55,3 @@
|
||||
* privilege escalation allowed
|
||||
* run as root allowed
|
||||
* run as privileged
|
||||
|
||||
# 0.1.1
|
||||
* [Fix](https://github.com/reactiveops/polaris/issues/93): missing `config.yaml` and dashboard assets in binary releases
|
||||
* Added some tests and better error handling
|
||||
|
||||
# 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.3
|
||||
* [Fix](https://github.com/reactiveops/polaris/issues/109): dashboard not updating when running persistently
|
||||
|
||||
# 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
|
||||
|
||||
# 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
|
||||
|
||||
+48
-16
@@ -1,11 +1,14 @@
|
||||
# Contributing
|
||||
|
||||
Issues, whether bugs, tasks, or feature requests are essential for keeping Polaris great. We believe it should be as easy as possible to contribute changes that get things working in your environment. There are a few guidelines that we need contributors to follow so that we can have a chance of keeping on top of things.
|
||||
Issues, whether bugs, tasks, or feature requests are essential for keeping Polaris great. We believe it should be as easy as possible to contribute changes that get things working in your environment. There are a few guidelines that we need contributors to follow so that we can keep on top of things.
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
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).
|
||||
|
||||
## Project Structure
|
||||
|
||||
Polaris is built on top of [controller-runtime](https://github.com/kubernetes-sigs/controller-runtime). It can run in 3 different modes, a dashboard, a webhook, or a reporter that prints or exports validation results. All of these modes make use of the shared `validator` and `config` packages. Adding new validations is possible by only making additions to those packages.
|
||||
@@ -14,6 +17,17 @@ Polaris is built on top of [controller-runtime](https://github.com/kubernetes-si
|
||||
|
||||
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.
|
||||
|
||||
## Setting Up Your Development Environment
|
||||
### Prerequisites
|
||||
* A properly configured Golang environment with Go 1.11 or higher
|
||||
* 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`
|
||||
* 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.
|
||||
|
||||
## Running Tests
|
||||
|
||||
The following commands are all required to pass as part of Polaris testing:
|
||||
@@ -43,18 +57,36 @@ Each new pull request should:
|
||||
- Be up to date and/or rebased on the master branch
|
||||
|
||||
## Creating a new release
|
||||
* Change the version number everywhere. E.g. for `0.1.4` to `0.1.5`:
|
||||
```
|
||||
find ./ -type f ! -path "./vendor/*" ! -path "./.git/*" ! -name CHANGELOG.md -exec sed -i 's/0\.1\.4/0.1.5/g' {} \;
|
||||
```
|
||||
* Add relevant entries to CHANGELOG.md
|
||||
* Create a PR to merge your changes
|
||||
* Once the PR is merged, wait for CircleCI to finish building the merge commit
|
||||
* Tag and push the latest:
|
||||
```
|
||||
git checkout master
|
||||
git pull
|
||||
git tag $VERSION
|
||||
git push --tags
|
||||
```
|
||||
* CircleCI will add an entry to the releases page on GitHub
|
||||
|
||||
### Minor/patch releases
|
||||
Minor and 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.
|
||||
|
||||
### 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. Bump the version number in:
|
||||
1. main.go
|
||||
2. README.md
|
||||
2. Update CHANGELOG.md
|
||||
3. 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.
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
helm-to-k8s:
|
||||
helm template deploy/helm/polaris/ --name polaris --namespace polaris --set templateOnly=true > deploy/dashboard.yaml
|
||||
helm template deploy/helm/polaris/ --name polaris --namespace polaris --set templateOnly=true --set webhook.enable=true --set dashboard.enable=false > deploy/webhook.yaml
|
||||
@@ -5,7 +5,7 @@
|
||||
[![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.1.5&color=239922
|
||||
[version-image]: https://img.shields.io/static/v1.svg?label=Version&message=0.2.1&color=239922
|
||||
[version-link]: https://github.com/reactiveops/polaris
|
||||
|
||||
[goreport-image]: https://goreportcard.com/badge/github.com/reactiveops/polaris
|
||||
@@ -19,7 +19,15 @@ Polaris helps keep your cluster healthy. It runs a variety of checks to ensure t
|
||||
- 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.
|
||||
|
||||
**Want to learn more?** ReactiveOps holds [office hours on Zoom](https://zoom.us/j/951540265) the first Friday of every month, at 12pm Eastern. You can also reach out via email at `opensource@reactiveops.com`
|
||||
**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`
|
||||
|
||||
## Quickstart
|
||||
|
||||
```
|
||||
kubectl apply -f https://github.com/reactiveops/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
|
||||
|
||||
@@ -31,63 +39,90 @@ 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.
|
||||
|
||||
### Deploying
|
||||
|
||||
To deploy Polaris with kubectl:
|
||||
|
||||
```
|
||||
kubectl apply -f https://raw.githubusercontent.com/reactiveops/polaris/master/deploy/dashboard.yaml
|
||||
```
|
||||
|
||||
Polaris can also be deployed with Helm:
|
||||
|
||||
```
|
||||
helm upgrade --install polaris deploy/helm/polaris/ --namespace polaris
|
||||
```
|
||||
|
||||
### Viewing the Dashboard
|
||||
|
||||
Once the dashboard is deployed, it can be viewed by using kubectl port-forward:
|
||||
|
||||
```
|
||||
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.
|
||||
|
||||
### Using a Binary Release
|
||||
|
||||
If you'd prefer to run Polaris locally, 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
|
||||
```
|
||||
|
||||
When running as a binary, Polaris will use your local kubeconfig to connect to a cluster. There are a variety of options available, but the most common usage will likely be to view the dashboard:
|
||||
|
||||
```
|
||||
polaris --dashboard
|
||||
```
|
||||
|
||||
## 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.*
|
||||
|
||||
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.
|
||||
|
||||
### Deploying
|
||||
|
||||
The Polaris webhook can be deployed with kubectl:
|
||||
## 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.
|
||||
|
||||
### kubectl
|
||||
#### Dashboard
|
||||
```
|
||||
kubectl apply -f https://raw.githubusercontent.com/reactiveops/polaris/master/deploy/webhook.yaml
|
||||
kubectl apply -f https://github.com/reactiveops/polaris/releases/latest/download/dashboard.yaml
|
||||
kubectl port-forward --namespace polaris svc/polaris-dashboard 8080:80
|
||||
```
|
||||
|
||||
Alternatively, the webhook can be enabled with Helm by setting `webhook.enable` to true:
|
||||
#### Webhook
|
||||
```
|
||||
kubectl apply -f https://github.com/reactiveops/polaris/releases/latest/download/webhook.yaml
|
||||
```
|
||||
|
||||
### 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.
|
||||
|
||||
```
|
||||
helm upgrade --install polaris deploy/helm/polaris/ --namespace polaris --set webhook.enable=true
|
||||
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
|
||||
|
||||
@@ -1,212 +0,0 @@
|
||||
---
|
||||
# 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
|
||||
|
||||
---
|
||||
# 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'
|
||||
verbs:
|
||||
- 'get'
|
||||
- 'list'
|
||||
- apiGroups:
|
||||
- ''
|
||||
resources:
|
||||
- 'nodes'
|
||||
- 'namespaces'
|
||||
- 'pods'
|
||||
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
|
||||
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:
|
||||
annotations:
|
||||
checksum/config: '6ec4a4dc87403cae67c01438398a5f1d4ef836ffeaf26a33b685c066b940495a'
|
||||
name: polaris-dashboard
|
||||
namespace: polaris
|
||||
labels:
|
||||
app: polaris
|
||||
component: dashboard
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: polaris
|
||||
component: dashboard
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: polaris
|
||||
component: dashboard
|
||||
spec:
|
||||
volumes:
|
||||
- name: config
|
||||
configMap:
|
||||
name: polaris
|
||||
containers:
|
||||
- command:
|
||||
- polaris
|
||||
- --dashboard
|
||||
- --config
|
||||
- /opt/app/config.yaml
|
||||
image: 'quay.io/reactiveops/polaris:0.1.5'
|
||||
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
|
||||
---
|
||||
# Source: polaris/templates/webhook.deployment.yaml
|
||||
|
||||
---
|
||||
# Source: polaris/templates/webhook.rbac.yaml
|
||||
|
||||
---
|
||||
# Source: polaris/templates/webhook.secret.yaml
|
||||
|
||||
---
|
||||
# Source: polaris/templates/webhook.service.yaml
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
apiVersion: v1
|
||||
description: Validation of best practices in your Kubernetes clusters
|
||||
name: polaris
|
||||
version: 0.1.5
|
||||
@@ -1,6 +0,0 @@
|
||||
{{- if .Values.templateOnly -}}
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: {{ .Release.Namespace }}
|
||||
{{- end -}}
|
||||
@@ -1,12 +0,0 @@
|
||||
** Please be patient while the chart is being deployed **
|
||||
|
||||
Enjoy Polaris and smooth sailing!
|
||||
|
||||
{{- if contains "ClusterIP" .Values.dashboard.service.type }}
|
||||
|
||||
## To view the dashboard execute these commands:
|
||||
|
||||
kubectl port-forward --namespace {{ .Release.Namespace }} svc/{{ include "polaris.fullname" . }}-dashboard 8080:80 &
|
||||
open http://localhost:8080
|
||||
|
||||
{{- end }}
|
||||
@@ -1,56 +0,0 @@
|
||||
{{/* vim: set filetype=mustache: */}}
|
||||
{{/*
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
{{- define "polaris.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Create a default fully qualified app name.
|
||||
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||
If release name contains chart name it will be used as a full name.
|
||||
*/}}
|
||||
{{- define "polaris.fullname" -}}
|
||||
{{- if .Values.fullnameOverride -}}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride -}}
|
||||
{{- if contains $name .Release.Name -}}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Create chart name and version as used by the chart label.
|
||||
*/}}
|
||||
{{- define "polaris.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Standard labels
|
||||
*/}}
|
||||
{{- define "polaris.labels" -}}
|
||||
app: {{ include "polaris.name" . }}
|
||||
{{- if not .Values.templateOnly }}
|
||||
app.kubernetes.io/name: {{ include "polaris.name" . }}
|
||||
helm.sh/chart: {{ include "polaris.chart" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Standard selector
|
||||
*/}}
|
||||
{{- define "polaris.selectors" -}}
|
||||
app: {{ include "polaris.name" . }}
|
||||
{{- if not .Values.templateOnly }}
|
||||
app.kubernetes.io/name: {{ include "polaris.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
@@ -1,9 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: {{ include "polaris.fullname" . }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "polaris.labels" . | nindent 4 }}
|
||||
data:
|
||||
config.yaml: {{- toYaml .Values.config | indent 2 -}}
|
||||
@@ -1,72 +0,0 @@
|
||||
{{- if .Values.dashboard.enable -}}
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
checksum/config: '{{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}'
|
||||
name: {{ include "polaris.fullname" . }}-dashboard
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "polaris.labels" . | nindent 4 }}
|
||||
component: dashboard
|
||||
spec:
|
||||
replicas: {{.Values.dashboard.replicas}}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "polaris.selectors" . | nindent 6 }}
|
||||
component: dashboard
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "polaris.selectors" . | nindent 8 }}
|
||||
component: dashboard
|
||||
spec:
|
||||
volumes:
|
||||
- name: config
|
||||
configMap:
|
||||
name: {{ include "polaris.fullname" . }}
|
||||
containers:
|
||||
- command:
|
||||
- polaris
|
||||
- --dashboard
|
||||
- --config
|
||||
- /opt/app/config.yaml
|
||||
image: '{{.Values.dashboard.image.repository}}:{{.Values.dashboard.image.tag}}'
|
||||
imagePullPolicy: '{{.Values.dashboard.image.pullPolicy}}'
|
||||
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: {{ include "polaris.fullname" . }}-dashboard
|
||||
{{- end -}}
|
||||
@@ -1,49 +0,0 @@
|
||||
{{- if and .Values.dashboard.enable .Values.rbac.create -}}
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: {{ include "polaris.fullname" . }}-dashboard
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "polaris.labels" . | nindent 4 }}
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: {{ include "polaris.fullname" . }}-dashboard
|
||||
labels:
|
||||
{{- include "polaris.labels" . | nindent 4 }}
|
||||
rules:
|
||||
- apiGroups:
|
||||
- 'apps'
|
||||
- 'extensions'
|
||||
resources:
|
||||
- 'deployments'
|
||||
verbs:
|
||||
- 'get'
|
||||
- 'list'
|
||||
- apiGroups:
|
||||
- ''
|
||||
resources:
|
||||
- 'nodes'
|
||||
- 'namespaces'
|
||||
- 'pods'
|
||||
verbs:
|
||||
- 'get'
|
||||
- 'list'
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: {{ include "polaris.fullname" . }}-dashboard
|
||||
labels:
|
||||
{{- include "polaris.labels" . | nindent 4 }}
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: {{ include "polaris.fullname" . }}-dashboard
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: {{ include "polaris.fullname" . }}-dashboard
|
||||
namespace: {{ .Release.Namespace }}
|
||||
{{- end -}}
|
||||
@@ -1,19 +0,0 @@
|
||||
{{- if and .Values.dashboard.enable .Values.dashboard.service -}}
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "polaris.fullname" . }}-dashboard
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "polaris.labels" . | nindent 4 }}
|
||||
spec:
|
||||
ports:
|
||||
- name: dashboard
|
||||
port: 80
|
||||
protocol: TCP
|
||||
targetPort: 8080
|
||||
selector:
|
||||
{{- include "polaris.selectors" . | nindent 4 }}
|
||||
component: dashboard
|
||||
type: {{ .Values.dashboard.service.type }}
|
||||
{{- end -}}
|
||||
@@ -1,90 +0,0 @@
|
||||
{{- if .Values.webhook.enable -}}
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
checksum/config: '{{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}'
|
||||
name: {{ include "polaris.fullname" . }}-webhook
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "polaris.labels" . | nindent 4 }}
|
||||
component: webhook
|
||||
spec:
|
||||
replicas: {{ .Values.webhook.replicas }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "polaris.selectors" . | nindent 6 }}
|
||||
component: webhook
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "polaris.selectors" . | nindent 8 }}
|
||||
component: webhook
|
||||
spec:
|
||||
containers:
|
||||
- name: webhook
|
||||
command:
|
||||
- polaris
|
||||
- --webhook
|
||||
- --config
|
||||
- /opt/app/config.yaml
|
||||
image: '{{.Values.webhook.image.repository}}:{{.Values.webhook.image.tag}}'
|
||||
imagePullPolicy: '{{.Values.webhook.image.pullPolicy}}'
|
||||
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: {{ include "polaris.fullname" . }}-webhook
|
||||
volumes:
|
||||
- name: config
|
||||
configMap:
|
||||
name: {{ include "polaris.fullname" . }}
|
||||
- name: secret
|
||||
secret:
|
||||
secretName: polaris-webhook
|
||||
- name: cr-logs
|
||||
emptyDir: {}
|
||||
{{- end -}}
|
||||
@@ -1,90 +0,0 @@
|
||||
{{- if and .Values.webhook.enable .Values.rbac.create -}}
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: {{ include "polaris.fullname" . }}-webhook
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "polaris.labels" . | nindent 4 }}
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: {{ include "polaris.fullname" . }}-webhook
|
||||
labels:
|
||||
{{- include "polaris.labels" . | nindent 4 }}
|
||||
rules:
|
||||
- apiGroups:
|
||||
- 'apps'
|
||||
- 'extensions'
|
||||
resources:
|
||||
- 'deployments'
|
||||
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: {{ include "polaris.fullname" . }}-webhook
|
||||
labels:
|
||||
{{- include "polaris.labels" . | nindent 4 }}
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: {{ include "polaris.fullname" . }}-webhook
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: {{ include "polaris.fullname" . }}-webhook
|
||||
namespace: {{ .Release.Namespace }}
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: {{ include "polaris.fullname" . }}-webhook
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "polaris.labels" . | nindent 4 }}
|
||||
rules:
|
||||
# required for current controller-runtime bootstrap method
|
||||
- apiGroups:
|
||||
- ''
|
||||
resources:
|
||||
- 'secrets'
|
||||
- 'services'
|
||||
verbs:
|
||||
- '*'
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: {{ include "polaris.fullname" . }}-webhook
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "polaris.labels" . | nindent 4 }}
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: Role
|
||||
name: {{ include "polaris.fullname" . }}-webhook
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: {{ include "polaris.fullname" . }}-webhook
|
||||
namespace: {{ .Release.Namespace }}
|
||||
{{- end -}}
|
||||
@@ -1,12 +0,0 @@
|
||||
{{- if .Values.webhook.enable -}}
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: polaris-webhook
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "polaris.labels" . | nindent 4 }}
|
||||
type: Opaque
|
||||
stringData:
|
||||
cert.pem: ''
|
||||
{{- end -}}
|
||||
@@ -1,19 +0,0 @@
|
||||
{{- if .Values.webhook.enable -}}
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: polaris-webhook
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "polaris.labels" . | nindent 4 }}
|
||||
spec:
|
||||
ports:
|
||||
- name: webhook
|
||||
port: 443
|
||||
protocol: TCP
|
||||
targetPort: 9876
|
||||
selector:
|
||||
{{- include "polaris.selectors" . | nindent 4 }}
|
||||
component: webhook
|
||||
type: ClusterIP
|
||||
{{- end -}}
|
||||
@@ -1,67 +0,0 @@
|
||||
---
|
||||
config: |
|
||||
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
|
||||
|
||||
dashboard:
|
||||
enable: true
|
||||
replicas: 1
|
||||
service:
|
||||
type: ClusterIP
|
||||
image:
|
||||
repository: quay.io/reactiveops/polaris
|
||||
tag: 0.1.5
|
||||
pullPolicy: Always
|
||||
|
||||
webhook:
|
||||
enable: false
|
||||
replicas: 1
|
||||
image:
|
||||
repository: quay.io/reactiveops/polaris
|
||||
tag: 0.1.5
|
||||
pullPolicy: Always
|
||||
|
||||
rbac:
|
||||
create: true
|
||||
|
||||
templateOnly: false
|
||||
@@ -1,280 +0,0 @@
|
||||
---
|
||||
# Source: polaris/templates/0-namespace.yaml
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: polaris
|
||||
---
|
||||
# Source: polaris/templates/webhook.secret.yaml
|
||||
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
|
||||
|
||||
---
|
||||
# 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:
|
||||
- apiGroups:
|
||||
- 'apps'
|
||||
- 'extensions'
|
||||
resources:
|
||||
- 'deployments'
|
||||
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:
|
||||
annotations:
|
||||
checksum/config: '6ec4a4dc87403cae67c01438398a5f1d4ef836ffeaf26a33b685c066b940495a'
|
||||
name: polaris-webhook
|
||||
namespace: polaris
|
||||
labels:
|
||||
app: polaris
|
||||
component: webhook
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: polaris
|
||||
component: webhook
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: polaris
|
||||
component: webhook
|
||||
spec:
|
||||
containers:
|
||||
- name: webhook
|
||||
command:
|
||||
- polaris
|
||||
- --webhook
|
||||
- --config
|
||||
- /opt/app/config.yaml
|
||||
image: 'quay.io/reactiveops/polaris:0.1.5'
|
||||
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
|
||||
volumes:
|
||||
- name: config
|
||||
configMap:
|
||||
name: polaris
|
||||
- name: secret
|
||||
secret:
|
||||
secretName: polaris-webhook
|
||||
- name: cr-logs
|
||||
emptyDir: {}
|
||||
---
|
||||
# Source: polaris/templates/dashboard.deployment.yaml
|
||||
|
||||
---
|
||||
# Source: polaris/templates/dashboard.rbac.yaml
|
||||
|
||||
---
|
||||
# Source: polaris/templates/dashboard.service.yaml
|
||||
|
||||
@@ -29,7 +29,6 @@ import (
|
||||
"github.com/reactiveops/polaris/pkg/validator"
|
||||
fwebhook "github.com/reactiveops/polaris/pkg/webhook"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gopkg.in/yaml.v2"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
|
||||
apitypes "k8s.io/apimachinery/pkg/types"
|
||||
@@ -38,11 +37,12 @@ import (
|
||||
"sigs.k8s.io/controller-runtime/pkg/manager"
|
||||
"sigs.k8s.io/controller-runtime/pkg/runtime/signals"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
const (
|
||||
// Version represents the current release version of Polaris
|
||||
Version = "0.1.5"
|
||||
Version = "0.2.1"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -55,6 +55,8 @@ func main() {
|
||||
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")
|
||||
displayName := flag.String("display-name", "", "An optional identifier for the audit")
|
||||
configPath := flag.String("config", "", "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")
|
||||
@@ -76,6 +78,9 @@ 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)
|
||||
@@ -90,7 +95,7 @@ func main() {
|
||||
} else if *dashboard {
|
||||
startDashboardServer(c, *auditPath, *dashboardPort, *dashboardBasePath)
|
||||
} else if *audit {
|
||||
runAudit(c, *auditPath, *auditOutputFile, *auditOutputURL)
|
||||
runAudit(c, *auditPath, *auditOutputFile, *auditOutputURL, *auditOutputFormat)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,7 +180,7 @@ func startWebhookServer(c conf.Configuration, disableWebhookConfigInstaller bool
|
||||
}
|
||||
}
|
||||
|
||||
func runAudit(c conf.Configuration, auditPath string, outputFile string, outputURL string) {
|
||||
func runAudit(c conf.Configuration, auditPath string, outputFile string, outputURL string, outputFormat string) {
|
||||
k, err := kube.CreateResourceProvider(auditPath)
|
||||
if err != nil {
|
||||
logrus.Errorf("Error fetching Kubernetes resources %v", err)
|
||||
@@ -187,33 +192,39 @@ func runAudit(c conf.Configuration, auditPath string, outputFile string, outputU
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if outputURL == "" && outputFile == "" {
|
||||
yamlBytes, err := yaml.Marshal(auditData)
|
||||
|
||||
if err != nil {
|
||||
logrus.Errorf("Error marshalling YAML: %v", err)
|
||||
os.Exit(1)
|
||||
var outputBytes []byte
|
||||
if outputFormat == "score" {
|
||||
outputBytes = []byte(fmt.Sprint(auditData.ClusterSummary.Score))
|
||||
} else if outputFormat == "yaml" {
|
||||
jsonBytes, err := json.Marshal(auditData)
|
||||
if err == nil {
|
||||
outputBytes, err = yaml.JSONToYAML(jsonBytes)
|
||||
}
|
||||
|
||||
os.Stdout.Write(yamlBytes)
|
||||
|
||||
} else {
|
||||
jsonData, err := json.MarshalIndent(auditData, "", " ")
|
||||
|
||||
if err != nil {
|
||||
logrus.Errorf("Error marshalling JSON: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
outputBytes, err = json.MarshalIndent(auditData, "", " ")
|
||||
}
|
||||
if err != nil {
|
||||
logrus.Errorf("Error marshalling audit: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if outputURL == "" && outputFile == "" {
|
||||
os.Stdout.Write(outputBytes)
|
||||
} else {
|
||||
if outputURL != "" {
|
||||
req, err := http.NewRequest("POST", outputURL, bytes.NewBuffer(jsonData))
|
||||
req, err := http.NewRequest("POST", outputURL, bytes.NewBuffer(outputBytes))
|
||||
|
||||
if err != nil {
|
||||
logrus.Errorf("Error building request for output: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
if outputFormat == "json" {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
} else if outputFormat == "yaml" {
|
||||
req.Header.Set("Content-Type", "application/x-yaml")
|
||||
} else {
|
||||
req.Header.Set("Content-Type", "text/plain")
|
||||
}
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
|
||||
@@ -235,7 +246,7 @@ func runAudit(c conf.Configuration, auditPath string, outputFile string, outputU
|
||||
}
|
||||
|
||||
if outputFile != "" {
|
||||
err := ioutil.WriteFile(outputFile, []byte(jsonData), 0644)
|
||||
err := ioutil.WriteFile(outputFile, []byte(outputBytes), 0644)
|
||||
if err != nil {
|
||||
logrus.Errorf("Error writing output to file: %v", err)
|
||||
os.Exit(1)
|
||||
|
||||
@@ -28,6 +28,7 @@ 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"`
|
||||
|
||||
@@ -9,11 +9,14 @@
|
||||
font-weight: 300;
|
||||
font-size: 28px;
|
||||
padding: 20px 20px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
margin-left: -2px; /* Fix for kerning issue */
|
||||
}
|
||||
|
||||
.card.cluster h3 {
|
||||
padding: 0px;
|
||||
padding-bottom: 10px;
|
||||
padding: 4px 0px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.card.namespace h3 strong {
|
||||
@@ -21,9 +24,7 @@
|
||||
}
|
||||
|
||||
.cluster-overview {
|
||||
position: relative;
|
||||
top: -60px;
|
||||
margin-bottom: -60px;
|
||||
}
|
||||
|
||||
.cluster-overview .graph {
|
||||
@@ -38,11 +39,12 @@
|
||||
display: inline-block;
|
||||
width: 32%;
|
||||
vertical-align: top;
|
||||
padding-top: 65px;
|
||||
}
|
||||
|
||||
.cluster-overview .cluster-score{
|
||||
padding-top: 15px;
|
||||
}
|
||||
.cluster-overview .result-messages {
|
||||
padding-top: 90px;
|
||||
padding-top: 70px;
|
||||
}
|
||||
|
||||
.cluster-overview .graph canvas {
|
||||
@@ -263,6 +265,11 @@ ul.message-list li i.message-icon {
|
||||
color: #a11f4c;
|
||||
}
|
||||
|
||||
.controller-type {
|
||||
display: inline-block;
|
||||
min-width: 115px;
|
||||
}
|
||||
|
||||
a.more-info {
|
||||
color: #bbb;
|
||||
font-size: 12px;
|
||||
|
||||
@@ -87,15 +87,15 @@ type templateData struct {
|
||||
// GetBaseTemplate puts together the dashboard template. Individual pieces can be overridden before rendering.
|
||||
func GetBaseTemplate(name string) (*template.Template, error) {
|
||||
tmpl := template.New(name).Funcs(template.FuncMap{
|
||||
"getWarningWidth": getWarningWidth,
|
||||
"getSuccessWidth": getSuccessWidth,
|
||||
"getWeatherIcon": getWeatherIcon,
|
||||
"getWeatherText": getWeatherText,
|
||||
"getGrade": getGrade,
|
||||
"getScore": getScore,
|
||||
"getIcon": getIcon,
|
||||
"getCategoryLink": getCategoryLink,
|
||||
"getCategoryInfo": getCategoryInfo,
|
||||
"getWarningWidth": getWarningWidth,
|
||||
"getSuccessWidth": getSuccessWidth,
|
||||
"getWeatherIcon": getWeatherIcon,
|
||||
"getWeatherText": getWeatherText,
|
||||
"getGrade": getGrade,
|
||||
"getIcon": getIcon,
|
||||
"getCategoryLink": getCategoryLink,
|
||||
"getCategoryInfo": getCategoryInfo,
|
||||
"getAllControllerResults": getAllControllerResults,
|
||||
})
|
||||
|
||||
templateFileNames := []string{
|
||||
|
||||
@@ -20,6 +20,13 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
func getAllControllerResults(nr validator.NamespaceResult) []validator.ControllerResult {
|
||||
results := []validator.ControllerResult{}
|
||||
results = append(results, nr.DeploymentResults...)
|
||||
results = append(results, nr.StatefulSetResults...)
|
||||
return results
|
||||
}
|
||||
|
||||
func getWarningWidth(counts validator.CountSummary, fullWidth int) uint {
|
||||
return uint(float64(counts.Successes+counts.Warnings) / float64(counts.Successes+counts.Warnings+counts.Errors) * float64(fullWidth))
|
||||
}
|
||||
@@ -29,7 +36,7 @@ func getSuccessWidth(counts validator.CountSummary, fullWidth int) uint {
|
||||
}
|
||||
|
||||
func getGrade(counts validator.CountSummary) string {
|
||||
score := getScore(counts)
|
||||
score := counts.GetScore()
|
||||
if score >= 97 {
|
||||
return "A+"
|
||||
} else if score >= 93 {
|
||||
@@ -59,13 +66,8 @@ func getGrade(counts validator.CountSummary) string {
|
||||
}
|
||||
}
|
||||
|
||||
func getScore(counts validator.CountSummary) uint {
|
||||
total := (counts.Successes * 2) + counts.Warnings + (counts.Errors * 2)
|
||||
return uint((float64(counts.Successes*2) / float64(total)) * 100)
|
||||
}
|
||||
|
||||
func getWeatherIcon(counts validator.CountSummary) string {
|
||||
score := getScore(counts)
|
||||
score := counts.GetScore()
|
||||
if score >= 90 {
|
||||
return "fa-sun"
|
||||
} else if score >= 80 {
|
||||
@@ -80,7 +82,7 @@ func getWeatherIcon(counts validator.CountSummary) string {
|
||||
}
|
||||
|
||||
func getWeatherText(counts validator.CountSummary) string {
|
||||
score := getScore(counts)
|
||||
score := counts.GetScore()
|
||||
if score >= 90 {
|
||||
return "Smooth sailing"
|
||||
} else if score >= 80 {
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
{{define "dashboard"}}
|
||||
<div class="card cluster">
|
||||
<h3>Cluster Overview</h3>
|
||||
<h3>
|
||||
<span>{{ .AuditData.SourceType }} Overview</span>
|
||||
{{- if .AuditData.DisplayName -}}
|
||||
<small>: {{ .AuditData.DisplayName }}</small>
|
||||
{{ end }}
|
||||
</h3>
|
||||
<div class="cluster-overview">
|
||||
<div class="cluster-score">
|
||||
<div class="score-details">
|
||||
<div class="weather"><i class="fas {{ getWeatherIcon .AuditData.ClusterSummary.Results.Totals }}"></i></div>
|
||||
<div class="sailing">{{ getWeatherText .AuditData.ClusterSummary.Results.Totals }}</div>
|
||||
<div class="scores"><span>Grade: </span><strong>{{ getGrade .AuditData.ClusterSummary.Results.Totals }}</strong></div>
|
||||
<div class="scores"><span>Score: </span><strong>{{ getScore .AuditData.ClusterSummary.Results.Totals }}%</strong></div>
|
||||
<div class="scores"><span>Score: </span><strong>{{ .AuditData.ClusterSummary.Results.Totals.GetScore }}%</strong></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="graph">
|
||||
@@ -60,7 +65,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="name"><span class="caret-expander"></span>{{ $category }}<span class="category-score">Score: <strong>{{ getScore $summary }}%</strong></span></div>
|
||||
<div class="name"><span class="caret-expander"></span>{{ $category }}<span class="category-score">Score: <strong>{{ $summary.GetScore }}%</strong></span></div>
|
||||
<div class="result-messages expandable-content">
|
||||
<p class="category-info">{{ getCategoryInfo $category }} Refer to the <a href="details/{{ getCategoryLink $category }}">Polaris documentation about {{ $category }}</a> for more information.</p>
|
||||
</div>
|
||||
@@ -73,7 +78,7 @@
|
||||
<div class="card namespace">
|
||||
<h3>Namespace: <strong>{{ $namespace }}</strong></h3>
|
||||
<div class="expandable-table">
|
||||
{{ range .DeploymentResults }}
|
||||
{{ range getAllControllerResults $nsResult }}
|
||||
<div class="resource-info">
|
||||
<div class="status-bar">
|
||||
<div class="status">
|
||||
@@ -85,7 +90,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="name"><span class="caret-expander"></span>Deployment: <strong>{{ .Name }}</strong></div>
|
||||
<div class="name"><span class="caret-expander"></span>
|
||||
<span class="controller-type">{{ .Type }}:</span>
|
||||
<strong>{{ .Name }}</strong></div>
|
||||
<div class="result-messages expandable-content">
|
||||
<h4>Pod Spec:</h4>
|
||||
<ul class="message-list">
|
||||
|
||||
+66
-44
@@ -7,6 +7,7 @@ import (
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
@@ -21,8 +22,12 @@ 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
|
||||
}
|
||||
@@ -43,49 +48,17 @@ func CreateResourceProvider(directory string) (*ResourceProvider, error) {
|
||||
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{},
|
||||
}
|
||||
|
||||
addYaml := func(contents string) error {
|
||||
contentBytes := []byte(contents)
|
||||
decoder := k8sYaml.NewYAMLOrJSONDecoder(bytes.NewReader(contentBytes), 1000)
|
||||
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
|
||||
}
|
||||
decoder = k8sYaml.NewYAMLOrJSONDecoder(bytes.NewReader(contentBytes), 1000)
|
||||
if resource.Kind == "Deployment" {
|
||||
dep := appsv1.Deployment{}
|
||||
err = decoder.Decode(&dep)
|
||||
if err != nil {
|
||||
logrus.Errorf("Error parsing deployment %v", err)
|
||||
return err
|
||||
}
|
||||
resources.Deployments = append(resources.Deployments, dep)
|
||||
} else if resource.Kind == "Namespace" {
|
||||
ns := corev1.Namespace{}
|
||||
err = decoder.Decode(&ns)
|
||||
if err != nil {
|
||||
logrus.Errorf("Error parsing namespace %v", err)
|
||||
return err
|
||||
}
|
||||
resources.Namespaces = append(resources.Namespaces, ns)
|
||||
} else if resource.Kind == "Pod" {
|
||||
pod := corev1.Pod{}
|
||||
err = decoder.Decode(&pod)
|
||||
if err != nil {
|
||||
logrus.Errorf("Error parsing pod %v", err)
|
||||
return err
|
||||
}
|
||||
resources.Pods = append(resources.Pods, pod)
|
||||
}
|
||||
return nil
|
||||
return addResourceFromString(contents, &resources)
|
||||
}
|
||||
|
||||
visitFile := func(path string, f os.FileInfo, err error) error {
|
||||
@@ -120,49 +93,98 @@ func CreateResourceProviderFromPath(directory string) (*ResourceProvider, error)
|
||||
|
||||
// CreateResourceProviderFromCluster creates a new ResourceProvider using live data from a cluster
|
||||
func CreateResourceProviderFromCluster() (*ResourceProvider, error) {
|
||||
kubeConf := config.GetConfigOrDie()
|
||||
kubeConf, configError := config.GetConfig()
|
||||
if configError != nil {
|
||||
logrus.Errorf("Error fetching KubeConfig %v", configError)
|
||||
return nil, configError
|
||||
}
|
||||
api, err := kubernetes.NewForConfig(kubeConf)
|
||||
if err != nil {
|
||||
logrus.Errorf("Error creating Kubernetes client %v", err)
|
||||
return nil, err
|
||||
}
|
||||
return CreateResourceProviderFromAPI(api)
|
||||
return CreateResourceProviderFromAPI(api, kubeConf.Host)
|
||||
}
|
||||
|
||||
// CreateResourceProviderFromAPI creates a new ResourceProvider from an existing k8s interface
|
||||
func CreateResourceProviderFromAPI(kube kubernetes.Interface) (*ResourceProvider, error) {
|
||||
func CreateResourceProviderFromAPI(kube kubernetes.Interface, clusterName string) (*ResourceProvider, error) {
|
||||
listOpts := metav1.ListOptions{}
|
||||
serverVersion, err := kube.Discovery().ServerVersion()
|
||||
if err != nil {
|
||||
logrus.Errorf("Error fetching Kubernetes API version %v", err)
|
||||
logrus.Errorf("Error fetching Cluster API version %v", err)
|
||||
return nil, err
|
||||
}
|
||||
deploys, err := kube.AppsV1().Deployments("").List(listOpts)
|
||||
if err != nil {
|
||||
logrus.Errorf("Error fetching Kubernetes Deployments %v", err)
|
||||
logrus.Errorf("Error fetching Deployments %v", err)
|
||||
return nil, err
|
||||
}
|
||||
statefulSets, err := kube.AppsV1().StatefulSets("").List(listOpts)
|
||||
if err != nil {
|
||||
logrus.Errorf("Error fetching StatefulSets%v", err)
|
||||
return nil, err
|
||||
}
|
||||
nodes, err := kube.CoreV1().Nodes().List(listOpts)
|
||||
if err != nil {
|
||||
logrus.Errorf("Error fetching Kubernetes Nodes %v", err)
|
||||
logrus.Errorf("Error fetching Nodes %v", err)
|
||||
return nil, err
|
||||
}
|
||||
namespaces, err := kube.CoreV1().Namespaces().List(listOpts)
|
||||
if err != nil {
|
||||
logrus.Errorf("Error fetching Kubernetes Namespaces %v", err)
|
||||
logrus.Errorf("Error fetching Namespaces %v", err)
|
||||
return nil, err
|
||||
}
|
||||
pods, err := kube.CoreV1().Pods("").List(listOpts)
|
||||
if err != nil {
|
||||
logrus.Errorf("Error fetching Kubernetes Pods %v", err)
|
||||
logrus.Errorf("Error fetching Pods %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
return &api, nil
|
||||
}
|
||||
|
||||
func addResourceFromString(contents string, resources *ResourceProvider) error {
|
||||
contentBytes := []byte(contents)
|
||||
decoder := k8sYaml.NewYAMLOrJSONDecoder(bytes.NewReader(contentBytes), 1000)
|
||||
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
|
||||
}
|
||||
decoder = k8sYaml.NewYAMLOrJSONDecoder(bytes.NewReader(contentBytes), 1000)
|
||||
if resource.Kind == "Deployment" {
|
||||
dep := appsv1.Deployment{}
|
||||
err = decoder.Decode(&dep)
|
||||
resources.Deployments = append(resources.Deployments, dep)
|
||||
} else if resource.Kind == "StatefulSet" {
|
||||
dep := appsv1.StatefulSet{}
|
||||
err = decoder.Decode(&dep)
|
||||
resources.StatefulSets = append(resources.StatefulSets, dep)
|
||||
} else if resource.Kind == "Namespace" {
|
||||
ns := corev1.Namespace{}
|
||||
err = decoder.Decode(&ns)
|
||||
resources.Namespaces = append(resources.Namespaces, ns)
|
||||
} else if resource.Kind == "Pod" {
|
||||
pod := corev1.Pod{}
|
||||
err = decoder.Decode(&pod)
|
||||
resources.Pods = append(resources.Pods, pod)
|
||||
}
|
||||
if err != nil {
|
||||
logrus.Errorf("Error parsing %s: %v", resource.Kind, err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"github.com/reactiveops/polaris/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestGetResourcesFromPath(t *testing.T) {
|
||||
@@ -11,13 +12,19 @@ func TestGetResourcesFromPath(t *testing.T) {
|
||||
|
||||
assert.Equal(t, nil, err, "Error should be nil")
|
||||
|
||||
assert.Equal(t, "Path", resources.SourceType, "Should have type Path")
|
||||
assert.Equal(t, "./test_files/test_1", resources.SourceName, "Should have filename as name")
|
||||
assert.Equal(t, "unknown", resources.ServerVersion, "Server version should be unknown")
|
||||
assert.IsType(t, time.Now(), resources.CreationTime, "Creation time should be set")
|
||||
|
||||
assert.Equal(t, 0, len(resources.Nodes), "Should not have any nodes")
|
||||
|
||||
assert.Equal(t, 1, len(resources.Deployments), "Should have a deployment")
|
||||
assert.Equal(t, "ubuntu", resources.Deployments[0].Spec.Template.Spec.Containers[0].Name)
|
||||
|
||||
assert.Equal(t, 1, len(resources.StatefulSets), "Should have a stateful set")
|
||||
assert.Equal(t, "nginx", resources.StatefulSets[0].Spec.Template.Spec.Containers[0].Name)
|
||||
|
||||
assert.Equal(t, 1, len(resources.Namespaces), "Should have a namespace")
|
||||
assert.Equal(t, "two", resources.Namespaces[0].ObjectMeta.Name)
|
||||
|
||||
@@ -31,7 +38,10 @@ func TestGetMultipleResourceFromSingleFile(t *testing.T) {
|
||||
|
||||
assert.Equal(t, nil, err, "Error should be nil")
|
||||
|
||||
assert.Equal(t, "Path", resources.SourceType, "Should have type Path")
|
||||
assert.Equal(t, "./test_files/test_2/multi.yaml", resources.SourceName, "Should have filename as name")
|
||||
assert.Equal(t, "unknown", resources.ServerVersion, "Server version should be unknown")
|
||||
assert.IsType(t, time.Now(), resources.CreationTime, "Creation time should be set")
|
||||
|
||||
assert.Equal(t, 0, len(resources.Nodes), "Should not have any nodes")
|
||||
|
||||
@@ -45,12 +55,17 @@ func TestGetMultipleResourceFromSingleFile(t *testing.T) {
|
||||
|
||||
func TestGetResourceFromAPI(t *testing.T) {
|
||||
k8s := test.SetupTestAPI()
|
||||
k8s = test.SetupAddDeploys(k8s, "test")
|
||||
resources, err := CreateResourceProviderFromAPI(k8s)
|
||||
k8s = test.SetupAddControllers(k8s, "test")
|
||||
resources, err := CreateResourceProviderFromAPI(k8s, "test")
|
||||
assert.Equal(t, nil, err, "Error should be nil")
|
||||
|
||||
assert.Equal(t, "Cluster", resources.SourceType, "Should have type Path")
|
||||
assert.Equal(t, "test", resources.SourceName, "Should have source name")
|
||||
assert.IsType(t, time.Now(), resources.CreationTime, "Creation time should be set")
|
||||
|
||||
assert.Equal(t, 0, len(resources.Nodes), "Should not have any nodes")
|
||||
assert.Equal(t, 1, len(resources.Deployments), "Should have a deployment")
|
||||
assert.Equal(t, 1, len(resources.StatefulSets), "Should have a stateful set")
|
||||
assert.Equal(t, 0, len(resources.Pods), "Should have a pod")
|
||||
|
||||
assert.Equal(t, "", resources.Deployments[0].ObjectMeta.Name)
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
apiVersion: apps/v1 # for k8s versions before 1.9.0 use apps/v1beta2 and before 1.8.0 use extensions/v1beta1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: web
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
serviceName: "nginx"
|
||||
selector:
|
||||
matchLabels:
|
||||
app: nginx
|
||||
replicas: 14
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: k8s.gcr.io/nginx-slim:0.8
|
||||
ports:
|
||||
- containerPort: 80
|
||||
name: web
|
||||
volumeMounts:
|
||||
- name: www
|
||||
mountPath: /usr/share/nginx/html
|
||||
volumeClaimTemplates:
|
||||
- metadata:
|
||||
name: www
|
||||
spec:
|
||||
accessModes: [ "ReadWriteOnce" ]
|
||||
resources:
|
||||
requests:
|
||||
storage: 1Gi
|
||||
storageClassName: thin-disk
|
||||
+59
-13
@@ -29,16 +29,34 @@ type ContainerValidation struct {
|
||||
*ResourceValidation
|
||||
Container *corev1.Container
|
||||
IsInitContainer bool
|
||||
parentPodSpec corev1.PodSpec
|
||||
}
|
||||
|
||||
// ValidateContainer validates that each pod conforms to the Polaris config, returns a ResourceResult.
|
||||
func ValidateContainer(cnConf *conf.Configuration, container *corev1.Container, isInit bool) ContainerResult {
|
||||
// FIXME When validating a container, there are some things in a container spec
|
||||
// that can be affected by the podSpec. This means we need a copy of the
|
||||
// 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 {
|
||||
cv := ContainerValidation{
|
||||
Container: container,
|
||||
ResourceValidation: &ResourceValidation{},
|
||||
IsInitContainer: isInit,
|
||||
}
|
||||
|
||||
// Support initializing
|
||||
// FIXME This is a product of pulling in the podSpec, ideally we'd never
|
||||
// expect this be nil but our tests have conditions in which the
|
||||
// parent podResult isn't initialized in this ContainerValidation
|
||||
// struct.
|
||||
if parentPodResult == nil {
|
||||
// initialize a blank pod spec
|
||||
cv.parentPodSpec = corev1.PodSpec{}
|
||||
} else {
|
||||
cv.parentPodSpec = parentPodResult.podSpec
|
||||
}
|
||||
|
||||
cv.validateResources(&cnConf.Resources)
|
||||
cv.validateHealthChecks(&cnConf.HealthChecks)
|
||||
cv.validateImage(&cnConf.Images)
|
||||
@@ -176,39 +194,58 @@ func (cv *ContainerValidation) validateNetworking(networkConf *conf.Networking)
|
||||
func (cv *ContainerValidation) validateSecurity(securityConf *conf.Security) {
|
||||
category := messages.CategorySecurity
|
||||
securityContext := cv.Container.SecurityContext
|
||||
podSecurityContext := cv.parentPodSpec.SecurityContext
|
||||
|
||||
// Support an empty container security context
|
||||
if securityContext == nil {
|
||||
securityContext = &corev1.SecurityContext{}
|
||||
}
|
||||
|
||||
// Support an empty pod security context
|
||||
if podSecurityContext == nil {
|
||||
podSecurityContext = &corev1.PodSecurityContext{}
|
||||
}
|
||||
|
||||
if securityConf.RunAsRootAllowed.IsActionable() {
|
||||
if securityContext.RunAsNonRoot == (*bool)(nil) || !*securityContext.RunAsNonRoot {
|
||||
cv.addFailure(messages.RunAsRootFailure, securityConf.RunAsRootAllowed, category)
|
||||
} else {
|
||||
if getBoolValue(securityContext.RunAsNonRoot) {
|
||||
// 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)
|
||||
}
|
||||
} else {
|
||||
cv.addFailure(messages.RunAsRootFailure, securityConf.RunAsRootAllowed, category)
|
||||
}
|
||||
}
|
||||
|
||||
if securityConf.RunAsPrivileged.IsActionable() {
|
||||
if securityContext.Privileged == (*bool)(nil) || !*securityContext.Privileged {
|
||||
cv.addSuccess(messages.RunAsPrivilegedSuccess, category)
|
||||
} else {
|
||||
if getBoolValue(securityContext.Privileged) {
|
||||
cv.addFailure(messages.RunAsPrivilegedFailure, securityConf.RunAsPrivileged, category)
|
||||
} else {
|
||||
cv.addSuccess(messages.RunAsPrivilegedSuccess, category)
|
||||
}
|
||||
}
|
||||
|
||||
if securityConf.NotReadOnlyRootFileSystem.IsActionable() {
|
||||
if securityContext.ReadOnlyRootFilesystem == (*bool)(nil) || !*securityContext.ReadOnlyRootFilesystem {
|
||||
cv.addFailure(messages.ReadOnlyFilesystemFailure, securityConf.NotReadOnlyRootFileSystem, category)
|
||||
} else {
|
||||
if getBoolValue(securityContext.ReadOnlyRootFilesystem) {
|
||||
cv.addSuccess(messages.ReadOnlyFilesystemSuccess, category)
|
||||
} else {
|
||||
cv.addFailure(messages.ReadOnlyFilesystemFailure, securityConf.NotReadOnlyRootFileSystem, category)
|
||||
}
|
||||
}
|
||||
|
||||
if securityConf.PrivilegeEscalationAllowed.IsActionable() {
|
||||
if securityContext.AllowPrivilegeEscalation == (*bool)(nil) || !*securityContext.AllowPrivilegeEscalation {
|
||||
cv.addSuccess(messages.PrivilegeEscalationSuccess, category)
|
||||
} else {
|
||||
if getBoolValue(securityContext.AllowPrivilegeEscalation) {
|
||||
cv.addFailure(messages.PrivilegeEscalationFailure, securityConf.PrivilegeEscalationAllowed, category)
|
||||
} else {
|
||||
cv.addSuccess(messages.PrivilegeEscalationSuccess, category)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -323,3 +360,12 @@ func capContains(list []corev1.Capability, val corev1.Capability) bool {
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// getBoolValue returns false if nil or returns the value of the bool pointer
|
||||
func getBoolValue(val *bool) bool {
|
||||
if val == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return *val
|
||||
}
|
||||
|
||||
@@ -567,6 +567,42 @@ func TestValidateSecurity(t *testing.T) {
|
||||
ResourceValidation: &ResourceValidation{},
|
||||
}
|
||||
|
||||
badCVWithGoodPodSpec := ContainerValidation{
|
||||
Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{
|
||||
RunAsNonRoot: &falseVar,
|
||||
ReadOnlyRootFilesystem: &falseVar,
|
||||
Privileged: &trueVar,
|
||||
AllowPrivilegeEscalation: &trueVar,
|
||||
Capabilities: &corev1.Capabilities{
|
||||
Add: []corev1.Capability{"AUDIT_CONTROL", "SYS_ADMIN", "NET_ADMIN"},
|
||||
},
|
||||
}},
|
||||
ResourceValidation: &ResourceValidation{},
|
||||
parentPodSpec: corev1.PodSpec{
|
||||
SecurityContext: &corev1.PodSecurityContext{
|
||||
RunAsNonRoot: &trueVar,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
badCVWithBadPodSpec := ContainerValidation{
|
||||
Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{
|
||||
RunAsNonRoot: nil, // this will use the default from the podspec
|
||||
ReadOnlyRootFilesystem: &falseVar,
|
||||
Privileged: &trueVar,
|
||||
AllowPrivilegeEscalation: &trueVar,
|
||||
Capabilities: &corev1.Capabilities{
|
||||
Add: []corev1.Capability{"AUDIT_CONTROL", "SYS_ADMIN", "NET_ADMIN"},
|
||||
},
|
||||
}},
|
||||
ResourceValidation: &ResourceValidation{},
|
||||
parentPodSpec: corev1.PodSpec{
|
||||
SecurityContext: &corev1.PodSecurityContext{
|
||||
RunAsNonRoot: &falseVar,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
goodCV := ContainerValidation{
|
||||
Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{
|
||||
RunAsNonRoot: &trueVar,
|
||||
@@ -593,6 +629,42 @@ func TestValidateSecurity(t *testing.T) {
|
||||
ResourceValidation: &ResourceValidation{},
|
||||
}
|
||||
|
||||
strongCVWithPodSpecSecurityContext := ContainerValidation{
|
||||
Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{
|
||||
RunAsNonRoot: nil, // not set but overridden via podSpec
|
||||
ReadOnlyRootFilesystem: &trueVar,
|
||||
Privileged: &falseVar,
|
||||
AllowPrivilegeEscalation: &falseVar,
|
||||
Capabilities: &corev1.Capabilities{
|
||||
Drop: []corev1.Capability{"ALL"},
|
||||
},
|
||||
}},
|
||||
ResourceValidation: &ResourceValidation{},
|
||||
parentPodSpec: corev1.PodSpec{
|
||||
SecurityContext: &corev1.PodSecurityContext{
|
||||
RunAsNonRoot: &trueVar,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
strongCVWithBadPodSpecSecurityContext := ContainerValidation{
|
||||
Container: &corev1.Container{Name: "", SecurityContext: &corev1.SecurityContext{
|
||||
RunAsNonRoot: &trueVar, // will override the bad setting in PodSpec
|
||||
ReadOnlyRootFilesystem: &trueVar,
|
||||
Privileged: &falseVar,
|
||||
AllowPrivilegeEscalation: &falseVar,
|
||||
Capabilities: &corev1.Capabilities{
|
||||
Drop: []corev1.Capability{"ALL"},
|
||||
},
|
||||
}},
|
||||
ResourceValidation: &ResourceValidation{},
|
||||
parentPodSpec: corev1.PodSpec{
|
||||
SecurityContext: &corev1.PodSecurityContext{
|
||||
RunAsNonRoot: &falseVar, // is overridden at container level with RunAsNonRoot:true
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var testCases = []struct {
|
||||
name string
|
||||
securityConf conf.Security
|
||||
@@ -661,6 +733,66 @@ func TestValidateSecurity(t *testing.T) {
|
||||
Category: "Security",
|
||||
}},
|
||||
},
|
||||
{
|
||||
name: "bad security context + standard validation config with good settings in podspec",
|
||||
securityConf: standardConf,
|
||||
cv: badCVWithGoodPodSpec,
|
||||
expectedMessages: []*ResultMessage{{
|
||||
Message: "The following security capabilities should not be added: SYS_ADMIN, NET_ADMIN",
|
||||
Type: "error",
|
||||
Category: "Security",
|
||||
}, {
|
||||
Message: "Privilege escalation should not be allowed",
|
||||
Type: "error",
|
||||
Category: "Security",
|
||||
}, {
|
||||
Message: "Should not be running as privileged",
|
||||
Type: "error",
|
||||
Category: "Security",
|
||||
}, {
|
||||
Message: "The following security capabilities should not be added: AUDIT_CONTROL, SYS_ADMIN, NET_ADMIN",
|
||||
Type: "warning",
|
||||
Category: "Security",
|
||||
}, {
|
||||
Message: "Should not be allowed to run as root",
|
||||
Type: "warning",
|
||||
Category: "Security",
|
||||
}, {
|
||||
Message: "Filesystem should be read only",
|
||||
Type: "warning",
|
||||
Category: "Security",
|
||||
}},
|
||||
},
|
||||
{
|
||||
name: "bad security context + standard validation config from default set in podspec",
|
||||
securityConf: standardConf,
|
||||
cv: badCVWithBadPodSpec,
|
||||
expectedMessages: []*ResultMessage{{
|
||||
Message: "The following security capabilities should not be added: SYS_ADMIN, NET_ADMIN",
|
||||
Type: "error",
|
||||
Category: "Security",
|
||||
}, {
|
||||
Message: "Privilege escalation should not be allowed",
|
||||
Type: "error",
|
||||
Category: "Security",
|
||||
}, {
|
||||
Message: "Should not be running as privileged",
|
||||
Type: "error",
|
||||
Category: "Security",
|
||||
}, {
|
||||
Message: "The following security capabilities should not be added: AUDIT_CONTROL, SYS_ADMIN, NET_ADMIN",
|
||||
Type: "warning",
|
||||
Category: "Security",
|
||||
}, {
|
||||
Message: "Should not be allowed to run as root",
|
||||
Type: "warning",
|
||||
Category: "Security",
|
||||
}, {
|
||||
Message: "Filesystem should be read only",
|
||||
Type: "warning",
|
||||
Category: "Security",
|
||||
}},
|
||||
},
|
||||
{
|
||||
name: "good security context + standard validation config",
|
||||
securityConf: standardConf,
|
||||
@@ -739,6 +871,58 @@ func TestValidateSecurity(t *testing.T) {
|
||||
Category: "Security",
|
||||
}},
|
||||
},
|
||||
{
|
||||
name: "strong security context + strong validation config via podspec default",
|
||||
securityConf: strongConf,
|
||||
cv: strongCVWithPodSpecSecurityContext,
|
||||
expectedMessages: []*ResultMessage{{
|
||||
Message: "Is not allowed to run as root",
|
||||
Type: "success",
|
||||
Category: "Security",
|
||||
}, {
|
||||
Message: "Filesystem is read only",
|
||||
Type: "success",
|
||||
Category: "Security",
|
||||
}, {
|
||||
Message: "Not running as privileged",
|
||||
Type: "success",
|
||||
Category: "Security",
|
||||
}, {
|
||||
Message: "Privilege escalation not allowed",
|
||||
Type: "success",
|
||||
Category: "Security",
|
||||
}, {
|
||||
Message: "Security capabilities are within the configured limits",
|
||||
Type: "success",
|
||||
Category: "Security",
|
||||
}},
|
||||
},
|
||||
{
|
||||
name: "strong security context + strong validation config with bad setting in podspec default",
|
||||
securityConf: strongConf,
|
||||
cv: strongCVWithBadPodSpecSecurityContext,
|
||||
expectedMessages: []*ResultMessage{{
|
||||
Message: "Is not allowed to run as root",
|
||||
Type: "success",
|
||||
Category: "Security",
|
||||
}, {
|
||||
Message: "Filesystem is read only",
|
||||
Type: "success",
|
||||
Category: "Security",
|
||||
}, {
|
||||
Message: "Not running as privileged",
|
||||
Type: "success",
|
||||
Category: "Security",
|
||||
}, {
|
||||
Message: "Privilege escalation not allowed",
|
||||
Type: "success",
|
||||
Category: "Security",
|
||||
}, {
|
||||
Message: "Security capabilities are within the configured limits",
|
||||
Type: "success",
|
||||
Category: "Security",
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range testCases {
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
// Copyright 2019 ReactiveOps
|
||||
//
|
||||
// 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 (
|
||||
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"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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)
|
||||
return ControllerResult{
|
||||
Type: controller.Type,
|
||||
Name: controller.Name,
|
||||
PodResult: podResult,
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateControllers validates that each deployment conforms to the Polaris config,
|
||||
// returns 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))
|
||||
}
|
||||
for _, deploy := range kubeResources.StatefulSets {
|
||||
controllers = append(controllers, ControllerFromStatefulSet(deploy))
|
||||
}
|
||||
for _, controller := range controllers {
|
||||
controllerResult := ValidateController(config, controller)
|
||||
nsResult := nsResults.getNamespaceResult(controller.Namespace)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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,
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
// Copyright 2019 ReactiveOps
|
||||
//
|
||||
// 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 (
|
||||
conf "github.com/reactiveops/polaris/pkg/config"
|
||||
"github.com/reactiveops/polaris/pkg/kube"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
)
|
||||
|
||||
// ValidateDeployment validates a single deployment, returns a PodResult.
|
||||
func ValidateDeployment(conf conf.Configuration, deploy *appsv1.Deployment) ControllerResult {
|
||||
pod := deploy.Spec.Template.Spec
|
||||
podResult := ValidatePod(conf, &pod)
|
||||
return ControllerResult{
|
||||
Name: deploy.Name,
|
||||
Type: "Deployment",
|
||||
PodResult: podResult,
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateDeployments validates that each deployment conforms to the Polaris config,
|
||||
// returns a list of ResourceResults organized by namespace.
|
||||
func ValidateDeployments(config conf.Configuration, kubeResources *kube.ResourceProvider) (NamespacedResults, error) {
|
||||
nsResults := NamespacedResults{}
|
||||
|
||||
for _, deploy := range kubeResources.Deployments {
|
||||
deploymentResult := ValidateDeployment(config, &deploy)
|
||||
nsResults = addResult(deploymentResult, nsResults, deploy.Namespace)
|
||||
}
|
||||
|
||||
return nsResults, nil
|
||||
}
|
||||
|
||||
func addResult(deploymentResult ControllerResult, nsResults NamespacedResults, nsName string) NamespacedResults {
|
||||
nsResult := &NamespaceResult{}
|
||||
|
||||
// If there is already data stored for this namespace name,
|
||||
// then append to the ResourceResults to the existing data.
|
||||
switch nsResults[nsName] {
|
||||
case nil:
|
||||
nsResult = &NamespaceResult{
|
||||
Summary: &ResultSummary{},
|
||||
DeploymentResults: []ControllerResult{},
|
||||
}
|
||||
nsResults[nsName] = nsResult
|
||||
default:
|
||||
nsResult = nsResults[nsName]
|
||||
}
|
||||
|
||||
nsResult.DeploymentResults = append(nsResult.DeploymentResults, deploymentResult)
|
||||
nsResult.Summary.appendResults(*deploymentResult.PodResult.Summary)
|
||||
|
||||
return nsResults
|
||||
}
|
||||
+37
-23
@@ -1,44 +1,44 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
conf "github.com/reactiveops/polaris/pkg/config"
|
||||
"github.com/reactiveops/polaris/pkg/kube"
|
||||
)
|
||||
|
||||
const (
|
||||
// PolarisOutputVersion is the version of the current output structure
|
||||
PolarisOutputVersion = "0.0"
|
||||
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
|
||||
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) {
|
||||
// TODO: Validate StatefulSets, DaemonSets, Cron jobs
|
||||
// in addition to deployments
|
||||
|
||||
// TODO: Once we are validating more than deployments,
|
||||
// we will need to merge the namespaceResults that get returned
|
||||
// from each validation.
|
||||
nsResults, err := ValidateDeployments(config, kubeResources)
|
||||
if err != nil {
|
||||
return AuditData{}, err
|
||||
}
|
||||
nsResults := NamespacedResults{}
|
||||
ValidateControllers(config, kubeResources, &nsResults)
|
||||
|
||||
clusterResults := ResultSummary{}
|
||||
|
||||
@@ -47,17 +47,31 @@ func RunAudit(config conf.Configuration, kubeResources *kube.ResourceProvider) (
|
||||
for _, dr := range nsRes.DeploymentResults {
|
||||
clusterResults.appendResults(*dr.PodResult.Summary)
|
||||
}
|
||||
for _, dr := range nsRes.StatefulSetResults {
|
||||
clusterResults.appendResults(*dr.PodResult.Summary)
|
||||
}
|
||||
}
|
||||
|
||||
displayName := config.DisplayName
|
||||
if displayName == "" {
|
||||
displayName = kubeResources.SourceName
|
||||
}
|
||||
|
||||
auditData := AuditData{
|
||||
PolarisOutputVersion: PolarisOutputVersion,
|
||||
AuditTime: kubeResources.CreationTime.Format(time.RFC3339),
|
||||
SourceType: kubeResources.SourceType,
|
||||
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),
|
||||
Results: clusterResults,
|
||||
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(),
|
||||
},
|
||||
NamespacedResults: nsResults,
|
||||
}
|
||||
|
||||
@@ -11,8 +11,8 @@ import (
|
||||
|
||||
func TestGetTemplateData(t *testing.T) {
|
||||
k8s := test.SetupTestAPI()
|
||||
k8s = test.SetupAddDeploys(k8s, "test")
|
||||
resources, err := kube.CreateResourceProviderFromAPI(k8s)
|
||||
k8s = test.SetupAddControllers(k8s, "test")
|
||||
resources, err := kube.CreateResourceProviderFromAPI(k8s, "test")
|
||||
assert.Equal(t, err, nil, "error should be nil")
|
||||
|
||||
c := conf.Configuration{
|
||||
@@ -24,19 +24,19 @@ func TestGetTemplateData(t *testing.T) {
|
||||
|
||||
sum := ResultSummary{
|
||||
Totals: CountSummary{
|
||||
Successes: uint(4),
|
||||
Warnings: uint(1),
|
||||
Errors: uint(1),
|
||||
Successes: uint(8),
|
||||
Warnings: uint(2),
|
||||
Errors: uint(2),
|
||||
},
|
||||
ByCategory: CategorySummary{},
|
||||
}
|
||||
sum.ByCategory["Health Checks"] = &CountSummary{
|
||||
Successes: uint(0),
|
||||
Warnings: uint(1),
|
||||
Errors: uint(1),
|
||||
Warnings: uint(2),
|
||||
Errors: uint(2),
|
||||
}
|
||||
sum.ByCategory["Resources"] = &CountSummary{
|
||||
Successes: uint(4),
|
||||
Successes: uint(8),
|
||||
Warnings: uint(0),
|
||||
Errors: uint(0),
|
||||
}
|
||||
@@ -45,8 +45,16 @@ func TestGetTemplateData(t *testing.T) {
|
||||
assert.Equal(t, err, nil, "error should be nil")
|
||||
|
||||
assert.EqualValues(t, sum, actualAudit.ClusterSummary.Results)
|
||||
assert.Equal(t, actualAudit.SourceType, "Cluster", "should be from a cluster")
|
||||
assert.Equal(t, actualAudit.SourceName, "test", "should be from a cluster")
|
||||
|
||||
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, 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")
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ func ValidatePod(podConf conf.Configuration, pod *corev1.PodSpec) PodResult {
|
||||
Messages: pv.messages(),
|
||||
ContainerResults: []ContainerResult{},
|
||||
Summary: pv.summary(),
|
||||
podSpec: *pod,
|
||||
}
|
||||
|
||||
pv.validateContainers(pod.InitContainers, &pRes, &podConf, true)
|
||||
@@ -54,7 +55,7 @@ func ValidatePod(podConf conf.Configuration, pod *corev1.PodSpec) PodResult {
|
||||
|
||||
func (pv *PodValidation) validateContainers(containers []corev1.Container, pRes *PodResult, podConf *conf.Configuration, isInit bool) {
|
||||
for _, container := range containers {
|
||||
cRes := ValidateContainer(podConf, &container, isInit)
|
||||
cRes := ValidateContainer(&container, pRes, podConf, isInit)
|
||||
pRes.ContainerResults = append(pRes.ContainerResults, cRes)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ func TestValidatePod(t *testing.T) {
|
||||
}
|
||||
|
||||
k8s := test.SetupTestAPI()
|
||||
k8s = test.SetupAddDeploys(k8s, "test")
|
||||
k8s = test.SetupAddControllers(k8s, "test")
|
||||
pod := test.MockPod()
|
||||
|
||||
expectedSum := ResultSummary{
|
||||
|
||||
+31
-5
@@ -14,6 +14,8 @@
|
||||
|
||||
package validator
|
||||
|
||||
import corev1 "k8s.io/api/core/v1"
|
||||
|
||||
// MessageType represents the type of Message
|
||||
type MessageType string
|
||||
|
||||
@@ -28,14 +30,31 @@ const (
|
||||
MessageTypeError MessageType = "error"
|
||||
)
|
||||
|
||||
// NamespaceResult groups container results by parent resource.
|
||||
type NamespaceResult struct {
|
||||
Name string
|
||||
Summary *ResultSummary
|
||||
DeploymentResults []ControllerResult
|
||||
StatefulSetResults []ControllerResult
|
||||
}
|
||||
|
||||
// NamespacedResults is a mapping of namespace name to the validation results.
|
||||
type NamespacedResults map[string]*NamespaceResult
|
||||
|
||||
// NamespaceResult groups container results by parent resource.
|
||||
type NamespaceResult struct {
|
||||
Name string
|
||||
Summary *ResultSummary
|
||||
DeploymentResults []ControllerResult
|
||||
func (nsResults NamespacedResults) getNamespaceResult(nsName string) *NamespaceResult {
|
||||
nsResult := &NamespaceResult{}
|
||||
switch nsResults[nsName] {
|
||||
case nil:
|
||||
nsResult = &NamespaceResult{
|
||||
Summary: &ResultSummary{},
|
||||
DeploymentResults: []ControllerResult{},
|
||||
StatefulSetResults: []ControllerResult{},
|
||||
}
|
||||
nsResults[nsName] = nsResult
|
||||
default:
|
||||
nsResult = nsResults[nsName]
|
||||
}
|
||||
return nsResult
|
||||
}
|
||||
|
||||
// CountSummary provides a high level overview of success, warnings, and errors.
|
||||
@@ -45,6 +64,12 @@ type CountSummary struct {
|
||||
Errors uint
|
||||
}
|
||||
|
||||
// GetScore returns an overall score in [0, 100] for the CountSummary
|
||||
func (cs *CountSummary) GetScore() uint {
|
||||
total := (cs.Successes * 2) + cs.Warnings + (cs.Errors * 2)
|
||||
return uint((float64(cs.Successes*2) / float64(total)) * 100)
|
||||
}
|
||||
|
||||
func (cs *CountSummary) appendCounts(toAppend CountSummary) {
|
||||
cs.Errors += toAppend.Errors
|
||||
cs.Warnings += toAppend.Warnings
|
||||
@@ -93,6 +118,7 @@ type PodResult struct {
|
||||
Summary *ResultSummary
|
||||
Messages []*ResultMessage
|
||||
ContainerResults []ContainerResult
|
||||
podSpec corev1.PodSpec
|
||||
}
|
||||
|
||||
// ResultMessage contains a message and a type indicator (success, warning, or error).
|
||||
|
||||
+17
-10
@@ -87,19 +87,24 @@ func (v *Validator) Handle(ctx context.Context, req types.Request) types.Respons
|
||||
var err error
|
||||
var podResult validator.PodResult
|
||||
|
||||
allowed := true
|
||||
reason := ""
|
||||
|
||||
switch req.AdmissionRequest.Kind.Kind {
|
||||
case "Deployment":
|
||||
deploy := appsv1.Deployment{}
|
||||
err = v.decoder.Decode(req, &deploy)
|
||||
deployResult := validator.ValidateDeployment(v.Config, &deploy)
|
||||
podResult = deployResult.PodResult
|
||||
case "Pod":
|
||||
if req.AdmissionRequest.Kind.Kind == "Pod" {
|
||||
pod := corev1.Pod{}
|
||||
err = v.decoder.Decode(req, &pod)
|
||||
podResult = validator.ValidatePod(v.Config, &pod.Spec)
|
||||
} else {
|
||||
var controller validator.Controller
|
||||
switch req.AdmissionRequest.Kind.Kind {
|
||||
case "Deployment":
|
||||
deploy := appsv1.Deployment{}
|
||||
err = v.decoder.Decode(req, &deploy)
|
||||
controller = validator.ControllerFromDeployment(deploy)
|
||||
case "StatefulSet":
|
||||
statefulSet := appsv1.StatefulSet{}
|
||||
err = v.decoder.Decode(req, &statefulSet)
|
||||
controller = validator.ControllerFromStatefulSet(statefulSet)
|
||||
}
|
||||
controllerResult := validator.ValidateController(v.Config, controller)
|
||||
podResult = controllerResult.PodResult
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@@ -107,6 +112,8 @@ func (v *Validator) Handle(ctx context.Context, req types.Request) types.Respons
|
||||
return admission.ErrorResponse(http.StatusBadRequest, err)
|
||||
}
|
||||
|
||||
allowed := true
|
||||
reason := ""
|
||||
if podResult.Summary.Totals.Errors > 0 {
|
||||
allowed = false
|
||||
reason = getFailureReason(podResult)
|
||||
|
||||
+17
-2
@@ -39,17 +39,32 @@ func mockDeploy() appsv1.Deployment {
|
||||
return d
|
||||
}
|
||||
|
||||
func mockStatefulSet() appsv1.StatefulSet {
|
||||
p := MockPod()
|
||||
s := appsv1.StatefulSet{
|
||||
Spec: appsv1.StatefulSetSpec{
|
||||
Template: p,
|
||||
},
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// SetupTestAPI creates a test kube API struct.
|
||||
func SetupTestAPI() kubernetes.Interface {
|
||||
return fake.NewSimpleClientset()
|
||||
}
|
||||
|
||||
// SetupAddDeploys creates a mock deployment and adds it to the test clientset.
|
||||
func SetupAddDeploys(k kubernetes.Interface, namespace string) 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 {
|
||||
fmt.Println(err)
|
||||
}
|
||||
s1 := mockStatefulSet()
|
||||
_, err = k.AppsV1().StatefulSets(namespace).Create(&s1)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
return k
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user