mirror of
https://github.com/FairwindsOps/polaris.git
synced 2026-08-23 22:26:34 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c33452bba0 | ||
|
|
300c8d9d94 | ||
|
|
7b1f046586 | ||
|
|
4e290e192d | ||
|
|
8d562f2490 | ||
|
|
47150155a0 | ||
|
|
b3d323d742 | ||
|
|
6060cf243f | ||
|
|
e044a540ed | ||
|
|
f6a4bffe88 | ||
|
|
343e24886d | ||
|
|
997d4bfb2b | ||
|
|
9ded3269c6 | ||
|
|
ca65a446bd | ||
|
|
4e9aa21cea | ||
|
|
ed014ecf8e | ||
|
|
2e11c04664 | ||
|
|
80662bd203 | ||
|
|
5705f81911 | ||
|
|
b4e3d40f4b | ||
|
|
cea0ca7fa8 | ||
|
|
69bed16e4f | ||
|
|
5b173cf4a7 | ||
|
|
0a0720a26c | ||
|
|
fa3504c350 | ||
|
|
4020000d68 | ||
|
|
ab7eeccfb0 | ||
|
|
78a16fa0ea | ||
|
|
b26c4be8d6 | ||
|
|
aada33f0e9 | ||
|
|
7b88c48a6e | ||
|
|
ded260ee19 | ||
|
|
3e9c270ac7 | ||
|
|
bf6e41566d | ||
|
|
ededabf00f | ||
|
|
3df9d35c57 | ||
|
|
a90c3b0f12 | ||
|
|
d19b0769b4 | ||
|
|
97f25cd7d1 | ||
|
|
03331147ca | ||
|
|
d44138bead | ||
|
|
eb21b030c2 | ||
|
|
3061756a55 | ||
|
|
2964280976 | ||
|
|
f657071a7f | ||
|
|
2cdc4775e0 | ||
|
|
af5b86e28c | ||
|
|
1021f6b1d7 | ||
|
|
0111b7ec2b | ||
|
|
33524757aa | ||
|
|
eea4a8e2c3 | ||
|
|
fb98d3112d | ||
|
|
97c81ccf60 | ||
|
|
e9b79fbdd4 | ||
|
|
217e3563d6 | ||
|
|
deb33bb05f | ||
|
|
e605eef755 | ||
|
|
c304036f97 | ||
|
|
c1061b50e0 | ||
|
|
3b07496a4d | ||
|
|
3eb2b6e2c9 | ||
|
|
5f5da069fd | ||
|
|
ceeffb97a0 | ||
|
|
27faab919c | ||
|
|
f4fc7c1b5d | ||
|
|
54d796b1f3 | ||
|
|
9065e3b2ae | ||
|
|
bd14ab8bc1 | ||
|
|
2ac6a2b540 | ||
|
|
9d81c393de | ||
|
|
b557786325 | ||
|
|
5572bb8118 | ||
|
|
ac9412920f | ||
|
|
d50d9c81f8 | ||
|
|
de98d9fbeb | ||
|
|
eac15d6258 | ||
|
|
0c671d0f62 | ||
|
|
e9064dfddc | ||
|
|
cf10a9617f | ||
|
|
c571b97bc9 | ||
|
|
fa5954c9ce | ||
|
|
58ec098c05 | ||
|
|
c9211b9e56 | ||
|
|
a3e9939f76 | ||
|
|
450d065f55 | ||
|
|
9ce3c06d5c | ||
|
|
ee951c8460 | ||
|
|
69621f7034 | ||
|
|
4cf3d15af0 | ||
|
|
5d875a225c | ||
|
|
6792fba91f | ||
|
|
f559f3328b | ||
|
|
38e512291d | ||
|
|
cfc0d213cd | ||
|
|
22125549a6 | ||
|
|
9e8378aece | ||
|
|
79603c8cf7 | ||
|
|
c21163aadc | ||
|
|
c03ecbde4a | ||
|
|
57f3a8b911 | ||
|
|
37660e0fe9 | ||
|
|
dcf02b9549 | ||
|
|
3c46f405a9 | ||
|
|
94548d0dcf | ||
|
|
1497c9a872 | ||
|
|
a5828a2d3b | ||
|
|
5769acf820 | ||
|
|
aa0658b5ac | ||
|
|
49dbd1d33b | ||
|
|
68fe23018a | ||
|
|
ea9db7e273 | ||
|
|
56ba00f524 | ||
|
|
d18bf855cb | ||
|
|
cbe1f104da | ||
|
|
6eb12b3bfe | ||
|
|
98533bc11a | ||
|
|
e4b8992301 | ||
|
|
ee2ccb0c26 | ||
|
|
61ecb69ab1 | ||
|
|
c43ace22a3 | ||
|
|
fb0d7c5d82 | ||
|
|
403d424d66 | ||
|
|
760e57c520 | ||
|
|
72b8bfa8f6 | ||
|
|
8c769e445c | ||
|
|
3c685279be | ||
|
|
7fdebfc4db | ||
|
|
0d3fe6130a | ||
|
|
bb34be7e02 | ||
|
|
fe55216e2a | ||
|
|
3191f087bb | ||
|
|
e80311d937 | ||
|
|
d4717302d4 | ||
|
|
a5d54af2b8 | ||
|
|
1b2a930a9c | ||
|
|
33d4192871 | ||
|
|
2a0ed94d0d | ||
|
|
9ff5d5a70e | ||
|
|
6a1cee846a | ||
|
|
cd002d0aff | ||
|
|
d2bb2f126b | ||
|
|
ec4e755f2f | ||
|
|
3e9193af7f | ||
|
|
efd0ec85b9 | ||
|
|
c036c91247 | ||
|
|
dad526245d | ||
|
|
c591765f70 | ||
|
|
51f3eaa3f0 | ||
|
|
574a1483eb | ||
|
|
a0604ba3a5 | ||
|
|
0da4ea69bf | ||
|
|
6213291be8 | ||
|
|
619e17d7a1 | ||
|
|
c64ba4bf91 | ||
|
|
44572936ec | ||
|
|
9c9e58c23d | ||
|
|
b9d6a1ea85 | ||
|
|
4c345f3799 | ||
|
|
6009ee67fa | ||
|
|
ddbf2e8c11 | ||
|
|
edaea49cd0 | ||
|
|
54727d933d | ||
|
|
3e1cc3c9e9 | ||
|
|
1cae69d080 | ||
|
|
6f8028fc17 | ||
|
|
d507039d5f | ||
|
|
b5ac0bc5d6 | ||
|
|
31de106ba8 | ||
|
|
aaa54cc401 | ||
|
|
51d639e2ef | ||
|
|
330d7f0516 | ||
|
|
73c4923334 | ||
|
|
10fdf4ef33 | ||
|
|
ed48daa092 | ||
|
|
7f71a352a7 | ||
|
|
ba412ab407 | ||
|
|
7b0817202c | ||
|
|
36876072f2 | ||
|
|
b7b5c43cdf | ||
|
|
100b4e8b9e | ||
|
|
e873a40cc3 | ||
|
|
1d9968b3af | ||
|
|
6338c53c2e | ||
|
|
0c70bacf4f | ||
|
|
56bba70ef3 | ||
|
|
03cc3fb1bd | ||
|
|
bcc1cc5384 | ||
|
|
5ef97ae896 | ||
|
|
dfa34e1880 | ||
|
|
542694841a | ||
|
|
e6a44c6ff8 | ||
|
|
7637108234 | ||
|
|
23bf4c81b0 | ||
|
|
090700f641 | ||
|
|
e129ba6971 | ||
|
|
51cd3523fc | ||
|
|
ac501a20d2 | ||
|
|
83a7448c99 | ||
|
|
d228acfe71 | ||
|
|
49c540e993 | ||
|
|
9f7caabef4 | ||
|
|
2770be643f | ||
|
|
5cd52242c4 | ||
|
|
6b4227da4d | ||
|
|
2e1aa0db4a | ||
|
|
e7baeac2e5 | ||
|
|
051319fab7 | ||
|
|
917e630697 | ||
|
|
68166559c5 | ||
|
|
fee55ba7f8 | ||
|
|
d0e0009efc | ||
|
|
57ff1d5c06 | ||
|
|
f0834d30a2 | ||
|
|
4d82220212 | ||
|
|
f8d5ce70cf | ||
|
|
5f060801ef | ||
|
|
04da47d83e | ||
|
|
5efa416ea9 | ||
|
|
7b0fe81d01 | ||
|
|
1067255173 | ||
|
|
7cc0be4188 | ||
|
|
b0035158d2 | ||
|
|
7070eb724c | ||
|
|
f7dccc079b | ||
|
|
6c588848ef | ||
|
|
ad3a8e6748 | ||
|
|
3fa627a2cd | ||
|
|
30b49c4d7b | ||
|
|
f2c5752718 | ||
|
|
d0dc7f4b0e | ||
|
|
3304285b4e | ||
|
|
d80d326f7c | ||
|
|
df48615a26 | ||
|
|
59785196d4 | ||
|
|
02252c690d | ||
|
|
25be9e41dd | ||
|
|
ddf815d636 | ||
|
|
95c04b1e9d | ||
|
|
0f2e5e0def | ||
|
|
8b20fd9dcf | ||
|
|
d4e3258d53 | ||
|
|
b4e3f78b72 | ||
|
|
20257cd917 | ||
|
|
317dfcefd2 | ||
|
|
42e8666356 | ||
|
|
49a419548f | ||
|
|
790607c6c0 | ||
|
|
91378c3d92 | ||
|
|
207297c11d | ||
|
|
0d86096f09 | ||
|
|
67ab987f7e | ||
|
|
97457d71c0 | ||
|
|
1159a380ba | ||
|
|
ca6aa76729 | ||
|
|
7ce71f1632 | ||
|
|
98b47e0aeb | ||
|
|
159c165468 | ||
|
|
4eeabb2c7f | ||
|
|
7e35b03452 | ||
|
|
c8e4f766a2 | ||
|
|
0ee39a4470 | ||
|
|
fc7c913122 | ||
|
|
3e15586597 | ||
|
|
3d07ce8004 | ||
|
|
22ab851681 | ||
|
|
152c876067 | ||
|
|
c10b2e0e55 | ||
|
|
2b15f11d57 | ||
|
|
b172f61c5f | ||
|
|
150b812371 | ||
|
|
434b1f604f | ||
|
|
6e3ab067d7 | ||
|
|
2e44d151fe | ||
|
|
c91a85a08a | ||
|
|
f8bb171209 | ||
|
|
f860c34d62 | ||
|
|
b8422a93b7 | ||
|
|
15b14555a6 | ||
|
|
467ab945cd | ||
|
|
dddeeb3332 | ||
|
|
f03f97ab07 | ||
|
|
75f70352ba | ||
|
|
4aa360a933 | ||
|
|
dce24a0a34 | ||
|
|
fae7a9fc17 | ||
|
|
79b0a00d5c | ||
|
|
e4fe5ab085 | ||
|
|
2049698c8e | ||
|
|
20bd32afb6 | ||
|
|
c132f7ee51 | ||
|
|
10025764cd | ||
|
|
c8c0e07a22 | ||
|
|
b55f4717b1 | ||
|
|
f493c380fc | ||
|
|
baa652197a | ||
|
|
a6525fc807 | ||
|
|
e01c39230a | ||
|
|
7b8b5fee4d | ||
|
|
819b77ae1a | ||
|
|
edccf88ccd |
@@ -1,7 +1,7 @@
|
||||
DOCKERFILE='Dockerfile'
|
||||
|
||||
EXTERNAL_REGISTRY_BASE_DOMAIN=quay.io
|
||||
REPOSITORY_NAME=reactiveops/polaris
|
||||
REPOSITORY_NAME=fairwinds/polaris
|
||||
DOCKERTAG=${EXTERNAL_REGISTRY_BASE_DOMAIN}/${REPOSITORY_NAME}
|
||||
if [[ -n $CI_TAG ]]; then
|
||||
ADDITIONAL_DOCKER_TAG_VERSIONS=()
|
||||
|
||||
+61
-40
@@ -15,10 +15,14 @@ references:
|
||||
echo 'export CI_BUILD_NUM=$CIRCLE_BUILD_NUM' >> ${BASH_ENV}
|
||||
echo 'export CI_TAG=$CIRCLE_TAG' >> ${BASH_ENV}
|
||||
echo 'export PUSH_ALL_VERSION_TAGS=true' >> ${BASH_ENV}
|
||||
echo 'export GOPROXY=https://proxy.golang.org' >> ${BASH_ENV}
|
||||
echo 'export GO111MODULE=on' >> ${BASH_ENV}
|
||||
|
||||
install_k8s: &install_k8s
|
||||
run:
|
||||
name: Install K8s
|
||||
command: |
|
||||
sudo apt-get update
|
||||
echo "Installing git and jq"
|
||||
sudo apt-get install -yqq jq git
|
||||
|
||||
@@ -49,7 +53,6 @@ references:
|
||||
name: Update Coverage
|
||||
command: |
|
||||
if [[ -z $CIRCLE_PR_NUMBER ]]; then
|
||||
go test ./pkg/... -coverprofile=coverage.txt -covermode=count
|
||||
bash <(curl -s https://codecov.io/bash)
|
||||
else
|
||||
echo "Skipping coverage for forked PR"
|
||||
@@ -58,51 +61,51 @@ references:
|
||||
run:
|
||||
name: Test Dashboard
|
||||
command: |
|
||||
go run main.go --dashboard --dashboard-port 3000 --audit-path ./examples &
|
||||
sleep 5
|
||||
go run main.go dashboard --port 3000 --audit-path ./examples &
|
||||
sleep 30
|
||||
curl -f http://localhost:3000 > /dev/null
|
||||
curl -f http://localhost:3000/health > /dev/null
|
||||
curl -f http://localhost:3000/favicon.ico > /dev/null
|
||||
curl -f http://localhost:3000/static/css/main.css > /dev/null
|
||||
curl -f http://localhost:3000/results.json > /dev/null
|
||||
curl -f http://localhost:3000/details/security > /dev/null
|
||||
test_kube_dashboard: &test_kube_dashboard
|
||||
test_k8s: &test_k8s
|
||||
run:
|
||||
name: Test Dashboard
|
||||
name: Test Kubernetes Deployments
|
||||
command: |
|
||||
kubectl apply -f ./deploy/dashboard.yaml
|
||||
sleep 10
|
||||
kubectl get pods --namespace polaris
|
||||
kubectl port-forward --namespace polaris svc/polaris-dashboard 3000:80 &
|
||||
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
|
||||
if [[ -z $CIRCLE_PR_NUMBER ]]; then
|
||||
./test/webhook_test.sh
|
||||
./test/kube_dashboard_test.sh
|
||||
else
|
||||
echo "Skipping Kubernetes tests for forked PR"
|
||||
fi
|
||||
|
||||
# Release scripts
|
||||
install_goreleaser: &install_goreleaser
|
||||
run:
|
||||
name: Install GoReleaser
|
||||
command: |
|
||||
curl -fsSLo goreleaser.deb https://github.com/goreleaser/goreleaser/releases/download/v0.94.0/goreleaser_amd64.deb
|
||||
echo "8dbad6683d6fc9367e637e6eed8e01a0d63c9660 goreleaser.deb" | sha1sum -c
|
||||
curl -fsSLo goreleaser.deb https://github.com/goreleaser/goreleaser/releases/download/v0.131.1/goreleaser_amd64.deb
|
||||
echo "640790dcbfa864f26de4c26c2d491f293a64525c8c6641c5bbdec7136b38977e goreleaser.deb" | sha256sum -c -
|
||||
sudo dpkg -i goreleaser.deb
|
||||
rm goreleaser.deb
|
||||
|
||||
docker_build: &docker_build
|
||||
run:
|
||||
name: Docker login and build
|
||||
command: |
|
||||
docker-pull -f .circleci/build.config
|
||||
docker-build -f .circleci/build.config
|
||||
|
||||
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
|
||||
docker login quay.io -u="${fairwinds_quay_user}" -p="${fairwinds_quay_token}"
|
||||
docker-push -f .circleci/build.config
|
||||
|
||||
release_deploy_configs: &release_deploy_configs
|
||||
run:
|
||||
name: Release deploy configs
|
||||
@@ -115,7 +118,16 @@ references:
|
||||
jobs:
|
||||
build:
|
||||
docker:
|
||||
- image: quay.io/reactiveops/ci-images:v8.0-stretch
|
||||
- image: quay.io/reactiveops/ci-images:v11.0-stretch
|
||||
steps:
|
||||
- checkout
|
||||
- setup_remote_docker
|
||||
- *set_environment_variables
|
||||
- *docker_build
|
||||
|
||||
push:
|
||||
docker:
|
||||
- image: quay.io/reactiveops/ci-images:v11.0-stretch
|
||||
steps:
|
||||
- checkout
|
||||
- setup_remote_docker
|
||||
@@ -129,38 +141,42 @@ jobs:
|
||||
steps:
|
||||
- checkout
|
||||
- *install_k8s
|
||||
- *test_kube_dashboard
|
||||
- *test_k8s
|
||||
|
||||
test:
|
||||
working_directory: /go/src/github.com/reactiveops/polaris/
|
||||
working_directory: /go/src/github.com/fairwindsops/polaris/
|
||||
docker:
|
||||
- image: circleci/golang:1.12
|
||||
- image: circleci/golang:1.13
|
||||
steps:
|
||||
- checkout
|
||||
- *set_environment_variables
|
||||
- run: go get -u golang.org/x/lint/golint
|
||||
- run: go list ./... | grep -v vendor | xargs golint -set_exit_status
|
||||
- run: go list ./... | grep -v vendor | xargs go vet
|
||||
- run: go test ./pkg/... -coverprofile=coverage.txt -covermode=count
|
||||
- run: go run main.go audit --audit-path ./deploy --set-exit-code-below-score 100 --set-exit-code-on-danger
|
||||
- *update_coverage
|
||||
- *test_binary_dashboard
|
||||
|
||||
release_binary:
|
||||
working_directory: /go/src/github.com/reactiveops/polaris/
|
||||
working_directory: /go/src/github.com/fairwindsops/polaris/
|
||||
docker:
|
||||
- image: circleci/golang:1.12
|
||||
- image: circleci/golang:1.13
|
||||
steps:
|
||||
- checkout
|
||||
- setup_remote_docker
|
||||
- *set_environment_variables
|
||||
- *install_goreleaser
|
||||
- run: go get -u github.com/gobuffalo/packr/v2/packr2
|
||||
- run: git checkout -- . # FIXME: the go get makes the directory dirty
|
||||
- run: packr2
|
||||
- run: goreleaser
|
||||
- *release_deploy_configs
|
||||
|
||||
release_images:
|
||||
working_directory: /go/src/github.com/reactiveops/polaris/
|
||||
working_directory: /go/src/github.com/fairwindsops/polaris/
|
||||
docker:
|
||||
- image: quay.io/reactiveops/ci-images:v8.0-stretch
|
||||
- image: quay.io/reactiveops/ci-images:v11.0-stretch
|
||||
steps:
|
||||
- checkout
|
||||
- setup_remote_docker
|
||||
@@ -169,22 +185,27 @@ jobs:
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
|
||||
build:
|
||||
jobs:
|
||||
- test
|
||||
- test_k8s:
|
||||
# Ignore update-version branch, which changes deploy/ image references before the images are built
|
||||
filters:
|
||||
branches:
|
||||
ignore: /.*\/update-version/
|
||||
- build:
|
||||
requires:
|
||||
- test
|
||||
- push:
|
||||
context: org-global
|
||||
# Allow using testing tags for testing circle test + build steps
|
||||
requires:
|
||||
- build
|
||||
filters:
|
||||
tags:
|
||||
only: /^testing-.*/
|
||||
branches:
|
||||
ignore: /pull\/[0-9]+/
|
||||
- test_k8s:
|
||||
requires:
|
||||
- push
|
||||
filters:
|
||||
branches:
|
||||
ignore: /pull\/[0-9]+/
|
||||
|
||||
release:
|
||||
jobs:
|
||||
- release_binary:
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
deploy
|
||||
examples
|
||||
.gitignore
|
||||
.git/*
|
||||
Dockerfile
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*.dylib
|
||||
polaris
|
||||
|
||||
!cmd/polaris
|
||||
!deploy/helm/polaris
|
||||
|
||||
# Test binary, build with `go test -c`
|
||||
@@ -20,3 +21,4 @@ main
|
||||
|
||||
*-packr.go
|
||||
dist
|
||||
.vscode
|
||||
+22
-21
@@ -1,29 +1,30 @@
|
||||
builds:
|
||||
- env:
|
||||
- CGO_ENABLED=0
|
||||
binary: polaris
|
||||
archive:
|
||||
replacements:
|
||||
darwin: Darwin
|
||||
linux: Linux
|
||||
windows: Windows
|
||||
386: i386
|
||||
amd64: x86_64
|
||||
checksum:
|
||||
name_template: 'checksums.txt'
|
||||
snapshot:
|
||||
name_template: "{{ .Tag }}-next"
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
exclude:
|
||||
- '^docs:'
|
||||
- '^test:'
|
||||
brew:
|
||||
github:
|
||||
owner: reactiveops
|
||||
name: homebrew-tap
|
||||
folder: Formula
|
||||
description: Open Source Best Practices for Kubernetes
|
||||
test: |
|
||||
system "#{bin}/polaris --version"
|
||||
builds:
|
||||
- id: watcher
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
- GO111MODULE=on
|
||||
goarch:
|
||||
- amd64
|
||||
- arm
|
||||
- arm64
|
||||
- 386
|
||||
goarm:
|
||||
- 6
|
||||
- 7
|
||||
brews:
|
||||
- name: polaris
|
||||
github:
|
||||
owner: FairwindsOps
|
||||
name: homebrew-tap
|
||||
folder: Formula
|
||||
description: Open Source Best Practices for Kubernetes
|
||||
test: |
|
||||
system "#{bin}/polaris version"
|
||||
|
||||
+80
-8
@@ -1,5 +1,77 @@
|
||||
# x.x.x (next release)
|
||||
|
||||
# 1.2.1
|
||||
* Update date on dashboard footer
|
||||
|
||||
# 1.2.0
|
||||
* Add ability to audit a single workload
|
||||
* Enable `pullPolicyAlways` by default
|
||||
* Fix for finding parent resources
|
||||
|
||||
# 1.1.1
|
||||
* Show controller checks on dashboard
|
||||
* Fix for orphaned pods w/ controller checks
|
||||
|
||||
# 1.1.0
|
||||
* Add namespace filter in UI
|
||||
* Add priorityClass check
|
||||
* Support reading from STDIN
|
||||
* Ensure severity is set for all custom checks
|
||||
* Support audit files which use \r or \r\n as newline character
|
||||
* Add option to exempt an entire controller from checks via config file
|
||||
* Fixed case where parent resources trigger error
|
||||
* Fixed UI zero-state
|
||||
|
||||
# 1.0.3
|
||||
* Fixed case where parent resources trigger error
|
||||
* Fixed dashboard link when `--base-path` is set
|
||||
|
||||
# 1.0.2
|
||||
* Fixed case where custom CRDs are not covered by RBAC
|
||||
|
||||
# 1.0.1
|
||||
* Added ARM binaries to releases
|
||||
|
||||
# 1.0.0
|
||||
## New Features
|
||||
* Added support for custom checks using JSON Schema
|
||||
* Added support for arbitrary controllers, rather than a pre-configured set
|
||||
* removed support for `controllers_to_scan` in config
|
||||
* Added the ability to exempt a particular controller from a particular check.
|
||||
* Docker image now includes the default config
|
||||
|
||||
## Breaking Changes
|
||||
* Breaking changes in both input and output formats. See [Examples](/examples) for examples of the new formats.
|
||||
* removed config-level configuration for checks like max/min memory settings
|
||||
* changed severity `error` to `danger`
|
||||
* Breaking changes [to the CLI](/docs/usage.md#cli-options)
|
||||
* CLI flag `--set-exit-code-on-error` is now `--set-exit-code-on-danger`
|
||||
* Flags `--version`, `--dashboard`, `--webhook`, and `--audit` are now arguments
|
||||
* Port flags are now just `--port`
|
||||
|
||||
# 0.6.0
|
||||
* Fixed webhook support in Kubernetes 1.16
|
||||
* this also removes support for 1.8
|
||||
* Added support for exemptions via controller annotations
|
||||
|
||||
# 0.5.2
|
||||
* Fixed missing success messages for resource requests/limits
|
||||
|
||||
# 0.5.1
|
||||
* Added a few more exemptions
|
||||
* Started checking exemptions based on controller name prefix
|
||||
* `runAsUser != 0` now passes the `runAsNonRoot` check
|
||||
|
||||
# 0.5.0
|
||||
* Added `--load-audit-file` flag to run the dashboard from an existing audit
|
||||
* Added an `ID` field to each check in the output
|
||||
* Skip health checks for jobs, cronjobs, initcontainers
|
||||
* Added support for exemptions
|
||||
* Fixed dashboard base path option
|
||||
|
||||
# 0.4.0
|
||||
* Added additional Pod Controllers to scan PodSpec (`jobs`, `cronjobs`, `daemonsets`, `replicationcontrollers`)
|
||||
|
||||
# 0.3.1
|
||||
* Changed dashboard branding to refer to new org name Fairwinds
|
||||
|
||||
@@ -7,7 +79,7 @@
|
||||
* Added `--set-exit-code-on-error` and `--set-exit-code-below-score` flags to better support CI/CD
|
||||
|
||||
# 0.2.1
|
||||
* [Fix](https://github.com/reactiveops/polaris/issues/146): Fixed logic on RunAsNonRoot check to incorporate settings in podSpec
|
||||
* [Fix](https://github.com/FairwindsOps/polaris/issues/146): Fixed logic on RunAsNonRoot check to incorporate settings in podSpec
|
||||
|
||||
# 0.2.0
|
||||
* Added `--output-format` flag for better CI/CD support
|
||||
@@ -16,23 +88,23 @@
|
||||
* Show error message if no kubeconfig is set
|
||||
|
||||
# 0.1.5
|
||||
* [Fix](https://github.com/reactiveops/polaris/issues/125): ignore limits/requests for initContainers
|
||||
* [Fix](https://github.com/reactiveops/polaris/issues/132): support custom base path
|
||||
* [Fix](https://github.com/FairwindsOps/polaris/issues/125): ignore limits/requests for initContainers
|
||||
* [Fix](https://github.com/FairwindsOps/polaris/issues/132): support custom base path
|
||||
|
||||
# 0.1.4
|
||||
* [Fix](https://github.com/reactiveops/polaris/issues/116): details pages getting template errors
|
||||
* [Fix](https://github.com/reactiveops/polaris/issues/114): support all auth providers
|
||||
* [Fix](https://github.com/reactiveops/polaris/issues/112): Ignore readiness probe for initContainers
|
||||
* [Fix](https://github.com/FairwindsOps/polaris/issues/116): details pages getting template errors
|
||||
* [Fix](https://github.com/FairwindsOps/polaris/issues/114): support all auth providers
|
||||
* [Fix](https://github.com/FairwindsOps/polaris/issues/112): Ignore readiness probe for initContainers
|
||||
|
||||
# 0.1.3
|
||||
* [Fix](https://github.com/reactiveops/polaris/issues/109): dashboard not updating when running persistently
|
||||
* [Fix](https://github.com/FairwindsOps/polaris/issues/109): dashboard not updating when running persistently
|
||||
|
||||
# 0.1.2
|
||||
* Stored all third-party assets (e.g. Charts.js) to local files to support offline dashboard viewing
|
||||
* Fix: custom configs in `ConfigMap` not respected
|
||||
|
||||
# 0.1.1
|
||||
* [Fix](https://github.com/reactiveops/polaris/issues/93): missing `config.yaml` and dashboard assets in binary releases
|
||||
* [Fix](https://github.com/FairwindsOps/polaris/issues/93): missing `config.yaml` and dashboard assets in binary releases
|
||||
* Added some tests and better error handling
|
||||
|
||||
# 0.1.0
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
* @bobby-brennan @endzyme @kimschles @robscott
|
||||
* @rbren @makoscafee @jordandoig @baderbuddy
|
||||
|
||||
+11
-12
@@ -7,7 +7,7 @@ Issues, whether bugs, tasks, or feature requests are essential for keeping Polar
|
||||
This project adheres to a [code of conduct](CODE_OF_CONDUCT.md). Please review this document before contributing to this project.
|
||||
|
||||
## Sign the CLA
|
||||
Before you can contribute, you will need to sign the [Contributor License Agreement](https://cla-assistant.io/reactiveops/polaris).
|
||||
Before you can contribute, you will need to sign the [Contributor License Agreement](https://cla-assistant.io/fairwindsops/polaris).
|
||||
|
||||
## Project Structure
|
||||
|
||||
@@ -15,7 +15,7 @@ Polaris is built on top of [controller-runtime](https://github.com/kubernetes-si
|
||||
|
||||
## Getting Started
|
||||
|
||||
We label issues with the ["good first issue" tag](https://github.com/reactiveops/polaris/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) if we believe they'll be a good starting point for new contributors. If you're interested in working on an issue, please start a conversation on that issue, and we can help answer any questions as they come up.
|
||||
We label issues with the ["good first issue" tag](https://github.com/FairwindsOps/polaris/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) if we believe they'll be a good starting point for new contributors. If you're interested in working on an issue, please start a conversation on that issue, and we can help answer any questions as they come up.
|
||||
|
||||
## Setting Up Your Development Environment
|
||||
### Prerequisites
|
||||
@@ -23,10 +23,10 @@ We label issues with the ["good first issue" tag](https://github.com/reactiveops
|
||||
* If you want to see the local changes you make on a Polaris dashboard, you will need access to a Kubernetes cluster defined in `~/.kube/config`
|
||||
|
||||
### Installation
|
||||
* Install the project with `go get github.com/reactiveops/polaris`
|
||||
* Change into the polaris directory which is installed at `$GOPATH/src/github.com/reactiveops/polaris`
|
||||
* 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.
|
||||
* Install the project with `go get github.com/fairwindsops/polaris`
|
||||
* Change into the polaris directory which is installed at `$GOPATH/src/github.com/fairwindsops/polaris`
|
||||
* See the dashboard with `go run main.go dashboard`, then open http://localhost:8080/
|
||||
* See the audit data `go run main.go audit`. This command shows the audit information on the command line.
|
||||
|
||||
## Running Tests
|
||||
|
||||
@@ -76,12 +76,12 @@ it needs to be a minor or major release in order to prevent breaking the Helm ch
|
||||
|
||||
### Minor/Major releases
|
||||
Minor and major releases need to change both this repository and the
|
||||
[Helm chart repo](https://github.com/reactiveops/charts/).
|
||||
[Helm chart repo](https://github.com/FairwindsOps/charts/).
|
||||
|
||||
The steps are:
|
||||
1. Modify the [Helm chart](https://github.com/reactiveops/charts/stable/polaris)
|
||||
1. Modify the [Helm chart](https://github.com/FairwindsOps/charts/stable/polaris)
|
||||
1. Clone the helm charts repo
|
||||
1. `git clone https://github.com/reactiveops/charts`
|
||||
1. `git clone https://github.com/FairwindsOps/charts`
|
||||
2. `git checkout -b yourname/update-polaris`
|
||||
1. Bump the version number in:
|
||||
1. stable/polaris/README.md
|
||||
@@ -94,9 +94,8 @@ The steps are:
|
||||
2. Bump the version number in:
|
||||
1. main.go
|
||||
2. README.md
|
||||
3. Regenerate the deployment files. Assuming you've cloned the charts repo to `./charts`:
|
||||
1. `helm template ./charts/stable/polaris/ --name polaris --namespace polaris --set templateOnly=true > deploy/dashboard.yaml`
|
||||
2. `helm template ./charts/stable/polaris/ --name polaris --namespace polaris --set templateOnly=true --set webhook.enable=true --set dashboard.enable=false > deploy/webhook.yaml`
|
||||
3. Regenerate the deployment files. Assuming you've cloned the charts repo to `~/git/charts`:
|
||||
1. `CHARTS_DIR=~/git/charts ./scripts/generate-deployment-files.sh`
|
||||
4. Update CHANGELOG.md
|
||||
5. Merge your PR
|
||||
3. Tag the latest branch for this repo
|
||||
|
||||
+16
-6
@@ -1,17 +1,27 @@
|
||||
FROM golang:1.12.4 AS build-env
|
||||
WORKDIR /go/src/github.com/reactiveops/polaris/
|
||||
FROM golang:1.13 AS build-env
|
||||
WORKDIR /go/src/github.com/fairwindsops/polaris/
|
||||
|
||||
ENV GO111MODULE=on
|
||||
ENV GOPROXY=https://proxy.golang.org
|
||||
ENV CGO_ENABLED=0
|
||||
ENV GOOS=linux
|
||||
ENV GOARCH=amd64
|
||||
|
||||
COPY go.mod .
|
||||
COPY go.sum .
|
||||
RUN go mod download
|
||||
RUN go get -u github.com/gobuffalo/packr/v2/packr2
|
||||
|
||||
COPY . .
|
||||
RUN go get -u github.com/gobuffalo/packr/v2/packr2
|
||||
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 packr2 build -a -o polaris *.go
|
||||
RUN packr2 build -a -o polaris *.go
|
||||
|
||||
FROM alpine:3.9
|
||||
FROM alpine:3.10
|
||||
WORKDIR /usr/local/bin
|
||||
RUN apk --no-cache add ca-certificates
|
||||
|
||||
RUN addgroup -S polaris && adduser -u 1200 -S polaris -G polaris
|
||||
USER 1200
|
||||
COPY --from=build-env /go/src/github.com/reactiveops/polaris/polaris .
|
||||
COPY --from=build-env /go/src/github.com/fairwindsops/polaris/polaris .
|
||||
|
||||
WORKDIR /opt/app
|
||||
|
||||
|
||||
Generated
-512
@@ -1,512 +0,0 @@
|
||||
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
||||
|
||||
|
||||
[[projects]]
|
||||
name = "cloud.google.com/go"
|
||||
packages = ["compute/metadata"]
|
||||
revision = "775730d6e48254a2430366162cf6298e5368833c"
|
||||
version = "v0.39.0"
|
||||
|
||||
[[projects]]
|
||||
name = "contrib.go.opencensus.io/exporter/ocagent"
|
||||
packages = ["."]
|
||||
revision = "dcb33c7f3b7cfe67e8a2cea10207ede1b7c40764"
|
||||
version = "v0.4.12"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/Azure/go-autorest"
|
||||
packages = ["autorest","autorest/adal","autorest/azure","autorest/date","logger","tracing"]
|
||||
revision = "fe1ebaab71ae2a2ab8a55f62ebe54cffd842acc2"
|
||||
version = "v12.0.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/appscode/jsonpatch"
|
||||
packages = ["."]
|
||||
revision = "7c0e3b262f30165a8ec3d0b4c6059fd92703bfb2"
|
||||
version = "1.0.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/beorn7/perks"
|
||||
packages = ["quantile"]
|
||||
revision = "4b2b341e8d7715fae06375aa633dbb6e91b3fb46"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/census-instrumentation/opencensus-proto"
|
||||
packages = ["gen-go/agent/common/v1","gen-go/agent/metrics/v1","gen-go/agent/trace/v1","gen-go/metrics/v1","gen-go/resource/v1","gen-go/trace/v1"]
|
||||
revision = "a105b96453fe85139acc07b68de48f2cbdd71249"
|
||||
version = "v0.2.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/davecgh/go-spew"
|
||||
packages = ["spew"]
|
||||
revision = "8991bc29aa16c548c550c7ff78260e27b9ab7c73"
|
||||
version = "v1.1.1"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/dgrijalva/jwt-go"
|
||||
packages = ["."]
|
||||
revision = "06ea1031745cb8b3dab3f6a236daf2b0aa468b7e"
|
||||
version = "v3.2.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/evanphx/json-patch"
|
||||
packages = ["."]
|
||||
revision = "5858425f75500d40c52783dce87d085a483ce135"
|
||||
version = "v4.2.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/go-logr/logr"
|
||||
packages = ["."]
|
||||
revision = "9fb12b3b21c5415d16ac18dc5cd42c1cfdd40c4e"
|
||||
version = "v0.1.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/go-logr/zapr"
|
||||
packages = ["."]
|
||||
revision = "03f06a783fbb7dfaf3f629c7825480e43a7105e6"
|
||||
version = "v0.1.1"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/gobuffalo/envy"
|
||||
packages = ["."]
|
||||
revision = "043cb4b8af871b49563291e32c66bb84378a60ac"
|
||||
version = "v1.7.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/gobuffalo/genny"
|
||||
packages = ["."]
|
||||
revision = "05dc5921e98daa5d406f90c2844ff163cb1e1c4e"
|
||||
version = "v0.1.1"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/gobuffalo/gogen"
|
||||
packages = [".","goimports","gomods"]
|
||||
revision = "5482e6ef5d999f8cfa9b038a53953c9b4ee090af"
|
||||
version = "v0.1.1"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/gobuffalo/logger"
|
||||
packages = ["."]
|
||||
revision = "86e12af44bc273e0063fad5b6ad588890b8cfe6b"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/gobuffalo/mapi"
|
||||
packages = ["."]
|
||||
revision = "0bb5e840be332d4280e40f2e6c50777c615bbac5"
|
||||
version = "v1.0.2"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/gobuffalo/packd"
|
||||
packages = ["."]
|
||||
revision = "9efbb667f2024a9370cadda45a57a0657c88a6fb"
|
||||
version = "v0.1.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/gobuffalo/packr"
|
||||
packages = ["v2","v2/file","v2/file/resolver","v2/file/resolver/encoding/hex","v2/jam/parser","v2/plog"]
|
||||
revision = "cda4ac25577350dac682c8715dc6659327232596"
|
||||
version = "v2.2.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/gobuffalo/syncx"
|
||||
packages = ["."]
|
||||
revision = "33c29581e754bd354236e977dfe426e55331c45d"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/gogo/protobuf"
|
||||
packages = ["proto","sortkeys"]
|
||||
revision = "ba06b47c162d49f2af050fb4c75bcbc86a159d5c"
|
||||
version = "v1.2.1"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/golang/groupcache"
|
||||
packages = ["lru"]
|
||||
revision = "5b532d6fd5efaf7fa130d4e859a2fde0fc3a9e1b"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/golang/protobuf"
|
||||
packages = ["jsonpb","proto","protoc-gen-go/descriptor","protoc-gen-go/generator","protoc-gen-go/generator/internal/remap","protoc-gen-go/plugin","ptypes","ptypes/any","ptypes/duration","ptypes/struct","ptypes/timestamp","ptypes/wrappers"]
|
||||
revision = "b5d812f8a3706043e23a9cd5babf2e5423744d30"
|
||||
version = "v1.3.1"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/google/btree"
|
||||
packages = ["."]
|
||||
revision = "4030bb1f1f0c35b30ca7009e9ebd06849dd45306"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/google/gofuzz"
|
||||
packages = ["."]
|
||||
revision = "f140a6486e521aad38f5917de355cbf147cc0496"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/google/uuid"
|
||||
packages = ["."]
|
||||
revision = "0cd6bf5da1e1c83f8b45653022c74f71af0538a4"
|
||||
version = "v1.1.1"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/googleapis/gnostic"
|
||||
packages = ["OpenAPIv2","compiler","extensions"]
|
||||
revision = "7c663266750e7d82587642f65e60bc4083f1f84e"
|
||||
version = "v0.2.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/gophercloud/gophercloud"
|
||||
packages = [".","openstack","openstack/identity/v2/tenants","openstack/identity/v2/tokens","openstack/identity/v3/tokens","openstack/utils","pagination"]
|
||||
revision = "b3a23cc94cc51857a508733a0bd3d6ef0bfc16d9"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/gorilla/mux"
|
||||
packages = ["."]
|
||||
revision = "ed099d42384823742bba0bf9a72b53b55c9e2e38"
|
||||
version = "v1.7.2"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/gregjones/httpcache"
|
||||
packages = [".","diskcache"]
|
||||
revision = "3befbb6ad0cc97d4c25d851e9528915809e1a22f"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/grpc-ecosystem/grpc-gateway"
|
||||
packages = ["internal","runtime","utilities"]
|
||||
revision = "8fd5fd9d19ce68183a6b0934519dfe7fe6269612"
|
||||
version = "v1.9.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/hashicorp/golang-lru"
|
||||
packages = [".","simplelru"]
|
||||
revision = "7087cb70de9f7a8bc0a10c375cb0d2280a8edf9c"
|
||||
version = "v0.5.1"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/imdario/mergo"
|
||||
packages = ["."]
|
||||
revision = "7c29201646fa3de8506f701213473dd407f19646"
|
||||
version = "v0.3.7"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/joho/godotenv"
|
||||
packages = ["."]
|
||||
revision = "23d116af351c84513e1946b527c88823e476be13"
|
||||
version = "v1.3.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/json-iterator/go"
|
||||
packages = ["."]
|
||||
revision = "0ff49de124c6f76f8494e194af75bde0f1a49a29"
|
||||
version = "v1.1.6"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/karrick/godirwalk"
|
||||
packages = ["."]
|
||||
revision = "5e617d9cfec6f870e63b168e9a2fb7c490a1c108"
|
||||
version = "v1.10.3"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/konsorten/go-windows-terminal-sequences"
|
||||
packages = ["."]
|
||||
revision = "f55edac94c9bbba5d6182a4be46d86a2c9b5b50e"
|
||||
version = "v1.0.2"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/markbates/oncer"
|
||||
packages = ["."]
|
||||
revision = "bf2de49a0be218916e69a11d22866e6cd0a560f2"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/markbates/safe"
|
||||
packages = ["."]
|
||||
revision = "6fea05a5732486546a4836b7a1d596c5ec687b98"
|
||||
version = "v1.0.1"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/matttproud/golang_protobuf_extensions"
|
||||
packages = ["pbutil"]
|
||||
revision = "c12348ce28de40eed0136aa2b644d0ee0650e56c"
|
||||
version = "v1.0.1"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/modern-go/concurrent"
|
||||
packages = ["."]
|
||||
revision = "bacd9c7ef1dd9b15be4a9909b8ac7a4e313eec94"
|
||||
version = "1.0.3"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/modern-go/reflect2"
|
||||
packages = ["."]
|
||||
revision = "4b7aa43c6742a2c18fdef89dd197aaae7dac7ccd"
|
||||
version = "1.0.1"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/pborman/uuid"
|
||||
packages = ["."]
|
||||
revision = "adf5a7427709b9deb95d29d3fa8a2bf9cfd388f1"
|
||||
version = "v1.2"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/petar/GoLLRB"
|
||||
packages = ["llrb"]
|
||||
revision = "33fb24c13b99c46c93183c291836c573ac382536"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/peterbourgon/diskv"
|
||||
packages = ["."]
|
||||
revision = "0be1b92a6df0e4f5cb0a5d15fb7f643d0ad93ce6"
|
||||
version = "v3.0.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/pkg/errors"
|
||||
packages = ["."]
|
||||
revision = "ba968bfe8b2f7e042a574c888954fccecfa385b4"
|
||||
version = "v0.8.1"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/pmezard/go-difflib"
|
||||
packages = ["difflib"]
|
||||
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/prometheus/client_golang"
|
||||
packages = ["prometheus","prometheus/internal","prometheus/promhttp"]
|
||||
revision = "50c4339db732beb2165735d2cde0bff78eb3c5a5"
|
||||
version = "v0.9.3"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/prometheus/client_model"
|
||||
packages = ["go"]
|
||||
revision = "fd36f4220a901265f90734c3183c5f0c91daa0b8"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/prometheus/common"
|
||||
packages = ["expfmt","internal/bitbucket.org/ww/goautoneg","model"]
|
||||
revision = "1ba88736f028e37bc17328369e94a537ae9e0234"
|
||||
version = "v0.4.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/prometheus/procfs"
|
||||
packages = [".","internal/fs"]
|
||||
revision = "9935e8e0588d26307d1d4a07bed7bd47271b6bbd"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/rogpeppe/go-internal"
|
||||
packages = ["modfile","module","semver"]
|
||||
revision = "438578804ca6f31be148c27683afc419ce47c06e"
|
||||
version = "v1.3.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/sirupsen/logrus"
|
||||
packages = ["."]
|
||||
revision = "839c75faf7f98a33d445d181f3018b5c3409a45e"
|
||||
version = "v1.4.2"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/spf13/pflag"
|
||||
packages = ["."]
|
||||
revision = "298182f68c66c05229eb03ac171abe6e309ee79a"
|
||||
version = "v1.0.3"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/stretchr/testify"
|
||||
packages = ["assert"]
|
||||
revision = "ffdc059bfe9ce6a4e144ba849dbedead332c6053"
|
||||
version = "v1.3.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "gitlab.com/golang-commonmark/html"
|
||||
packages = ["."]
|
||||
revision = "cfaf75183c4a5f0e44e3d51ece1065accafdd2b3"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "gitlab.com/golang-commonmark/linkify"
|
||||
packages = ["."]
|
||||
revision = "c22b7bdb117955ff2cc0e3724083d35e3419d71e"
|
||||
|
||||
[[projects]]
|
||||
name = "gitlab.com/golang-commonmark/markdown"
|
||||
packages = ["."]
|
||||
revision = "772775880e1fbf39a7804149ed11c0f714ec89fe"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "gitlab.com/golang-commonmark/mdurl"
|
||||
packages = ["."]
|
||||
revision = "e5bce34c34f29ecf232fec34f3982e55533ec14e"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "gitlab.com/golang-commonmark/puny"
|
||||
packages = ["."]
|
||||
revision = "2cd490539afe7c6fc0eda6c59ef88fa93a00ea0d"
|
||||
|
||||
[[projects]]
|
||||
name = "go.opencensus.io"
|
||||
packages = [".","internal","internal/tagencoding","metric/metricdata","metric/metricproducer","plugin/ocgrpc","plugin/ochttp","plugin/ochttp/propagation/b3","plugin/ochttp/propagation/tracecontext","resource","stats","stats/internal","stats/view","tag","trace","trace/internal","trace/propagation","trace/tracestate"]
|
||||
revision = "43463a80402d8447b7fce0d2c58edf1687ff0b58"
|
||||
version = "v0.19.3"
|
||||
|
||||
[[projects]]
|
||||
name = "go.uber.org/atomic"
|
||||
packages = ["."]
|
||||
revision = "df976f2515e274675050de7b3f42545de80594fd"
|
||||
version = "v1.4.0"
|
||||
|
||||
[[projects]]
|
||||
name = "go.uber.org/multierr"
|
||||
packages = ["."]
|
||||
revision = "3c4937480c32f4c13a875a1829af76c98ca3d40a"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
name = "go.uber.org/zap"
|
||||
packages = [".","buffer","internal/bufferpool","internal/color","internal/exit","zapcore"]
|
||||
revision = "27376062155ad36be76b0f12cf1572a221d3a48c"
|
||||
version = "v1.10.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/crypto"
|
||||
packages = ["ssh/terminal"]
|
||||
revision = "22d7a77e9e5f409e934ed268692e56707cd169e5"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/net"
|
||||
packages = ["context","context/ctxhttp","http/httpguts","http2","http2/hpack","idna","internal/timeseries","trace"]
|
||||
revision = "3ec19112720433827bbce8be9342797f5a6aaaf9"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/oauth2"
|
||||
packages = [".","google","internal","jws","jwt"]
|
||||
revision = "950ef44c6e079baf075030377d90bf0c7e4b7b7a"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/sync"
|
||||
packages = ["semaphore"]
|
||||
revision = "112230192c580c3556b8cee6403af37a4fc5f28c"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/sys"
|
||||
packages = ["unix","windows"]
|
||||
revision = "ad400b1274690a55531a013560dc08706088f82b"
|
||||
|
||||
[[projects]]
|
||||
name = "golang.org/x/text"
|
||||
packages = ["collate","collate/build","internal/colltab","internal/gen","internal/language","internal/language/compact","internal/tag","internal/triegen","internal/ucd","language","secure/bidirule","transform","unicode/bidi","unicode/cldr","unicode/norm","unicode/rangetable"]
|
||||
revision = "342b2e1fbaa52c93f31447ad2c6abc048c63e475"
|
||||
version = "v0.3.2"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/time"
|
||||
packages = ["rate"]
|
||||
revision = "9d24e82272b4f38b78bc8cff74fa936d31ccd8ef"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/tools"
|
||||
packages = ["go/ast/astutil","go/gcexportdata","go/internal/gcimporter","go/internal/packagesdriver","go/packages","go/types/typeutil","imports","internal/fastwalk","internal/gopathwalk","internal/module","internal/semver"]
|
||||
revision = "d88f79806bbd013f54a668506864ce559edf6f0a"
|
||||
|
||||
[[projects]]
|
||||
name = "google.golang.org/api"
|
||||
packages = ["support/bundler"]
|
||||
revision = "721295fe20d585ce7e948146f82188429d14da33"
|
||||
version = "v0.5.0"
|
||||
|
||||
[[projects]]
|
||||
name = "google.golang.org/appengine"
|
||||
packages = [".","internal","internal/app_identity","internal/base","internal/datastore","internal/log","internal/modules","internal/remote_api","internal/urlfetch","urlfetch"]
|
||||
revision = "4c25cacc810c02874000e4f7071286a8e96b2515"
|
||||
version = "v1.6.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "google.golang.org/genproto"
|
||||
packages = ["googleapis/api/httpbody","googleapis/rpc/status","protobuf/field_mask"]
|
||||
revision = "bb713bdc0e5239f2b68e560efbe1c701a6fe78f9"
|
||||
|
||||
[[projects]]
|
||||
name = "google.golang.org/grpc"
|
||||
packages = [".","balancer","balancer/base","balancer/roundrobin","binarylog/grpc_binarylog_v1","codes","connectivity","credentials","credentials/internal","encoding","encoding/proto","grpclog","internal","internal/backoff","internal/balancerload","internal/binarylog","internal/channelz","internal/envconfig","internal/grpcrand","internal/grpcsync","internal/syscall","internal/transport","keepalive","metadata","naming","peer","resolver","resolver/dns","resolver/passthrough","stats","status","tap"]
|
||||
revision = "25c4f928eaa6d96443009bd842389fb4fa48664e"
|
||||
version = "v1.20.1"
|
||||
|
||||
[[projects]]
|
||||
name = "gopkg.in/inf.v0"
|
||||
packages = ["."]
|
||||
revision = "d2d2541c53f18d2a059457998ce2876cc8e67cbf"
|
||||
version = "v0.9.1"
|
||||
|
||||
[[projects]]
|
||||
name = "gopkg.in/yaml.v2"
|
||||
packages = ["."]
|
||||
revision = "51d6538a90f86fe93ac480b35f37b2be17fef232"
|
||||
version = "v2.2.2"
|
||||
|
||||
[[projects]]
|
||||
name = "k8s.io/api"
|
||||
packages = ["admission/v1beta1","admissionregistration/v1alpha1","admissionregistration/v1beta1","apps/v1","apps/v1beta1","apps/v1beta2","auditregistration/v1alpha1","authentication/v1","authentication/v1beta1","authorization/v1","authorization/v1beta1","autoscaling/v1","autoscaling/v2beta1","autoscaling/v2beta2","batch/v1","batch/v1beta1","batch/v2alpha1","certificates/v1beta1","coordination/v1beta1","core/v1","events/v1beta1","extensions/v1beta1","networking/v1","policy/v1beta1","rbac/v1","rbac/v1alpha1","rbac/v1beta1","scheduling/v1alpha1","scheduling/v1beta1","settings/v1alpha1","storage/v1","storage/v1alpha1","storage/v1beta1"]
|
||||
revision = "05914d821849570fba9eacfb29466f2d8d3cd229"
|
||||
version = "kubernetes-1.13.1"
|
||||
|
||||
[[projects]]
|
||||
name = "k8s.io/apimachinery"
|
||||
packages = ["pkg/api/errors","pkg/api/meta","pkg/api/resource","pkg/apis/meta/internalversion","pkg/apis/meta/v1","pkg/apis/meta/v1/unstructured","pkg/apis/meta/v1beta1","pkg/conversion","pkg/conversion/queryparams","pkg/fields","pkg/labels","pkg/runtime","pkg/runtime/schema","pkg/runtime/serializer","pkg/runtime/serializer/json","pkg/runtime/serializer/protobuf","pkg/runtime/serializer/recognizer","pkg/runtime/serializer/streaming","pkg/runtime/serializer/versioning","pkg/selection","pkg/types","pkg/util/cache","pkg/util/clock","pkg/util/diff","pkg/util/errors","pkg/util/framer","pkg/util/intstr","pkg/util/json","pkg/util/mergepatch","pkg/util/naming","pkg/util/net","pkg/util/runtime","pkg/util/sets","pkg/util/strategicpatch","pkg/util/uuid","pkg/util/validation","pkg/util/validation/field","pkg/util/wait","pkg/util/yaml","pkg/version","pkg/watch","third_party/forked/golang/json","third_party/forked/golang/reflect"]
|
||||
revision = "2b1284ed4c93a43499e781493253e2ac5959c4fd"
|
||||
version = "kubernetes-1.13.0"
|
||||
|
||||
[[projects]]
|
||||
name = "k8s.io/client-go"
|
||||
packages = ["discovery","discovery/fake","dynamic","kubernetes","kubernetes/fake","kubernetes/scheme","kubernetes/typed/admissionregistration/v1alpha1","kubernetes/typed/admissionregistration/v1alpha1/fake","kubernetes/typed/admissionregistration/v1beta1","kubernetes/typed/admissionregistration/v1beta1/fake","kubernetes/typed/apps/v1","kubernetes/typed/apps/v1/fake","kubernetes/typed/apps/v1beta1","kubernetes/typed/apps/v1beta1/fake","kubernetes/typed/apps/v1beta2","kubernetes/typed/apps/v1beta2/fake","kubernetes/typed/auditregistration/v1alpha1","kubernetes/typed/auditregistration/v1alpha1/fake","kubernetes/typed/authentication/v1","kubernetes/typed/authentication/v1/fake","kubernetes/typed/authentication/v1beta1","kubernetes/typed/authentication/v1beta1/fake","kubernetes/typed/authorization/v1","kubernetes/typed/authorization/v1/fake","kubernetes/typed/authorization/v1beta1","kubernetes/typed/authorization/v1beta1/fake","kubernetes/typed/autoscaling/v1","kubernetes/typed/autoscaling/v1/fake","kubernetes/typed/autoscaling/v2beta1","kubernetes/typed/autoscaling/v2beta1/fake","kubernetes/typed/autoscaling/v2beta2","kubernetes/typed/autoscaling/v2beta2/fake","kubernetes/typed/batch/v1","kubernetes/typed/batch/v1/fake","kubernetes/typed/batch/v1beta1","kubernetes/typed/batch/v1beta1/fake","kubernetes/typed/batch/v2alpha1","kubernetes/typed/batch/v2alpha1/fake","kubernetes/typed/certificates/v1beta1","kubernetes/typed/certificates/v1beta1/fake","kubernetes/typed/coordination/v1beta1","kubernetes/typed/coordination/v1beta1/fake","kubernetes/typed/core/v1","kubernetes/typed/core/v1/fake","kubernetes/typed/events/v1beta1","kubernetes/typed/events/v1beta1/fake","kubernetes/typed/extensions/v1beta1","kubernetes/typed/extensions/v1beta1/fake","kubernetes/typed/networking/v1","kubernetes/typed/networking/v1/fake","kubernetes/typed/policy/v1beta1","kubernetes/typed/policy/v1beta1/fake","kubernetes/typed/rbac/v1","kubernetes/typed/rbac/v1/fake","kubernetes/typed/rbac/v1alpha1","kubernetes/typed/rbac/v1alpha1/fake","kubernetes/typed/rbac/v1beta1","kubernetes/typed/rbac/v1beta1/fake","kubernetes/typed/scheduling/v1alpha1","kubernetes/typed/scheduling/v1alpha1/fake","kubernetes/typed/scheduling/v1beta1","kubernetes/typed/scheduling/v1beta1/fake","kubernetes/typed/settings/v1alpha1","kubernetes/typed/settings/v1alpha1/fake","kubernetes/typed/storage/v1","kubernetes/typed/storage/v1/fake","kubernetes/typed/storage/v1alpha1","kubernetes/typed/storage/v1alpha1/fake","kubernetes/typed/storage/v1beta1","kubernetes/typed/storage/v1beta1/fake","pkg/apis/clientauthentication","pkg/apis/clientauthentication/v1alpha1","pkg/apis/clientauthentication/v1beta1","pkg/version","plugin/pkg/client/auth","plugin/pkg/client/auth/azure","plugin/pkg/client/auth/exec","plugin/pkg/client/auth/gcp","plugin/pkg/client/auth/oidc","plugin/pkg/client/auth/openstack","rest","rest/watch","restmapper","testing","third_party/forked/golang/template","tools/auth","tools/cache","tools/clientcmd","tools/clientcmd/api","tools/clientcmd/api/latest","tools/clientcmd/api/v1","tools/leaderelection","tools/leaderelection/resourcelock","tools/metrics","tools/pager","tools/record","tools/reference","transport","util/buffer","util/cert","util/connrotation","util/flowcontrol","util/homedir","util/integer","util/jsonpath","util/retry","util/workqueue"]
|
||||
revision = "8d9ed539ba3134352c586810e749e58df4e94e4f"
|
||||
version = "kubernetes-1.13.1"
|
||||
|
||||
[[projects]]
|
||||
name = "k8s.io/klog"
|
||||
packages = ["."]
|
||||
revision = "e531227889390a39d9533dde61f590fe9f4b0035"
|
||||
version = "v0.3.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "k8s.io/kube-openapi"
|
||||
packages = ["pkg/util/proto"]
|
||||
revision = "a01b7d5d6c2258c80a4a10070f3dee9cd575d9c7"
|
||||
|
||||
[[projects]]
|
||||
name = "sigs.k8s.io/controller-runtime"
|
||||
packages = ["pkg/cache","pkg/cache/internal","pkg/client","pkg/client/apiutil","pkg/client/config","pkg/internal/recorder","pkg/leaderelection","pkg/manager","pkg/metrics","pkg/patch","pkg/recorder","pkg/runtime/inject","pkg/runtime/log","pkg/runtime/signals","pkg/webhook","pkg/webhook/admission","pkg/webhook/admission/builder","pkg/webhook/admission/types","pkg/webhook/internal/cert","pkg/webhook/internal/cert/generator","pkg/webhook/internal/cert/writer","pkg/webhook/internal/cert/writer/atomic","pkg/webhook/internal/metrics","pkg/webhook/types"]
|
||||
revision = "12d98582e72927b6cd0123e2b4e819f9341ce62c"
|
||||
version = "v0.1.10"
|
||||
|
||||
[[projects]]
|
||||
name = "sigs.k8s.io/yaml"
|
||||
packages = ["."]
|
||||
revision = "fd68e9863619f6ec2fdd8625fe1f02e7c877e480"
|
||||
version = "v1.1.0"
|
||||
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "205aba713edfc3b46886718dc86dbc24edc9e9758efd16724c2cdaa4256e6d50"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
-62
@@ -1,62 +0,0 @@
|
||||
# Gopkg.toml example
|
||||
#
|
||||
# Refer to https://golang.github.io/dep/docs/Gopkg.toml.html
|
||||
# for detailed Gopkg.toml documentation.
|
||||
#
|
||||
# required = ["github.com/user/thing/cmd/thing"]
|
||||
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
|
||||
#
|
||||
# [[constraint]]
|
||||
# name = "github.com/user/project"
|
||||
# version = "1.0.0"
|
||||
#
|
||||
# [[constraint]]
|
||||
# name = "github.com/user/project2"
|
||||
# branch = "dev"
|
||||
# source = "github.com/myfork/project2"
|
||||
#
|
||||
# [[override]]
|
||||
# name = "github.com/x/y"
|
||||
# version = "2.4.0"
|
||||
#
|
||||
# [prune]
|
||||
# non-go = false
|
||||
# go-tests = true
|
||||
# unused-packages = true
|
||||
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/stretchr/testify"
|
||||
version = "1.3.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "k8s.io/api"
|
||||
version = "kubernetes-1.13.1"
|
||||
|
||||
[[constraint]]
|
||||
name = "k8s.io/apimachinery"
|
||||
version = "kubernetes-1.13.1"
|
||||
|
||||
[[constraint]]
|
||||
name = "k8s.io/client-go"
|
||||
version = "kubernetes-1.13.1"
|
||||
|
||||
[[constraint]]
|
||||
name = "sigs.k8s.io/controller-runtime"
|
||||
version = "0.1.10"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/gobuffalo/packr"
|
||||
version = "2.2.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/gorilla/mux"
|
||||
version = "1.7.1"
|
||||
|
||||
[[constraint]]
|
||||
revision = "772775880e1fbf39a7804149ed11c0f714ec89fe"
|
||||
name = "gitlab.com/golang-commonmark/markdown"
|
||||
|
||||
[[constraint]]
|
||||
revision = "v1.3.1"
|
||||
name = "github.com/golang/protobuf"
|
||||
@@ -186,7 +186,7 @@
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2019 ReactiveOps
|
||||
Copyright 2019 FairwindsOps Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<div align="center">
|
||||
<img src="/pkg/dashboard/assets/images/polaris-logo.png" alt="Polaris Logo" />
|
||||
<img src="/img/polaris-logo.png" alt="Polaris Logo" />
|
||||
<br>
|
||||
|
||||
[![Version][version-image]][version-link] [![CircleCI][circleci-image]][circleci-link] [![Go Report Card][goreport-image]][goreport-link]
|
||||
</div>
|
||||
|
||||
[version-image]: https://img.shields.io/static/v1.svg?label=Version&message=0.3.1&color=239922
|
||||
[version-image]: https://img.shields.io/static/v1.svg?label=Version&message=1.2.0&color=239922
|
||||
[version-link]: https://github.com/FairwindsOps/polaris
|
||||
|
||||
[goreport-image]: https://goreportcard.com/badge/github.com/FairwindsOps/polaris
|
||||
@@ -14,190 +14,71 @@
|
||||
[circleci-image]: https://circleci.com/gh/FairwindsOps/polaris.svg?style=svg
|
||||
[circleci-link]: https://circleci.com/gh/FairwindsOps/polaris.svg
|
||||
|
||||
Polaris helps keep your cluster healthy. It runs a variety of checks to ensure that
|
||||
Kubernetes deployments are configured using best practices, helping you avoid
|
||||
Fairwinds' Polaris keeps your clusters sailing smoothly. It runs a variety of checks to ensure that
|
||||
Kubernetes pods and controllers are configured using best practices, helping you avoid
|
||||
problems in the future. Polaris can be run in a few different modes:
|
||||
|
||||
- A dashboard that provides an overview of how well current deployments are configured within a cluster.
|
||||
- An experimental validating webhook that can prevent any future deployments that do not live up to a configured standard.
|
||||
- A command-line audit that can be incorporated into your CI/CD pipeline
|
||||
Polaris can be run in three different modes:
|
||||
* As a [dashboard](#dashboard), so you can audit what's running inside your cluster.
|
||||
* As a [validating webhook](#webhook), so you can automatically reject workloads that don't adhere to your organization's policies.
|
||||
* As a [command-line tool](#cli), so you can test local YAML files, e.g. as part of a CI/CD process.
|
||||
|
||||
**Want to learn more?** ReactiveOps holds [office hours on Zoom](https://zoom.us/j/242508205) the first Friday of every month, at 12pm Eastern. You can also reach out via email at `opensource@fairwinds.com`
|
||||
**Want to learn more?** Reach out on [the Slack channel](https://fairwindscommunity.slack.com/messages/polaris) ([request invite](https://join.slack.com/t/fairwindscommunity/shared_invite/zt-e3c6vj4l-3lIH6dvKqzWII5fSSFDi1g)), send an email to `opensource@fairwinds.com`, or join us for [office hours on Zoom](https://fairwindscommunity.slack.com/messages/office-hours)
|
||||
|
||||
## Quickstart
|
||||
# Dashboard Quickstart
|
||||
|
||||
```
|
||||
kubectl apply -f https://github.com/reactiveops/polaris/releases/latest/download/dashboard.yaml
|
||||
```bash
|
||||
kubectl apply -f https://github.com/FairwindsOps/polaris/releases/latest/download/dashboard.yaml
|
||||
kubectl port-forward --namespace polaris svc/polaris-dashboard 8080:80
|
||||
```
|
||||
With the port forwarding in place, you can open http://localhost:8080 in your browser to view the dashboard.
|
||||
|
||||
## Dashboard
|
||||
* * *
|
||||
|
||||
The Polaris dashboard is a way to get a simple visual overview of the current state of your Kubernetes deployments as well as a roadmap for what can be improved. The dashboard provides a cluster wide overview as well as breaking out results by category, namespace, and deployment.
|
||||
# Components
|
||||
|
||||
## Dashboard
|
||||
> [View installation instructions](docs/usage.md#dashboard)
|
||||
|
||||
The Polaris dashboard is a way to get a simple visual overview of the current state of your Kubernetes workloads as well as a roadmap for what can be improved. The dashboard provides a cluster wide overview as well as breaking out results by category, namespace, and workload.
|
||||
|
||||
<p align="center">
|
||||
<img src="/dashboard-screenshot.png" alt="Polaris Dashboard" />
|
||||
<img src="/img/dashboard-screenshot.png" alt="Polaris Dashboard" width="550"/>
|
||||
</p>
|
||||
|
||||
Our default standards in Polaris are rather high, so don’t be surprised if your score is lower than you might expect. A key goal for Polaris was to set a high standard and aim for great configuration by default. If the defaults we’ve included are too strict, it’s easy to adjust the configuration as part of the deployment configuration to better suit your workloads.
|
||||
|
||||
## 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.*
|
||||
## Admission Controller: Validating Webhook
|
||||
> [View installation instructions](docs/usage.md#webhook)
|
||||
|
||||
Unfortunately we have not found a way to display warnings as part of `kubectl` output unless we are rejecting a deployment altogether. That means that any checks with a severity of `warning` will still pass webhook validation, and the only evidence of that warning will either be in the Polaris dashboard or the Polaris webhook logs.
|
||||
Polaris can be run as an admission controller that acts as a validating webhook. This accepts the same configuration as the dashboard, and can run the same validations. This webhook will reject any workloads that trigger a danger-level check. This is indicative of the greater goal of Polaris, not just to encourage better configuration through dashboard visibility, but to actually enforce it with this webhook. Polaris will not fix your workloads, only block them.
|
||||
|
||||
## 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.
|
||||
The webhook only supports a fixed set of controller types. You can see the
|
||||
[list of supported controllers here](https://github.com/FairwindsOps/polaris/blob/master/cmd/polaris/webhook.go#L40)
|
||||
|
||||
### kubectl
|
||||
#### Dashboard
|
||||
```
|
||||
kubectl apply -f https://github.com/reactiveops/polaris/releases/latest/download/dashboard.yaml
|
||||
kubectl port-forward --namespace polaris svc/polaris-dashboard 8080:80
|
||||
```
|
||||
Unfortunately we have not found a way to display warnings as part of `kubectl` output unless we are rejecting a workload altogether. That means that any checks with a severity of `warning` will still pass webhook validation, and the only evidence of that warning will either be in the Polaris dashboard or the Polaris webhook logs.
|
||||
|
||||
#### Webhook
|
||||
```
|
||||
kubectl apply -f https://github.com/reactiveops/polaris/releases/latest/download/webhook.yaml
|
||||
```
|
||||
## CLI
|
||||
> [View installation instructions](docs/usage.md#cli)
|
||||
|
||||
### Helm
|
||||
Start by adding the ReactiveOps Helm repo:
|
||||
```
|
||||
helm repo add reactiveops-stable https://charts.reactiveops.com/stable
|
||||
```
|
||||
Polaris can also be used on the command line, either to audit local files or a running cluster.
|
||||
This is particularly helpful for running Polaris against your infrastructure-as-code as part of a
|
||||
CI/CD pipeline. Use the available [command line flags](docs/usage.md#running-with-ci-cd)
|
||||
to cause CI/CD to fail if your Polaris score drops below a certain threshold, or if any danger-level issues arise.
|
||||
|
||||
#### Dashboard
|
||||
```
|
||||
helm upgrade --install polaris reactiveops-stable/polaris --namespace polaris
|
||||
kubectl port-forward --namespace polaris svc/polaris-dashboard 8080:80
|
||||
```
|
||||
# Installation and Usage
|
||||
See the [Usage Guide](/docs/usage.md) for details on different methods for installing and using Polaris.
|
||||
|
||||
#### Webhook
|
||||
```
|
||||
helm upgrade --install polaris reactiveops-stable/polaris --namespace polaris \
|
||||
--set webhook.enable=true --set dashboard.enable=false
|
||||
```
|
||||
|
||||
### Local Binary
|
||||
#### Installation
|
||||
Binary releases are available on the [releases page](https://github.com/reactiveops/polaris/releases) or can be installed with [Homebrew](https://brew.sh/):
|
||||
```
|
||||
brew tap reactiveops/tap
|
||||
brew install reactiveops/tap/polaris
|
||||
polaris --version
|
||||
```
|
||||
|
||||
You can run `polaris --help` to see a full list of options.
|
||||
|
||||
#### Dashboard
|
||||
The dashboard can be run on your local machine, without installing anything on the cluster.
|
||||
Polaris will use your local kubeconfig to connect to the cluster.
|
||||
|
||||
```
|
||||
polaris --dashboard --dashboard-port 8080
|
||||
```
|
||||
|
||||
#### Audits
|
||||
You can also run audits on the command line and see the output as JSON, YAML, or a raw score:
|
||||
```
|
||||
polaris --audit --output-format yaml > report.yaml
|
||||
polaris --audit --output-format score
|
||||
# 92
|
||||
```
|
||||
|
||||
Both the dashboard and audits can run against a local directory or YAML file
|
||||
rather than a cluster:
|
||||
```
|
||||
polaris --audit --audit-path ./deploy/
|
||||
```
|
||||
|
||||
##### Running with CI/CD
|
||||
You can integrate Polaris into CI/CD for repositories containing infrastructure-as-code.
|
||||
For example, to fail if polaris detects *any* error-level issues, or if the score drops below 90%:
|
||||
```bash
|
||||
polaris --audit --audit-path ./deploy/ \
|
||||
--set-exit-code-on-error \
|
||||
--set-exit-code-below-score 90
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Polaris supports a wide range of validations covering a number of Kubernetes best practices. Here's a sample configuration file that includes all currently supported checks. The [default configuration](https://github.com/reactiveops/polaris/blob/master/examples/config.yaml) contains a number of those checks. This repository also includes a sample [full configuration file](https://github.com/reactiveops/polaris/blob/master/examples/config-full.yaml) that enables all available checks.
|
||||
|
||||
Each check can be assigned a `severity`. Only checks with a severity of `error` or `warning` will be validated. The results of these validations are visible on the dashboard. In the case of the validating webhook, only failures with a severity of `error` will result in a change being rejected.
|
||||
|
||||
Polaris validation checks fall into several different categories:
|
||||
|
||||
- [Health Checks](docs/health-checks.md)
|
||||
- [Images](docs/images.md)
|
||||
- [Networking](docs/networking.md)
|
||||
- [Resources](docs/resources.md)
|
||||
- [Security](docs/security.md)
|
||||
|
||||
## CLI Options
|
||||
|
||||
```
|
||||
# high-level flags
|
||||
-version
|
||||
Prints the version of Polaris
|
||||
-config string
|
||||
Location of Polaris configuration file
|
||||
-kubeconfig string
|
||||
Path to a kubeconfig. Only required if out-of-cluster.
|
||||
-log-level string
|
||||
Logrus log level (default "info")
|
||||
-master string
|
||||
The address of the Kubernetes API server. Overrides any value in kubeconfig. Only required if out-of-cluster.
|
||||
|
||||
# dashboard flags
|
||||
-dashboard
|
||||
Runs the webserver for Polaris dashboard.
|
||||
-dashboard-base-path string
|
||||
Path on which the dashboard is served (default "/")
|
||||
-dashboard-port int
|
||||
Port for the dashboard webserver (default 8080)
|
||||
-display-name string
|
||||
An optional identifier for the audit
|
||||
|
||||
# audit flags
|
||||
-audit
|
||||
Runs a one-time audit.
|
||||
-audit-path string
|
||||
If specified, audits one or more YAML files instead of a cluster
|
||||
-output-file string
|
||||
Destination file for audit results
|
||||
-output-format string
|
||||
Output format for results - json, yaml, or score (default "json")
|
||||
-output-url string
|
||||
Destination URL to send audit results
|
||||
-set-exit-code-below-score int
|
||||
When running with --audit, set an exit code of 4 when the score is below this threshold (1-100)
|
||||
-set-exit-code-on-error
|
||||
When running with --audit, set an exit code of 3 when the audit contains error-level issues.
|
||||
|
||||
# webhook flags
|
||||
-webhook
|
||||
Runs the webhook webserver.
|
||||
-webhook-port int
|
||||
Port for the webhook webserver (default 9876)
|
||||
-disable-webhook-config-installer
|
||||
disable the installer in the webhook server, so it won't install webhook configuration resources during bootstrapping
|
||||
```
|
||||
|
||||
## Contributing
|
||||
# Contributing
|
||||
PRs welcome! Check out the [Contributing Guidelines](CONTRIBUTING.md),
|
||||
[Code of Conduct](CODE_OF_CONDUCT.md), and [Roadmap](ROADMAP.md) for more information.
|
||||
|
||||
## Further Information
|
||||
# Further Information
|
||||
A history of changes to this project can be viewed in the [Changelog](CHANGELOG.md)
|
||||
|
||||
If you'd like to learn more about Polaris, or if you'd like to speak with
|
||||
a Kubernetes expert, you can contact `info@fairwinds.com` or [visit our website](https://fairwinds.com)
|
||||
|
||||
## License
|
||||
# License
|
||||
Apache License 2.0
|
||||
|
||||
+16
-50
@@ -5,55 +5,21 @@ as well as the project's overall functionality.
|
||||
If you have ideas for a new check, or for new Polaris features,
|
||||
you can submit a pull request for this file, or open an issue in GitHub.
|
||||
|
||||
## Q3 2019
|
||||
Below is a list of work we plan to get done this quarter. Some more details can be found under
|
||||
[Future Work](#future_work), or
|
||||
[in the milestone](https://github.com/FairwindsOps/polaris/milestone/1)
|
||||
* Rule exceptions - we'd like to provide a way of overriding Polaris checks on individual controllers, e.g. through annotations
|
||||
* More controller types - we plan to implement checks for more than just deployments
|
||||
* Image repo checks - we plan to provide a check that ensures all repos conform to a set of user-defined patterns
|
||||
* OPA integration (investigate only) - we plan to investigate the potential use of OPA to let users define custom Polaris checks
|
||||
|
||||
## Future Work
|
||||
### Validating Webhook
|
||||
The validating webhook rejects incoming workloads if they cause `error`-level
|
||||
issues, as defined by the Polaris configuration.
|
||||
|
||||
The webhook server is currently functional, but largely untested. Because
|
||||
the validating webhook has the potential to cause headaches for users,
|
||||
we have marked it as `beta` for now.
|
||||
|
||||
We will continue to gather feedback here and will move the webhook out of `beta`
|
||||
when we feel it's ready.
|
||||
|
||||
#### TODO
|
||||
* Pass `warning`-level messages back to `kubectl` if possible (may require changes
|
||||
to core k8s or `kubectl`)
|
||||
|
||||
### Ability to override checks
|
||||
We plan to add the ability to override Polaris checks for particular namespaces
|
||||
and workloads. This may be something that is set in the Polaris configuration,
|
||||
or an annotation that can be added to the resource definition.
|
||||
|
||||
This is important, as certain workloads have good reason to skip particular Polaris
|
||||
checks - for instance, the `dns-controller` deployment in `kube-system` needs to have
|
||||
the host network configured.
|
||||
|
||||
### Images Registries Check
|
||||
We'd like to allow users to restrict images to a list of allowed/disallowed registries.
|
||||
We'll likely allow patterns, e.g. `*.dkr.ecr.*.amazonaws.com`
|
||||
|
||||
### New controller types
|
||||
Currently we only look at Deployments. We plan to also validate these types of controllers:
|
||||
* StatefulSet
|
||||
* DaemonSet
|
||||
* Job
|
||||
* CronJob
|
||||
* ReplicaSet
|
||||
|
||||
### Check non-controllers
|
||||
## Check non-controllers
|
||||
We would like to implement checks for non-controller types, such as:
|
||||
* Ingress
|
||||
* Service
|
||||
* Secret
|
||||
* Ingresses
|
||||
* Services
|
||||
* RBAC roles/bindings
|
||||
|
||||
## Expand list of checks
|
||||
We'd like to increase our library of checks. Not all checks need to be enabled by default,
|
||||
so anything that would be useful across different organizations is worth checking in.
|
||||
|
||||
## Support more controllers in the validating webhook
|
||||
Currently the webhook only checks for a fixed set of controllers.
|
||||
|
||||
While we can't listen for _all_ possible controller types, we can block Pods that violate policy.
|
||||
We've chosen not to do this for now, because it could prevent existing controllers from scaling.
|
||||
|
||||
However, there may be a way to check for the owner's age, in order to determine if it's a new
|
||||
controller or a pre-existing controller.
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
successMessage: CPU limits are set
|
||||
failureMessage: CPU limits should be set
|
||||
category: Resources
|
||||
target: Container
|
||||
containers:
|
||||
exclude:
|
||||
- initContainer
|
||||
schema:
|
||||
'$schema': http://json-schema.org/draft-07/schema
|
||||
type: object
|
||||
required:
|
||||
- resources
|
||||
properties:
|
||||
resources:
|
||||
type: object
|
||||
required:
|
||||
- limits
|
||||
properties:
|
||||
limits:
|
||||
type: object
|
||||
required:
|
||||
- cpu
|
||||
properties:
|
||||
cpu:
|
||||
type: string
|
||||
not:
|
||||
const: ''
|
||||
@@ -0,0 +1,27 @@
|
||||
successMessage: CPU requests are set
|
||||
failureMessage: CPU requests should be set
|
||||
category: Resources
|
||||
target: Container
|
||||
containers:
|
||||
exclude:
|
||||
- initContainer
|
||||
schema:
|
||||
'$schema': http://json-schema.org/draft-07/schema
|
||||
type: object
|
||||
required:
|
||||
- resources
|
||||
properties:
|
||||
resources:
|
||||
type: object
|
||||
required:
|
||||
- requests
|
||||
properties:
|
||||
requests:
|
||||
type: object
|
||||
required:
|
||||
- cpu
|
||||
properties:
|
||||
cpu:
|
||||
type: string
|
||||
not:
|
||||
const: ''
|
||||
@@ -0,0 +1,25 @@
|
||||
successMessage: Container does not have any dangerous capabilities
|
||||
failureMessage: Container should not have dangerous capabilities
|
||||
category: Security
|
||||
target: Container
|
||||
schema:
|
||||
'$schema': http://json-schema.org/draft-07/schema
|
||||
type: object
|
||||
properties:
|
||||
securityContext:
|
||||
type: object
|
||||
properties:
|
||||
capabilities:
|
||||
type: object
|
||||
properties:
|
||||
add:
|
||||
type: array
|
||||
not:
|
||||
contains:
|
||||
const: ALL
|
||||
not:
|
||||
contains:
|
||||
const: SYS_ADMIN
|
||||
not:
|
||||
contains:
|
||||
const: NET_ADMIN
|
||||
@@ -0,0 +1,11 @@
|
||||
successMessage: Host IPC is not configured
|
||||
failureMessage: Host IPC should not be configured
|
||||
category: Security
|
||||
target: Pod
|
||||
schema:
|
||||
'$schema': http://json-schema.org/draft-07/schema
|
||||
type: object
|
||||
properties:
|
||||
hostIPC:
|
||||
not:
|
||||
const: true
|
||||
@@ -0,0 +1,11 @@
|
||||
successMessage: Host network is not configured
|
||||
failureMessage: Host network should not be configured
|
||||
category: Networking
|
||||
target: Pod
|
||||
schema:
|
||||
'$schema': http://json-schema.org/draft-07/schema
|
||||
type: object
|
||||
properties:
|
||||
hostNetwork:
|
||||
not:
|
||||
const: true
|
||||
@@ -0,0 +1,11 @@
|
||||
successMessage: Host PID is not configured
|
||||
failureMessage: Host PID should not be configured
|
||||
category: Security
|
||||
target: Pod
|
||||
schema:
|
||||
'$schema': http://json-schema.org/draft-07/schema
|
||||
type: object
|
||||
properties:
|
||||
hostPID:
|
||||
not:
|
||||
const: true
|
||||
@@ -0,0 +1,15 @@
|
||||
successMessage: Host port is not configured
|
||||
failureMessage: Host port should not be configured
|
||||
category: Networking
|
||||
target: Container
|
||||
schema:
|
||||
'$schema': http://json-schema.org/draft-07/schema
|
||||
type: object
|
||||
required:
|
||||
properties:
|
||||
ports:
|
||||
type: array
|
||||
items:
|
||||
properties:
|
||||
hostPort:
|
||||
const: 0
|
||||
@@ -0,0 +1,31 @@
|
||||
successMessage: Container does not have any insecure capabilities
|
||||
failureMessage: Container should not have insecure capabilities
|
||||
category: Security
|
||||
target: Container
|
||||
schema:
|
||||
'$schema': http://json-schema.org/draft-07/schema
|
||||
type: object
|
||||
properties:
|
||||
securityContext:
|
||||
type: object
|
||||
properties:
|
||||
capabilities:
|
||||
type: object
|
||||
properties:
|
||||
add:
|
||||
enum:
|
||||
- CHOWN
|
||||
- DAC_OVERRIDE
|
||||
- FSETID
|
||||
- FOWNER
|
||||
- MKNOD
|
||||
- NET_RAW
|
||||
- SETGID
|
||||
- SETUID
|
||||
- SETFCAP
|
||||
- SETPCAP
|
||||
- NET_BIND_SERVICE
|
||||
- SYS_CHROOT
|
||||
- KILL
|
||||
- AUDIT_WRITE
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
successMessage: Liveness probe is configured
|
||||
failureMessage: Liveness probe should be configured
|
||||
category: Health Checks
|
||||
controllers:
|
||||
exclude:
|
||||
- Job
|
||||
- CronJob
|
||||
containers:
|
||||
exclude:
|
||||
- initContainer
|
||||
target: Container
|
||||
schema:
|
||||
'$schema': http://json-schema.org/draft-07/schema
|
||||
type: object
|
||||
required:
|
||||
- livenessProbe
|
||||
properties:
|
||||
livenessProbe:
|
||||
type: object
|
||||
not:
|
||||
const: null
|
||||
@@ -0,0 +1,27 @@
|
||||
successMessage: Memory limits are set
|
||||
failureMessage: Memory limits should be set
|
||||
category: Resources
|
||||
target: Container
|
||||
containers:
|
||||
exclude:
|
||||
- initContainer
|
||||
schema:
|
||||
'$schema': http://json-schema.org/draft-07/schema
|
||||
type: object
|
||||
required:
|
||||
- resources
|
||||
properties:
|
||||
resources:
|
||||
type: object
|
||||
required:
|
||||
- limits
|
||||
properties:
|
||||
limits:
|
||||
type: object
|
||||
required:
|
||||
- memory
|
||||
properties:
|
||||
memory:
|
||||
type: string
|
||||
not:
|
||||
const: ''
|
||||
@@ -0,0 +1,27 @@
|
||||
successMessage: Memory requests are set
|
||||
failureMessage: Memory requests should be set
|
||||
category: Resources
|
||||
target: Container
|
||||
containers:
|
||||
exclude:
|
||||
- initContainer
|
||||
schema:
|
||||
'$schema': http://json-schema.org/draft-07/schema
|
||||
type: object
|
||||
required:
|
||||
- resources
|
||||
properties:
|
||||
resources:
|
||||
type: object
|
||||
required:
|
||||
- requests
|
||||
properties:
|
||||
requests:
|
||||
type: object
|
||||
required:
|
||||
- memory
|
||||
properties:
|
||||
memory:
|
||||
type: string
|
||||
not:
|
||||
const: ''
|
||||
@@ -0,0 +1,26 @@
|
||||
successMessage: Multiple replicas are scheduled
|
||||
failureMessage: Only one replica is scheduled
|
||||
category: Reliability
|
||||
target: Controller
|
||||
controllers:
|
||||
include:
|
||||
- Deployment
|
||||
schema:
|
||||
'$schema': http://json-schema.org/draft-07/schema
|
||||
type: object
|
||||
required:
|
||||
- Object
|
||||
properties:
|
||||
Object:
|
||||
type: object
|
||||
required:
|
||||
- spec
|
||||
properties:
|
||||
spec:
|
||||
type: object
|
||||
required:
|
||||
- replicas
|
||||
properties:
|
||||
replicas:
|
||||
type: integer
|
||||
minimum: 2
|
||||
@@ -0,0 +1,16 @@
|
||||
successMessage: Filesystem is read only
|
||||
failureMessage: Filesystem should be read only
|
||||
category: Security
|
||||
target: Container
|
||||
schema:
|
||||
'$schema': http://json-schema.org/draft-07/schema
|
||||
type: object
|
||||
required:
|
||||
- securityContext
|
||||
properties:
|
||||
securityContext:
|
||||
required:
|
||||
- readOnlyRootFilesystem
|
||||
properties:
|
||||
readOnlyRootFilesystem:
|
||||
const: true
|
||||
@@ -0,0 +1,9 @@
|
||||
successMessage: Priority class has been set
|
||||
failureMessage: Priority class should be set
|
||||
category: Security
|
||||
target: Pod
|
||||
schema:
|
||||
'$schema': http://json-schema.org/draft-07/schema
|
||||
type: object
|
||||
required:
|
||||
- priorityClassName
|
||||
@@ -0,0 +1,13 @@
|
||||
successMessage: Privilege escalation not allowed
|
||||
failureMessage: Privilege escalation should not be allowed
|
||||
category: Security
|
||||
target: Container
|
||||
schema:
|
||||
'$schema': http://json-schema.org/draft-07/schema
|
||||
type: object
|
||||
properties:
|
||||
securityContext:
|
||||
properties:
|
||||
allowPrivilegeEscalation:
|
||||
not:
|
||||
const: true
|
||||
@@ -0,0 +1,11 @@
|
||||
successMessage: Image pull policy is "Always"
|
||||
failureMessage: Image pull policy should be "Always"
|
||||
category: Images
|
||||
target: Container
|
||||
schema:
|
||||
'$schema': http://json-schema.org/draft-07/schema
|
||||
required:
|
||||
- imagePullPolicy
|
||||
properties:
|
||||
imagePullPolicy:
|
||||
const: Always
|
||||
@@ -0,0 +1,21 @@
|
||||
successMessage: Readiness probe is configured
|
||||
failureMessage: Readiness probe should be configured
|
||||
category: Health Checks
|
||||
controllers:
|
||||
exclude:
|
||||
- Job
|
||||
- CronJob
|
||||
containers:
|
||||
exclude:
|
||||
- initContainer
|
||||
target: Container
|
||||
schema:
|
||||
'$schema': http://json-schema.org/draft-07/schema
|
||||
type: object
|
||||
required:
|
||||
- readinessProbe
|
||||
properties:
|
||||
readinessProbe:
|
||||
type: object
|
||||
not:
|
||||
const: null
|
||||
@@ -0,0 +1,13 @@
|
||||
successMessage: Not running as privileged
|
||||
failureMessage: Should not be running as privileged
|
||||
category: Security
|
||||
target: Container
|
||||
schema:
|
||||
'$schema': http://json-schema.org/draft-07/schema
|
||||
type: object
|
||||
properties:
|
||||
securityContext:
|
||||
properties:
|
||||
privileged:
|
||||
not:
|
||||
const: true
|
||||
@@ -0,0 +1,52 @@
|
||||
successMessage: Is not allowed to run as root
|
||||
failureMessage: Should not be allowed to run as root
|
||||
category: Security
|
||||
target: Container
|
||||
schemaTarget: Pod
|
||||
schema:
|
||||
'$schema': http://json-schema.org/draft-07/schema
|
||||
definitions:
|
||||
goodSecurityContext:
|
||||
type: object
|
||||
anyOf:
|
||||
- required:
|
||||
- runAsUser
|
||||
properties:
|
||||
runAsUser:
|
||||
minimum: 1
|
||||
- required:
|
||||
- runAsNonRoot
|
||||
properties:
|
||||
runAsNonRoot:
|
||||
const: true
|
||||
notBadSecurityContext:
|
||||
type: object
|
||||
properties:
|
||||
runAsUser:
|
||||
minimum: 1
|
||||
runAsNonRoot:
|
||||
const: true
|
||||
type: object
|
||||
anyOf:
|
||||
# non-root specified at pod-level, and not overridden at container level
|
||||
- required:
|
||||
- securityContext
|
||||
properties:
|
||||
securityContext:
|
||||
$ref: "#/definitions/goodSecurityContext"
|
||||
containers:
|
||||
type: array
|
||||
items:
|
||||
properties:
|
||||
securityContext:
|
||||
$ref: "#/definitions/notBadSecurityContext"
|
||||
# non-root specified at container level
|
||||
- properties:
|
||||
containers:
|
||||
type: array
|
||||
items:
|
||||
required:
|
||||
- securityContext
|
||||
properties:
|
||||
securityContext:
|
||||
$ref: "#/definitions/goodSecurityContext"
|
||||
@@ -0,0 +1,16 @@
|
||||
successMessage: Image tag is specified
|
||||
failureMessage: Image tag should be specified
|
||||
category: Images
|
||||
target: Container
|
||||
schema:
|
||||
'$schema': http://json-schema.org/draft-07/schema
|
||||
required:
|
||||
- image
|
||||
allOf:
|
||||
- properties:
|
||||
image:
|
||||
pattern: ^.+:.+$
|
||||
- properties:
|
||||
image:
|
||||
not:
|
||||
pattern: ^.+:latest$
|
||||
@@ -0,0 +1,151 @@
|
||||
// Copyright 2020 FairwindsOps Inc
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
conf "github.com/fairwindsops/polaris/pkg/config"
|
||||
"github.com/fairwindsops/polaris/pkg/kube"
|
||||
"github.com/fairwindsops/polaris/pkg/validator"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
var setExitCode bool
|
||||
var minScore int
|
||||
var auditOutputURL string
|
||||
var auditOutputFile string
|
||||
var auditOutputFormat string
|
||||
var resourceToAudit string
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(auditCmd)
|
||||
auditCmd.PersistentFlags().StringVar(&auditPath, "audit-path", "", "If specified, audits one or more YAML files instead of a cluster.")
|
||||
auditCmd.PersistentFlags().BoolVar(&setExitCode, "set-exit-code-on-danger", false, "Set an exit code of 3 when the audit contains danger-level issues.")
|
||||
auditCmd.PersistentFlags().IntVar(&minScore, "set-exit-code-below-score", 0, "Set an exit code of 4 when the score is below this threshold (1-100).")
|
||||
auditCmd.PersistentFlags().StringVar(&auditOutputURL, "output-url", "", "Destination URL to send audit results.")
|
||||
auditCmd.PersistentFlags().StringVar(&auditOutputFile, "output-file", "", "Destination file for audit results.")
|
||||
auditCmd.PersistentFlags().StringVarP(&auditOutputFormat, "format", "f", "json", "Output format for results - json, yaml, or score.")
|
||||
auditCmd.PersistentFlags().StringVar(&displayName, "display-name", "", "An optional identifier for the audit.")
|
||||
auditCmd.PersistentFlags().StringVar(&resourceToAudit, "resource", "", "Audit a specific resource, in the format namespace/kind/version/name, e.g. nginx-ingress/Deployment.apps/v1/default-backend.")
|
||||
}
|
||||
|
||||
var auditCmd = &cobra.Command{
|
||||
Use: "audit",
|
||||
Short: "Runs a one-time audit.",
|
||||
Long: `Runs a one-time audit.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if displayName != "" {
|
||||
config.DisplayName = displayName
|
||||
}
|
||||
|
||||
auditData := runAndReportAudit(config, auditPath, resourceToAudit, auditOutputFile, auditOutputURL, auditOutputFormat)
|
||||
|
||||
summary := auditData.GetSummary()
|
||||
score := summary.GetScore()
|
||||
if setExitCode && summary.Dangers > 0 {
|
||||
logrus.Infof("%d danger items found in audit", summary.Dangers)
|
||||
os.Exit(3)
|
||||
} else if minScore != 0 && score < uint(minScore) {
|
||||
logrus.Infof("Audit score of %d is less than the provided minimum of %d", score, minScore)
|
||||
os.Exit(4)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func runAndReportAudit(c conf.Configuration, auditPath, workload, outputFile, outputURL, outputFormat string) validator.AuditData {
|
||||
// Create a kubernetes client resource provider
|
||||
k, err := kube.CreateResourceProvider(auditPath, workload)
|
||||
if err != nil {
|
||||
logrus.Errorf("Error fetching Kubernetes resources %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
auditData, err := validator.RunAudit(c, k)
|
||||
|
||||
if err != nil {
|
||||
logrus.Errorf("Error while running audit on resources: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var outputBytes []byte
|
||||
if outputFormat == "score" {
|
||||
outputBytes = []byte(fmt.Sprintf("%d\n", auditData.GetSummary().GetScore()))
|
||||
} else if outputFormat == "yaml" {
|
||||
jsonBytes, err := json.Marshal(auditData)
|
||||
if err == nil {
|
||||
outputBytes, err = yaml.JSONToYAML(jsonBytes)
|
||||
}
|
||||
} else {
|
||||
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(outputBytes))
|
||||
|
||||
if err != nil {
|
||||
logrus.Errorf("Error building request for output: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
if err != nil {
|
||||
logrus.Errorf("Error making request for output: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
|
||||
if err != nil {
|
||||
logrus.Errorf("Error reading response: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
logrus.Infof("Received response: %v", body)
|
||||
}
|
||||
|
||||
if outputFile != "" {
|
||||
err := ioutil.WriteFile(outputFile, []byte(outputBytes), 0644)
|
||||
if err != nil {
|
||||
logrus.Errorf("Error writing output to file: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
return auditData
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
// Copyright 2020 FairwindsOps Inc
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/fairwindsops/polaris/pkg/dashboard"
|
||||
"github.com/fairwindsops/polaris/pkg/validator"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var serverPort int
|
||||
var basePath string
|
||||
var loadAuditFile string
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(dashboardCmd)
|
||||
dashboardCmd.PersistentFlags().IntVarP(&serverPort, "port", "p", 8080, "Port for the dashboard webserver.")
|
||||
dashboardCmd.PersistentFlags().StringVar(&basePath, "base-path", "/", "Path on which the dashboard is served.")
|
||||
dashboardCmd.PersistentFlags().StringVar(&loadAuditFile, "load-audit-file", "", "Runs the dashboard with data saved from a past audit.")
|
||||
dashboardCmd.PersistentFlags().StringVar(&auditPath, "audit-path", "", "If specified, audits one or more YAML files instead of a cluster.")
|
||||
dashboardCmd.PersistentFlags().StringVar(&displayName, "display-name", "", "An optional identifier for the audit.")
|
||||
|
||||
}
|
||||
|
||||
var dashboardCmd = &cobra.Command{
|
||||
Use: "dashboard",
|
||||
Short: "Runs the webserver for Polaris dashboard.",
|
||||
Long: `Runs the webserver for Polaris dashboard.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if displayName != "" {
|
||||
config.DisplayName = displayName
|
||||
}
|
||||
|
||||
var auditDataPtr *validator.AuditData
|
||||
if loadAuditFile != "" {
|
||||
auditData := validator.ReadAuditFromFile(loadAuditFile)
|
||||
auditDataPtr = &auditData
|
||||
}
|
||||
router := dashboard.GetRouter(config, auditPath, serverPort, basePath, auditDataPtr)
|
||||
router.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte("OK"))
|
||||
})
|
||||
http.Handle("/", router)
|
||||
|
||||
logrus.Infof("Starting Polaris dashboard server on port %d", serverPort)
|
||||
logrus.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", serverPort), nil))
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
// Copyright 2020 FairwindsOps Inc
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"os"
|
||||
|
||||
conf "github.com/fairwindsops/polaris/pkg/config"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
var configPath string
|
||||
var disallowExemptions bool
|
||||
var logLevel string
|
||||
var auditPath string
|
||||
var displayName string
|
||||
|
||||
var (
|
||||
version string
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Flags
|
||||
rootCmd.PersistentFlags().StringVarP(&configPath, "config", "c", "", "Location of Polaris configuration file.")
|
||||
rootCmd.PersistentFlags().BoolVarP(&disallowExemptions, "disallow-exemptions", "", false, "Disallow any exemptions from configuration file.")
|
||||
rootCmd.PersistentFlags().StringVarP(&logLevel, "log-level", "", logrus.InfoLevel.String(), "Logrus log level.")
|
||||
flag.Parse()
|
||||
pflag.CommandLine.AddGoFlagSet(flag.CommandLine)
|
||||
}
|
||||
|
||||
var config conf.Configuration
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "polaris",
|
||||
Short: "polaris",
|
||||
Long: `Validation of best practices in your Kubernetes clusters.`,
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||
parsedLevel, err := logrus.ParseLevel(logLevel)
|
||||
if err != nil {
|
||||
logrus.Errorf("log-level flag has invalid value %s", logLevel)
|
||||
} else {
|
||||
logrus.SetLevel(parsedLevel)
|
||||
}
|
||||
|
||||
config, err = conf.ParseFile(configPath)
|
||||
if err != nil {
|
||||
logrus.Errorf("Error parsing config at %s: %v", configPath, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if disallowExemptions {
|
||||
config.DisallowExemptions = true
|
||||
}
|
||||
|
||||
},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
logrus.Error("You must specify a sub-command.")
|
||||
err := cmd.Help()
|
||||
if err != nil {
|
||||
logrus.Error(err)
|
||||
}
|
||||
os.Exit(1)
|
||||
},
|
||||
}
|
||||
|
||||
// Execute the stuff
|
||||
func Execute(VERSION string) {
|
||||
version = VERSION
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
logrus.Error(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// Copyright 2020 FairwindsOps Inc
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(versionCmd)
|
||||
}
|
||||
|
||||
var versionCmd = &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Prints the current version.",
|
||||
Long: `Prints the current version of the tool.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Println("Polaris version:" + version)
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
// Copyright 2020 FairwindsOps Inc
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
fwebhook "github.com/fairwindsops/polaris/pkg/webhook"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
appsv1beta1 "k8s.io/api/apps/v1beta1"
|
||||
appsv1beta2 "k8s.io/api/apps/v1beta2"
|
||||
batchv1 "k8s.io/api/batch/v1"
|
||||
batchv1beta1 "k8s.io/api/batch/v1beta1"
|
||||
batchv2alpha1 "k8s.io/api/batch/v2alpha1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
apitypes "k8s.io/apimachinery/pkg/types"
|
||||
k8sConfig "sigs.k8s.io/controller-runtime/pkg/client/config"
|
||||
"sigs.k8s.io/controller-runtime/pkg/manager"
|
||||
"sigs.k8s.io/controller-runtime/pkg/runtime/signals"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook"
|
||||
)
|
||||
|
||||
var supportedVersions = map[string]runtime.Object{
|
||||
"appsv1/Deployment": &appsv1.Deployment{},
|
||||
"appsv1beta1/Deployment": &appsv1beta1.Deployment{},
|
||||
"appsv1beta2/Deployment": &appsv1beta2.Deployment{},
|
||||
|
||||
"appsv1/StatefulSet": &appsv1.StatefulSet{},
|
||||
"appsv1beta1/StatefulSet": &appsv1beta1.StatefulSet{},
|
||||
"appsv1beta2/StatefulSet": &appsv1beta2.StatefulSet{},
|
||||
|
||||
"appsv1/DaemonSet": &appsv1.DaemonSet{},
|
||||
"appsv1beta2/DaemonSet": &appsv1beta2.DaemonSet{},
|
||||
|
||||
"batchv1/Job": &batchv1.Job{},
|
||||
|
||||
"batchv1beta1/CronJob": &batchv1beta1.CronJob{},
|
||||
"batchv2alpha1/CronJob": &batchv2alpha1.CronJob{},
|
||||
|
||||
"corev1/ReplicationController": &corev1.ReplicationController{},
|
||||
|
||||
"corev1/Pod": &corev1.Pod{},
|
||||
}
|
||||
|
||||
var webhookPort int
|
||||
var disableWebhookConfigInstaller bool
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(webhookCmd)
|
||||
webhookCmd.PersistentFlags().IntVarP(&webhookPort, "port", "p", 9876, "Port for the dashboard webserver.")
|
||||
webhookCmd.PersistentFlags().BoolVar(&disableWebhookConfigInstaller, "disable-webhook-config-installer", false, "disable the installer in the webhook server, so it won't install webhook configuration resources during bootstrapping.")
|
||||
}
|
||||
|
||||
var webhookCmd = &cobra.Command{
|
||||
Use: "webhook",
|
||||
Short: "Runs the webhook webserver.",
|
||||
Long: `Runs the webhook webserver.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
logrus.Debug("Setting up controller manager")
|
||||
mgr, err := manager.New(k8sConfig.GetConfigOrDie(), manager.Options{})
|
||||
if err != nil {
|
||||
logrus.Errorf("Unable to set up overall controller manager: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
polarisAppName := "polaris"
|
||||
polarisResourceName := "polaris-webhook"
|
||||
polarisNamespaceBytes, err := ioutil.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace")
|
||||
|
||||
if err != nil {
|
||||
// Not exiting here as we have fallback options
|
||||
logrus.Debugf("Error reading namespace information: %v", err)
|
||||
}
|
||||
|
||||
polarisNamespace := string(polarisNamespaceBytes)
|
||||
if polarisNamespace == "" {
|
||||
polarisNamespace = polarisResourceName
|
||||
logrus.Debugf("Could not determine current namespace, creating resources in %s namespace", polarisNamespace)
|
||||
}
|
||||
|
||||
logrus.Info("Setting up webhook server")
|
||||
as, err := webhook.NewServer(polarisResourceName, mgr, webhook.ServerOptions{
|
||||
Port: int32(webhookPort),
|
||||
CertDir: "/opt/cert",
|
||||
DisableWebhookConfigInstaller: &disableWebhookConfigInstaller,
|
||||
BootstrapOptions: &webhook.BootstrapOptions{
|
||||
ValidatingWebhookConfigName: polarisResourceName,
|
||||
Secret: &apitypes.NamespacedName{
|
||||
Namespace: polarisNamespace,
|
||||
Name: polarisResourceName,
|
||||
},
|
||||
|
||||
Service: &webhook.Service{
|
||||
Namespace: polarisNamespace,
|
||||
Name: polarisResourceName,
|
||||
|
||||
// Selectors should select the pods that runs this webhook server.
|
||||
Selectors: map[string]string{
|
||||
"app": polarisAppName,
|
||||
"component": "webhook",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
logrus.Errorf("Error setting up webhook server: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
logrus.Infof("Polaris webhook server listening on port %d", webhookPort)
|
||||
|
||||
// Iterate all the configurations supported controllers to scan and register them for webhooks
|
||||
// Should only register controllers that are configured to be scanned
|
||||
logrus.Debug("Registering webhooks to the webhook server")
|
||||
var webhooks []webhook.Webhook
|
||||
for name, supportedAPIType := range supportedVersions {
|
||||
webhookName := strings.ToLower(name)
|
||||
webhookName = strings.ReplaceAll(webhookName, "/", "-")
|
||||
hook, err := fwebhook.NewWebhook(webhookName, mgr, fwebhook.Validator{Config: config}, supportedAPIType)
|
||||
if err != nil {
|
||||
logrus.Warningf("Couldn't build webhook %s: %v", webhookName, err)
|
||||
continue
|
||||
}
|
||||
webhooks = append(webhooks, hook)
|
||||
logrus.Infof("%s webhook started", webhookName)
|
||||
}
|
||||
|
||||
if err = as.Register(webhooks...); err != nil {
|
||||
logrus.Debugf("Unable to register webhooks in the admission server: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
logrus.Debug("Starting webhook manager")
|
||||
if err := mgr.Start(signals.SetupSignalHandler()); err != nil {
|
||||
logrus.Errorf("Error starting manager: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
},
|
||||
}
|
||||
+35
-111
@@ -5,108 +5,63 @@ kind: Namespace
|
||||
metadata:
|
||||
name: polaris
|
||||
---
|
||||
# Source: polaris/templates/configmap.yaml
|
||||
# Source: polaris/templates/rbac.yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: polaris
|
||||
namespace: polaris
|
||||
labels:
|
||||
app: polaris
|
||||
data:
|
||||
config.yaml: |
|
||||
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
|
||||
---
|
||||
# Source: polaris/templates/rbac.yaml
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: polaris-dashboard
|
||||
name: polaris
|
||||
labels:
|
||||
app: polaris
|
||||
rules:
|
||||
- apiGroups:
|
||||
- 'apps'
|
||||
- 'extensions'
|
||||
resources:
|
||||
- 'deployments'
|
||||
- 'statefulsets'
|
||||
verbs:
|
||||
- 'get'
|
||||
- 'list'
|
||||
# required by controller-runtime code doing a cluster wide lookup
|
||||
# when it seems namespace would suffice
|
||||
- apiGroups:
|
||||
- ''
|
||||
resources:
|
||||
- 'nodes'
|
||||
- 'namespaces'
|
||||
- 'pods'
|
||||
verbs:
|
||||
- 'get'
|
||||
- 'list'
|
||||
---
|
||||
# Source: polaris/templates/rbac.yaml
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: polaris-dashboard
|
||||
name: polaris-view
|
||||
labels:
|
||||
app: polaris
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: polaris-dashboard
|
||||
name: view
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: polaris-dashboard
|
||||
name: polaris
|
||||
namespace: polaris
|
||||
---
|
||||
# Source: polaris/templates/rbac.yaml
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: polaris
|
||||
labels:
|
||||
app: polaris
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: polaris
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: polaris
|
||||
namespace: polaris
|
||||
---
|
||||
# Source: polaris/templates/dashboard.service.yaml
|
||||
@@ -117,9 +72,10 @@ metadata:
|
||||
namespace: polaris
|
||||
labels:
|
||||
app: polaris
|
||||
annotations:
|
||||
spec:
|
||||
ports:
|
||||
- name: dashboard
|
||||
- name: http-dashboard
|
||||
port: 80
|
||||
protocol: TCP
|
||||
targetPort: 8080
|
||||
@@ -132,8 +88,6 @@ spec:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
checksum/config: '6ec4a4dc87403cae67c01438398a5f1d4ef836ffeaf26a33b685c066b940495a'
|
||||
name: polaris-dashboard
|
||||
namespace: polaris
|
||||
labels:
|
||||
@@ -151,17 +105,11 @@ spec:
|
||||
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.3'
|
||||
- dashboard
|
||||
image: 'quay.io/fairwinds/polaris:1.2'
|
||||
imagePullPolicy: 'Always'
|
||||
name: dashboard
|
||||
ports:
|
||||
@@ -180,8 +128,8 @@ spec:
|
||||
periodSeconds: 20
|
||||
resources:
|
||||
limits:
|
||||
cpu: 100m
|
||||
memory: 128Mi
|
||||
cpu: 150m
|
||||
memory: 512Mi
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 128Mi
|
||||
@@ -193,30 +141,6 @@ spec:
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
volumeMounts:
|
||||
- name: config
|
||||
mountPath: /opt/app/config.yaml
|
||||
subPath: config.yaml
|
||||
readOnly: true
|
||||
serviceAccountName: polaris-dashboard
|
||||
---
|
||||
# Source: polaris/templates/audit.job.yaml
|
||||
|
||||
---
|
||||
# Source: polaris/templates/audit.rbac.yaml
|
||||
|
||||
---
|
||||
# Source: polaris/templates/ingress.yaml
|
||||
|
||||
---
|
||||
# Source: polaris/templates/webhook.deployment.yaml
|
||||
|
||||
---
|
||||
# Source: polaris/templates/webhook.rbac.yaml
|
||||
|
||||
---
|
||||
# Source: polaris/templates/webhook.secret.yaml
|
||||
|
||||
---
|
||||
# Source: polaris/templates/webhook.service.yaml
|
||||
|
||||
serviceAccountName: polaris
|
||||
nodeSelector:
|
||||
tolerations:
|
||||
|
||||
+64
-99
@@ -5,7 +5,17 @@ kind: Namespace
|
||||
metadata:
|
||||
name: polaris
|
||||
---
|
||||
# Source: polaris/templates/rbac.yaml
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: polaris
|
||||
namespace: polaris
|
||||
labels:
|
||||
app: polaris
|
||||
---
|
||||
# Source: polaris/templates/webhook.secret.yaml
|
||||
# The name of this secret is static as it is populated by the webhook pod.
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
@@ -17,69 +27,25 @@ type: Opaque
|
||||
stringData:
|
||||
cert.pem: ''
|
||||
---
|
||||
# Source: polaris/templates/configmap.yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
# Source: polaris/templates/rbac.yaml
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: polaris
|
||||
namespace: polaris
|
||||
labels:
|
||||
app: polaris
|
||||
data:
|
||||
config.yaml: |
|
||||
rules:
|
||||
# required by controller-runtime code doing a cluster wide lookup
|
||||
# when it seems namespace would suffice
|
||||
- apiGroups:
|
||||
- ''
|
||||
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
|
||||
|
||||
- 'nodes'
|
||||
verbs:
|
||||
- 'get'
|
||||
- 'list'
|
||||
---
|
||||
# Source: polaris/templates/webhook.rbac.yaml
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: polaris-webhook
|
||||
namespace: polaris
|
||||
labels:
|
||||
app: polaris
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
@@ -87,15 +53,6 @@ metadata:
|
||||
labels:
|
||||
app: polaris
|
||||
rules:
|
||||
- apiGroups:
|
||||
- 'apps'
|
||||
- 'extensions'
|
||||
resources:
|
||||
- 'deployments'
|
||||
- 'statefulsets'
|
||||
verbs:
|
||||
- 'get'
|
||||
- 'list'
|
||||
# required by controller-runtime code doing a cluster wide lookup
|
||||
# when it seems namespace would suffice
|
||||
- apiGroups:
|
||||
@@ -114,6 +71,39 @@ rules:
|
||||
verbs:
|
||||
- '*'
|
||||
---
|
||||
# Source: polaris/templates/rbac.yaml
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: polaris-view
|
||||
labels:
|
||||
app: polaris
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: view
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: polaris
|
||||
namespace: polaris
|
||||
---
|
||||
# Source: polaris/templates/rbac.yaml
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: polaris
|
||||
labels:
|
||||
app: polaris
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: polaris
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: polaris
|
||||
namespace: polaris
|
||||
---
|
||||
# Source: polaris/templates/webhook.rbac.yaml
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
@@ -126,9 +116,10 @@ roleRef:
|
||||
name: polaris-webhook
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: polaris-webhook
|
||||
name: polaris
|
||||
namespace: polaris
|
||||
---
|
||||
# Source: polaris/templates/webhook.rbac.yaml
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
kind: Role
|
||||
metadata:
|
||||
@@ -146,6 +137,7 @@ rules:
|
||||
verbs:
|
||||
- '*'
|
||||
---
|
||||
# Source: polaris/templates/webhook.rbac.yaml
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
@@ -159,7 +151,7 @@ roleRef:
|
||||
name: polaris-webhook
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: polaris-webhook
|
||||
name: polaris
|
||||
namespace: polaris
|
||||
---
|
||||
# Source: polaris/templates/webhook.service.yaml
|
||||
@@ -185,8 +177,6 @@ spec:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
checksum/config: '6ec4a4dc87403cae67c01438398a5f1d4ef836ffeaf26a33b685c066b940495a'
|
||||
name: polaris-webhook
|
||||
namespace: polaris
|
||||
labels:
|
||||
@@ -208,10 +198,8 @@ spec:
|
||||
- name: webhook
|
||||
command:
|
||||
- polaris
|
||||
- --webhook
|
||||
- --config
|
||||
- /opt/app/config.yaml
|
||||
image: 'quay.io/reactiveops/polaris:0.3'
|
||||
- webhook
|
||||
image: 'quay.io/fairwinds/polaris:1.2'
|
||||
imagePullPolicy: 'Always'
|
||||
ports:
|
||||
- containerPort: 9876
|
||||
@@ -250,41 +238,18 @@ spec:
|
||||
drop:
|
||||
- ALL
|
||||
volumeMounts:
|
||||
- name: config
|
||||
mountPath: /opt/app/config.yaml
|
||||
subPath: config.yaml
|
||||
readOnly: true
|
||||
- name: secret
|
||||
mountPath: /opt/cert/
|
||||
readOnly: true
|
||||
- name: cr-logs
|
||||
mountPath: /tmp/
|
||||
readOnly: false
|
||||
serviceAccountName: polaris-webhook
|
||||
serviceAccountName: polaris
|
||||
nodeSelector:
|
||||
tolerations:
|
||||
volumes:
|
||||
- name: config
|
||||
configMap:
|
||||
name: polaris
|
||||
- name: secret
|
||||
secret:
|
||||
secretName: polaris-webhook
|
||||
- name: cr-logs
|
||||
emptyDir: {}
|
||||
---
|
||||
# Source: polaris/templates/audit.job.yaml
|
||||
|
||||
---
|
||||
# Source: polaris/templates/audit.rbac.yaml
|
||||
|
||||
---
|
||||
# Source: polaris/templates/dashboard.deployment.yaml
|
||||
|
||||
---
|
||||
# Source: polaris/templates/dashboard.rbac.yaml
|
||||
|
||||
---
|
||||
# Source: polaris/templates/dashboard.service.yaml
|
||||
|
||||
---
|
||||
# Source: polaris/templates/ingress.yaml
|
||||
|
||||
|
||||
@@ -4,14 +4,14 @@ Polaris supports a number of checks related to the image specified by pods.
|
||||
|
||||
key | default | description
|
||||
----|---------|------------
|
||||
`images.tagNotSpecified` | `error` | Fails when an image tag is either not specified or `latest`.
|
||||
`images.tagNotSpecified` | `danger` | Fails when an image tag is either not specified or `latest`.
|
||||
`images.pullPolicyNotAlways` | `ignore` | Fails when an image pull policy is not `always`.
|
||||
|
||||
## Background
|
||||
|
||||
Docker's `latest` tag is applied by default to images where a tag hasn't been specified. Not specifying a specific version of an image can lead to a wide variety of problems. The underlying image could include unexpected breaking changes that break your application whenever the latest image is pulled. Reusing the same tag for multiple versions of an image can lead to different nodes in the same cluster having different versions of an image, even if the tag is identical.
|
||||
|
||||
Related to that, relying on cached versions of a Docker image can become a security vulnerability. By default, an image will be pulled if it isn't already cached on the node attempting to run it. This can result in variations in images that are running per node, or potentially provide a way to gain access to an image without having direct access to the ImagePullSecret. With that in mind, it's often better to ensure the a pod has `pullPolicy: Always` specified, so images are always pulled directly from their source. This is not a check enabled by default with Polaris as organizations may not wish to add the overhead involved with pulling images for each pod.
|
||||
Related to that, relying on cached versions of a Docker image can become a security vulnerability. By default, an image will be pulled if it isn't already cached on the node attempting to run it. This can result in variations in images that are running per node, or potentially provide a way to gain access to an image without having direct access to the ImagePullSecret. With that in mind, it's often better to ensure the a pod has `pullPolicy: Always` specified, so images are always pulled directly from their source.
|
||||
|
||||
## Further Reading
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
# Resources
|
||||
|
||||
Polaris supports a number of checks related to CPU and Memory requests and limits.
|
||||
|
||||
## Presence Checks
|
||||
|
||||
To simplify ensure that these values have been set, the following attributes are available:
|
||||
|
||||
key | default | description
|
||||
----|---------|------------
|
||||
`resources.cpuRequestsMissing` | `danger` | Fails when `resources.requests.cpu` attribute is not configured.
|
||||
`resources.memoryRequestsMissing` | `danger` | Fails when `resources.requests.memory` attribute is not configured.
|
||||
`resources.cpuLimitsMissing` | `danger` | Fails when `resources.limits.cpu` attribute is not configured.
|
||||
`resources.memoryLimitsMissing` | `danger` | Fails when `resources.limits.memory` attribute is not configured.
|
||||
|
||||
## Background
|
||||
|
||||
Configuring resource requests and limits for containers running in Kubernetes is an important best practice to follow. Setting appropriate resource requests will ensure that all your applications have sufficient compute resources. Setting appropriate resource limits will ensure that your applications do not consume too many resources.
|
||||
|
||||
Having these values appropriately configured ensures that:
|
||||
|
||||
* Cluster autoscaling can function as intended. New nodes are scheduled once pods are unable to be scheduled on an existing node due to insufficient resources. This will not happen if resource requests are not configured.
|
||||
|
||||
* Each container has sufficient access to compute resources. Without resource requests, a pod may be scheduled on a node that is already overutilized. Without resource limits, a single poorly behaving pod could utilize the majority of resources on a node, significantly impacting the performance of other pods on the same node.
|
||||
|
||||
## Further Reading
|
||||
|
||||
- [Kubernetes Docs: Managing Compute Resources for Containers](https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/)
|
||||
- [Kubernetes best practices: Resource requests and limits](https://cloud.google.com/blog/products/gcp/kubernetes-best-practices-resource-requests-and-limits)
|
||||
- [Vertical Pod Autoscaler (can automatically set resource requests and limits)](https://github.com/kubernetes/autoscaler/tree/master/vertical-pod-autoscaler)
|
||||
@@ -0,0 +1,27 @@
|
||||
# Security
|
||||
|
||||
Polaris supports a number of checks related to security.
|
||||
|
||||
key | default | description
|
||||
----|---------|------------
|
||||
`security.hostIPCSet` | `danger` | Fails when `hostIPC` attribute is configured.
|
||||
`security.hostPIDSet` | `danger` | Fails when `hostPID` attribute is configured.
|
||||
`security.notReadOnlyRootFilesystem` | `warning` | Fails when `securityContext.readOnlyRootFilesystem` is not true.
|
||||
`security.privilegeEscalationAllowed` | `danger` | Fails when `securityContext.allowPrivilegeEscalation` is true.
|
||||
`security.runAsRootAllowed` | `danger` | Fails when `securityContext.runAsNonRoot` is not true.
|
||||
`security.runAsPrivileged` | `danger` | Fails when `securityContext.privileged` is true.
|
||||
`security.insecureCapabilities` | `warning` | Fails when `securityContext.capabilities` includes one of the capabilities [listed here](/checks/insecureCapabilities.yaml)
|
||||
`security.dangerousCapabilities` | `danger` | Fails when `securityContext.capabilities` includes one of the capabilities [listed here](/checks/dangerousCapabilities.yaml)
|
||||
|
||||
## Background
|
||||
|
||||
Securing workloads in Kubernetes is an important part of overall cluster security. The overall goal should be to ensure that containers are running with as minimal privileges as possible. This includes avoiding privilege escalation, not running containers with a root user, and using read only file systems wherever possible.
|
||||
|
||||
Much of this configuration can be found in the `securityContext` attribute for both Kubernetes pods and containers. Where configuration is available at both a pod and container level, Polaris validates both.
|
||||
|
||||
## Further Reading
|
||||
- [Kubernetes Docs: Configure a Security Context for a Pod or Container](https://kubernetes.io/docs/tasks/configure-pod-container/security-context/)
|
||||
- [KubeCon 2018 Keynote: Running with Scissors](https://www.youtube.com/watch?v=ltrV-Qmh3oY)
|
||||
- [Kubernetes Security Book](https://kubernetes-security.info/)
|
||||
- [Kubernetes Docs: Set capabilities for a Container](https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-capabilities-for-a-container)
|
||||
- [Linux Programmer's Manual: Capabilities](http://man7.org/linux/man-pages/man7/capabilities.7.html)
|
||||
@@ -0,0 +1,13 @@
|
||||
# Polaris Exit Codes for Audit Runs
|
||||
<dl>
|
||||
<dt>Exit 0</dt>
|
||||
<dd>Successful exit code</dd>
|
||||
<dt>Exit 1</dt>
|
||||
<dd>Could not run audit, or application had a failure while running.</dd>
|
||||
<dt>Exit 2</dt>
|
||||
<dd>Unused</dd>
|
||||
<dt>Exit 3</dt>
|
||||
<dd>Exiting due to `--set-exit-code-on-danger` being set and at least one danger was found after an audit.</dd>
|
||||
<dt>Edit 4</dd>
|
||||
<dd>Exiting due to `--set-exit-code-below-score` being set and the audit resulted in a score less than the minimum score value.</dd>
|
||||
</dl>
|
||||
@@ -1,41 +0,0 @@
|
||||
# Resources
|
||||
|
||||
Polaris supports a number of checks related to CPU and Memory requests and limits.
|
||||
|
||||
## Presence Checks
|
||||
|
||||
To simplify ensure that these values have been set, the following attributes are available:
|
||||
|
||||
key | default | description
|
||||
----|---------|------------
|
||||
`resources.cpuRequestsMissing` | `error` | Fails when `resources.requests.cpu` attribute is not configured.
|
||||
`resources.memoryRequestsMissing` | `error` | Fails when `resources.requests.memory` attribute is not configured.
|
||||
`resources.cpuLimitsMissing` | `error` | Fails when `resources.limits.cpu` attribute is not configured.
|
||||
`resources.memoryLimitsMissing` | `error` | Fails when `resources.limits.memory` attribute is not configured.
|
||||
|
||||
## Range Checks
|
||||
|
||||
Polaris can also verify that those values fall within a certain range. These checks are not enabled by default, and as such do not have default values. The `cpuRequestRanges`, `cpuLimitRanges`, `memoryRequestRanges`, and `memoryLimitRanges` all support the following attributes:
|
||||
|
||||
key | description
|
||||
----|------------
|
||||
`warning.below` | Warn when resource is below this value (or not defined)
|
||||
`warning.above` | Warn when resource is above this value
|
||||
`error.below` | Error when resource is below this value (or not defined)
|
||||
`error.above` | Error when resource is above this value
|
||||
|
||||
## Background
|
||||
|
||||
Configuring resource requests and limits for containers running in Kubernetes is an important best practice to follow. Setting appropriate resource requests will ensure that all your applications have sufficient compute resources. Setting appropriate resource limits will ensure that your applications do not consume too many resources.
|
||||
|
||||
Having these values appropriately configured ensures that:
|
||||
|
||||
* Cluster autoscaling can function as intended. New nodes are scheduled once pods are unable to be scheduled on an existing node due to insufficient resources. This will not happen if resource requests are not configured.
|
||||
|
||||
* Each container has sufficient access to compute resources. Without resource requests, a pod may be scheduled on a node that is already overutilized. Without resource limits, a single poorly behaving pod could utilize the majority of resources on a node, significantly impacting the performance of other pods on the same node.
|
||||
|
||||
## Further Reading
|
||||
|
||||
- [Kubernetes Docs: Managing Compute Resources for Containers](https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/)
|
||||
- [Kubernetes best practices: Resource requests and limits](https://cloud.google.com/blog/products/gcp/kubernetes-best-practices-resource-requests-and-limits)
|
||||
- [Vertical Pod Autoscaler (can automatically set resource requests and limits)](https://github.com/kubernetes/autoscaler/tree/master/vertical-pod-autoscaler)
|
||||
@@ -1,23 +0,0 @@
|
||||
# Security Capabilities
|
||||
|
||||
Polaris supports a number of checks to ensure pods are running with a limited set of capabilities. Under `security.capabilities`, there are `error` and `warning` sections indicating the severity of failures for the following checks.
|
||||
|
||||
key | default | description
|
||||
----|---------|------------
|
||||
`security.capabilities.error.ifAnyAdded` | [`SYS_ADMIN`, `NET_ADMIN`, `ALL`] | Fails when any of the listed capabilities have been added.
|
||||
`security.capabilities.error.ifAnyAddedBeyond` | `nil` | Fails when any capabilities have been added beyond the specified list.
|
||||
`security.capabilities.error.ifAnyNotDropped` | `nil` | Fails when any of the listed capabilities have not been dropped.
|
||||
`security.capabilities.warning.ifAnyAdded` | `nil` | Fails when any of the listed capabilities have been added.
|
||||
`security.capabilities.warning.ifAnyAddedBeyond` | [`CHOWN`, `DAC_OVERRIDE`, `FSETID`, `FOWNER`, `MKNOD`, `NET_RAW`, `SETGID`, `SETUID`, `SETFCAP`, `SETPCAP`, `NET_BIND_SERVICE`, `SYS_CHROOT`, `KILL`,`AUDIT_WRITE`] | Fails when any capabilities have been added beyond the specified list.
|
||||
`security.capabilities.warning.ifAnyNotDropped` | `nil` | Fails when any of the listed capabilities have not been dropped.
|
||||
|
||||
## Background
|
||||
|
||||
Linux Capabilities allow you to specify privileges for a process at a granular level. The [default list of capabilities](https://github.com/moby/moby/blob/master/oci/defaults.go#L15) included with a container are already fairly minimal, but often can be further restricted.
|
||||
|
||||
With Kubernetes configuration, these capabilities can be added or removed by adjusting `securityContext.capabilities`.
|
||||
|
||||
## Further Reading
|
||||
|
||||
- [Kubernetes Docs: Set capabilities for a Container](https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-capabilities-for-a-container)
|
||||
- [Linux Programmer's Manual: Capabilities](http://man7.org/linux/man-pages/man7/capabilities.7.html)
|
||||
@@ -1,27 +0,0 @@
|
||||
# Security
|
||||
|
||||
Polaris supports a number of checks related to security.
|
||||
|
||||
key | default | description
|
||||
----|---------|------------
|
||||
`security.hostIPCSet` | `error` | Fails when `hostIPC` attribute is configured.
|
||||
`security.hostPIDSet` | `error` | Fails when `hostPID` attribute is configured.
|
||||
`security.notReadOnlyRootFileSystem` | `warning` | Fails when `securityContext.readOnlyRootFilesystem` is not true.
|
||||
`security.privilegeEscalationAllowed` | `error` | Fails when `securityContext.allowPrivilegeEscalation` is true.
|
||||
`security.runAsRootAllowed` | `error` | Fails when `securityContext.runAsNonRoot` is not true.
|
||||
`security.runAsPrivileged` | `error` | Fails when `securityContext.privileged` is true.
|
||||
|
||||
## Security Capabilities
|
||||
|
||||
Additional validations are available to ensure pods are running with a limited set of capabilities. More information is available in our [Security Capabilities documentation](security-capabilities.md).
|
||||
|
||||
## Background
|
||||
|
||||
Securing workloads in Kubernetes is an important part of overall cluster security. The overall goal should be to ensure that containers are running with as minimal privileges as possible. This includes avoiding privilege escalation, not running containers with a root user, and using read only file systems wherever possible.
|
||||
|
||||
Much of this configuration can be found in the `securityContext` attribute for both Kubernetes pods and containers. Where configuration is available at both a pod and container level, Polaris validates both.
|
||||
|
||||
## Further Reading
|
||||
- [Kubernetes Docs: Configure a Security Context for a Pod or Container](https://kubernetes.io/docs/tasks/configure-pod-container/security-context/)
|
||||
- [KubeCon 2018 Keynote: Running with Scissors](https://www.youtube.com/watch?v=ltrV-Qmh3oY)
|
||||
- [Kubernetes Security Book](https://kubernetes-security.info/)
|
||||
+241
@@ -0,0 +1,241 @@
|
||||
# Installation and Usage
|
||||
Polaris can be installed on your cluster using kubectl or Helm. It can also
|
||||
be run as a local binary, which will use your kubeconfig to connect to the cluster
|
||||
or run against local YAML files.
|
||||
|
||||
## Configuration
|
||||
Polaris supports a wide range of validations covering a number of Kubernetes best practices.
|
||||
Here's a [sample configuration file](/examples/config-full.yaml) that includes all currently supported checks.
|
||||
The [default configuration](/examples/config.yaml) contains a number of those checks.
|
||||
|
||||
|
||||
### Checks
|
||||
Each check can be assigned a `severity`. Only checks with a severity of `danger` or `warning` will be validated. The results of these validations are visible on the dashboard. In the case of the validating webhook, only failures with a severity of `danger` will result in a change being rejected.
|
||||
|
||||
Polaris validation checks fall into several different categories:
|
||||
|
||||
- [Health Checks](check-documentation/health-checks.md)
|
||||
- [Images](check-documentation/images.md)
|
||||
- [Networking](check-documentation/networking.md)
|
||||
- [Resources](check-documentation/resources.md)
|
||||
- [Security](check-documentation/security.md)
|
||||
|
||||
#### Custom Checks
|
||||
If you'd like to create your own checks, you can use [JSON Schema](https://json-schema.org/). For example,
|
||||
to disallow images from quay.io:
|
||||
|
||||
```yaml
|
||||
checks:
|
||||
imageRegistry: warning
|
||||
customChecks:
|
||||
imageRegistry:
|
||||
successMessage: Image comes from allowed registries
|
||||
failureMessage: Image should not be from disallowed registry
|
||||
category: Images
|
||||
target: Container # target can be "Container" or "Pod"
|
||||
schema:
|
||||
'$schema': http://json-schema.org/draft-07/schema
|
||||
type: object
|
||||
properties:
|
||||
image:
|
||||
type: string
|
||||
not:
|
||||
pattern: ^quay.io
|
||||
```
|
||||
|
||||
Schemas can also be specified as JSON strings instead of YAML, for easier copy/pasting:
|
||||
```yaml
|
||||
customChecks:
|
||||
foo:
|
||||
jsonSchema: |
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema",
|
||||
"type": "object"
|
||||
}
|
||||
```
|
||||
|
||||
We extend JSON Schema with `resourceMinimum` and `resourceMaximum` fields to help compare memory and CPU resource
|
||||
strings like `1000m` and `1G`. You can see an example in [the extended config](/examples/config-full.yaml)
|
||||
|
||||
There are additional examples in the [checks folder](/checks).
|
||||
|
||||
### Exemptions
|
||||
Sometimes a workload really does need to do things that Polaris considers insecure. For instance,
|
||||
many of the `kube-system` workloads need to run as root, or need access to the host network. In these
|
||||
cases, we can add **exemptions** to allow the workload to pass Polaris checks.
|
||||
|
||||
Exemptions can be added two ways: by annotating a controller, or editing the Polaris config.
|
||||
|
||||
To exempt a controller from all checks via annotations, use the annotation `polaris.fairwinds.com/exempt=true`, e.g.
|
||||
```
|
||||
kubectl annotate deployment my-deployment polaris.fairwinds.com/exempt=true
|
||||
```
|
||||
|
||||
To exempt a controller via the config, you have to specify a list of controller names and a list of rules, e.g.
|
||||
```yaml
|
||||
exemptions:
|
||||
- controllerNames:
|
||||
- dns-controller
|
||||
rules:
|
||||
- hostNetworkSet
|
||||
```
|
||||
|
||||
To exempt a controller from a particular check via annotations, use an annotation in the form of `polaris.fairwinds.com/<check>-exempt=true`, e.g.
|
||||
```
|
||||
kubectl annotate deployment my-deployment polaris.fairwinds.com/cpuRequestsMissing-exempt=true
|
||||
```
|
||||
|
||||
# Installing
|
||||
There are several ways to install and use Polaris. Below outline ways to install using `kubectl`, `helm` and `local binary`.
|
||||
|
||||
## Dashboard
|
||||
The dashboard can be installed on a cluster using kubectl or Helm. It can also be run locally,
|
||||
connecting to your cluster using the credentials stored in your `KUBECONFIG`.
|
||||
|
||||
### kubectl
|
||||
```bash
|
||||
kubectl apply -f https://github.com/fairwindsops/polaris/releases/latest/download/dashboard.yaml
|
||||
kubectl port-forward --namespace polaris svc/polaris-dashboard 8080:80
|
||||
```
|
||||
### Helm
|
||||
```bash
|
||||
helm repo add fairwinds-stable https://charts.fairwinds.com/stable
|
||||
helm upgrade --install polaris fairwinds-stable/polaris --namespace polaris
|
||||
kubectl port-forward --namespace polaris svc/polaris-dashboard 8080:80
|
||||
```
|
||||
|
||||
### Local Binary
|
||||
You'll need a valid `KUBECONFIG` set up for the dashboard to connect to your cluster.
|
||||
|
||||
Binary releases can be dowloaded from the [releases page](https://github.com/fairwindsops/polaris/releases)
|
||||
or can be installed with [Homebrew](https://brew.sh/):
|
||||
```bash
|
||||
brew tap reactiveops/tap
|
||||
brew install reactiveops/tap/polaris
|
||||
polaris dashboard --port 8080
|
||||
```
|
||||
|
||||
You can also point the dashboard to the local filesystem, instead of a live cluster:
|
||||
```bash
|
||||
polaris dashboard --port 8080 --audit-path=./deploy/
|
||||
```
|
||||
|
||||
## Webhook
|
||||
### kubectl
|
||||
```bash
|
||||
kubectl apply -f https://github.com/fairwindsops/polaris/releases/latest/download/webhook.yaml
|
||||
```
|
||||
|
||||
### Helm
|
||||
```bash
|
||||
helm repo add fairwindsops-stable https://charts.fairwindsops.com/stable
|
||||
helm upgrade --install polaris fairwindsops-stable/polaris --namespace polaris \
|
||||
--set webhook.enable=true --set dashboard.enable=false
|
||||
```
|
||||
|
||||
## CLI
|
||||
### Installation
|
||||
Binary releases can be downloaded from the [releases page](https://github.com/fairwindsops/polaris/releases)
|
||||
or can be installed with [Homebrew](https://brew.sh/):
|
||||
```bash
|
||||
brew tap FairwindsOps/tap
|
||||
brew install FairwindsOps/tap/polaris
|
||||
polaris version
|
||||
```
|
||||
|
||||
You can run audits on the command line and see the output as JSON, YAML, or a raw score:
|
||||
```bash
|
||||
polaris audit --format yaml > report.yaml
|
||||
polaris audit --format score
|
||||
# 92
|
||||
```
|
||||
|
||||
Audits can run against a local directory or YAML file rather than a cluster:
|
||||
```bash
|
||||
polaris audit --audit-path ./deploy/
|
||||
|
||||
# or to use STDIN
|
||||
cat pod.yaml | polaris audit --audit-path -
|
||||
```
|
||||
|
||||
You can also run the audit on a single resource instead of the entire cluster:
|
||||
```bash
|
||||
polaris audit --resource "nginx-ingress/Deployment.apps/v1/default-backend"
|
||||
```
|
||||
|
||||
#### Running with CI/CD
|
||||
You can integrate Polaris into CI/CD for repositories containing infrastructure-as-code.
|
||||
For example, to fail if polaris detects *any* danger-level issues, or if the score drops below 90%:
|
||||
```bash
|
||||
polaris audit --audit-path ./deploy/ \
|
||||
--set-exit-code-on-danger \
|
||||
--set-exit-code-below-score 90
|
||||
```
|
||||
|
||||
For more on exit code meanings, see [exit-code docs](exit-codes.md).
|
||||
|
||||
#### CLI Options
|
||||
|
||||
```
|
||||
# top-level commands
|
||||
audit
|
||||
Runs a one-time audit.
|
||||
dashboard
|
||||
Runs the webserver for Polaris dashboard.
|
||||
help
|
||||
Prints help, if you give it a command then it will print help for that command. Same as -h
|
||||
version
|
||||
Prints the version of Polaris
|
||||
webhook
|
||||
Runs the webhook webserver
|
||||
|
||||
# high-level flags
|
||||
-c, --config string
|
||||
Location of Polaris configuration file
|
||||
--disallow-exemptions
|
||||
Disallow any exemptions from configuration file.
|
||||
-h, --help
|
||||
Help for Polaris (same as help command)
|
||||
--kubeconfig string
|
||||
Path to a kubeconfig. Only required if out-of-cluster.
|
||||
--log-level string
|
||||
Logrus log level (default "info")
|
||||
--master string
|
||||
The address of the Kubernetes API server. Overrides any value in kubeconfig. Only required if out-of-cluster.
|
||||
|
||||
# dashboard flags
|
||||
--audit-path string
|
||||
If specified, audits one or more YAML files instead of a cluster
|
||||
--base-path string
|
||||
Path on which the dashboard is served (default "/")
|
||||
--display-name string
|
||||
An optional identifier for the audit
|
||||
--load-audit-file string
|
||||
Runs the dashboard with data saved from a past audit.
|
||||
-p, --port int
|
||||
Port for the dashboard webserver (default 8080)
|
||||
|
||||
# audit flags
|
||||
--audit-path string
|
||||
If specified, audits one or more YAML files instead of a cluster
|
||||
--resource string
|
||||
If specified, audit a specific resource, in the format namespace/kind/version/name, e.g. nginx-ingress/Deployment.apps/v1/default-backend
|
||||
--display-name string
|
||||
An optional identifier for the audit
|
||||
--format string
|
||||
Output format for results - json, yaml, or score (default "json")
|
||||
--output-file string
|
||||
Destination file for audit results
|
||||
--output-url string
|
||||
Destination URL to send audit results
|
||||
--set-exit-code-below-score int
|
||||
Set an exit code of 4 when the score is below this threshold (1-100)
|
||||
--set-exit-code-on-danger
|
||||
Set an exit code of 3 when the audit contains danger-level issues.
|
||||
|
||||
# webhook flags
|
||||
--disable-webhook-config-installer
|
||||
disable the installer in the webhook server, so it won't install webhook configuration resources during bootstrapping
|
||||
-p, --port int
|
||||
Port for the webhook webserver (default 9876)
|
||||
```
|
||||
+78
-57
@@ -1,65 +1,86 @@
|
||||
resources:
|
||||
checks:
|
||||
# reliability
|
||||
multipleReplicasForDeployment: warning
|
||||
priorityClassNotSet: warning
|
||||
# resources
|
||||
cpuRequestsMissing: warning
|
||||
cpuRequestRanges:
|
||||
warning:
|
||||
below: 50m
|
||||
above: 1000m
|
||||
error:
|
||||
below: 500m
|
||||
above: 2000m
|
||||
cpuLimitsMissing: warning
|
||||
cpuLimitRanges:
|
||||
warning:
|
||||
below: 50m
|
||||
above: 1000m
|
||||
error:
|
||||
below: 500m
|
||||
above: 2000m
|
||||
memoryRequestsMissing: warning
|
||||
memoryRequestRanges:
|
||||
warning:
|
||||
below: 50M
|
||||
above: 2G
|
||||
error:
|
||||
below: 100M
|
||||
above: 4G
|
||||
memoryLimitsMissing: warning
|
||||
memoryLimitRanges:
|
||||
warning:
|
||||
below: 50M
|
||||
above: 2G
|
||||
error:
|
||||
below: 100M
|
||||
above: 4G
|
||||
images:
|
||||
tagNotSpecified: error
|
||||
# images
|
||||
tagNotSpecified: danger
|
||||
pullPolicyNotAlways: warning
|
||||
whitelist:
|
||||
error:
|
||||
- gcr.io/*
|
||||
blacklist:
|
||||
warning:
|
||||
- docker.io/*
|
||||
healthChecks:
|
||||
# healthChecks
|
||||
readinessProbeMissing: warning
|
||||
livenessProbeMissing: warning
|
||||
networking:
|
||||
hostNetworkSet: error
|
||||
hostPortSet: error
|
||||
security:
|
||||
hostIPCSet: error
|
||||
hostPIDSet: error
|
||||
# networking
|
||||
hostNetworkSet: warning
|
||||
hostPortSet: warning
|
||||
# security
|
||||
hostIPCSet: danger
|
||||
hostPIDSet: danger
|
||||
notReadOnlyRootFilesystem: warning
|
||||
privilegeEscalationAllowed: danger
|
||||
runAsRootAllowed: warning
|
||||
runAsPrivileged: error
|
||||
notReadOnlyRootFileSystem: warning
|
||||
privilegeEscalationAllowed: error
|
||||
capabilities:
|
||||
error:
|
||||
ifAnyAdded:
|
||||
- SYS_ADMIN
|
||||
- ALL
|
||||
ifAnyNotDropped:
|
||||
- ALL
|
||||
warning:
|
||||
ifAnyAddedBeyond:
|
||||
- NONE
|
||||
runAsPrivileged: danger
|
||||
dangerousCapabilities: danger
|
||||
insecureCapabilities: warning
|
||||
# custom
|
||||
resourceLimits: warning
|
||||
imageRegistry: danger
|
||||
|
||||
exemptions:
|
||||
- controllerNames:
|
||||
- my-network-controller
|
||||
rules:
|
||||
- hostNetworkSet
|
||||
- hostPortSet
|
||||
|
||||
customChecks:
|
||||
resourceLimits:
|
||||
containers:
|
||||
exclude:
|
||||
- initContainer
|
||||
successMessage: Resource limits are within the required range
|
||||
failureMessage: Resource limits should be within the required range
|
||||
category: Resources
|
||||
target: Container
|
||||
schema:
|
||||
'$schema': http://json-schema.org/draft-07/schema
|
||||
type: object
|
||||
required:
|
||||
- resources
|
||||
properties:
|
||||
resources:
|
||||
type: object
|
||||
required:
|
||||
- limits
|
||||
properties:
|
||||
limits:
|
||||
type: object
|
||||
required:
|
||||
- memory
|
||||
- cpu
|
||||
properties:
|
||||
memory:
|
||||
type: string
|
||||
resourceMinimum: 100M
|
||||
resourceMaximum: 6G
|
||||
cpu:
|
||||
type: string
|
||||
resourceMinimum: 100m
|
||||
resourceMaximum: "2"
|
||||
imageRegistry:
|
||||
successMessage: Image comes from allowed registries
|
||||
failureMessage: Image should not be from disallowed registry
|
||||
category: Images
|
||||
target: Container
|
||||
schema:
|
||||
'$schema': http://json-schema.org/draft-07/schema
|
||||
type: object
|
||||
properties:
|
||||
image:
|
||||
type: string
|
||||
not:
|
||||
pattern: ^quay.io
|
||||
|
||||
|
||||
+191
-34
@@ -1,43 +1,200 @@
|
||||
resources:
|
||||
checks:
|
||||
# reliability
|
||||
multipleReplicasForDeployment: ignore
|
||||
priorityClassNotSet: ignore
|
||||
# resources
|
||||
cpuRequestsMissing: warning
|
||||
cpuLimitsMissing: warning
|
||||
memoryRequestsMissing: warning
|
||||
memoryLimitsMissing: warning
|
||||
images:
|
||||
tagNotSpecified: error
|
||||
pullPolicyNotAlways: ignore
|
||||
healthChecks:
|
||||
# images
|
||||
tagNotSpecified: danger
|
||||
pullPolicyNotAlways: warning
|
||||
# healthChecks
|
||||
readinessProbeMissing: warning
|
||||
livenessProbeMissing: warning
|
||||
networking:
|
||||
# networking
|
||||
hostNetworkSet: warning
|
||||
hostPortSet: warning
|
||||
security:
|
||||
hostIPCSet: error
|
||||
hostPIDSet: error
|
||||
notReadOnlyRootFileSystem: warning
|
||||
privilegeEscalationAllowed: error
|
||||
# security
|
||||
hostIPCSet: danger
|
||||
hostPIDSet: danger
|
||||
notReadOnlyRootFilesystem: warning
|
||||
privilegeEscalationAllowed: danger
|
||||
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
|
||||
runAsPrivileged: danger
|
||||
dangerousCapabilities: danger
|
||||
insecureCapabilities: warning
|
||||
|
||||
exemptions:
|
||||
- controllerNames:
|
||||
- kube-apiserver
|
||||
- kube-proxy
|
||||
- kube-scheduler
|
||||
- etcd-manager-events
|
||||
- kube-controller-manager
|
||||
- kube-dns
|
||||
- etcd-manager-main
|
||||
rules:
|
||||
- hostPortSet
|
||||
- hostNetworkSet
|
||||
- readinessProbeMissing
|
||||
- livenessProbeMissing
|
||||
- cpuRequestsMissing
|
||||
- cpuLimitsMissing
|
||||
- memoryRequestsMissing
|
||||
- memoryLimitsMissing
|
||||
- runAsRootAllowed
|
||||
- runAsPrivileged
|
||||
- notReadOnlyRootFilesystem
|
||||
- hostPIDSet
|
||||
|
||||
- controllerNames:
|
||||
- kube-flannel-ds
|
||||
rules:
|
||||
- notReadOnlyRootFilesystem
|
||||
- runAsRootAllowed
|
||||
- notReadOnlyRootFilesystem
|
||||
- readinessProbeMissing
|
||||
- livenessProbeMissing
|
||||
- cpuLimitsMissing
|
||||
|
||||
- controllerNames:
|
||||
- cert-manager
|
||||
rules:
|
||||
- notReadOnlyRootFilesystem
|
||||
- runAsRootAllowed
|
||||
- readinessProbeMissing
|
||||
- livenessProbeMissing
|
||||
|
||||
- controllerNames:
|
||||
- cluster-autoscaler
|
||||
rules:
|
||||
- notReadOnlyRootFilesystem
|
||||
- runAsRootAllowed
|
||||
- readinessProbeMissing
|
||||
|
||||
- controllerNames:
|
||||
- vpa
|
||||
rules:
|
||||
- runAsRootAllowed
|
||||
- readinessProbeMissing
|
||||
- livenessProbeMissing
|
||||
- notReadOnlyRootFilesystem
|
||||
|
||||
- controllerNames:
|
||||
- datadog
|
||||
rules:
|
||||
- runAsRootAllowed
|
||||
- readinessProbeMissing
|
||||
- livenessProbeMissing
|
||||
- notReadOnlyRootFilesystem
|
||||
|
||||
- controllerNames:
|
||||
- nginx-ingress-controller
|
||||
rules:
|
||||
- privilegeEscalationAllowed
|
||||
- insecureCapabilities
|
||||
- runAsRootAllowed
|
||||
|
||||
- controllerNames:
|
||||
- dns-controller
|
||||
- datadog-datadog
|
||||
- kube-flannel-ds
|
||||
- kube2iam
|
||||
- aws-iam-authenticator
|
||||
- datadog
|
||||
- kube2iam
|
||||
rules:
|
||||
- hostNetworkSet
|
||||
|
||||
- controllerNames:
|
||||
- aws-iam-authenticator
|
||||
- aws-cluster-autoscaler
|
||||
- kube-state-metrics
|
||||
- dns-controller
|
||||
- external-dns
|
||||
- dnsmasq
|
||||
- autoscaler
|
||||
- kubernetes-dashboard
|
||||
- install-cni
|
||||
- kube2iam
|
||||
rules:
|
||||
- readinessProbeMissing
|
||||
- livenessProbeMissing
|
||||
|
||||
- controllerNames:
|
||||
- aws-iam-authenticator
|
||||
- nginx-ingress-default-backend
|
||||
- aws-cluster-autoscaler
|
||||
- kube-state-metrics
|
||||
- dns-controller
|
||||
- external-dns
|
||||
- kubedns
|
||||
- dnsmasq
|
||||
- autoscaler
|
||||
- tiller
|
||||
- kube2iam
|
||||
rules:
|
||||
- runAsRootAllowed
|
||||
|
||||
- controllerNames:
|
||||
- aws-iam-authenticator
|
||||
- nginx-ingress-controller
|
||||
- nginx-ingress-default-backend
|
||||
- aws-cluster-autoscaler
|
||||
- kube-state-metrics
|
||||
- dns-controller
|
||||
- external-dns
|
||||
- kubedns
|
||||
- dnsmasq
|
||||
- autoscaler
|
||||
- tiller
|
||||
- kube2iam
|
||||
rules:
|
||||
- notReadOnlyRootFilesystem
|
||||
|
||||
- controllerNames:
|
||||
- cert-manager
|
||||
- dns-controller
|
||||
- kubedns
|
||||
- dnsmasq
|
||||
- autoscaler
|
||||
- insights-agent-goldilocks-vpa-install
|
||||
- datadog
|
||||
rules:
|
||||
- cpuRequestsMissing
|
||||
- cpuLimitsMissing
|
||||
- memoryRequestsMissing
|
||||
- memoryLimitsMissing
|
||||
|
||||
- controllerNames:
|
||||
- kube2iam
|
||||
- kube-flannel-ds
|
||||
rules:
|
||||
- runAsPrivileged
|
||||
|
||||
- controllerNames:
|
||||
- kube-hunter
|
||||
rules:
|
||||
- hostPIDSet
|
||||
|
||||
- controllerNames:
|
||||
- polaris
|
||||
- kube-hunter
|
||||
- goldilocks
|
||||
- insights-agent-goldilocks-vpa-install
|
||||
rules:
|
||||
- notReadOnlyRootFilesystem
|
||||
|
||||
- controllerNames:
|
||||
- insights-agent-goldilocks-controller
|
||||
rules:
|
||||
- livenessProbeMissing
|
||||
- readinessProbeMissing
|
||||
|
||||
- controllerNames:
|
||||
- insights-agent-goldilocks-vpa-install
|
||||
- kube-hunter
|
||||
rules:
|
||||
- runAsRootAllowed
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: polaris-failing-deployment
|
||||
labels:
|
||||
app: polaris-failing-deployment
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: polaris-failing-deployment
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: polaris-failing-deployment
|
||||
spec:
|
||||
containers:
|
||||
- name: this-will-fail
|
||||
image: 'alpine'
|
||||
command: [ "/bin/sh", "-c", "--" ]
|
||||
args: [ "while true; do sleep 30; done;" ]
|
||||
securityContext:
|
||||
privileged: true
|
||||
allowPrivilegeEscalation: true
|
||||
readOnlyRootFilesystem: false
|
||||
runAsNonRoot: false
|
||||
capabilities:
|
||||
add:
|
||||
- ALL
|
||||
@@ -0,0 +1,281 @@
|
||||
{
|
||||
"PolarisOutputVersion": "1.0",
|
||||
"AuditTime": "0001-01-01T00:00:00Z",
|
||||
"SourceType": "Path",
|
||||
"SourceName": "deploy/",
|
||||
"DisplayName": "deploy/",
|
||||
"ClusterInfo": {
|
||||
"Version": "unknown",
|
||||
"Nodes": 0,
|
||||
"Pods": 0,
|
||||
"Namespaces": 2,
|
||||
"Deployments": 2,
|
||||
"StatefulSets": 0,
|
||||
"DaemonSets": 0,
|
||||
"Jobs": 0,
|
||||
"CronJobs": 0,
|
||||
"ReplicationControllers": 0
|
||||
},
|
||||
"Results": [
|
||||
{
|
||||
"Name": "polaris-dashboard",
|
||||
"Namespace": "polaris",
|
||||
"Kind": "Deployment",
|
||||
"Messages": {},
|
||||
"PodResult": {
|
||||
"Name": "",
|
||||
"Messages": {
|
||||
"hostIPCSet": {
|
||||
"ID": "hostIPCSet",
|
||||
"Message": "Host IPC is not configured",
|
||||
"Success": true,
|
||||
"Severity": "danger",
|
||||
"Category": "Security"
|
||||
},
|
||||
"hostNetworkSet": {
|
||||
"ID": "hostNetworkSet",
|
||||
"Message": "Host network is not configured",
|
||||
"Success": true,
|
||||
"Severity": "warning",
|
||||
"Category": "Networking"
|
||||
},
|
||||
"hostPIDSet": {
|
||||
"ID": "hostPIDSet",
|
||||
"Message": "Host PID is not configured",
|
||||
"Success": true,
|
||||
"Severity": "danger",
|
||||
"Category": "Security"
|
||||
}
|
||||
},
|
||||
"ContainerResults": [
|
||||
{
|
||||
"Name": "dashboard",
|
||||
"Messages": {
|
||||
"cpuLimitsMissing": {
|
||||
"ID": "cpuLimitsMissing",
|
||||
"Message": "CPU limits are set",
|
||||
"Success": true,
|
||||
"Severity": "warning",
|
||||
"Category": "Resources"
|
||||
},
|
||||
"cpuRequestsMissing": {
|
||||
"ID": "cpuRequestsMissing",
|
||||
"Message": "CPU requests are set",
|
||||
"Success": true,
|
||||
"Severity": "warning",
|
||||
"Category": "Resources"
|
||||
},
|
||||
"dangerousCapabilities": {
|
||||
"ID": "dangerousCapabilities",
|
||||
"Message": "Container does not have any dangerous capabilities",
|
||||
"Success": true,
|
||||
"Severity": "danger",
|
||||
"Category": "Security"
|
||||
},
|
||||
"hostPortSet": {
|
||||
"ID": "hostPortSet",
|
||||
"Message": "Host port is not configured",
|
||||
"Success": true,
|
||||
"Severity": "warning",
|
||||
"Category": "Networking"
|
||||
},
|
||||
"insecureCapabilities": {
|
||||
"ID": "insecureCapabilities",
|
||||
"Message": "Container does not have any insecure capabilities",
|
||||
"Success": true,
|
||||
"Severity": "warning",
|
||||
"Category": "Security"
|
||||
},
|
||||
"livenessProbeMissing": {
|
||||
"ID": "livenessProbeMissing",
|
||||
"Message": "Liveness probe is configured",
|
||||
"Success": true,
|
||||
"Severity": "warning",
|
||||
"Category": "Health Checks"
|
||||
},
|
||||
"memoryLimitsMissing": {
|
||||
"ID": "memoryLimitsMissing",
|
||||
"Message": "Memory limits are set",
|
||||
"Success": true,
|
||||
"Severity": "warning",
|
||||
"Category": "Resources"
|
||||
},
|
||||
"memoryRequestsMissing": {
|
||||
"ID": "memoryRequestsMissing",
|
||||
"Message": "Memory requests are set",
|
||||
"Success": true,
|
||||
"Severity": "warning",
|
||||
"Category": "Resources"
|
||||
},
|
||||
"privilegeEscalationAllowed": {
|
||||
"ID": "privilegeEscalationAllowed",
|
||||
"Message": "Privilege escalation not allowed",
|
||||
"Success": true,
|
||||
"Severity": "danger",
|
||||
"Category": "Security"
|
||||
},
|
||||
"readinessProbeMissing": {
|
||||
"ID": "readinessProbeMissing",
|
||||
"Message": "Readiness probe is configured",
|
||||
"Success": true,
|
||||
"Severity": "warning",
|
||||
"Category": "Health Checks"
|
||||
},
|
||||
"runAsPrivileged": {
|
||||
"ID": "runAsPrivileged",
|
||||
"Message": "Not running as privileged",
|
||||
"Success": true,
|
||||
"Severity": "danger",
|
||||
"Category": "Security"
|
||||
},
|
||||
"runAsRootAllowed": {
|
||||
"ID": "runAsRootAllowed",
|
||||
"Message": "Is not allowed to run as root",
|
||||
"Success": true,
|
||||
"Severity": "warning",
|
||||
"Category": "Security"
|
||||
},
|
||||
"tagNotSpecified": {
|
||||
"ID": "tagNotSpecified",
|
||||
"Message": "Image tag is specified",
|
||||
"Success": true,
|
||||
"Severity": "danger",
|
||||
"Category": "Images"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"Name": "polaris-webhook",
|
||||
"Namespace": "polaris",
|
||||
"Kind": "Deployment",
|
||||
"Messages": {},
|
||||
"PodResult": {
|
||||
"Name": "",
|
||||
"Messages": {
|
||||
"hostIPCSet": {
|
||||
"ID": "hostIPCSet",
|
||||
"Message": "Host IPC is not configured",
|
||||
"Success": true,
|
||||
"Severity": "danger",
|
||||
"Category": "Security"
|
||||
},
|
||||
"hostNetworkSet": {
|
||||
"ID": "hostNetworkSet",
|
||||
"Message": "Host network is not configured",
|
||||
"Success": true,
|
||||
"Severity": "warning",
|
||||
"Category": "Networking"
|
||||
},
|
||||
"hostPIDSet": {
|
||||
"ID": "hostPIDSet",
|
||||
"Message": "Host PID is not configured",
|
||||
"Success": true,
|
||||
"Severity": "danger",
|
||||
"Category": "Security"
|
||||
}
|
||||
},
|
||||
"ContainerResults": [
|
||||
{
|
||||
"Name": "webhook",
|
||||
"Messages": {
|
||||
"cpuLimitsMissing": {
|
||||
"ID": "cpuLimitsMissing",
|
||||
"Message": "CPU limits are set",
|
||||
"Success": true,
|
||||
"Severity": "warning",
|
||||
"Category": "Resources"
|
||||
},
|
||||
"cpuRequestsMissing": {
|
||||
"ID": "cpuRequestsMissing",
|
||||
"Message": "CPU requests are set",
|
||||
"Success": true,
|
||||
"Severity": "warning",
|
||||
"Category": "Resources"
|
||||
},
|
||||
"dangerousCapabilities": {
|
||||
"ID": "dangerousCapabilities",
|
||||
"Message": "Container does not have any dangerous capabilities",
|
||||
"Success": true,
|
||||
"Severity": "danger",
|
||||
"Category": "Security"
|
||||
},
|
||||
"hostPortSet": {
|
||||
"ID": "hostPortSet",
|
||||
"Message": "Host port is not configured",
|
||||
"Success": true,
|
||||
"Severity": "warning",
|
||||
"Category": "Networking"
|
||||
},
|
||||
"insecureCapabilities": {
|
||||
"ID": "insecureCapabilities",
|
||||
"Message": "Container does not have any insecure capabilities",
|
||||
"Success": true,
|
||||
"Severity": "warning",
|
||||
"Category": "Security"
|
||||
},
|
||||
"livenessProbeMissing": {
|
||||
"ID": "livenessProbeMissing",
|
||||
"Message": "Liveness probe is configured",
|
||||
"Success": true,
|
||||
"Severity": "warning",
|
||||
"Category": "Health Checks"
|
||||
},
|
||||
"memoryLimitsMissing": {
|
||||
"ID": "memoryLimitsMissing",
|
||||
"Message": "Memory limits are set",
|
||||
"Success": true,
|
||||
"Severity": "warning",
|
||||
"Category": "Resources"
|
||||
},
|
||||
"memoryRequestsMissing": {
|
||||
"ID": "memoryRequestsMissing",
|
||||
"Message": "Memory requests are set",
|
||||
"Success": true,
|
||||
"Severity": "warning",
|
||||
"Category": "Resources"
|
||||
},
|
||||
"privilegeEscalationAllowed": {
|
||||
"ID": "privilegeEscalationAllowed",
|
||||
"Message": "Privilege escalation not allowed",
|
||||
"Success": true,
|
||||
"Severity": "danger",
|
||||
"Category": "Security"
|
||||
},
|
||||
"readinessProbeMissing": {
|
||||
"ID": "readinessProbeMissing",
|
||||
"Message": "Readiness probe is configured",
|
||||
"Success": true,
|
||||
"Severity": "warning",
|
||||
"Category": "Health Checks"
|
||||
},
|
||||
"runAsPrivileged": {
|
||||
"ID": "runAsPrivileged",
|
||||
"Message": "Not running as privileged",
|
||||
"Success": true,
|
||||
"Severity": "danger",
|
||||
"Category": "Security"
|
||||
},
|
||||
"runAsRootAllowed": {
|
||||
"ID": "runAsRootAllowed",
|
||||
"Message": "Is not allowed to run as root",
|
||||
"Success": true,
|
||||
"Severity": "warning",
|
||||
"Category": "Security"
|
||||
},
|
||||
"tagNotSpecified": {
|
||||
"ID": "tagNotSpecified",
|
||||
"Message": "Image tag is specified",
|
||||
"Success": true,
|
||||
"Severity": "danger",
|
||||
"Category": "Images"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: polaris-passing-deployment
|
||||
labels:
|
||||
app: polaris-passing-deployment
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: polaris-passing-deployment
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: polaris-passing-deployment
|
||||
spec:
|
||||
containers:
|
||||
- command:
|
||||
- polaris
|
||||
- --dashboard
|
||||
image: 'quay.io/reactiveops/polaris:0.1.0'
|
||||
imagePullPolicy: 'Always'
|
||||
name: dashboard
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 8080
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 20
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 8080
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 20
|
||||
resources:
|
||||
limits:
|
||||
cpu: 100m
|
||||
memory: 128Mi
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 128Mi
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
privileged: false
|
||||
readOnlyRootFilesystem: true
|
||||
runAsNonRoot: true
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
@@ -0,0 +1,97 @@
|
||||
module github.com/fairwindsops/polaris
|
||||
|
||||
go 1.13
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.61.0
|
||||
contrib.go.opencensus.io/exporter/ocagent v0.7.0
|
||||
git.apache.org/thrift.git v0.12.0 // indirect
|
||||
github.com/Azure/go-autorest v12.4.3+incompatible
|
||||
github.com/Azure/go-autorest/autorest v0.10.0 // indirect
|
||||
github.com/appscode/jsonpatch v0.0.0-20190108182946-7c0e3b262f30
|
||||
github.com/beorn7/perks v1.0.1
|
||||
github.com/census-instrumentation/opencensus-proto v0.3.0
|
||||
github.com/davecgh/go-spew v1.1.1
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||
github.com/evanphx/json-patch v4.5.0+incompatible
|
||||
github.com/go-logr/logr v0.1.0
|
||||
github.com/go-logr/zapr v0.1.1
|
||||
github.com/gobuffalo/depgen v0.1.0 // indirect
|
||||
github.com/gobuffalo/envy v1.9.0
|
||||
github.com/gobuffalo/genny v0.6.0
|
||||
github.com/gobuffalo/gogen v0.2.0
|
||||
github.com/gobuffalo/logger v1.0.3
|
||||
github.com/gobuffalo/mapi v1.2.1
|
||||
github.com/gobuffalo/packd v1.0.0
|
||||
github.com/gobuffalo/packr/v2 v2.8.0
|
||||
github.com/gobuffalo/syncx v0.1.0
|
||||
github.com/gogo/protobuf v1.3.1
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e
|
||||
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7 // indirect
|
||||
github.com/golang/protobuf v1.4.2
|
||||
github.com/google/btree v1.0.0
|
||||
github.com/google/gofuzz v1.1.0
|
||||
github.com/google/uuid v1.1.1
|
||||
github.com/googleapis/gnostic v0.3.1
|
||||
github.com/gophercloud/gophercloud v0.0.0-20190516165734-b3a23cc94cc5
|
||||
github.com/gorilla/mux v1.7.4
|
||||
github.com/gregjones/httpcache v0.0.0-20190212212710-3befbb6ad0cc
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.14.6
|
||||
github.com/hashicorp/golang-lru v0.5.4
|
||||
github.com/imdario/mergo v0.3.10
|
||||
github.com/joho/godotenv v1.3.0
|
||||
github.com/json-iterator/go v1.1.10
|
||||
github.com/karrick/godirwalk v1.15.6
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3
|
||||
github.com/markbates/oncer v1.0.0
|
||||
github.com/markbates/safe v1.0.1
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd
|
||||
github.com/modern-go/reflect2 v1.0.1
|
||||
github.com/pborman/uuid v0.0.0-20180906182336-adf5a7427709
|
||||
github.com/petar/GoLLRB v0.0.0-20190514000832-33fb24c13b99
|
||||
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/pmezard/go-difflib v1.0.0
|
||||
github.com/prometheus/client_golang v1.7.1
|
||||
github.com/prometheus/client_model v0.2.0
|
||||
github.com/prometheus/common v0.10.0
|
||||
github.com/prometheus/procfs v0.1.3
|
||||
github.com/qri-io/jsonschema v0.1.1
|
||||
github.com/rogpeppe/go-internal v1.6.0
|
||||
github.com/sirupsen/logrus v1.6.0
|
||||
github.com/spf13/cobra v1.0.0
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/stretchr/testify v1.6.1
|
||||
gitlab.com/golang-commonmark/html v0.0.0-20180917080848-cfaf75183c4a
|
||||
gitlab.com/golang-commonmark/linkify v0.0.0-20180917065525-c22b7bdb1179
|
||||
gitlab.com/golang-commonmark/markdown v0.0.0-20181102083822-772775880e1f
|
||||
gitlab.com/golang-commonmark/mdurl v0.0.0-20180912090424-e5bce34c34f2
|
||||
gitlab.com/golang-commonmark/puny v0.0.0-20180912090636-2cd490539afe
|
||||
go.opencensus.io v0.22.4
|
||||
go.uber.org/atomic v1.6.0
|
||||
go.uber.org/multierr v1.5.0
|
||||
go.uber.org/zap v1.15.0
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208
|
||||
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1
|
||||
golang.org/x/text v0.3.3
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0
|
||||
golang.org/x/tools v0.0.0-20200713011307-fd294ab11aed
|
||||
google.golang.org/api v0.29.0
|
||||
google.golang.org/appengine v1.6.6
|
||||
google.golang.org/genproto v0.0.0-20200711021454-869866162049
|
||||
google.golang.org/grpc v1.30.0
|
||||
gopkg.in/inf.v0 v0.9.1
|
||||
gopkg.in/yaml.v2 v2.3.0
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c
|
||||
k8s.io/api v0.0.0-20181213150558-05914d821849
|
||||
k8s.io/apimachinery v0.0.0-20181127025237-2b1284ed4c93
|
||||
k8s.io/client-go v0.0.0-20181213151034-8d9ed539ba31
|
||||
k8s.io/klog v0.4.0
|
||||
k8s.io/kube-openapi v0.0.0-20190510232812-a01b7d5d6c22
|
||||
sigs.k8s.io/controller-runtime v0.1.10
|
||||
sigs.k8s.io/yaml v1.2.0
|
||||
)
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 186 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019 ReactiveOps
|
||||
// Copyright 2019 FairwindsOps Inc
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -15,252 +15,14 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
conf "github.com/reactiveops/polaris/pkg/config"
|
||||
"github.com/reactiveops/polaris/pkg/dashboard"
|
||||
"github.com/reactiveops/polaris/pkg/kube"
|
||||
"github.com/reactiveops/polaris/pkg/validator"
|
||||
fwebhook "github.com/reactiveops/polaris/pkg/webhook"
|
||||
"github.com/sirupsen/logrus"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
|
||||
apitypes "k8s.io/apimachinery/pkg/types"
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth" // Required for other auth providers like GKE.
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/config"
|
||||
"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"
|
||||
"github.com/fairwindsops/polaris/cmd/polaris"
|
||||
)
|
||||
|
||||
const (
|
||||
// Version represents the current release version of Polaris
|
||||
Version = "0.3.1"
|
||||
Version = "1.2.1"
|
||||
)
|
||||
|
||||
func main() {
|
||||
dashboard := flag.Bool("dashboard", false, "Runs the webserver for Polaris dashboard.")
|
||||
webhook := flag.Bool("webhook", false, "Runs the webhook webserver.")
|
||||
audit := flag.Bool("audit", false, "Runs a one-time audit.")
|
||||
auditPath := flag.String("audit-path", "", "If specified, audits one or more YAML files instead of a cluster")
|
||||
setExitCode := flag.Bool("set-exit-code-on-error", false, "When running with --audit, set an exit code of 3 when the audit contains error-level issues.")
|
||||
minScore := flag.Int("set-exit-code-below-score", 0, "When running with --audit, set an exit code of 4 when the score is below this threshold (1-100)")
|
||||
dashboardPort := flag.Int("dashboard-port", 8080, "Port for the dashboard webserver")
|
||||
dashboardBasePath := flag.String("dashboard-base-path", "/", "Path on which the dashboard is served")
|
||||
webhookPort := flag.Int("webhook-port", 9876, "Port for the webhook webserver")
|
||||
auditOutputURL := flag.String("output-url", "", "Destination URL to send audit results")
|
||||
auditOutputFile := flag.String("output-file", "", "Destination file for audit results")
|
||||
auditOutputFormat := flag.String("output-format", "json", "Output format for results - json, yaml, or score")
|
||||
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")
|
||||
disableWebhookConfigInstaller := flag.Bool("disable-webhook-config-installer", false,
|
||||
"disable the installer in the webhook server, so it won't install webhook configuration resources during bootstrapping")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
if *version {
|
||||
fmt.Printf("Polaris version %s\n", Version)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
parsedLevel, err := logrus.ParseLevel(*logLevel)
|
||||
if err != nil {
|
||||
logrus.Errorf("log-level flag has invalid value %s", *logLevel)
|
||||
} else {
|
||||
logrus.SetLevel(parsedLevel)
|
||||
}
|
||||
|
||||
c, err := conf.ParseFile(*configPath)
|
||||
if *displayName != "" {
|
||||
c.DisplayName = *displayName
|
||||
}
|
||||
if err != nil {
|
||||
logrus.Errorf("Error parsing config at %s: %v", *configPath, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if !*dashboard && !*webhook && !*audit {
|
||||
*audit = true
|
||||
}
|
||||
|
||||
if *webhook {
|
||||
startWebhookServer(c, *disableWebhookConfigInstaller, *webhookPort)
|
||||
} else if *dashboard {
|
||||
startDashboardServer(c, *auditPath, *dashboardPort, *dashboardBasePath)
|
||||
} else if *audit {
|
||||
auditData := runAndReportAudit(c, *auditPath, *auditOutputFile, *auditOutputURL, *auditOutputFormat)
|
||||
if *setExitCode && auditData.ClusterSummary.Results.Totals.Errors > 0 {
|
||||
logrus.Infof("%d errors found in audit", auditData.ClusterSummary.Results.Totals.Errors)
|
||||
os.Exit(3)
|
||||
} else if *minScore != 0 && auditData.ClusterSummary.Score < uint(*minScore) {
|
||||
logrus.Infof("Audit score of %d is less than the provided minimum of %d", auditData.ClusterSummary.Score, *minScore)
|
||||
os.Exit(4)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func startDashboardServer(c conf.Configuration, auditPath string, port int, basePath string) {
|
||||
router := dashboard.GetRouter(c, auditPath, port, basePath)
|
||||
router.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte("OK"))
|
||||
})
|
||||
http.Handle("/", router)
|
||||
|
||||
logrus.Infof("Starting Polaris dashboard server on port %d", port)
|
||||
logrus.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", port), nil))
|
||||
}
|
||||
|
||||
func startWebhookServer(c conf.Configuration, disableWebhookConfigInstaller bool, port int) {
|
||||
logrus.Debug("Setting up controller manager")
|
||||
mgr, err := manager.New(config.GetConfigOrDie(), manager.Options{})
|
||||
if err != nil {
|
||||
logrus.Errorf("Unable to set up overall controller manager: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
polarisAppName := "polaris"
|
||||
polarisResourceName := "polaris-webhook"
|
||||
polarisNamespaceBytes, err := ioutil.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace")
|
||||
|
||||
if err != nil {
|
||||
// Not exiting here as we have fallback options
|
||||
logrus.Debugf("Error reading namespace information: %v", err)
|
||||
}
|
||||
|
||||
polarisNamespace := string(polarisNamespaceBytes)
|
||||
if polarisNamespace == "" {
|
||||
polarisNamespace = polarisResourceName
|
||||
logrus.Debugf("Could not determine current namespace, creating resources in %s namespace", polarisNamespace)
|
||||
}
|
||||
|
||||
logrus.Info("Setting up webhook server")
|
||||
as, err := webhook.NewServer(polarisResourceName, mgr, webhook.ServerOptions{
|
||||
Port: int32(port),
|
||||
CertDir: "/opt/cert",
|
||||
DisableWebhookConfigInstaller: &disableWebhookConfigInstaller,
|
||||
BootstrapOptions: &webhook.BootstrapOptions{
|
||||
ValidatingWebhookConfigName: polarisResourceName,
|
||||
Secret: &apitypes.NamespacedName{
|
||||
Namespace: polarisNamespace,
|
||||
Name: polarisResourceName,
|
||||
},
|
||||
|
||||
Service: &webhook.Service{
|
||||
Namespace: polarisNamespace,
|
||||
Name: polarisResourceName,
|
||||
|
||||
// Selectors should select the pods that runs this webhook server.
|
||||
Selectors: map[string]string{
|
||||
"app": polarisAppName,
|
||||
"component": "webhook",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
logrus.Errorf("Error setting up webhook server: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
logrus.Infof("Polaris webhook server listening on port %d", port)
|
||||
|
||||
d1 := fwebhook.NewWebhook("deployments", mgr, fwebhook.Validator{Config: c}, &appsv1.Deployment{})
|
||||
d2 := fwebhook.NewWebhook("deployments-ext", mgr, fwebhook.Validator{Config: c}, &extensionsv1beta1.Deployment{})
|
||||
logrus.Debug("Registering webhooks to the webhook server")
|
||||
if err = as.Register(d1, d2); err != nil {
|
||||
logrus.Debugf("Unable to register webhooks in the admission server: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
logrus.Debug("Starting webhook manager")
|
||||
if err := mgr.Start(signals.SetupSignalHandler()); err != nil {
|
||||
logrus.Errorf("Error starting manager: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func runAndReportAudit(c conf.Configuration, auditPath string, outputFile string, outputURL string, outputFormat string) validator.AuditData {
|
||||
k, err := kube.CreateResourceProvider(auditPath)
|
||||
if err != nil {
|
||||
logrus.Errorf("Error fetching Kubernetes resources %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
auditData, err := validator.RunAudit(c, k)
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var outputBytes []byte
|
||||
if outputFormat == "score" {
|
||||
outputBytes = []byte(fmt.Sprintf("%d\n", auditData.ClusterSummary.Score))
|
||||
} else if outputFormat == "yaml" {
|
||||
jsonBytes, err := json.Marshal(auditData)
|
||||
if err == nil {
|
||||
outputBytes, err = yaml.JSONToYAML(jsonBytes)
|
||||
}
|
||||
} else {
|
||||
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(outputBytes))
|
||||
|
||||
if err != nil {
|
||||
logrus.Errorf("Error building request for output: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
if err != nil {
|
||||
logrus.Errorf("Error making request for output: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
|
||||
if err != nil {
|
||||
logrus.Errorf("Error reading response: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
logrus.Infof("Received response: %v", body)
|
||||
}
|
||||
|
||||
if outputFile != "" {
|
||||
err := ioutil.WriteFile(outputFile, []byte(outputBytes), 0644)
|
||||
if err != nil {
|
||||
logrus.Errorf("Error writing output to file: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
return auditData
|
||||
cmd.Execute(Version)
|
||||
}
|
||||
|
||||
+48
-81
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019 ReactiveOps
|
||||
// Copyright 2019 FairwindsOps Inc
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -16,108 +16,56 @@ package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
packr "github.com/gobuffalo/packr/v2"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
"k8s.io/apimachinery/pkg/util/yaml"
|
||||
)
|
||||
|
||||
// Configuration contains all of the config for the validation checks.
|
||||
type Configuration struct {
|
||||
DisplayName string `json:"displayName"`
|
||||
Resources Resources `json:"resources"`
|
||||
HealthChecks HealthChecks `json:"healthChecks"`
|
||||
Images Images `json:"images"`
|
||||
Networking Networking `json:"networking"`
|
||||
Security Security `json:"security"`
|
||||
DisplayName string `json:"displayName"`
|
||||
Checks map[string]Severity `json:"checks"`
|
||||
CustomChecks map[string]SchemaCheck `json:"customChecks"`
|
||||
Exemptions []Exemption `json:"exemptions"`
|
||||
DisallowExemptions bool `json:"disallowExemptions"`
|
||||
}
|
||||
|
||||
// Resources contains config for resource requests and limits.
|
||||
type Resources struct {
|
||||
CPURequestsMissing Severity `json:"cpuRequestsMissing"`
|
||||
CPURequestRanges ResourceRanges `json:"cpuRequestRanges"`
|
||||
CPULimitsMissing Severity `json:"cpuLimitsMissing"`
|
||||
CPULimitRanges ResourceRanges `json:"cpuLimitRanges"`
|
||||
MemoryRequestsMissing Severity `json:"memoryRequestsMissing"`
|
||||
MemoryRequestRanges ResourceRanges `json:"memoryRequestRanges"`
|
||||
MemoryLimitsMissing Severity `json:"memoryLimitsMissing"`
|
||||
MemoryLimitRanges ResourceRanges `json:"memoryLimitRanges"`
|
||||
// Exemption represents an exemption to normal rules
|
||||
type Exemption struct {
|
||||
Rules []string `json:"rules"`
|
||||
ControllerNames []string `json:"controllerNames"`
|
||||
}
|
||||
|
||||
// ResourceRanges contains config for requests or limits for a specific resource.
|
||||
type ResourceRanges struct {
|
||||
Warning ResourceRange `json:"warning"`
|
||||
Error ResourceRange `json:"error"`
|
||||
}
|
||||
var configBox = (*packr.Box)(nil)
|
||||
|
||||
// ResourceRange can contain below and above conditions for validation.
|
||||
type ResourceRange struct {
|
||||
Below *resource.Quantity `json:"below"`
|
||||
Above *resource.Quantity `json:"above"`
|
||||
}
|
||||
|
||||
// HealthChecks contains config for readiness and liveness probes.
|
||||
type HealthChecks struct {
|
||||
ReadinessProbeMissing Severity `json:"readinessProbeMissing"`
|
||||
LivenessProbeMissing Severity `json:"livenessProbeMissing"`
|
||||
}
|
||||
|
||||
// Images contains the config for images.
|
||||
type Images struct {
|
||||
TagNotSpecified Severity `json:"tagNotSpecified"`
|
||||
PullPolicyNotAlways Severity `json:"pullPolicyNotAlways"`
|
||||
Whitelist ErrorWarningLists `json:"whitelist"`
|
||||
Blacklist ErrorWarningLists `json:"blacklist"`
|
||||
}
|
||||
|
||||
// ErrorWarningLists provides lists of patterns to match or avoid in image tags.
|
||||
type ErrorWarningLists struct {
|
||||
Error []string `json:"error"`
|
||||
Warning []string `json:"warning"`
|
||||
}
|
||||
|
||||
// Networking contains the config for networking validations.
|
||||
type Networking struct {
|
||||
HostNetworkSet Severity `json:"hostNetworkSet"`
|
||||
HostPortSet Severity `json:"hostPortSet"`
|
||||
}
|
||||
|
||||
// Security contains the config for security validations.
|
||||
type Security struct {
|
||||
HostIPCSet Severity `json:"hostIPCSet"`
|
||||
HostPIDSet Severity `json:"hostPIDSet"`
|
||||
RunAsRootAllowed Severity `json:"runAsRootAllowed"`
|
||||
RunAsPrivileged Severity `json:"RunAsPrivileged"`
|
||||
NotReadOnlyRootFileSystem Severity `json:"notReadOnlyRootFileSystem"`
|
||||
PrivilegeEscalationAllowed Severity `json:"privilegeEscalationAllowed"`
|
||||
Capabilities SecurityCapabilities `json:"capabilities"`
|
||||
}
|
||||
|
||||
// SecurityCapabilities contains the config for security capabilities validations.
|
||||
type SecurityCapabilities struct {
|
||||
Error SecurityCapabilityLists `json:"error"`
|
||||
Warning SecurityCapabilityLists `json:"warning"`
|
||||
}
|
||||
|
||||
// SecurityCapabilityLists contains the config for security capabilitie list validations.
|
||||
type SecurityCapabilityLists struct {
|
||||
IfAnyAdded []corev1.Capability `json:"ifAnyAdded"`
|
||||
IfAnyAddedBeyond []corev1.Capability `json:"ifAnyAddedBeyond"`
|
||||
IfAnyNotDropped []corev1.Capability `json:"ifAnyNotDropped"`
|
||||
func getConfigBox() *packr.Box {
|
||||
if configBox == (*packr.Box)(nil) {
|
||||
configBox = packr.New("Config", "../../examples")
|
||||
}
|
||||
return configBox
|
||||
}
|
||||
|
||||
// ParseFile parses config from a file.
|
||||
func ParseFile(path string) (Configuration, error) {
|
||||
configBox := packr.New("Config", "../../examples")
|
||||
var rawBytes []byte
|
||||
var err error
|
||||
if path == "" {
|
||||
rawBytes, err = configBox.Find("config.yaml")
|
||||
rawBytes, err = getConfigBox().Find("config.yaml")
|
||||
} else if strings.HasPrefix(path, "https://") || strings.HasPrefix(path, "http://") {
|
||||
//path is a url
|
||||
response, err2 := http.Get(path)
|
||||
if err2 != nil {
|
||||
return Configuration{}, err2
|
||||
}
|
||||
rawBytes, err = ioutil.ReadAll(response.Body)
|
||||
} else {
|
||||
//path is local
|
||||
rawBytes, err = ioutil.ReadFile(path)
|
||||
}
|
||||
if err != nil {
|
||||
@@ -134,9 +82,28 @@ func Parse(rawBytes []byte) (Configuration, error) {
|
||||
for {
|
||||
if err := d.Decode(&conf); err != nil {
|
||||
if err == io.EOF {
|
||||
return conf, nil
|
||||
break
|
||||
}
|
||||
return conf, fmt.Errorf("Decoding config failed: %v", err)
|
||||
}
|
||||
}
|
||||
for key, check := range conf.CustomChecks {
|
||||
err := check.Initialize(key)
|
||||
if err != nil {
|
||||
return conf, err
|
||||
}
|
||||
conf.CustomChecks[key] = check
|
||||
if _, ok := conf.Checks[key]; !ok {
|
||||
return conf, fmt.Errorf("no severity specified for custom check %s. Please add the following to your configuration:\n\nchecks:\n %s: warning # or danger/ignore\n\nto enable your check", key, key)
|
||||
}
|
||||
}
|
||||
return conf, conf.Validate()
|
||||
}
|
||||
|
||||
// Validate checks if a config is valid
|
||||
func (c Configuration) Validate() error {
|
||||
if len(c.Checks) == 0 {
|
||||
return errors.New("No checks were enabled")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
+134
-101
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019 ReactiveOps
|
||||
// Copyright 2019 FairwindsOps Inc
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -15,133 +15,166 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
)
|
||||
|
||||
var resourceConfInvalid1 = `test`
|
||||
var confInvalid = `test`
|
||||
|
||||
var resourceConfYAML1 = `---
|
||||
resources:
|
||||
cpuRequestRanges:
|
||||
error:
|
||||
below: 100m
|
||||
above: 1
|
||||
warning:
|
||||
below: 200m
|
||||
above: 800m
|
||||
memoryRequestRanges:
|
||||
error:
|
||||
below: 100M
|
||||
above: 3G
|
||||
warning:
|
||||
below: 200M
|
||||
above: 2G
|
||||
cpuLimitRanges:
|
||||
error:
|
||||
below: 100m
|
||||
above: 2
|
||||
warning:
|
||||
below: 300m
|
||||
above: 1800m
|
||||
memoryLimitRanges:
|
||||
error:
|
||||
below: 200M
|
||||
above: 6G
|
||||
warning:
|
||||
below: 300M
|
||||
above: 4G
|
||||
var confValidYAML = `
|
||||
checks:
|
||||
cpuRequestsMissing: warning
|
||||
`
|
||||
|
||||
var resourceConfJSON1 = `{
|
||||
"resources": {
|
||||
"cpuRequestRanges": {
|
||||
"error": {
|
||||
"below": "100m",
|
||||
"above": 1
|
||||
},
|
||||
"warning": {
|
||||
"below": "200m",
|
||||
"above": "800m"
|
||||
}
|
||||
},
|
||||
"memoryRequestRanges": {
|
||||
"error": {
|
||||
"below": "100M",
|
||||
"above": "3G"
|
||||
},
|
||||
"warning": {
|
||||
"below": "200M",
|
||||
"above": "2G"
|
||||
}
|
||||
},
|
||||
"cpuLimitRanges": {
|
||||
"error": {
|
||||
"below": "100m",
|
||||
"above": 2
|
||||
},
|
||||
"warning": {
|
||||
"below": "300m",
|
||||
"above": "1800m"
|
||||
}
|
||||
},
|
||||
"memoryLimitRanges": {
|
||||
"error": {
|
||||
"below": "200M",
|
||||
"above": "6G"
|
||||
},
|
||||
"warning": {
|
||||
"below": "300M",
|
||||
"above": "4G"
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
var confValidJSON = `
|
||||
{
|
||||
"checks": {
|
||||
"cpuRequestsMissing": "warning"
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
var confCustomChecks = `
|
||||
checks:
|
||||
foo: warning
|
||||
customChecks:
|
||||
foo:
|
||||
successMessage: Security context is set
|
||||
failureMessage: Security context should be set
|
||||
category: Security
|
||||
target: Container
|
||||
schema:
|
||||
'$schema': http://json-schema.org/draft-07/schema
|
||||
type: object
|
||||
required:
|
||||
- securityContext
|
||||
`
|
||||
|
||||
var confCustomChecksWithJSONSchema = `
|
||||
checks:
|
||||
foo: warning
|
||||
customChecks:
|
||||
foo:
|
||||
successMessage: Security context is set
|
||||
failureMessage: Security context should be set
|
||||
category: Security
|
||||
target: Container
|
||||
jsonSchema: >
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema",
|
||||
"type": "object",
|
||||
"required": ["securityContext"]
|
||||
}
|
||||
`
|
||||
|
||||
var confCustomChecksMissing = `
|
||||
customChecks:
|
||||
foo:
|
||||
successMessage: Security context is set
|
||||
failureMessage: Security context should be set
|
||||
category: Security
|
||||
target: Container
|
||||
schema:
|
||||
'$schema': http://json-schema.org/draft-07/schema
|
||||
type: object
|
||||
required:
|
||||
- securityContext
|
||||
|
||||
`
|
||||
|
||||
func TestParseError(t *testing.T) {
|
||||
_, err := Parse([]byte(resourceConfInvalid1))
|
||||
_, err := Parse([]byte(confInvalid))
|
||||
expectedErr := "Decoding config failed: error unmarshaling JSON: while decoding JSON: json: cannot unmarshal string into Go value of type config.Configuration"
|
||||
assert.EqualError(t, err, expectedErr)
|
||||
}
|
||||
|
||||
func TestParseYaml(t *testing.T) {
|
||||
parsedConf, err := Parse([]byte(resourceConfYAML1))
|
||||
parsedConf, err := Parse([]byte(confValidYAML))
|
||||
assert.NoError(t, err, "Expected no error when parsing YAML config")
|
||||
|
||||
testParsedConfig(t, &parsedConf)
|
||||
}
|
||||
|
||||
func TestParseJson(t *testing.T) {
|
||||
parsedConf, err := Parse([]byte(resourceConfJSON1))
|
||||
parsedConf, err := Parse([]byte(confValidJSON))
|
||||
assert.NoError(t, err, "Expected no error when parsing JSON config")
|
||||
|
||||
testParsedConfig(t, &parsedConf)
|
||||
}
|
||||
|
||||
func testParsedConfig(t *testing.T, config *Configuration) {
|
||||
cpuRequests := config.Resources.CPURequestRanges
|
||||
assert.Equal(t, int64(100), cpuRequests.Error.Below.ScaledValue(resource.Milli))
|
||||
assert.Equal(t, int64(1000), cpuRequests.Error.Above.ScaledValue(resource.Milli))
|
||||
assert.Equal(t, int64(200), cpuRequests.Warning.Below.ScaledValue(resource.Milli))
|
||||
assert.Equal(t, int64(800), cpuRequests.Warning.Above.ScaledValue(resource.Milli))
|
||||
func TestConfigFromURL(t *testing.T) {
|
||||
var err error
|
||||
var parsedConf Configuration
|
||||
srv := &http.Server{Addr: ":8081"}
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
io.WriteString(w, confValidYAML)
|
||||
})
|
||||
|
||||
memRequests := config.Resources.MemoryRequestRanges
|
||||
assert.Equal(t, int64(100), memRequests.Error.Below.ScaledValue(resource.Mega))
|
||||
assert.Equal(t, int64(3000), memRequests.Error.Above.ScaledValue(resource.Mega))
|
||||
assert.Equal(t, int64(200), memRequests.Warning.Below.ScaledValue(resource.Mega))
|
||||
assert.Equal(t, int64(2000), memRequests.Warning.Above.ScaledValue(resource.Mega))
|
||||
go func() {
|
||||
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
|
||||
log.Fatalf("ListenAndServe(): %s", err)
|
||||
}
|
||||
}()
|
||||
time.Sleep(time.Second)
|
||||
|
||||
cpuLimits := config.Resources.CPULimitRanges
|
||||
assert.Equal(t, int64(100), cpuLimits.Error.Below.ScaledValue(resource.Milli))
|
||||
assert.Equal(t, int64(2000), cpuLimits.Error.Above.ScaledValue(resource.Milli))
|
||||
assert.Equal(t, int64(300), cpuLimits.Warning.Below.ScaledValue(resource.Milli))
|
||||
assert.Equal(t, int64(1800), cpuLimits.Warning.Above.ScaledValue(resource.Milli))
|
||||
parsedConf, err = ParseFile("http://localhost:8081/exampleURL")
|
||||
assert.NoError(t, err, "Expected no error when parsing YAML from URL")
|
||||
if err := srv.Shutdown(context.TODO()); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
testParsedConfig(t, &parsedConf)
|
||||
|
||||
memLimits := config.Resources.MemoryLimitRanges
|
||||
assert.Equal(t, int64(200), memLimits.Error.Below.ScaledValue(resource.Mega))
|
||||
assert.Equal(t, int64(6000), memLimits.Error.Above.ScaledValue(resource.Mega))
|
||||
assert.Equal(t, int64(300), memLimits.Warning.Below.ScaledValue(resource.Mega))
|
||||
assert.Equal(t, int64(4000), memLimits.Warning.Above.ScaledValue(resource.Mega))
|
||||
}
|
||||
|
||||
func TestConfigNoServerError(t *testing.T) {
|
||||
var err error
|
||||
_, err = ParseFile("http://localhost:8081/exampleURL")
|
||||
assert.Error(t, err)
|
||||
assert.Regexp(t, regexp.MustCompile("connection refused"), err.Error())
|
||||
}
|
||||
|
||||
func TestConfigWithCustomChecks(t *testing.T) {
|
||||
valid := map[string]interface{}{
|
||||
"securityContext": map[string]interface{}{},
|
||||
}
|
||||
invalid := map[string]interface{}{
|
||||
"notSecurityContext": map[string]interface{}{},
|
||||
}
|
||||
|
||||
parsedConf, err := Parse([]byte(confCustomChecks))
|
||||
assert.NoError(t, err, "Expected no error when parsing YAML config")
|
||||
assert.Equal(t, 1, len(parsedConf.CustomChecks))
|
||||
isValid, err := parsedConf.CustomChecks["foo"].CheckObject(valid)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, true, isValid)
|
||||
isValid, err = parsedConf.CustomChecks["foo"].CheckObject(invalid)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, false, isValid)
|
||||
|
||||
parsedConf, err = Parse([]byte(confCustomChecksWithJSONSchema))
|
||||
assert.NoError(t, err, "Expected no error when parsing YAML config")
|
||||
assert.Equal(t, 1, len(parsedConf.CustomChecks))
|
||||
isValid, err = parsedConf.CustomChecks["foo"].CheckObject(valid)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, true, isValid)
|
||||
isValid, err = parsedConf.CustomChecks["foo"].CheckObject(invalid)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, false, isValid)
|
||||
}
|
||||
|
||||
func TestCustomChecksMissingSeverity(t *testing.T) {
|
||||
_, err := Parse([]byte(confCustomChecksMissing))
|
||||
assert.Error(t, err, "Expected error when check has no severity set")
|
||||
}
|
||||
|
||||
func testParsedConfig(t *testing.T, config *Configuration) {
|
||||
assert.Equal(t, SeverityWarning, config.Checks["cpuRequestsMissing"])
|
||||
assert.Equal(t, Severity(""), config.Checks["cpuLimitsMissing"])
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// IsActionable determines whether a check is actionable given the current configuration
|
||||
func (conf Configuration) IsActionable(ruleID, controllerName string) bool {
|
||||
if severity, ok := conf.Checks[ruleID]; !ok || !severity.IsActionable() {
|
||||
return false
|
||||
}
|
||||
if conf.DisallowExemptions {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, example := range conf.Exemptions {
|
||||
for _, rule := range example.Rules {
|
||||
if rule != ruleID {
|
||||
continue
|
||||
}
|
||||
for _, controller := range example.ControllerNames {
|
||||
if strings.HasPrefix(controllerName, controller) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(example.Rules) == 0 {
|
||||
for _, controller := range example.ControllerNames {
|
||||
if strings.HasPrefix(controllerName, controller) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// Copyright 2019 FairwindsOps Inc
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var confExemptRuleTest = `
|
||||
checks:
|
||||
ANY: warning
|
||||
OTHER: warning
|
||||
exemptions:
|
||||
- controllerNames:
|
||||
- test
|
||||
rules:
|
||||
- ANY
|
||||
`
|
||||
|
||||
var confExemptTest = `
|
||||
checks:
|
||||
ANY: warning
|
||||
exemptions:
|
||||
- controllerNames:
|
||||
- test
|
||||
`
|
||||
|
||||
func TestInclusiveExemption(t *testing.T) {
|
||||
parsedConf, _ := Parse([]byte(confExemptTest))
|
||||
applicable := parsedConf.IsActionable("ANY", "test")
|
||||
applicableOtherController := parsedConf.IsActionable("ANY", "other")
|
||||
|
||||
assert.False(t, applicable, "Expected all checks to be exempted when their controller is specified.")
|
||||
assert.True(t, applicableOtherController, "Expected checks to only be exempted when their controller is specified.")
|
||||
}
|
||||
|
||||
func TestIndividualRuleException(t *testing.T) {
|
||||
parsedConf, _ := Parse([]byte(confExemptRuleTest))
|
||||
applicable := parsedConf.IsActionable("ANY", "test")
|
||||
applicableOtherRule := parsedConf.IsActionable("OTHER", "test")
|
||||
applicableOtherRuleOtherController := parsedConf.IsActionable("OTHER", "other")
|
||||
applicableRuleOtherController := parsedConf.IsActionable("ANY", "other")
|
||||
|
||||
assert.False(t, applicable, "Expected all checks to be exempted when their controller and rule are specified.")
|
||||
assert.True(t, applicableOtherRule, "Expected checks to only be exempted when their controller and rule are specified.")
|
||||
assert.True(t, applicableOtherRuleOtherController, "Expected checks to only be exempted when their controller and rule are specified.")
|
||||
assert.True(t, applicableRuleOtherController, "Expected checks to only be exempted when their controller and rule are specified.")
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/qri-io/jsonschema"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
)
|
||||
|
||||
// TargetKind represents the part of the config to be validated
|
||||
type TargetKind string
|
||||
|
||||
const (
|
||||
// TargetContainer points to the container spec
|
||||
TargetContainer TargetKind = "Container"
|
||||
// TargetPod points to the pod spec
|
||||
TargetPod TargetKind = "Pod"
|
||||
// TargetController points to the controller's spec
|
||||
TargetController TargetKind = "Controller"
|
||||
)
|
||||
|
||||
// SchemaCheck is a Polaris check that runs using JSON Schema
|
||||
type SchemaCheck struct {
|
||||
ID string `yaml:"id"`
|
||||
Category string `yaml:"category"`
|
||||
SuccessMessage string `yaml:"successMessage"`
|
||||
FailureMessage string `yaml:"failureMessage"`
|
||||
Controllers includeExcludeList `yaml:"controllers"`
|
||||
Containers includeExcludeList `yaml:"containers"`
|
||||
Target TargetKind `yaml:"target"`
|
||||
SchemaTarget TargetKind `yaml:"schemaTarget"`
|
||||
Schema jsonschema.RootSchema `yaml:"schema"`
|
||||
JSONSchema string `yaml:"jsonSchema"`
|
||||
}
|
||||
|
||||
type resourceMinimum string
|
||||
type resourceMaximum string
|
||||
|
||||
func init() {
|
||||
jsonschema.RegisterValidator("resourceMinimum", newResourceMinimum)
|
||||
jsonschema.RegisterValidator("resourceMaximum", newResourceMaximum)
|
||||
}
|
||||
|
||||
type includeExcludeList struct {
|
||||
Include []string `yaml:"include"`
|
||||
Exclude []string `yaml:"exclude"`
|
||||
}
|
||||
|
||||
func newResourceMinimum() jsonschema.Validator {
|
||||
return new(resourceMinimum)
|
||||
}
|
||||
|
||||
func newResourceMaximum() jsonschema.Validator {
|
||||
return new(resourceMaximum)
|
||||
}
|
||||
|
||||
// Validate checks that a specified quanitity is not less than the minimum
|
||||
func (min resourceMinimum) Validate(path string, data interface{}, errs *[]jsonschema.ValError) {
|
||||
err := validateRange(path, string(min), data, true)
|
||||
if err != nil {
|
||||
*errs = append(*errs, *err...)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate checks that a specified quanitity is not greater than the maximum
|
||||
func (max resourceMaximum) Validate(path string, data interface{}, errs *[]jsonschema.ValError) {
|
||||
err := validateRange(path, string(max), data, false)
|
||||
if err != nil {
|
||||
*errs = append(*errs, *err...)
|
||||
}
|
||||
}
|
||||
|
||||
func parseQuantity(i interface{}) (resource.Quantity, *[]jsonschema.ValError) {
|
||||
resStr, ok := i.(string)
|
||||
if !ok {
|
||||
return resource.Quantity{}, &[]jsonschema.ValError{
|
||||
{Message: fmt.Sprintf("Resource quantity %v is not a string", i)},
|
||||
}
|
||||
}
|
||||
q, err := resource.ParseQuantity(resStr)
|
||||
if err != nil {
|
||||
return resource.Quantity{}, &[]jsonschema.ValError{
|
||||
{Message: fmt.Sprintf("Could not parse resource quantity: %s", resStr)},
|
||||
}
|
||||
}
|
||||
return q, nil
|
||||
}
|
||||
|
||||
func validateRange(path string, limit interface{}, data interface{}, isMinimum bool) *[]jsonschema.ValError {
|
||||
limitQuantity, err := parseQuantity(limit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
actualQuantity, err := parseQuantity(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmp := limitQuantity.Cmp(actualQuantity)
|
||||
if isMinimum {
|
||||
if cmp == 1 {
|
||||
return &[]jsonschema.ValError{
|
||||
{Message: fmt.Sprintf("%s quantity %v is > %v", path, actualQuantity, limitQuantity)},
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if cmp == -1 {
|
||||
return &[]jsonschema.ValError{
|
||||
{Message: fmt.Sprintf("%s quantity %v is < %v", path, actualQuantity, limitQuantity)},
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Initialize sets up the schema
|
||||
func (check *SchemaCheck) Initialize(id string) error {
|
||||
check.ID = id
|
||||
if check.JSONSchema != "" {
|
||||
if err := json.Unmarshal([]byte(check.JSONSchema), &check.Schema); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckPod checks a pod spec against the schema
|
||||
func (check SchemaCheck) CheckPod(pod *corev1.PodSpec) (bool, error) {
|
||||
return check.CheckObject(pod)
|
||||
}
|
||||
|
||||
// CheckController checks a controler's spec against the schema
|
||||
func (check SchemaCheck) CheckController(bytes []byte) (bool, error) {
|
||||
errs, err := check.Schema.ValidateBytes(bytes)
|
||||
return len(errs) == 0, err
|
||||
}
|
||||
|
||||
// CheckContainer checks a container spec against the schema
|
||||
func (check SchemaCheck) CheckContainer(container *corev1.Container) (bool, error) {
|
||||
return check.CheckObject(container)
|
||||
}
|
||||
|
||||
// CheckObject checks arbitrary data against the schema
|
||||
func (check SchemaCheck) CheckObject(obj interface{}) (bool, error) {
|
||||
bytes, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
errs, err := check.Schema.ValidateBytes(bytes)
|
||||
return len(errs) == 0, err
|
||||
}
|
||||
|
||||
// IsActionable decides if this check applies to a particular target
|
||||
func (check SchemaCheck) IsActionable(target TargetKind, controllerType string, isInit bool) bool {
|
||||
if check.Target != target {
|
||||
return false
|
||||
}
|
||||
isIncluded := len(check.Controllers.Include) == 0
|
||||
for _, inclusion := range check.Controllers.Include {
|
||||
if inclusion == controllerType {
|
||||
isIncluded = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !isIncluded {
|
||||
return false
|
||||
}
|
||||
for _, exclusion := range check.Controllers.Exclude {
|
||||
if exclusion == controllerType {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if check.Target == TargetContainer {
|
||||
isIncluded := len(check.Containers.Include) == 0
|
||||
for _, inclusion := range check.Containers.Include {
|
||||
if (inclusion == "initContainer" && isInit) || (inclusion == "container" && !isInit) {
|
||||
isIncluded = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !isIncluded {
|
||||
return false
|
||||
}
|
||||
for _, exclusion := range check.Containers.Exclude {
|
||||
if (exclusion == "initContainer" && isInit) || (exclusion == "container" && !isInit) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019 ReactiveOps
|
||||
// Copyright 2019 FairwindsOps Inc
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -24,11 +24,11 @@ const (
|
||||
// SeverityWarning warns on validation failures
|
||||
SeverityWarning Severity = "warning"
|
||||
|
||||
// SeverityError errors on validation failures
|
||||
SeverityError Severity = "error"
|
||||
// SeverityDanger errors on validation failures
|
||||
SeverityDanger Severity = "danger"
|
||||
)
|
||||
|
||||
// IsActionable returns true if the severity level is warning or error
|
||||
func (severity *Severity) IsActionable() bool {
|
||||
return *severity == SeverityWarning || *severity == SeverityError
|
||||
return *severity == SeverityWarning || *severity == SeverityDanger
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.filters .title,
|
||||
.card h3 {
|
||||
margin: 0;
|
||||
font-weight: 300;
|
||||
@@ -40,11 +41,9 @@
|
||||
width: 32%;
|
||||
vertical-align: top;
|
||||
}
|
||||
.cluster-overview .cluster-score{
|
||||
padding-top: 15px;
|
||||
}
|
||||
.cluster-overview .cluster-score,
|
||||
.cluster-overview .result-messages {
|
||||
padding-top: 70px;
|
||||
padding-top: 15px;
|
||||
}
|
||||
|
||||
.cluster-overview .graph canvas {
|
||||
@@ -93,45 +92,24 @@
|
||||
|
||||
.cluster-overview .result-messages ul {
|
||||
font-size: 20px;
|
||||
line-height: 35px;
|
||||
line-height: 42px;
|
||||
}
|
||||
|
||||
.cluster-overview-footer {
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
right: 10px;
|
||||
right: 40px;
|
||||
left: 40px;
|
||||
}
|
||||
|
||||
.cluster-overview-footer p {
|
||||
display: inline-block;
|
||||
.cluster-overview-footer .kubernetes-stats {
|
||||
font-size: 11px;
|
||||
color: #777;
|
||||
width: 48%;
|
||||
}
|
||||
|
||||
.cluster-overview-footer p.kubernetes-overview {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.cluster-overview-footer p.kubernetes-overview .kubernetes-stat {
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
.kubernetes-overview ul {
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0 10px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.kubernetes-overview li {
|
||||
display: inline-block;
|
||||
margin: 20px 0 0 10px;
|
||||
}
|
||||
|
||||
.kubernetes-overview li span {
|
||||
margin-right: 3px;
|
||||
.cluster-overview-footer .kubernetes-stats {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.category .name {
|
||||
@@ -191,7 +169,7 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.expandable-table .resource-info .caret-expander {
|
||||
.resource-info .caret-expander {
|
||||
display: inline-block;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
@@ -202,16 +180,16 @@
|
||||
background-position: 2px center;
|
||||
}
|
||||
|
||||
.expandable-table .resource-info.expanded .caret-expander {
|
||||
.resource-info.expanded .caret-expander {
|
||||
background-image: url('../images/caret-bottom.svg');
|
||||
background-position: center 2px;
|
||||
}
|
||||
|
||||
.expandable-table .resource-info .expandable-content {
|
||||
.resource-info .expandable-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.expandable-table .resource-info.expanded .expandable-content {
|
||||
.resource-info.expanded .expandable-content {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -236,6 +214,26 @@ ul.message-list {
|
||||
|
||||
.cluster-overview ul.message-list {
|
||||
margin: 0px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.cluster-overview ul.message-list .message-group {
|
||||
display: inline-block;
|
||||
min-width: 130px;
|
||||
}
|
||||
|
||||
.cluster-overview ul.message-list .message {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
line-height: 12px;
|
||||
}
|
||||
|
||||
.cluster-overview ul.message-list li i.message-icon {
|
||||
font-size: 42px;
|
||||
text-align: center;
|
||||
min-width: 42px;
|
||||
vertical-align: top;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
ul.message-list li {
|
||||
@@ -257,11 +255,11 @@ ul.message-list li i.message-icon {
|
||||
color: #8BD2DC;
|
||||
}
|
||||
|
||||
.result-messages .warning i.message-icon {
|
||||
.result-messages .failure.warning i.message-icon {
|
||||
color: #f26c21;
|
||||
}
|
||||
|
||||
.result-messages .error i.message-icon {
|
||||
.result-messages .failure.danger i.message-icon {
|
||||
color: #a11f4c;
|
||||
}
|
||||
|
||||
@@ -319,3 +317,46 @@ a.more-info:hover {
|
||||
0% {opacity: 0;}
|
||||
100% {opacity: 1;}
|
||||
}
|
||||
|
||||
.filters .title {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.filters .caret-expander {
|
||||
margin-bottom: 2px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.filters input[type="checkbox"] {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
|
||||
.filters input[type="submit"] {
|
||||
padding: 4px 10px;
|
||||
box-shadow: none;
|
||||
background-color: #23103A;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
font-size: 20px;
|
||||
width: 100px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
form.namespace-list {
|
||||
line-height: 20px;
|
||||
margin: 5px 35px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.namespace-row {
|
||||
display: flex;
|
||||
padding: 3px 0;
|
||||
}
|
||||
|
||||
.namespace-row label {
|
||||
margin-left: 4px;
|
||||
font-size: 20px;
|
||||
}
|
||||
@@ -22,20 +22,11 @@ body {
|
||||
}
|
||||
|
||||
.navbar .logo {
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
.navbar span.oss-text {
|
||||
color: #23103A;
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
margin-bottom: 3px;
|
||||
padding-left: 66px;
|
||||
margin-bottom: -8px;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.navbar .fw-logo {
|
||||
height: 50px;
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
.navbar a {
|
||||
@@ -66,3 +57,13 @@ body {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.exemption-alert {
|
||||
margin-top: 15px;
|
||||
padding: 15px;
|
||||
border: 1px solid #f26c21;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.exemption-alert .fa-exclamation {
|
||||
margin-right: 10px;
|
||||
color: #f26c21;
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
@@ -5,9 +5,9 @@ $(function () {
|
||||
labels: ["Passing", "Warning", "Error"],
|
||||
datasets: [{
|
||||
data: [
|
||||
polarisAuditData.ClusterSummary.Results.Totals.Successes,
|
||||
polarisAuditData.ClusterSummary.Results.Totals.Warnings,
|
||||
polarisAuditData.ClusterSummary.Results.Totals.Errors,
|
||||
polarisSummary.Successes,
|
||||
polarisSummary.Warnings,
|
||||
polarisSummary.Dangers,
|
||||
],
|
||||
backgroundColor: ['#8BD2DC', '#f26c21', '#a11f4c'],
|
||||
}]
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
$(function () {
|
||||
|
||||
// Check selected namespace options on page load
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const currentNamespaces = urlParams.getAll('ns');
|
||||
currentNamespaces.forEach(ns => {
|
||||
$(`input#namespace-${ns}`).prop('checked', true);
|
||||
});
|
||||
|
||||
// Handle new filter submissions
|
||||
$('#namespaceFiltersForm').on('submit', e => {
|
||||
e.preventDefault();
|
||||
let newParams = new URLSearchParams();
|
||||
$('#namespaceFiltersForm input[type="checkbox"]').each((index, checkbox) => {
|
||||
if (checkbox.checked) {
|
||||
newParams.append('ns', checkbox.name);
|
||||
}
|
||||
});
|
||||
window.location = new URL(`?${newParams.toString()}`, window.location).toString();
|
||||
});
|
||||
});
|
||||
|
||||
+104
-52
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019 ReactiveOps
|
||||
// Copyright 2019 FairwindsOps Inc
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -19,13 +19,15 @@ import (
|
||||
"encoding/json"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/fairwindsops/polaris/pkg/config"
|
||||
"github.com/fairwindsops/polaris/pkg/kube"
|
||||
"github.com/fairwindsops/polaris/pkg/validator"
|
||||
packr "github.com/gobuffalo/packr/v2"
|
||||
"github.com/gorilla/mux"
|
||||
conf "github.com/reactiveops/polaris/pkg/config"
|
||||
"github.com/reactiveops/polaris/pkg/kube"
|
||||
"github.com/reactiveops/polaris/pkg/validator"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gitlab.com/golang-commonmark/markdown"
|
||||
)
|
||||
@@ -72,30 +74,32 @@ func GetTemplateBox() *packr.Box {
|
||||
// GetMarkdownBox returns a binary-friendly set of markdown files with error details
|
||||
func GetMarkdownBox() *packr.Box {
|
||||
if markdownBox == (*packr.Box)(nil) {
|
||||
markdownBox = packr.New("Markdown", "../../docs")
|
||||
markdownBox = packr.New("Markdown", "../../docs/check-documentation")
|
||||
}
|
||||
return markdownBox
|
||||
}
|
||||
|
||||
// templateData is passed to the dashboard HTML template
|
||||
type templateData struct {
|
||||
BasePath string
|
||||
AuditData validator.AuditData
|
||||
JSON template.JS
|
||||
BasePath string
|
||||
Config config.Configuration
|
||||
AuditData validator.AuditData
|
||||
FilteredAuditData validator.AuditData
|
||||
JSON template.JS
|
||||
}
|
||||
|
||||
// GetBaseTemplate puts together the dashboard template. Individual pieces can be overridden before rendering.
|
||||
func GetBaseTemplate(name string) (*template.Template, error) {
|
||||
tmpl := template.New(name).Funcs(template.FuncMap{
|
||||
"getWarningWidth": getWarningWidth,
|
||||
"getSuccessWidth": getSuccessWidth,
|
||||
"getWeatherIcon": getWeatherIcon,
|
||||
"getWeatherText": getWeatherText,
|
||||
"getGrade": getGrade,
|
||||
"getIcon": getIcon,
|
||||
"getCategoryLink": getCategoryLink,
|
||||
"getCategoryInfo": getCategoryInfo,
|
||||
"getAllControllerResults": getAllControllerResults,
|
||||
"getWarningWidth": getWarningWidth,
|
||||
"getSuccessWidth": getSuccessWidth,
|
||||
"getWeatherIcon": getWeatherIcon,
|
||||
"getWeatherText": getWeatherText,
|
||||
"getGrade": getGrade,
|
||||
"getIcon": getIcon,
|
||||
"getResultClass": getResultClass,
|
||||
"getCategoryLink": getCategoryLink,
|
||||
"getCategoryInfo": getCategoryInfo,
|
||||
})
|
||||
|
||||
templateFileNames := []string{
|
||||
@@ -135,12 +139,38 @@ func writeTemplate(tmpl *template.Template, data *templateData, w http.ResponseW
|
||||
buf.WriteTo(w)
|
||||
}
|
||||
|
||||
func getConfigForQuery(base config.Configuration, query url.Values) config.Configuration {
|
||||
c := base
|
||||
exemptions := query.Get("disallowExemptions")
|
||||
if exemptions == "false" {
|
||||
c.DisallowExemptions = false
|
||||
}
|
||||
if exemptions == "true" {
|
||||
c.DisallowExemptions = true
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func stripUnselectedNamespaces(data *validator.AuditData, selectedNamespaces []string) {
|
||||
newResults := []validator.ControllerResult{}
|
||||
for _, res := range data.Results {
|
||||
if stringInSlice(res.Namespace, selectedNamespaces) {
|
||||
newResults = append(newResults, res)
|
||||
}
|
||||
}
|
||||
data.Results = newResults
|
||||
}
|
||||
|
||||
// GetRouter returns a mux router serving all routes necessary for the dashboard
|
||||
func GetRouter(c conf.Configuration, auditPath string, port int, basePath string) *mux.Router {
|
||||
router := mux.NewRouter()
|
||||
func GetRouter(c config.Configuration, auditPath string, port int, basePath string, auditData *validator.AuditData) *mux.Router {
|
||||
router := mux.NewRouter().PathPrefix(basePath).Subrouter()
|
||||
fileServer := http.FileServer(GetAssetBox())
|
||||
router.PathPrefix("/static/").Handler(http.StripPrefix(path.Join(basePath, "/static/"), fileServer))
|
||||
|
||||
router.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte("OK"))
|
||||
})
|
||||
|
||||
router.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
|
||||
favicon, err := GetAssetBox().Find("favicon-32x32.png")
|
||||
if err != nil {
|
||||
@@ -150,58 +180,86 @@ func GetRouter(c conf.Configuration, auditPath string, port int, basePath string
|
||||
}
|
||||
w.Write(favicon)
|
||||
})
|
||||
|
||||
router.HandleFunc("/results.json", func(w http.ResponseWriter, r *http.Request) {
|
||||
k, err := kube.CreateResourceProvider(auditPath)
|
||||
if err != nil {
|
||||
logrus.Errorf("Error fetching Kubernetes resources %v", err)
|
||||
http.Error(w, "Error fetching Kubernetes resources", http.StatusInternalServerError)
|
||||
return
|
||||
adjustedConf := getConfigForQuery(c, r.URL.Query())
|
||||
if auditData == nil {
|
||||
k, err := kube.CreateResourceProvider(auditPath, "")
|
||||
if err != nil {
|
||||
logrus.Errorf("Error fetching Kubernetes resources %v", err)
|
||||
http.Error(w, "Error fetching Kubernetes resources", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
auditDataObj, err := validator.RunAudit(adjustedConf, k)
|
||||
if err != nil {
|
||||
http.Error(w, "Error Fetching Deployments", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
auditData = &auditDataObj
|
||||
}
|
||||
JSONHandler(w, r, c, k)
|
||||
|
||||
JSONHandler(w, r, auditData)
|
||||
})
|
||||
|
||||
router.HandleFunc("/details/{category}", func(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
category := vars["category"]
|
||||
category = strings.Replace(category, ".md", "", -1)
|
||||
DetailsHandler(w, r, category, basePath)
|
||||
})
|
||||
fileServer := http.FileServer(GetAssetBox())
|
||||
router.PathPrefix("/static/").Handler(http.StripPrefix("/static/", fileServer))
|
||||
|
||||
router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/" {
|
||||
if r.URL.Path != "/" && r.URL.Path != basePath {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
k, err := kube.CreateResourceProvider(auditPath)
|
||||
if err != nil {
|
||||
logrus.Errorf("Error fetching Kubernetes resources %v", err)
|
||||
http.Error(w, "Error fetching Kubernetes resources", http.StatusInternalServerError)
|
||||
return
|
||||
adjustedConf := getConfigForQuery(c, r.URL.Query())
|
||||
|
||||
if auditData == nil {
|
||||
k, err := kube.CreateResourceProvider(auditPath, "")
|
||||
if err != nil {
|
||||
logrus.Errorf("Error fetching Kubernetes resources %v", err)
|
||||
http.Error(w, "Error fetching Kubernetes resources", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
auditData, err := validator.RunAudit(adjustedConf, k)
|
||||
if err != nil {
|
||||
logrus.Errorf("Error getting audit data: %v", err)
|
||||
http.Error(w, "Error running audit", 500)
|
||||
return
|
||||
}
|
||||
MainHandler(w, r, adjustedConf, auditData, basePath)
|
||||
} else {
|
||||
MainHandler(w, r, adjustedConf, *auditData, basePath)
|
||||
}
|
||||
auditData, err := validator.RunAudit(c, k)
|
||||
if err != nil {
|
||||
logrus.Errorf("Error getting audit data: %v", err)
|
||||
http.Error(w, "Error running audit", 500)
|
||||
return
|
||||
}
|
||||
MainHandler(w, r, auditData, basePath)
|
||||
|
||||
})
|
||||
return router
|
||||
}
|
||||
|
||||
// MainHandler gets template data and renders the dashboard with it.
|
||||
func MainHandler(w http.ResponseWriter, r *http.Request, auditData validator.AuditData, basePath string) {
|
||||
jsonData, err := json.Marshal(auditData)
|
||||
func MainHandler(w http.ResponseWriter, r *http.Request, c config.Configuration, auditData validator.AuditData, basePath string) {
|
||||
jsonData, err := json.Marshal(auditData.GetSummary())
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, "Error serializing audit data", 500)
|
||||
return
|
||||
}
|
||||
|
||||
filteredAuditData := auditData
|
||||
namespaces := r.URL.Query()["ns"]
|
||||
if len(namespaces) > 0 {
|
||||
stripUnselectedNamespaces(&filteredAuditData, namespaces)
|
||||
}
|
||||
|
||||
data := templateData{
|
||||
BasePath: basePath,
|
||||
AuditData: auditData,
|
||||
JSON: template.JS(jsonData),
|
||||
BasePath: basePath,
|
||||
AuditData: auditData,
|
||||
FilteredAuditData: filteredAuditData,
|
||||
JSON: template.JS(jsonData),
|
||||
Config: c,
|
||||
}
|
||||
tmpl, err := GetBaseTemplate("main")
|
||||
if err != nil {
|
||||
@@ -213,13 +271,7 @@ func MainHandler(w http.ResponseWriter, r *http.Request, auditData validator.Aud
|
||||
}
|
||||
|
||||
// JSONHandler gets template data and renders json with it.
|
||||
func JSONHandler(w http.ResponseWriter, r *http.Request, c conf.Configuration, kubeResources *kube.ResourceProvider) {
|
||||
auditData, err := validator.RunAudit(c, kubeResources)
|
||||
if err != nil {
|
||||
http.Error(w, "Error Fetching Deployments", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
func JSONHandler(w http.ResponseWriter, r *http.Request, auditData *validator.AuditData) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(auditData)
|
||||
|
||||
+38
-15
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019 ReactiveOps
|
||||
// Copyright 2019 FairwindsOps Inc
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -16,23 +16,28 @@ package dashboard
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/reactiveops/polaris/pkg/validator"
|
||||
"strings"
|
||||
|
||||
"github.com/fairwindsops/polaris/pkg/config"
|
||||
"github.com/fairwindsops/polaris/pkg/validator"
|
||||
)
|
||||
|
||||
func getAllControllerResults(nr validator.NamespaceResult) []validator.ControllerResult {
|
||||
results := []validator.ControllerResult{}
|
||||
results = append(results, nr.DeploymentResults...)
|
||||
results = append(results, nr.StatefulSetResults...)
|
||||
return results
|
||||
}
|
||||
|
||||
func getWarningWidth(counts validator.CountSummary, fullWidth int) uint {
|
||||
return uint(float64(counts.Successes+counts.Warnings) / float64(counts.Successes+counts.Warnings+counts.Errors) * float64(fullWidth))
|
||||
denom := counts.Successes + counts.Warnings + counts.Dangers
|
||||
if denom == 0 {
|
||||
return uint(0)
|
||||
}
|
||||
res := float64(counts.Successes+counts.Warnings) / float64(denom) * float64(fullWidth)
|
||||
return uint(res)
|
||||
}
|
||||
|
||||
func getSuccessWidth(counts validator.CountSummary, fullWidth int) uint {
|
||||
return uint(float64(counts.Successes) / float64(counts.Successes+counts.Warnings+counts.Errors) * float64(fullWidth))
|
||||
denom := counts.Successes + counts.Warnings + counts.Dangers
|
||||
if denom == 0 {
|
||||
return uint(0)
|
||||
}
|
||||
res := float64(counts.Successes) / float64(denom) * float64(fullWidth)
|
||||
return uint(res)
|
||||
}
|
||||
|
||||
func getGrade(counts validator.CountSummary) string {
|
||||
@@ -81,6 +86,16 @@ func getWeatherIcon(counts validator.CountSummary) string {
|
||||
}
|
||||
}
|
||||
|
||||
func getResultClass(result validator.ResultMessage) string {
|
||||
cls := string(result.Severity)
|
||||
if result.Success {
|
||||
cls += " success"
|
||||
} else {
|
||||
cls += " failure"
|
||||
}
|
||||
return cls
|
||||
}
|
||||
|
||||
func getWeatherText(counts validator.CountSummary) string {
|
||||
score := counts.GetScore()
|
||||
if score >= 90 {
|
||||
@@ -97,12 +112,11 @@ func getWeatherText(counts validator.CountSummary) string {
|
||||
}
|
||||
|
||||
func getIcon(rm validator.ResultMessage) string {
|
||||
switch rm.Type {
|
||||
case "success":
|
||||
if rm.Success {
|
||||
return "fas fa-check"
|
||||
case "warning":
|
||||
} else if rm.Severity == config.SeverityWarning {
|
||||
return "fas fa-exclamation"
|
||||
default:
|
||||
} else {
|
||||
return "fas fa-times"
|
||||
}
|
||||
}
|
||||
@@ -157,3 +171,12 @@ func getCategoryInfo(category string) string {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func stringInSlice(a string, list []string) bool {
|
||||
for _, b := range list {
|
||||
if b == a {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -9,10 +9,13 @@
|
||||
<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>{{ .AuditData.ClusterSummary.Results.Totals.GetScore }}%</strong></div>
|
||||
<div class="weather"><i class="fas {{ getWeatherIcon .AuditData.GetSummary }}"></i></div>
|
||||
<div class="sailing">{{ getWeatherText .FilteredAuditData.GetSummary }}</div>
|
||||
<div class="scores"><span>Grade: </span><strong>{{ getGrade .FilteredAuditData.GetSummary }}</strong></div>
|
||||
<div class="scores"><span>Score: </span><strong>{{ .FilteredAuditData.GetSummary.GetScore }}%</strong></div>
|
||||
<p class="score-description">
|
||||
Score is the percentage of passing checks. Warnings get half the weight of dangerous checks.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="graph">
|
||||
@@ -20,41 +23,60 @@
|
||||
</div>
|
||||
<div class="result-messages">
|
||||
<ul class="message-list">
|
||||
<li class="success"><i class="message-icon fas fa-check"></i> {{ .AuditData.ClusterSummary.Results.Totals.Successes }} checks passed</li>
|
||||
<li class="warning"><i class="message-icon fas fa-exclamation"></i> {{ .AuditData.ClusterSummary.Results.Totals.Warnings }} checks had warnings</li>
|
||||
<li class="error"><i class="message-icon fas fa-times"></i> {{ .AuditData.ClusterSummary.Results.Totals.Errors }} checks had errors</li>
|
||||
<li class="success">
|
||||
<i class="message-icon fas fa-check"></i>
|
||||
<div class="message-group">
|
||||
<span class="count"> {{ .FilteredAuditData.GetSummary.Successes }}</span>
|
||||
<span class="message"> passing checks</span>
|
||||
</div>
|
||||
</li>
|
||||
<li class="failure warning">
|
||||
<i class="message-icon fas fa-exclamation"></i>
|
||||
<div class="message-group">
|
||||
<span class="count"> {{ .FilteredAuditData.GetSummary.Warnings }}</span>
|
||||
<span class="message"> warning checks</span>
|
||||
</div>
|
||||
</li>
|
||||
<li class="failure danger">
|
||||
<i class="message-icon fas fa-times"></i>
|
||||
<div class="message-group">
|
||||
<span class="count"> {{ .FilteredAuditData.GetSummary.Dangers }}</span>
|
||||
<span class="message"> dangerous checks</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cluster-overview-footer">
|
||||
<p class="score-description">
|
||||
Score is the percentage of passing checks. Warnings get half the weight of errors.
|
||||
</p>
|
||||
<p class="kubernetes-overview">
|
||||
<span class="kubernetes-stat">
|
||||
<div class="kubernetes-stats">
|
||||
<div class="kubernetes-stat">
|
||||
<span>Kubernetes Version:</span>
|
||||
<strong>{{.AuditData.ClusterSummary.Version}}</strong>
|
||||
</span>
|
||||
<span class="kubernetes-stat">
|
||||
<strong>{{.FilteredAuditData.ClusterInfo.Version}}</strong>
|
||||
</div>
|
||||
<div class="kubernetes-stat">
|
||||
<span>Nodes:</span>
|
||||
<strong>{{.AuditData.ClusterSummary.Nodes}}</strong>
|
||||
</span>
|
||||
<span class="kubernetes-stat">
|
||||
<span>Pods:</span>
|
||||
<strong>{{.AuditData.ClusterSummary.Pods}}</strong>
|
||||
</span>
|
||||
<span class="kubernetes-stat">
|
||||
<strong>{{.FilteredAuditData.ClusterInfo.Nodes}}</strong>
|
||||
</div>
|
||||
<div class="kubernetes-stat">
|
||||
<span>Namespaces:</span>
|
||||
<strong>{{.AuditData.ClusterSummary.Namespaces}}</strong>
|
||||
</span>
|
||||
</p>
|
||||
<strong>{{.FilteredAuditData.ClusterInfo.Namespaces}}</strong>
|
||||
</div>
|
||||
<div class="kubernetes-stat">
|
||||
<span>Controllers:</span>
|
||||
<strong>{{.FilteredAuditData.ClusterInfo.Controllers}}</strong>
|
||||
</div>
|
||||
<div class="kubernetes-stat">
|
||||
<span>Pods:</span>
|
||||
<strong>{{.FilteredAuditData.ClusterInfo.Pods}}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card category">
|
||||
<h3>Results by Category</h3>
|
||||
<div class="expandable-table">
|
||||
{{ range $category, $summary := .AuditData.ClusterSummary.Results.ByCategory }}
|
||||
{{ range $category, $summary := .FilteredAuditData.GetSummaryByCategory }}
|
||||
<div class="resource-info">
|
||||
<div class="status-bar">
|
||||
<div class="status">
|
||||
@@ -66,7 +88,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="name"><span class="caret-expander"></span>{{ $category }}<span class="category-score">Score: <strong>{{ $summary.GetScore }}%</strong></span></div>
|
||||
<div class="result-messages expandable-content">
|
||||
<div class="result-messages expandable-content">
|
||||
<p class="category-info">{{ getCategoryInfo $category }} Refer to the <a href="details/{{ getCategoryLink $category }}">Polaris documentation about {{ $category }}</a> for more information.</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -74,30 +96,51 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ range $namespace, $nsResult := .AuditData.NamespacedResults }}
|
||||
<div class="card filters">
|
||||
<div class="resource-info">
|
||||
<div class="name">
|
||||
<span class="caret-expander"></span>
|
||||
<span class="title">Filter by Namespace</span>
|
||||
</div>
|
||||
<div class="result-messages expandable-content">
|
||||
<form id="namespaceFiltersForm" class="namespace-list">
|
||||
{{ range $namespace, $ctrlResults := .AuditData.GetResultsByNamespace }}
|
||||
<div class="namespace-row">
|
||||
<input type="checkbox" name="{{ $namespace }}" id="namespace-{{ $namespace }}">
|
||||
<label for="namespace-{{ $namespace }}">{{ $namespace }}</label>
|
||||
</div>
|
||||
{{ end }}
|
||||
<input type="submit" value="Apply">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ range $namespace, $ctrlResults := .FilteredAuditData.GetResultsByNamespace }}
|
||||
<div class="card namespace">
|
||||
<h3>Namespace: <strong>{{ $namespace }}</strong></h3>
|
||||
<div class="expandable-table">
|
||||
{{ range getAllControllerResults $nsResult }}
|
||||
{{ range $ctrlResults }}
|
||||
<div class="resource-info">
|
||||
<div class="status-bar">
|
||||
<div class="status">
|
||||
<div class="failing">
|
||||
<div class="warning" style="width: {{ getWarningWidth .PodResult.Summary.Totals 200 }}px;">
|
||||
<div class="passing" style="width: {{ getSuccessWidth .PodResult.Summary.Totals 200 }}px;"></div>
|
||||
<div class="warning" style="width: {{ getWarningWidth .PodResult.GetSummary 200 }}px;">
|
||||
<div class="passing" style="width: {{ getSuccessWidth .PodResult.GetSummary 200 }}px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="name"><span class="caret-expander"></span>
|
||||
<span class="controller-type">{{ .Type }}:</span>
|
||||
<span class="controller-type">{{ .Kind }}:</span>
|
||||
<strong>{{ .Name }}</strong></div>
|
||||
|
||||
<div class="result-messages expandable-content">
|
||||
<h4>Pod Spec:</h4>
|
||||
<h4>Controller Spec:</h4>
|
||||
<ul class="message-list">
|
||||
{{ range $message := .PodResult.Messages}}
|
||||
<li class="{{ .Type }}">
|
||||
{{ range $message := .Results.GetSortedResults }}
|
||||
<li class="{{ getResultClass . }}">
|
||||
<i class="message-icon {{ getIcon $message }}"></i>
|
||||
<span class="message">{{ .Message }}</span>
|
||||
<a class="more-info" href="details/{{ getCategoryLink .Category }}">
|
||||
@@ -107,12 +150,28 @@
|
||||
{{ end }}
|
||||
</ul>
|
||||
</div>
|
||||
{{ range .PodResult.ContainerResults}}
|
||||
|
||||
<div class="result-messages expandable-content">
|
||||
<h4>Pod Spec:</h4>
|
||||
<ul class="message-list">
|
||||
{{ range $message := .PodResult.Results.GetSortedResults }}
|
||||
<li class="{{ getResultClass . }}">
|
||||
<i class="message-icon {{ getIcon $message }}"></i>
|
||||
<span class="message">{{ .Message }}</span>
|
||||
<a class="more-info" href="details/{{ getCategoryLink .Category }}">
|
||||
<i class="far fa-question-circle"></i>
|
||||
</a>
|
||||
</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{{ range .PodResult.ContainerResults }}
|
||||
<div class="result-messages expandable-content">
|
||||
<h4>Container: {{ .Name }}</h4>
|
||||
<ul class="message-list">
|
||||
{{ range $message := .Messages}}
|
||||
<li class="{{ .Type }}">
|
||||
{{ range $message := .Results.GetSortedResults }}
|
||||
<li class="{{ getResultClass . }}">
|
||||
<i class="message-icon {{ getIcon $message }}"></i>
|
||||
<span class="message">{{ .Message }}</span>
|
||||
<a class="more-info" href="details/{{ getCategoryLink .Category }}">
|
||||
@@ -124,10 +183,10 @@
|
||||
</div>
|
||||
{{ end }} {{/* end range .PodResult.ContainerResults */}}
|
||||
</div>
|
||||
{{ end }} {{/* end range .DeploymentResults */}}
|
||||
{{ end }} {{/* end range .Results.GetSortedResults */}}
|
||||
</div>
|
||||
</div>
|
||||
{{ end }} {{/* end range .AuditData.NamespacedResults */}}
|
||||
<script src="static/js/charts.js">
|
||||
</script>
|
||||
{{ end }} {{/* end range .AuditData.GetResultsByNamespace */}}
|
||||
<script src="static/js/charts.js"></script>
|
||||
<script src="static/js/filter.js"></script>
|
||||
{{end}}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{{define "footer"}}
|
||||
<div class="footer">
|
||||
<a href="https://fairwinds.com?source=polaris" target="_blank">©2019 FairwindsOps Inc.</a>
|
||||
<a href="https://fairwinds.com?source=polaris" target="_blank">©2020 FairwindsOps Inc.</a>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
<head>
|
||||
<script>
|
||||
window.polarisAuditData = {{ .JSON }};
|
||||
window.polarisSummary = {{ .JSON }};
|
||||
</script>
|
||||
{{ template "head" . }}
|
||||
</head>
|
||||
|
||||
@@ -1,15 +1,24 @@
|
||||
{{define "navbar"}}
|
||||
<div class="navbar">
|
||||
<div class="navbar-content">
|
||||
<a href="/">
|
||||
<a href="{{ .BasePath }}">
|
||||
<img class="logo" src="static/images/polaris-logo.png" alt="Polaris" />
|
||||
</a>
|
||||
<div class="navbar-right">
|
||||
<a href="https://fairwinds.com?source=polaris" target="_blank">
|
||||
<span class="oss-text">An Open Source Project By</span>
|
||||
<img class="fw-logo" src="static/images/fw-logo.svg" alt="Fairwinds" />
|
||||
<img class="fw-logo" src="static/images/oss-logo.png" alt="Fairwinds" />
|
||||
</a>
|
||||
</div>
|
||||
{{ if and (not .Config.DisallowExemptions) (gt (len .Config.Exemptions) 0) }}
|
||||
<div class="exemption-alert">
|
||||
<i class="fas fa-exclamation"></i>
|
||||
<span>
|
||||
Some checks were skipped based on configured exemptions.
|
||||
<a href="?disallowExemptions=true">Click here</a>
|
||||
to view the report with these checks included.
|
||||
</span>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
+203
-64
@@ -2,6 +2,8 @@ package kube
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -10,12 +12,15 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
k8sYaml "k8s.io/apimachinery/pkg/util/yaml"
|
||||
"k8s.io/client-go/dynamic"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth" // Required for other auth providers like GKE.
|
||||
"k8s.io/client-go/restmapper"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/config"
|
||||
)
|
||||
|
||||
@@ -26,24 +31,88 @@ type ResourceProvider struct {
|
||||
SourceName string
|
||||
SourceType string
|
||||
Nodes []corev1.Node
|
||||
Deployments []appsv1.Deployment
|
||||
StatefulSets []appsv1.StatefulSet
|
||||
Namespaces []corev1.Namespace
|
||||
Pods []corev1.Pod
|
||||
Controllers []GenericWorkload
|
||||
}
|
||||
|
||||
type k8sResource struct {
|
||||
Kind string `yaml:"kind"`
|
||||
}
|
||||
|
||||
var podSpecFields = []string{"jobTemplate", "spec", "template"}
|
||||
|
||||
// CreateResourceProvider returns a new ResourceProvider object to interact with k8s resources
|
||||
func CreateResourceProvider(directory string) (*ResourceProvider, error) {
|
||||
func CreateResourceProvider(directory, workload string) (*ResourceProvider, error) {
|
||||
if workload != "" {
|
||||
return CreateResourceProviderFromWorkload(workload)
|
||||
}
|
||||
if directory != "" {
|
||||
return CreateResourceProviderFromPath(directory)
|
||||
}
|
||||
return CreateResourceProviderFromCluster()
|
||||
}
|
||||
|
||||
// CreateResourceProviderFromWorkload creates a new ResourceProvider that just contains one workload
|
||||
func CreateResourceProviderFromWorkload(workload string) (*ResourceProvider, error) {
|
||||
kubeConf, configError := config.GetConfig()
|
||||
if configError != nil {
|
||||
logrus.Errorf("Error fetching KubeConfig: %v", configError)
|
||||
return nil, configError
|
||||
}
|
||||
kube, err := kubernetes.NewForConfig(kubeConf)
|
||||
if err != nil {
|
||||
logrus.Errorf("Error creating Kubernetes client: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
serverVersion, err := kube.Discovery().ServerVersion()
|
||||
if err != nil {
|
||||
logrus.Errorf("Error fetching Cluster API version: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
resources := ResourceProvider{
|
||||
ServerVersion: serverVersion.Major + "." + serverVersion.Minor,
|
||||
SourceType: "Workload",
|
||||
SourceName: workload,
|
||||
CreationTime: time.Now(),
|
||||
Nodes: []corev1.Node{},
|
||||
Namespaces: []corev1.Namespace{},
|
||||
}
|
||||
|
||||
parts := strings.Split(workload, "/")
|
||||
if len(parts) != 4 {
|
||||
return nil, fmt.Errorf("Invalid workload identifier %s. Should be in format namespace/kind/version/name, e.g. nginx-ingress/Deployment.apps/v1/default-backend", workload)
|
||||
}
|
||||
namespace := parts[0]
|
||||
kind := parts[1]
|
||||
version := parts[2]
|
||||
name := parts[3]
|
||||
|
||||
dynamicInterface, err := dynamic.NewForConfig(kubeConf)
|
||||
if err != nil {
|
||||
logrus.Errorf("Error connecting to dynamic interface: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
groupResources, err := restmapper.GetAPIGroupResources(kube.Discovery())
|
||||
if err != nil {
|
||||
logrus.Errorf("Error getting API Group resources: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
restMapper := restmapper.NewDiscoveryRESTMapper(groupResources)
|
||||
obj, err := getObject(namespace, kind, version, name, &dynamicInterface, &restMapper)
|
||||
if err != nil {
|
||||
logrus.Errorf("Could not find workload %s: %v", workload, err)
|
||||
return nil, err
|
||||
}
|
||||
workloadObj, err := NewGenericWorkloadFromUnstructured(kind, obj)
|
||||
if err != nil {
|
||||
logrus.Errorf("Could not parse workload %s: %v", workload, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resources.Controllers = []GenericWorkload{workloadObj}
|
||||
return &resources, nil
|
||||
}
|
||||
|
||||
// CreateResourceProviderFromPath returns a new ResourceProvider using the YAML files in a directory
|
||||
func CreateResourceProviderFromPath(directory string) (*ResourceProvider, error) {
|
||||
resources := ResourceProvider{
|
||||
@@ -51,14 +120,18 @@ func CreateResourceProviderFromPath(directory string) (*ResourceProvider, error)
|
||||
SourceType: "Path",
|
||||
SourceName: directory,
|
||||
Nodes: []corev1.Node{},
|
||||
Deployments: []appsv1.Deployment{},
|
||||
StatefulSets: []appsv1.StatefulSet{},
|
||||
Namespaces: []corev1.Namespace{},
|
||||
Pods: []corev1.Pod{},
|
||||
Controllers: []GenericWorkload{},
|
||||
}
|
||||
|
||||
addYaml := func(contents string) error {
|
||||
return addResourceFromString(contents, &resources)
|
||||
if directory == "-" {
|
||||
fi, err := os.Stdin.Stat()
|
||||
if err == nil && fi.Mode()&os.ModeNamedPipe == os.ModeNamedPipe {
|
||||
if err := addResourcesFromReader(os.Stdin, &resources); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &resources, nil
|
||||
}
|
||||
}
|
||||
|
||||
visitFile := func(path string, f os.FileInfo, err error) error {
|
||||
@@ -67,21 +140,10 @@ func CreateResourceProviderFromPath(directory string) (*ResourceProvider, error)
|
||||
}
|
||||
contents, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
logrus.Errorf("Error reading file %v", path)
|
||||
logrus.Errorf("Error reading file: %v", path)
|
||||
return err
|
||||
}
|
||||
specs := regexp.MustCompile("\n-+\n").Split(string(contents), -1)
|
||||
for _, spec := range specs {
|
||||
if strings.TrimSpace(spec) == "" {
|
||||
continue
|
||||
}
|
||||
err = addYaml(spec)
|
||||
if err != nil {
|
||||
logrus.Errorf("Error parsing YAML %v", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return addResourcesFromYaml(string(contents), &resources)
|
||||
}
|
||||
|
||||
err := filepath.Walk(directory, visitFile)
|
||||
@@ -95,48 +157,59 @@ func CreateResourceProviderFromPath(directory string) (*ResourceProvider, error)
|
||||
func CreateResourceProviderFromCluster() (*ResourceProvider, error) {
|
||||
kubeConf, configError := config.GetConfig()
|
||||
if configError != nil {
|
||||
logrus.Errorf("Error fetching KubeConfig %v", configError)
|
||||
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)
|
||||
logrus.Errorf("Error creating Kubernetes client: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
return CreateResourceProviderFromAPI(api, kubeConf.Host)
|
||||
dynamicInterface, err := dynamic.NewForConfig(kubeConf)
|
||||
if err != nil {
|
||||
logrus.Errorf("Error connecting to dynamic interface: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
return CreateResourceProviderFromAPI(api, kubeConf.Host, &dynamicInterface)
|
||||
}
|
||||
|
||||
// CreateResourceProviderFromAPI creates a new ResourceProvider from an existing k8s interface
|
||||
func CreateResourceProviderFromAPI(kube kubernetes.Interface, clusterName string) (*ResourceProvider, error) {
|
||||
func CreateResourceProviderFromAPI(kube kubernetes.Interface, clusterName string, dynamic *dynamic.Interface) (*ResourceProvider, error) {
|
||||
listOpts := metav1.ListOptions{}
|
||||
serverVersion, err := kube.Discovery().ServerVersion()
|
||||
if err != nil {
|
||||
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 Deployments %v", err)
|
||||
return nil, err
|
||||
}
|
||||
statefulSets, err := kube.AppsV1().StatefulSets("").List(listOpts)
|
||||
if err != nil {
|
||||
logrus.Errorf("Error fetching StatefulSets%v", err)
|
||||
logrus.Errorf("Error fetching Cluster API version: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nodes, err := kube.CoreV1().Nodes().List(listOpts)
|
||||
if err != nil {
|
||||
logrus.Errorf("Error fetching 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 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 Pods %v", err)
|
||||
logrus.Errorf("Error fetching Pods: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resources, err := restmapper.GetAPIGroupResources(kube.Discovery())
|
||||
if err != nil {
|
||||
logrus.Errorf("Error getting API Group resources: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
restMapper := restmapper.NewDiscoveryRESTMapper(resources)
|
||||
|
||||
objectCache := map[string]unstructured.Unstructured{}
|
||||
|
||||
controllers, err := LoadControllers(pods.Items, dynamic, &restMapper, objectCache)
|
||||
if err != nil {
|
||||
logrus.Errorf("Error loading controllers from pods: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -145,46 +218,112 @@ func CreateResourceProviderFromAPI(kube kubernetes.Interface, clusterName string
|
||||
SourceType: "Cluster",
|
||||
SourceName: clusterName,
|
||||
CreationTime: time.Now(),
|
||||
Deployments: deploys.Items,
|
||||
StatefulSets: statefulSets.Items,
|
||||
Nodes: nodes.Items,
|
||||
Namespaces: namespaces.Items,
|
||||
Pods: pods.Items,
|
||||
Controllers: controllers,
|
||||
}
|
||||
return &api, nil
|
||||
}
|
||||
|
||||
// LoadControllers loads a list of controllers from the kubeResources Pods
|
||||
func LoadControllers(pods []corev1.Pod, dynamicClientPointer *dynamic.Interface, restMapperPointer *meta.RESTMapper, objectCache map[string]unstructured.Unstructured) ([]GenericWorkload, error) {
|
||||
interfaces := []GenericWorkload{}
|
||||
deduped := map[string]corev1.Pod{}
|
||||
for _, pod := range pods {
|
||||
owners := pod.ObjectMeta.OwnerReferences
|
||||
if len(owners) == 0 {
|
||||
deduped[pod.ObjectMeta.Namespace+"/Pod/"+pod.ObjectMeta.Name] = pod
|
||||
continue
|
||||
}
|
||||
deduped[pod.ObjectMeta.Namespace+"/"+owners[0].Kind+"/"+owners[0].Name] = pod
|
||||
}
|
||||
for _, pod := range deduped {
|
||||
workload, err := NewGenericWorkload(pod, dynamicClientPointer, restMapperPointer, objectCache)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
interfaces = append(interfaces, workload)
|
||||
}
|
||||
return deduplicateControllers(interfaces), nil
|
||||
}
|
||||
|
||||
// Because the controllers with an Owner take on the name of the Owner, this eliminates any duplicates.
|
||||
// In cases like CronJobs older children can hang around, so this takes the most recent.
|
||||
func deduplicateControllers(inputControllers []GenericWorkload) []GenericWorkload {
|
||||
controllerMap := make(map[string]GenericWorkload)
|
||||
for _, controller := range inputControllers {
|
||||
key := controller.ObjectMeta.GetNamespace() + "/" + controller.Kind + "/" + controller.ObjectMeta.GetName()
|
||||
oldController, ok := controllerMap[key]
|
||||
if !ok || controller.ObjectMeta.GetCreationTimestamp().Time.After(oldController.ObjectMeta.GetCreationTimestamp().Time) {
|
||||
controllerMap[key] = controller
|
||||
}
|
||||
}
|
||||
results := make([]GenericWorkload, 0)
|
||||
for _, controller := range controllerMap {
|
||||
results = append(results, controller)
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
func addResourcesFromReader(reader io.Reader, resources *ResourceProvider) error {
|
||||
contents, err := ioutil.ReadAll(reader)
|
||||
if err != nil {
|
||||
logrus.Errorf("Error reading from %v: %v", reader, err)
|
||||
return err
|
||||
}
|
||||
if err := addResourcesFromYaml(string(contents), resources); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func addResourcesFromYaml(contents string, resources *ResourceProvider) error {
|
||||
specs := regexp.MustCompile("[\r\n]-+[\r\n]").Split(string(contents), -1)
|
||||
for _, spec := range specs {
|
||||
if strings.TrimSpace(spec) == "" {
|
||||
continue
|
||||
}
|
||||
err := addResourceFromString(spec, resources)
|
||||
if err != nil {
|
||||
logrus.Errorf("Error parsing YAML: (%v)", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
return 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" {
|
||||
|
||||
if err != nil {
|
||||
logrus.Errorf("Invalid YAML: %s", string(contents))
|
||||
return err
|
||||
}
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
workload, err := NewGenericWorkloadFromPod(pod, pod)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resources.Controllers = append(resources.Controllers, workload)
|
||||
} else {
|
||||
newController, err := GetWorkloadFromBytes(contentBytes)
|
||||
if err != nil || newController == nil {
|
||||
return err
|
||||
}
|
||||
resources.Controllers = append(resources.Controllers, *newController)
|
||||
}
|
||||
if err != nil {
|
||||
logrus.Errorf("Error parsing %s: %v", resource.Kind, err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
|
||||
+51
-19
@@ -1,10 +1,14 @@
|
||||
package kube
|
||||
|
||||
import (
|
||||
"github.com/reactiveops/polaris/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/fairwindsops/polaris/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
func TestGetResourcesFromPath(t *testing.T) {
|
||||
@@ -19,18 +23,16 @@ func TestGetResourcesFromPath(t *testing.T) {
|
||||
|
||||
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)
|
||||
|
||||
assert.Equal(t, 2, len(resources.Pods), "Should have two pods")
|
||||
assert.Equal(t, "", resources.Pods[0].ObjectMeta.Namespace, "Should have one pod in default namespace")
|
||||
assert.Equal(t, "two", resources.Pods[1].ObjectMeta.Namespace, "Should have one pod in namespace 'two'")
|
||||
assert.Equal(t, 8, len(resources.Controllers), "Should have eight controllers")
|
||||
namespaceCount := map[string]int{}
|
||||
for _, controller := range resources.Controllers {
|
||||
namespaceCount[controller.ObjectMeta.GetNamespace()]++
|
||||
}
|
||||
assert.Equal(t, 7, namespaceCount[""], "Should have seven controller in default namespace")
|
||||
assert.Equal(t, 1, namespaceCount["two"], "Should have one controller in namespace 'two'")
|
||||
}
|
||||
|
||||
func TestGetMultipleResourceFromSingleFile(t *testing.T) {
|
||||
@@ -45,8 +47,38 @@ func TestGetMultipleResourceFromSingleFile(t *testing.T) {
|
||||
|
||||
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, "dashboard", resources.Deployments[0].Spec.Template.Spec.Containers[0].Name)
|
||||
assert.Equal(t, 1, len(resources.Controllers), "Should have one controller")
|
||||
assert.Equal(t, "dashboard", resources.Controllers[0].PodSpec.Containers[0].Name)
|
||||
|
||||
assert.Equal(t, 2, len(resources.Namespaces), "Should have a namespace")
|
||||
assert.Equal(t, "polaris", resources.Namespaces[0].ObjectMeta.Name)
|
||||
assert.Equal(t, "polaris-2", resources.Namespaces[1].ObjectMeta.Name)
|
||||
}
|
||||
|
||||
func TestGetMultipleResourceFromBadFile(t *testing.T) {
|
||||
_, err := CreateResourceProviderFromPath("./test_files/test_3")
|
||||
assert.NotEqual(t, nil, err, "CreateResource From Path should fail with bad yaml")
|
||||
}
|
||||
|
||||
func TestAddResourcesFromReader(t *testing.T) {
|
||||
contents, err := ioutil.ReadFile("./test_files/test_2/multi.yaml")
|
||||
assert.NoError(t, err)
|
||||
reader := bytes.NewBuffer(contents)
|
||||
resources := &ResourceProvider{
|
||||
ServerVersion: "unknown",
|
||||
SourceType: "Path",
|
||||
SourceName: "-",
|
||||
Nodes: []corev1.Node{},
|
||||
Namespaces: []corev1.Namespace{},
|
||||
Controllers: []GenericWorkload{},
|
||||
}
|
||||
err = addResourcesFromReader(reader, resources)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 0, len(resources.Nodes), "Should not have any nodes")
|
||||
|
||||
assert.Equal(t, 1, len(resources.Controllers), "Should have one controller")
|
||||
assert.Equal(t, "dashboard", resources.Controllers[0].PodSpec.Containers[0].Name)
|
||||
|
||||
assert.Equal(t, 2, len(resources.Namespaces), "Should have a namespace")
|
||||
assert.Equal(t, "polaris", resources.Namespaces[0].ObjectMeta.Name)
|
||||
@@ -54,9 +86,11 @@ func TestGetMultipleResourceFromSingleFile(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetResourceFromAPI(t *testing.T) {
|
||||
k8s := test.SetupTestAPI()
|
||||
k8s, dynamicInterface := test.SetupTestAPI()
|
||||
k8s = test.SetupAddControllers(k8s, "test")
|
||||
resources, err := CreateResourceProviderFromAPI(k8s, "test")
|
||||
// TODO find a way to mock out the dynamic client
|
||||
// and create fake pods in order to find all of the controllers.
|
||||
resources, err := CreateResourceProviderFromAPI(k8s, "test", &dynamicInterface)
|
||||
assert.Equal(t, nil, err, "Error should be nil")
|
||||
|
||||
assert.Equal(t, "Cluster", resources.SourceType, "Should have type Path")
|
||||
@@ -64,9 +98,7 @@ func TestGetResourceFromAPI(t *testing.T) {
|
||||
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, 1, len(resources.Controllers), "Should have 1 controller")
|
||||
|
||||
assert.Equal(t, "", resources.Deployments[0].ObjectMeta.Name)
|
||||
assert.Equal(t, "", resources.Controllers[0].ObjectMeta.GetName())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
apiVersion: batch/v1beta1
|
||||
kind: CronJob
|
||||
metadata:
|
||||
name: test
|
||||
spec:
|
||||
schedule: "*/1 * * * *"
|
||||
jobTemplate:
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: test
|
||||
image: busybox
|
||||
args:
|
||||
- whoami
|
||||
restartPolicy: OnFailure
|
||||
@@ -0,0 +1,20 @@
|
||||
apiVersion: apps/v1
|
||||
kind: DaemonSet
|
||||
metadata:
|
||||
name: test
|
||||
labels:
|
||||
k8s-app: test
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
name: test
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
name: test
|
||||
spec:
|
||||
containers:
|
||||
- name: test
|
||||
image: busybox
|
||||
command: ["whoami"]
|
||||
resources: {}
|
||||
@@ -0,0 +1,12 @@
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: test
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: test
|
||||
image: alpine
|
||||
command: ["whoami"]
|
||||
restartPolicy: Never
|
||||
@@ -0,0 +1,19 @@
|
||||
apiVersion: v1
|
||||
kind: ReplicationController
|
||||
metadata:
|
||||
name: test
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
app: test
|
||||
template:
|
||||
metadata:
|
||||
name: test
|
||||
labels:
|
||||
app: test
|
||||
spec:
|
||||
containers:
|
||||
- name: test
|
||||
image: busybox
|
||||
command: ["tail"]
|
||||
args: ["-f", "/dev/null"]
|
||||
@@ -0,0 +1,11 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: test-service
|
||||
spec:
|
||||
ports:
|
||||
- port: 8080
|
||||
protocol: TCP
|
||||
targetPort: 8080
|
||||
selector:
|
||||
app: test
|
||||
@@ -0,0 +1,5 @@
|
||||
--
|
||||
# Source: polaris/templates/dashboard.deployment.yaml
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Deployment
|
||||
::::
|
||||
@@ -0,0 +1,228 @@
|
||||
package kube
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"gopkg.in/yaml.v3"
|
||||
kubeAPICoreV1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
kubeAPIMetaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
k8sYaml "k8s.io/apimachinery/pkg/util/yaml"
|
||||
"k8s.io/client-go/dynamic"
|
||||
)
|
||||
|
||||
// GenericWorkload is a base implementation with some free methods for inherited structs
|
||||
type GenericWorkload struct {
|
||||
Kind string
|
||||
PodSpec kubeAPICoreV1.PodSpec
|
||||
ObjectMeta kubeAPIMetaV1.Object
|
||||
OriginalObjectJSON []byte
|
||||
}
|
||||
|
||||
// NewGenericWorkloadFromUnstructured creates a workload from an unstructured.Unstructured
|
||||
func NewGenericWorkloadFromUnstructured(kind string, unst *unstructured.Unstructured) (GenericWorkload, error) {
|
||||
workload := GenericWorkload{
|
||||
Kind: kind,
|
||||
}
|
||||
|
||||
objMeta, err := meta.Accessor(unst)
|
||||
if err != nil {
|
||||
return workload, err
|
||||
}
|
||||
workload.ObjectMeta = objMeta
|
||||
|
||||
b, err := json.Marshal(unst)
|
||||
if err != nil {
|
||||
return workload, err
|
||||
}
|
||||
workload.OriginalObjectJSON = b
|
||||
|
||||
m := make(map[string]interface{})
|
||||
err = json.Unmarshal(b, &m)
|
||||
if err != nil {
|
||||
return workload, err
|
||||
}
|
||||
podSpecMap := GetPodSpec(m)
|
||||
b, err = json.Marshal(podSpecMap)
|
||||
if err != nil {
|
||||
return workload, err
|
||||
}
|
||||
podSpec := kubeAPICoreV1.PodSpec{}
|
||||
err = json.Unmarshal(b, &podSpec)
|
||||
if err != nil {
|
||||
return workload, err
|
||||
}
|
||||
workload.PodSpec = podSpec
|
||||
|
||||
return workload, nil
|
||||
}
|
||||
|
||||
// NewGenericWorkloadFromPod builds a new workload for a given Pod without looking at parents
|
||||
func NewGenericWorkloadFromPod(podResource kubeAPICoreV1.Pod, originalObject interface{}) (GenericWorkload, error) {
|
||||
workload := GenericWorkload{
|
||||
Kind: "Pod",
|
||||
PodSpec: podResource.Spec,
|
||||
ObjectMeta: podResource.ObjectMeta.GetObjectMeta(),
|
||||
}
|
||||
if originalObject != nil {
|
||||
bytes, err := json.Marshal(originalObject)
|
||||
if err != nil {
|
||||
return workload, err
|
||||
}
|
||||
workload.OriginalObjectJSON = bytes
|
||||
}
|
||||
return workload, nil
|
||||
}
|
||||
|
||||
// NewGenericWorkload builds a new workload for a given Pod
|
||||
func NewGenericWorkload(podResource kubeAPICoreV1.Pod, dynamicClient *dynamic.Interface, restMapper *meta.RESTMapper, objectCache map[string]unstructured.Unstructured) (GenericWorkload, error) {
|
||||
workload, err := newGenericWorkload(podResource, dynamicClient, restMapper, objectCache)
|
||||
if err != nil {
|
||||
return workload, err
|
||||
}
|
||||
if len(workload.OriginalObjectJSON) == 0 {
|
||||
return NewGenericWorkloadFromPod(podResource, podResource)
|
||||
}
|
||||
return workload, err
|
||||
}
|
||||
|
||||
func newGenericWorkload(podResource kubeAPICoreV1.Pod, dynamicClient *dynamic.Interface, restMapper *meta.RESTMapper, objectCache map[string]unstructured.Unstructured) (GenericWorkload, error) {
|
||||
workload, err := NewGenericWorkloadFromPod(podResource, nil)
|
||||
if err != nil {
|
||||
return workload, err
|
||||
}
|
||||
// If an owner exists then set the name to the workload.
|
||||
// This allows us to handle CRDs creating Workloads or DeploymentConfigs in OpenShift.
|
||||
owners := workload.ObjectMeta.GetOwnerReferences()
|
||||
lastKey := ""
|
||||
for len(owners) > 0 {
|
||||
if len(owners) > 1 {
|
||||
logrus.Warn("More than 1 owner found")
|
||||
}
|
||||
firstOwner := owners[0]
|
||||
if firstOwner.Kind == "Node" {
|
||||
break
|
||||
}
|
||||
workload.Kind = firstOwner.Kind
|
||||
key := fmt.Sprintf("%s/%s/%s", firstOwner.Kind, workload.ObjectMeta.GetNamespace(), firstOwner.Name)
|
||||
lastKey = key
|
||||
abstractObject, ok := objectCache[key]
|
||||
if !ok {
|
||||
err = cacheAllObjectsOfKind(firstOwner.APIVersion, firstOwner.Kind, dynamicClient, restMapper, objectCache)
|
||||
if err != nil {
|
||||
logrus.Warnf("Error caching objects of Kind %s %v", firstOwner.Kind, err)
|
||||
break
|
||||
}
|
||||
abstractObject, ok = objectCache[key]
|
||||
if !ok {
|
||||
logrus.Errorf("Cache missed %s again", key)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
objMeta, err := meta.Accessor(&abstractObject)
|
||||
if err != nil {
|
||||
logrus.Warnf("Error retrieving parent metadata %s of API %s and Kind %s because of error: %v ", firstOwner.Name, firstOwner.APIVersion, firstOwner.Kind, err)
|
||||
return workload, err
|
||||
}
|
||||
workload.ObjectMeta = objMeta
|
||||
owners = abstractObject.GetOwnerReferences()
|
||||
}
|
||||
|
||||
if lastKey != "" {
|
||||
bytes, err := json.Marshal(objectCache[lastKey])
|
||||
if err != nil {
|
||||
return workload, err
|
||||
}
|
||||
workload.OriginalObjectJSON = bytes
|
||||
} else {
|
||||
bytes, err := json.Marshal(podResource)
|
||||
if err != nil {
|
||||
return workload, err
|
||||
}
|
||||
workload.OriginalObjectJSON = bytes
|
||||
}
|
||||
return workload, nil
|
||||
}
|
||||
|
||||
func cacheAllObjectsOfKind(apiVersion, kind string, dynamicClient *dynamic.Interface, restMapper *meta.RESTMapper, objectCache map[string]unstructured.Unstructured) error {
|
||||
fqKind := schema.FromAPIVersionAndKind(apiVersion, kind)
|
||||
mapping, err := (*restMapper).RESTMapping(fqKind.GroupKind(), fqKind.Version)
|
||||
if err != nil {
|
||||
logrus.Warnf("Error retrieving mapping of API %s and Kind %s because of error: %v ", apiVersion, kind, err)
|
||||
return err
|
||||
}
|
||||
|
||||
objects, err := (*dynamicClient).Resource(mapping.Resource).Namespace("").List(kubeAPIMetaV1.ListOptions{})
|
||||
if err != nil {
|
||||
logrus.Warnf("Error retrieving parent object API %s and Kind %s because of error: %v ", mapping.Resource.Version, mapping.Resource.Resource, err)
|
||||
return err
|
||||
}
|
||||
for idx, object := range objects.Items {
|
||||
key := fmt.Sprintf("%s/%s/%s", object.GetKind(), object.GetNamespace(), object.GetName())
|
||||
objectCache[key] = objects.Items[idx]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getObject(namespace, kind, version, name string, dynamicClient *dynamic.Interface, restMapper *meta.RESTMapper) (*unstructured.Unstructured, error) {
|
||||
fqKind := schema.ParseGroupKind(kind)
|
||||
mapping, err := (*restMapper).RESTMapping(fqKind, version)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
object, err := (*dynamicClient).Resource(mapping.Resource).Namespace(namespace).Get(name, kubeAPIMetaV1.GetOptions{})
|
||||
return object, err
|
||||
}
|
||||
|
||||
// GetPodSpec looks inside arbitrary YAML for a PodSpec
|
||||
func GetPodSpec(yaml map[string]interface{}) interface{} {
|
||||
for _, child := range podSpecFields {
|
||||
if childYaml, ok := yaml[child]; ok {
|
||||
return GetPodSpec(childYaml.(map[string]interface{}))
|
||||
}
|
||||
}
|
||||
if _, ok := yaml["containers"]; ok {
|
||||
return yaml
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetWorkloadFromBytes parses a GenericWorkload
|
||||
func GetWorkloadFromBytes(contentBytes []byte) (*GenericWorkload, error) {
|
||||
yamlNode := make(map[string]interface{})
|
||||
err := yaml.Unmarshal(contentBytes, &yamlNode)
|
||||
if err != nil {
|
||||
logrus.Errorf("Invalid YAML: %s", string(contentBytes))
|
||||
return nil, err
|
||||
}
|
||||
finalDoc := make(map[string]interface{})
|
||||
finalDoc["metadata"] = yamlNode["metadata"]
|
||||
finalDoc["apiVersion"] = "v1"
|
||||
finalDoc["kind"] = "Pod"
|
||||
podSpec := GetPodSpec(yamlNode)
|
||||
if podSpec == nil {
|
||||
return nil, nil
|
||||
}
|
||||
finalDoc["spec"] = podSpec
|
||||
marshaledYaml, err := yaml.Marshal(finalDoc)
|
||||
if err != nil {
|
||||
logrus.Errorf("Could not marshal yaml: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
decoder := k8sYaml.NewYAMLOrJSONDecoder(bytes.NewReader(marshaledYaml), 1000)
|
||||
pod := kubeAPICoreV1.Pod{}
|
||||
err = decoder.Decode(&pod)
|
||||
newController, err := NewGenericWorkloadFromPod(pod, yamlNode)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
newController.Kind = yamlNode["kind"].(string)
|
||||
return &newController, nil
|
||||
}
|
||||
+28
-341
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019 ReactiveOps
|
||||
// Copyright 2019 FairwindsOps Inc
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -15,357 +15,44 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"github.com/fairwindsops/polaris/pkg/config"
|
||||
"github.com/fairwindsops/polaris/pkg/kube"
|
||||
|
||||
conf "github.com/reactiveops/polaris/pkg/config"
|
||||
"github.com/reactiveops/polaris/pkg/validator/messages"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
)
|
||||
|
||||
// ContainerValidation tracks validation failures associated with a Container.
|
||||
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.
|
||||
// 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,
|
||||
// ValidateContainer validates a single container from a given controller
|
||||
func ValidateContainer(conf *config.Configuration, controller kube.GenericWorkload, container *corev1.Container, isInit bool) (ContainerResult, error) {
|
||||
results, err := applyContainerSchemaChecks(conf, controller, container, isInit)
|
||||
if err != nil {
|
||||
return ContainerResult{}, err
|
||||
}
|
||||
|
||||
// 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)
|
||||
cv.validateNetworking(&cnConf.Networking)
|
||||
cv.validateSecurity(&cnConf.Security)
|
||||
|
||||
cRes := ContainerResult{
|
||||
Name: container.Name,
|
||||
Messages: cv.messages(),
|
||||
Summary: cv.summary(),
|
||||
Name: container.Name,
|
||||
Results: results,
|
||||
}
|
||||
|
||||
return cRes
|
||||
return cRes, nil
|
||||
}
|
||||
|
||||
func (cv *ContainerValidation) validateResources(resConf *conf.Resources) {
|
||||
// Only validate resources for primary containers. Although it can
|
||||
// be helpful to set these in certain cases, it usually isn't
|
||||
if cv.IsInitContainer {
|
||||
return
|
||||
// ValidateAllContainers validates both init and regular containers
|
||||
func ValidateAllContainers(conf *config.Configuration, controller kube.GenericWorkload) ([]ContainerResult, error) {
|
||||
results := []ContainerResult{}
|
||||
pod := controller.PodSpec
|
||||
for _, container := range pod.InitContainers {
|
||||
result, err := ValidateContainer(conf, controller, &container, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
results = append(results, result)
|
||||
}
|
||||
|
||||
category := messages.CategoryResources
|
||||
res := cv.Container.Resources
|
||||
|
||||
if resConf.CPURequestsMissing.IsActionable() && res.Requests.Cpu().MilliValue() == 0 {
|
||||
cv.addFailure(messages.CPURequestsFailure, resConf.CPURequestsMissing, category)
|
||||
} else {
|
||||
cv.validateResourceRange(messages.CPURequestsLabel, &resConf.CPURequestRanges, res.Requests.Cpu())
|
||||
}
|
||||
|
||||
if resConf.CPULimitsMissing.IsActionable() && res.Limits.Cpu().MilliValue() == 0 {
|
||||
cv.addFailure(messages.CPULimitsFailure, resConf.CPULimitsMissing, category)
|
||||
} else {
|
||||
cv.validateResourceRange(messages.CPULimitsLabel, &resConf.CPULimitRanges, res.Requests.Cpu())
|
||||
}
|
||||
|
||||
if resConf.MemoryRequestsMissing.IsActionable() && res.Requests.Memory().MilliValue() == 0 {
|
||||
cv.addFailure(messages.MemoryRequestsFailure, resConf.MemoryRequestsMissing, category)
|
||||
} else {
|
||||
cv.validateResourceRange(messages.MemoryRequestsLabel, &resConf.MemoryRequestRanges, res.Requests.Memory())
|
||||
}
|
||||
|
||||
if resConf.MemoryLimitsMissing.IsActionable() && res.Limits.Memory().MilliValue() == 0 {
|
||||
cv.addFailure(messages.MemoryLimitsFailure, resConf.MemoryLimitsMissing, category)
|
||||
} else {
|
||||
cv.validateResourceRange(messages.MemoryLimitsLabel, &resConf.MemoryLimitRanges, res.Limits.Memory())
|
||||
for _, container := range pod.Containers {
|
||||
result, err := ValidateContainer(conf, controller, &container, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
results = append(results, result)
|
||||
}
|
||||
}
|
||||
|
||||
func (cv *ContainerValidation) validateResourceRange(resourceName string, rangeConf *conf.ResourceRanges, res *resource.Quantity) {
|
||||
warnAbove := rangeConf.Warning.Above
|
||||
warnBelow := rangeConf.Warning.Below
|
||||
errorAbove := rangeConf.Error.Above
|
||||
errorBelow := rangeConf.Error.Below
|
||||
category := messages.CategoryResources
|
||||
|
||||
if errorAbove != nil && errorAbove.MilliValue() < res.MilliValue() {
|
||||
cv.addError(fmt.Sprintf(messages.ResourceAmountTooHighFailure, resourceName, errorAbove.String()), category)
|
||||
} else if warnAbove != nil && warnAbove.MilliValue() < res.MilliValue() {
|
||||
cv.addWarning(fmt.Sprintf(messages.ResourceAmountTooHighFailure, resourceName, warnAbove.String()), category)
|
||||
} else if errorBelow != nil && errorBelow.MilliValue() > res.MilliValue() {
|
||||
cv.addError(fmt.Sprintf(messages.ResourceAmountTooLowFailure, resourceName, errorBelow.String()), category)
|
||||
} else if warnBelow != nil && warnBelow.MilliValue() > res.MilliValue() {
|
||||
cv.addWarning(fmt.Sprintf(messages.ResourceAmountTooLowFailure, resourceName, warnBelow.String()), category)
|
||||
} else {
|
||||
if warnAbove != nil || warnBelow != nil || errorAbove != nil || errorBelow != nil {
|
||||
cv.addSuccess(fmt.Sprintf(messages.ResourceAmountSuccess, resourceName), category)
|
||||
} else {
|
||||
cv.addSuccess(fmt.Sprintf(messages.ResourcePresentSuccess, resourceName), category)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (cv *ContainerValidation) validateHealthChecks(conf *conf.HealthChecks) {
|
||||
category := messages.CategoryHealthChecks
|
||||
|
||||
// Don't validate readiness probes on init containers
|
||||
if !cv.IsInitContainer && conf.ReadinessProbeMissing.IsActionable() {
|
||||
if cv.Container.ReadinessProbe == nil {
|
||||
cv.addFailure(messages.ReadinessProbeFailure, conf.ReadinessProbeMissing, category)
|
||||
} else {
|
||||
cv.addSuccess(messages.ReadinessProbeSuccess, category)
|
||||
}
|
||||
}
|
||||
|
||||
if conf.LivenessProbeMissing.IsActionable() {
|
||||
if cv.Container.LivenessProbe == nil {
|
||||
cv.addFailure(messages.LivenessProbeFailure, conf.LivenessProbeMissing, category)
|
||||
} else {
|
||||
cv.addSuccess(messages.LivenessProbeSuccess, category)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (cv *ContainerValidation) validateImage(imageConf *conf.Images) {
|
||||
category := messages.CategoryImages
|
||||
if imageConf.PullPolicyNotAlways.IsActionable() {
|
||||
if cv.Container.ImagePullPolicy != corev1.PullAlways {
|
||||
cv.addFailure(messages.ImagePullPolicyFailure, imageConf.PullPolicyNotAlways, category)
|
||||
} else {
|
||||
cv.addSuccess(messages.ImagePullPolicySuccess, category)
|
||||
}
|
||||
}
|
||||
|
||||
if imageConf.TagNotSpecified.IsActionable() {
|
||||
img := strings.Split(cv.Container.Image, ":")
|
||||
if len(img) == 1 || img[1] == "latest" {
|
||||
cv.addFailure(messages.ImageTagFailure, imageConf.TagNotSpecified, category)
|
||||
} else {
|
||||
cv.addSuccess(messages.ImageTagSuccess, category)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (cv *ContainerValidation) validateNetworking(networkConf *conf.Networking) {
|
||||
category := messages.CategoryNetworking
|
||||
if networkConf.HostPortSet.IsActionable() {
|
||||
hostPortSet := false
|
||||
for _, port := range cv.Container.Ports {
|
||||
if port.HostPort != 0 {
|
||||
hostPortSet = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if hostPortSet {
|
||||
cv.addFailure(messages.HostPortFailure, networkConf.HostPortSet, category)
|
||||
} else {
|
||||
cv.addSuccess(messages.HostPortSuccess, category)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 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 getBoolValue(securityContext.Privileged) {
|
||||
cv.addFailure(messages.RunAsPrivilegedFailure, securityConf.RunAsPrivileged, category)
|
||||
} else {
|
||||
cv.addSuccess(messages.RunAsPrivilegedSuccess, category)
|
||||
}
|
||||
}
|
||||
|
||||
if securityConf.NotReadOnlyRootFileSystem.IsActionable() {
|
||||
if getBoolValue(securityContext.ReadOnlyRootFilesystem) {
|
||||
cv.addSuccess(messages.ReadOnlyFilesystemSuccess, category)
|
||||
} else {
|
||||
cv.addFailure(messages.ReadOnlyFilesystemFailure, securityConf.NotReadOnlyRootFileSystem, category)
|
||||
}
|
||||
}
|
||||
|
||||
if securityConf.PrivilegeEscalationAllowed.IsActionable() {
|
||||
if getBoolValue(securityContext.AllowPrivilegeEscalation) {
|
||||
cv.addFailure(messages.PrivilegeEscalationFailure, securityConf.PrivilegeEscalationAllowed, category)
|
||||
} else {
|
||||
cv.addSuccess(messages.PrivilegeEscalationSuccess, category)
|
||||
}
|
||||
}
|
||||
|
||||
hasSecurityError :=
|
||||
!cv.validateCapabilities(securityConf.Capabilities.Error, conf.SeverityError)
|
||||
hasSecurityWarning :=
|
||||
!cv.validateCapabilities(securityConf.Capabilities.Warning, conf.SeverityWarning)
|
||||
hasSecurityCheck := func(confLists conf.SecurityCapabilityLists) bool {
|
||||
return len(confLists.IfAnyAdded) > 0 ||
|
||||
len(confLists.IfAnyAddedBeyond) > 0 ||
|
||||
len(confLists.IfAnyNotDropped) > 0
|
||||
}
|
||||
if !hasSecurityError && !hasSecurityWarning &&
|
||||
(hasSecurityCheck(securityConf.Capabilities.Error) ||
|
||||
hasSecurityCheck(securityConf.Capabilities.Warning)) {
|
||||
cv.addSuccess(messages.SecurityCapabilitiesSuccess, category)
|
||||
}
|
||||
}
|
||||
|
||||
func (cv *ContainerValidation) validateCapabilities(confLists conf.SecurityCapabilityLists, severity conf.Severity) bool {
|
||||
category := messages.CategorySecurity
|
||||
capabilities := &corev1.Capabilities{}
|
||||
if cv.Container.SecurityContext != nil && cv.Container.SecurityContext.Capabilities != nil {
|
||||
capabilities = cv.Container.SecurityContext.Capabilities
|
||||
}
|
||||
|
||||
everythingOK := true
|
||||
if len(confLists.IfAnyAdded) > 0 {
|
||||
intersectAdds := capIntersection(capabilities.Add, confLists.IfAnyAdded)
|
||||
if len(intersectAdds) > 0 {
|
||||
capsString := commaSeparatedCapabilities(intersectAdds)
|
||||
cv.addFailure(fmt.Sprintf(messages.SecurityCapabilitiesAddedFailure, capsString), severity, category)
|
||||
everythingOK = false
|
||||
} else if capContains(capabilities.Add, "ALL") {
|
||||
cv.addFailure(fmt.Sprintf(messages.SecurityCapabilitiesAddedFailure, "ALL"), severity, category)
|
||||
everythingOK = false
|
||||
}
|
||||
}
|
||||
|
||||
if len(confLists.IfAnyAddedBeyond) > 0 {
|
||||
differentAdds := capDifference(capabilities.Add, confLists.IfAnyAddedBeyond)
|
||||
if len(differentAdds) > 0 {
|
||||
capsString := commaSeparatedCapabilities(differentAdds)
|
||||
cv.addFailure(fmt.Sprintf(messages.SecurityCapabilitiesAddedFailure, capsString), severity, category)
|
||||
everythingOK = false
|
||||
} else if capContains(capabilities.Add, "ALL") {
|
||||
cv.addFailure(fmt.Sprintf(messages.SecurityCapabilitiesAddedFailure, "ALL"), severity, category)
|
||||
everythingOK = false
|
||||
}
|
||||
}
|
||||
|
||||
if len(confLists.IfAnyNotDropped) > 0 {
|
||||
missingDrops := capDifference(confLists.IfAnyNotDropped, capabilities.Drop)
|
||||
if len(missingDrops) > 0 && !capContains(capabilities.Drop, "ALL") {
|
||||
capsString := commaSeparatedCapabilities(missingDrops)
|
||||
cv.addFailure(fmt.Sprintf(messages.SecurityCapabilitiesNotDroppedFailure, capsString), severity, category)
|
||||
everythingOK = false
|
||||
}
|
||||
}
|
||||
|
||||
return everythingOK
|
||||
}
|
||||
|
||||
func commaSeparatedCapabilities(caps []corev1.Capability) string {
|
||||
capsString := ""
|
||||
for _, cap := range caps {
|
||||
capsString = fmt.Sprintf("%s, %s", capsString, cap)
|
||||
}
|
||||
return capsString[2:]
|
||||
}
|
||||
|
||||
func capIntersection(a, b []corev1.Capability) []corev1.Capability {
|
||||
result := []corev1.Capability{}
|
||||
hash := map[corev1.Capability]bool{}
|
||||
|
||||
for _, s := range a {
|
||||
hash[s] = true
|
||||
}
|
||||
|
||||
for _, s := range b {
|
||||
if hash[s] {
|
||||
result = append(result, s)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func capDifference(b, a []corev1.Capability) []corev1.Capability {
|
||||
result := []corev1.Capability{}
|
||||
hash := map[corev1.Capability]bool{}
|
||||
|
||||
for _, s := range a {
|
||||
hash[s] = true
|
||||
}
|
||||
|
||||
for _, s := range b {
|
||||
if !hash[s] {
|
||||
result = append(result, s)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func capContains(list []corev1.Capability, val corev1.Capability) bool {
|
||||
for _, s := range list {
|
||||
if s == val {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
return results, nil
|
||||
}
|
||||
|
||||
+780
-542
File diff suppressed because it is too large
Load Diff
+46
-66
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019 ReactiveOps
|
||||
// Copyright 2019 FairwindsOps Inc
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -15,82 +15,62 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
conf "github.com/reactiveops/polaris/pkg/config"
|
||||
"github.com/reactiveops/polaris/pkg/kube"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"strings"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
conf "github.com/fairwindsops/polaris/pkg/config"
|
||||
"github.com/fairwindsops/polaris/pkg/kube"
|
||||
)
|
||||
|
||||
// ControllerSpec is a generic type for k8s controller specs
|
||||
type ControllerSpec struct {
|
||||
Template corev1.PodTemplateSpec
|
||||
}
|
||||
|
||||
// Controller is a generic type for k8s controllers (e.g. Deployments and StatefulSets)
|
||||
type Controller struct {
|
||||
Type string
|
||||
Name string
|
||||
Namespace string
|
||||
Spec ControllerSpec
|
||||
}
|
||||
const exemptionAnnotationKey = "polaris.fairwinds.com/exempt"
|
||||
|
||||
// ValidateController validates a single controller, returns a ControllerResult.
|
||||
func ValidateController(conf conf.Configuration, controller Controller) ControllerResult {
|
||||
pod := controller.Spec.Template.Spec
|
||||
podResult := ValidatePod(conf, &pod)
|
||||
return ControllerResult{
|
||||
Type: controller.Type,
|
||||
Name: controller.Name,
|
||||
func ValidateController(conf *conf.Configuration, controller kube.GenericWorkload) (ControllerResult, error) {
|
||||
podResult, err := ValidatePod(conf, controller)
|
||||
if err != nil {
|
||||
return ControllerResult{}, err
|
||||
}
|
||||
|
||||
controllerResult, err := applyControllerSchemaChecks(conf, controller)
|
||||
if err != nil {
|
||||
return ControllerResult{}, err
|
||||
}
|
||||
|
||||
result := ControllerResult{
|
||||
Kind: controller.Kind,
|
||||
Name: controller.ObjectMeta.GetName(),
|
||||
Namespace: controller.ObjectMeta.GetNamespace(),
|
||||
Results: controllerResult,
|
||||
PodResult: podResult,
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// 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)
|
||||
// builds a list of ResourceResults organized by namespace.
|
||||
func ValidateControllers(config *conf.Configuration, kubeResources *kube.ResourceProvider) ([]ControllerResult, error) {
|
||||
controllersToAudit := kubeResources.Controllers
|
||||
|
||||
results := []ControllerResult{}
|
||||
for _, controller := range controllersToAudit {
|
||||
if !config.DisallowExemptions && hasExemptionAnnotation(controller) {
|
||||
continue
|
||||
}
|
||||
result, err := ValidateController(config, controller)
|
||||
if err != nil {
|
||||
logrus.Warn("An error occured validating controller:", err)
|
||||
return nil, err
|
||||
}
|
||||
results = append(results, result)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// ControllerFrom* functions are 100% boilerplate
|
||||
|
||||
// ControllerFromDeployment creates a controller
|
||||
func ControllerFromDeployment(c appsv1.Deployment) Controller {
|
||||
spec := ControllerSpec{
|
||||
Template: c.Spec.Template,
|
||||
}
|
||||
return Controller{
|
||||
Type: "Deployment",
|
||||
Name: c.Name,
|
||||
Namespace: c.Namespace,
|
||||
Spec: spec,
|
||||
}
|
||||
}
|
||||
|
||||
// ControllerFromStatefulSet creates a controller
|
||||
func ControllerFromStatefulSet(c appsv1.StatefulSet) Controller {
|
||||
spec := ControllerSpec{
|
||||
Template: c.Spec.Template,
|
||||
}
|
||||
return Controller{
|
||||
Type: "StatefulSet",
|
||||
Name: c.Name,
|
||||
Namespace: c.Namespace,
|
||||
Spec: spec,
|
||||
}
|
||||
func hasExemptionAnnotation(ctrl kube.GenericWorkload) bool {
|
||||
annot := ctrl.ObjectMeta.GetAnnotations()
|
||||
val := annot[exemptionAnnotationKey]
|
||||
return strings.ToLower(val) == "true"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
// Copyright 2019 FairwindsOps Inc
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package validator
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
|
||||
conf "github.com/fairwindsops/polaris/pkg/config"
|
||||
"github.com/fairwindsops/polaris/pkg/kube"
|
||||
"github.com/fairwindsops/polaris/test"
|
||||
)
|
||||
|
||||
func TestValidateController(t *testing.T) {
|
||||
c := conf.Configuration{
|
||||
Checks: map[string]conf.Severity{
|
||||
"hostIPCSet": conf.SeverityDanger,
|
||||
"hostPIDSet": conf.SeverityDanger,
|
||||
},
|
||||
}
|
||||
deployment, err := kube.NewGenericWorkloadFromPod(test.MockPod(), nil)
|
||||
assert.NoError(t, err)
|
||||
deployment.Kind = "Deployment"
|
||||
expectedSum := CountSummary{
|
||||
Successes: uint(2),
|
||||
Warnings: uint(0),
|
||||
Dangers: uint(0),
|
||||
}
|
||||
|
||||
expectedResults := ResultSet{
|
||||
"hostIPCSet": {ID: "hostIPCSet", Message: "Host IPC is not configured", Success: true, Severity: "danger", Category: "Security"},
|
||||
"hostPIDSet": {ID: "hostPIDSet", Message: "Host PID is not configured", Success: true, Severity: "danger", Category: "Security"},
|
||||
}
|
||||
|
||||
actualResult, err := ValidateController(&c, deployment)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "Deployment", actualResult.Kind)
|
||||
assert.Equal(t, 1, len(actualResult.PodResult.ContainerResults), "should be equal")
|
||||
assert.EqualValues(t, expectedSum, actualResult.GetSummary())
|
||||
assert.EqualValues(t, expectedResults, actualResult.PodResult.Results)
|
||||
}
|
||||
|
||||
func TestControllerLevelChecks(t *testing.T) {
|
||||
c := conf.Configuration{
|
||||
Checks: map[string]conf.Severity{
|
||||
"multipleReplicasForDeployment": conf.SeverityDanger,
|
||||
},
|
||||
}
|
||||
resources, err := kube.CreateResourceProviderFromPath("../kube/test_files/test_1")
|
||||
|
||||
assert.Equal(t, nil, err, "Error should be nil")
|
||||
|
||||
assert.Equal(t, 8, len(resources.Controllers), "Should have eight controllers")
|
||||
|
||||
expectedSum := CountSummary{
|
||||
Successes: uint(0),
|
||||
Warnings: uint(0),
|
||||
Dangers: uint(1),
|
||||
}
|
||||
|
||||
expectedResults := ResultSet{
|
||||
"multipleReplicasForDeployment": {ID: "multipleReplicasForDeployment", Message: "Only one replica is scheduled", Success: false, Severity: "danger", Category: "Reliability"},
|
||||
}
|
||||
|
||||
for _, controller := range resources.Controllers {
|
||||
if controller.Kind == "Deployment" && controller.ObjectMeta.GetName() == "test-deployment" {
|
||||
actualResult, err := ValidateController(&c, controller)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "Deployment", actualResult.Kind)
|
||||
assert.Equal(t, 1, len(actualResult.Results), "should be equal")
|
||||
assert.EqualValues(t, expectedSum, actualResult.GetSummary())
|
||||
assert.EqualValues(t, expectedResults, actualResult.Results)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestSkipHealthChecks(t *testing.T) {
|
||||
c := conf.Configuration{
|
||||
Checks: map[string]conf.Severity{
|
||||
"readinessProbeMissing": conf.SeverityDanger,
|
||||
"livenessProbeMissing": conf.SeverityWarning,
|
||||
},
|
||||
}
|
||||
pod := test.MockPod()
|
||||
pod.Spec.InitContainers = []corev1.Container{test.MockContainer("test")}
|
||||
deployment, err := kube.NewGenericWorkloadFromPod(pod, nil)
|
||||
assert.NoError(t, err)
|
||||
deployment.Kind = "Deployment"
|
||||
expectedSum := CountSummary{
|
||||
Successes: uint(0),
|
||||
Warnings: uint(1),
|
||||
Dangers: uint(1),
|
||||
}
|
||||
expectedResults := ResultSet{
|
||||
"readinessProbeMissing": {ID: "readinessProbeMissing", Message: "Readiness probe should be configured", Success: false, Severity: "danger", Category: "Health Checks"},
|
||||
"livenessProbeMissing": {ID: "livenessProbeMissing", Message: "Liveness probe should be configured", Success: false, Severity: "warning", Category: "Health Checks"},
|
||||
}
|
||||
actualResult, err := ValidateController(&c, deployment)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
assert.Equal(t, "Deployment", actualResult.Kind)
|
||||
assert.Equal(t, 2, len(actualResult.PodResult.ContainerResults), "should be equal")
|
||||
assert.EqualValues(t, expectedSum, actualResult.GetSummary())
|
||||
assert.EqualValues(t, ResultSet{}, actualResult.PodResult.ContainerResults[0].Results)
|
||||
assert.EqualValues(t, expectedResults, actualResult.PodResult.ContainerResults[1].Results)
|
||||
|
||||
job, err := kube.NewGenericWorkloadFromPod(test.MockPod(), nil)
|
||||
assert.NoError(t, err)
|
||||
job.Kind = "Job"
|
||||
expectedSum = CountSummary{
|
||||
Successes: uint(0),
|
||||
Warnings: uint(0),
|
||||
Dangers: uint(0),
|
||||
}
|
||||
expectedResults = ResultSet{}
|
||||
actualResult, err = ValidateController(&c, job)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
assert.Equal(t, "Job", actualResult.Kind)
|
||||
assert.Equal(t, 1, len(actualResult.PodResult.ContainerResults), "should be equal")
|
||||
assert.EqualValues(t, expectedSum, actualResult.GetSummary())
|
||||
assert.EqualValues(t, expectedResults, actualResult.PodResult.ContainerResults[0].Results)
|
||||
|
||||
cronjob, err := kube.NewGenericWorkloadFromPod(test.MockPod(), nil)
|
||||
assert.NoError(t, err)
|
||||
cronjob.Kind = "CronJob"
|
||||
expectedSum = CountSummary{
|
||||
Successes: uint(0),
|
||||
Warnings: uint(0),
|
||||
Dangers: uint(0),
|
||||
}
|
||||
expectedResults = ResultSet{}
|
||||
actualResult, err = ValidateController(&c, cronjob)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
assert.Equal(t, "CronJob", actualResult.Kind)
|
||||
assert.Equal(t, 1, len(actualResult.PodResult.ContainerResults), "should be equal")
|
||||
assert.EqualValues(t, expectedSum, actualResult.GetSummary())
|
||||
assert.EqualValues(t, expectedResults, actualResult.PodResult.ContainerResults[0].Results)
|
||||
}
|
||||
|
||||
func TestControllerExemptions(t *testing.T) {
|
||||
c := conf.Configuration{
|
||||
Checks: map[string]conf.Severity{
|
||||
"readinessProbeMissing": conf.SeverityDanger,
|
||||
"livenessProbeMissing": conf.SeverityWarning,
|
||||
},
|
||||
}
|
||||
pod := test.MockPod()
|
||||
workload, err := kube.NewGenericWorkloadFromPod(pod, nil)
|
||||
assert.NoError(t, err)
|
||||
workload.Kind = "Deployment"
|
||||
resources := &kube.ResourceProvider{
|
||||
Controllers: []kube.GenericWorkload{workload},
|
||||
}
|
||||
|
||||
expectedSum := CountSummary{
|
||||
Successes: uint(0),
|
||||
Warnings: uint(1),
|
||||
Dangers: uint(1),
|
||||
}
|
||||
actualResults, err := ValidateControllers(&c, resources)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
assert.Equal(t, 1, len(actualResults))
|
||||
assert.Equal(t, "Deployment", actualResults[0].Kind)
|
||||
assert.EqualValues(t, expectedSum, actualResults[0].GetSummary())
|
||||
|
||||
resources.Controllers[0].ObjectMeta.SetAnnotations(map[string]string{
|
||||
exemptionAnnotationKey: "true",
|
||||
})
|
||||
actualResults, err = ValidateControllers(&c, resources)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
assert.Equal(t, 0, len(actualResults))
|
||||
}
|
||||
+53
-55
@@ -1,79 +1,77 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
conf "github.com/reactiveops/polaris/pkg/config"
|
||||
"github.com/reactiveops/polaris/pkg/kube"
|
||||
conf "github.com/fairwindsops/polaris/pkg/config"
|
||||
"github.com/fairwindsops/polaris/pkg/kube"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
apiMachineryYAML "k8s.io/apimachinery/pkg/util/yaml"
|
||||
)
|
||||
|
||||
const (
|
||||
// PolarisOutputVersion is the version of the current output structure
|
||||
PolarisOutputVersion = "0.2"
|
||||
)
|
||||
|
||||
// ClusterSummary contains Polaris results as well as some high-level stats
|
||||
type ClusterSummary struct {
|
||||
Results ResultSummary
|
||||
Version string
|
||||
Nodes int
|
||||
Pods int
|
||||
Namespaces int
|
||||
Deployments int
|
||||
StatefulSets int
|
||||
Score uint
|
||||
}
|
||||
|
||||
// AuditData contains all the data from a full Polaris audit
|
||||
type AuditData struct {
|
||||
PolarisOutputVersion string
|
||||
AuditTime string
|
||||
SourceType string
|
||||
SourceName string
|
||||
DisplayName string
|
||||
ClusterSummary ClusterSummary
|
||||
NamespacedResults NamespacedResults
|
||||
}
|
||||
|
||||
// RunAudit runs a full Polaris audit and returns an AuditData object
|
||||
func RunAudit(config conf.Configuration, kubeResources *kube.ResourceProvider) (AuditData, error) {
|
||||
nsResults := NamespacedResults{}
|
||||
ValidateControllers(config, kubeResources, &nsResults)
|
||||
|
||||
clusterResults := ResultSummary{}
|
||||
|
||||
// Aggregate all summary counts to get a clusterwide count.
|
||||
for _, nsRes := range nsResults {
|
||||
for _, dr := range nsRes.DeploymentResults {
|
||||
clusterResults.appendResults(*dr.PodResult.Summary)
|
||||
}
|
||||
for _, dr := range nsRes.StatefulSetResults {
|
||||
clusterResults.appendResults(*dr.PodResult.Summary)
|
||||
}
|
||||
}
|
||||
|
||||
displayName := config.DisplayName
|
||||
if displayName == "" {
|
||||
displayName = kubeResources.SourceName
|
||||
}
|
||||
|
||||
results, err := ValidateControllers(&config, kubeResources)
|
||||
if err != nil {
|
||||
return AuditData{}, err
|
||||
}
|
||||
|
||||
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),
|
||||
StatefulSets: len(kubeResources.StatefulSets),
|
||||
Results: clusterResults,
|
||||
Score: clusterResults.Totals.GetScore(),
|
||||
ClusterInfo: ClusterInfo{
|
||||
Version: kubeResources.ServerVersion,
|
||||
Nodes: len(kubeResources.Nodes),
|
||||
Pods: len(kubeResources.Controllers), // TODO validate that this is still valuable
|
||||
Namespaces: len(kubeResources.Namespaces),
|
||||
Controllers: len(results),
|
||||
},
|
||||
NamespacedResults: nsResults,
|
||||
Results: results,
|
||||
}
|
||||
return auditData, nil
|
||||
}
|
||||
|
||||
// ReadAuditFromFile reads the data from a past audit stored in a JSON or YAML file.
|
||||
func ReadAuditFromFile(fileName string) AuditData {
|
||||
auditData := AuditData{}
|
||||
oldFileBytes, err := ioutil.ReadFile(fileName)
|
||||
if err != nil {
|
||||
logrus.Errorf("Unable to read contents of loaded file: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
auditData, err = ParseAudit(oldFileBytes)
|
||||
if err != nil {
|
||||
logrus.Errorf("Error parsing file contents into auditData: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
return auditData
|
||||
}
|
||||
|
||||
// ParseAudit decodes either a YAML or JSON file and returns AuditData.
|
||||
func ParseAudit(oldFileBytes []byte) (AuditData, error) {
|
||||
reader := bytes.NewReader(oldFileBytes)
|
||||
conf := AuditData{}
|
||||
d := apiMachineryYAML.NewYAMLOrJSONDecoder(reader, 4096)
|
||||
for {
|
||||
if err := d.Decode(&conf); err != nil {
|
||||
if err == io.EOF {
|
||||
return conf, nil
|
||||
}
|
||||
return conf, fmt.Errorf("Decoding config failed: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,58 +3,52 @@ package validator
|
||||
import (
|
||||
"testing"
|
||||
|
||||
conf "github.com/reactiveops/polaris/pkg/config"
|
||||
"github.com/reactiveops/polaris/pkg/kube"
|
||||
"github.com/reactiveops/polaris/test"
|
||||
conf "github.com/fairwindsops/polaris/pkg/config"
|
||||
"github.com/fairwindsops/polaris/pkg/kube"
|
||||
"github.com/fairwindsops/polaris/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetTemplateData(t *testing.T) {
|
||||
k8s := test.SetupTestAPI()
|
||||
k8s, dynamicClient := test.SetupTestAPI()
|
||||
k8s = test.SetupAddControllers(k8s, "test")
|
||||
resources, err := kube.CreateResourceProviderFromAPI(k8s, "test")
|
||||
k8s = test.SetupAddExtraControllerVersions(k8s, "test-extra")
|
||||
// TODO figure out how to mock out dynamic client.
|
||||
// and add in pods for all controllers to fill out tests.
|
||||
resources, err := kube.CreateResourceProviderFromAPI(k8s, "test", &dynamicClient)
|
||||
assert.Equal(t, err, nil, "error should be nil")
|
||||
|
||||
c := conf.Configuration{
|
||||
HealthChecks: conf.HealthChecks{
|
||||
ReadinessProbeMissing: conf.SeverityError,
|
||||
LivenessProbeMissing: conf.SeverityWarning,
|
||||
Checks: map[string]conf.Severity{
|
||||
"readinessProbeMissing": conf.SeverityDanger,
|
||||
"livenessProbeMissing": conf.SeverityWarning,
|
||||
},
|
||||
}
|
||||
|
||||
sum := ResultSummary{
|
||||
Totals: CountSummary{
|
||||
Successes: uint(8),
|
||||
Warnings: uint(2),
|
||||
Errors: uint(2),
|
||||
},
|
||||
ByCategory: CategorySummary{},
|
||||
}
|
||||
sum.ByCategory["Health Checks"] = &CountSummary{
|
||||
sum := CountSummary{
|
||||
Successes: uint(0),
|
||||
Warnings: uint(2),
|
||||
Errors: uint(2),
|
||||
}
|
||||
sum.ByCategory["Resources"] = &CountSummary{
|
||||
Successes: uint(8),
|
||||
Warnings: uint(0),
|
||||
Errors: uint(0),
|
||||
Warnings: uint(1),
|
||||
Dangers: uint(1),
|
||||
}
|
||||
|
||||
actualAudit, err := RunAudit(c, resources)
|
||||
assert.Equal(t, err, nil, "error should be nil")
|
||||
|
||||
assert.EqualValues(t, sum, actualAudit.ClusterSummary.Results)
|
||||
assert.EqualValues(t, sum, actualAudit.GetSummary())
|
||||
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")
|
||||
expected := []struct {
|
||||
kind string
|
||||
results int
|
||||
}{
|
||||
{kind: "Pod", results: 2},
|
||||
}
|
||||
|
||||
assert.Equal(t, 1, len(actualAudit.NamespacedResults["test"].StatefulSetResults), "should be equal")
|
||||
assert.Equal(t, 1, len(actualAudit.NamespacedResults["test"].StatefulSetResults), "should be equal")
|
||||
assert.Equal(t, 1, len(actualAudit.NamespacedResults["test"].StatefulSetResults[0].PodResult.ContainerResults), "should be equal")
|
||||
assert.Equal(t, 6, len(actualAudit.NamespacedResults["test"].StatefulSetResults[0].PodResult.ContainerResults[0].Messages), "should be equal")
|
||||
assert.Equal(t, len(expected), len(actualAudit.Results))
|
||||
for idx, result := range actualAudit.Results {
|
||||
assert.Equal(t, expected[idx].kind, result.Kind)
|
||||
assert.Equal(t, 1, len(result.PodResult.ContainerResults))
|
||||
assert.Equal(t, expected[idx].results, len(result.PodResult.ContainerResults[0].Results))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
package messages
|
||||
|
||||
const (
|
||||
// CategoryHealthChecks category
|
||||
CategoryHealthChecks = "Health Checks"
|
||||
// CategorySecurity category
|
||||
CategorySecurity = "Security"
|
||||
// CategoryNetworking category
|
||||
CategoryNetworking = "Networking"
|
||||
// CategoryResources category
|
||||
CategoryResources = "Resources"
|
||||
// CategoryImages category
|
||||
CategoryImages = "Images"
|
||||
|
||||
// CPURequestsLabel label
|
||||
CPURequestsLabel = "CPU requests"
|
||||
// CPULimitsLabel label
|
||||
CPULimitsLabel = "CPU limits"
|
||||
// MemoryRequestsLabel label
|
||||
MemoryRequestsLabel = "Memory requests"
|
||||
// MemoryLimitsLabel label
|
||||
MemoryLimitsLabel = "Memory limits"
|
||||
|
||||
// CPURequestsFailure message
|
||||
CPURequestsFailure = "CPU requests should be set"
|
||||
// CPULimitsFailure message
|
||||
CPULimitsFailure = "CPU limits should be set"
|
||||
// MemoryRequestsFailure message
|
||||
MemoryRequestsFailure = "Memory requests should be set"
|
||||
// MemoryLimitsFailure message
|
||||
MemoryLimitsFailure = "Memory limits should be set"
|
||||
// ResourceAmountTooHighFailure message
|
||||
ResourceAmountTooHighFailure = "%s should be lower than %s"
|
||||
// ResourceAmountTooLowFailure message
|
||||
ResourceAmountTooLowFailure = "%s should be higher than %s"
|
||||
// ResourceAmountSuccess message
|
||||
ResourceAmountSuccess = "%s are within the expected range"
|
||||
// ResourcePresentSuccess message
|
||||
ResourcePresentSuccess = "%s are set"
|
||||
// ReadinessProbeFailure message
|
||||
ReadinessProbeFailure = "Readiness probe should be configured"
|
||||
// ReadinessProbeSuccess message
|
||||
ReadinessProbeSuccess = "Readiness probe configured"
|
||||
// LivenessProbeFailure message
|
||||
LivenessProbeFailure = "Liveness probe should be configured"
|
||||
// LivenessProbeSuccess message
|
||||
LivenessProbeSuccess = "Liveness probe is configured"
|
||||
// ImageTagFailure message
|
||||
ImageTagFailure = "Image tag should be specified"
|
||||
// ImageTagSuccess message
|
||||
ImageTagSuccess = "Image tag is specified"
|
||||
// ImagePullPolicyFailure message
|
||||
ImagePullPolicyFailure = "Image pull policy should be \"Always\""
|
||||
// ImagePullPolicySuccess message
|
||||
ImagePullPolicySuccess = "Image pull policy is \"Always\""
|
||||
// HostPortFailure message
|
||||
HostPortFailure = "Host port should not be configured"
|
||||
// HostPortSuccess message
|
||||
HostPortSuccess = "Host port is not configured"
|
||||
// RunAsRootFailure message
|
||||
RunAsRootFailure = "Should not be allowed to run as root"
|
||||
// RunAsRootSuccess message
|
||||
RunAsRootSuccess = "Is not allowed to run as root"
|
||||
// RunAsPrivilegedFailure message
|
||||
RunAsPrivilegedFailure = "Should not be running as privileged"
|
||||
// RunAsPrivilegedSuccess message
|
||||
RunAsPrivilegedSuccess = "Not running as privileged"
|
||||
// ReadOnlyFilesystemSuccess message
|
||||
ReadOnlyFilesystemSuccess = "Filesystem is read only"
|
||||
// ReadOnlyFilesystemFailure message
|
||||
ReadOnlyFilesystemFailure = "Filesystem should be read only"
|
||||
// PrivilegeEscalationFailure message
|
||||
PrivilegeEscalationFailure = "Privilege escalation should not be allowed"
|
||||
// PrivilegeEscalationSuccess message
|
||||
PrivilegeEscalationSuccess = "Privilege escalation not allowed"
|
||||
// SecurityCapabilitiesAddedFailure message
|
||||
SecurityCapabilitiesAddedFailure = "The following security capabilities should not be added: %v"
|
||||
// SecurityCapabilitiesNotDroppedFailure message
|
||||
SecurityCapabilitiesNotDroppedFailure = "The following security capabilities should be dropped: %v"
|
||||
// SecurityCapabilitiesSuccess message
|
||||
SecurityCapabilitiesSuccess = "Security capabilities are within the configured limits"
|
||||
|
||||
// HostAliasFailure message
|
||||
HostAliasFailure = "Host alias should not be configured"
|
||||
// HostAliasSuccess message
|
||||
HostAliasSuccess = "Host alias is not configured"
|
||||
// HostIPCFailure message
|
||||
HostIPCFailure = "Host IPC should not be configured"
|
||||
// HostIPCSuccess message
|
||||
HostIPCSuccess = "Host IPC is not configured"
|
||||
// HostPIDFailure message
|
||||
HostPIDFailure = "Host PID should not be configured"
|
||||
// HostPIDSuccess message
|
||||
HostPIDSuccess = "Host PID is not configured"
|
||||
// HostNetworkFailure message
|
||||
HostNetworkFailure = "Host network should not be configured"
|
||||
// HostNetworkSuccess message
|
||||
HostNetworkSuccess = "Host network is not configured"
|
||||
)
|
||||
@@ -0,0 +1,81 @@
|
||||
// Copyright 2019 FairwindsOps Inc
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package validator
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/fairwindsops/polaris/pkg/config"
|
||||
)
|
||||
|
||||
const (
|
||||
// PolarisOutputVersion is the version of the current output structure
|
||||
PolarisOutputVersion = "1.0"
|
||||
)
|
||||
|
||||
// AuditData contains all the data from a full Polaris audit
|
||||
type AuditData struct {
|
||||
PolarisOutputVersion string
|
||||
AuditTime string
|
||||
SourceType string
|
||||
SourceName string
|
||||
DisplayName string
|
||||
ClusterInfo ClusterInfo
|
||||
Results []ControllerResult
|
||||
}
|
||||
|
||||
// ClusterInfo contains Polaris results as well as some high-level stats
|
||||
type ClusterInfo struct {
|
||||
Version string
|
||||
Nodes int
|
||||
Pods int
|
||||
Namespaces int
|
||||
Controllers int
|
||||
}
|
||||
|
||||
// ResultMessage is the result of a given check
|
||||
type ResultMessage struct {
|
||||
ID string
|
||||
Message string
|
||||
Success bool
|
||||
Severity config.Severity
|
||||
Category string
|
||||
}
|
||||
|
||||
// ResultSet contiains the results for a set of checks
|
||||
type ResultSet map[string]ResultMessage
|
||||
|
||||
// ControllerResult provides results for a controller
|
||||
type ControllerResult struct {
|
||||
Name string
|
||||
Namespace string
|
||||
Kind string
|
||||
Results ResultSet
|
||||
PodResult PodResult
|
||||
CreatedTime time.Time
|
||||
}
|
||||
|
||||
// PodResult provides a list of validation messages for each pod.
|
||||
type PodResult struct {
|
||||
Name string
|
||||
Results ResultSet
|
||||
ContainerResults []ContainerResult
|
||||
}
|
||||
|
||||
// ContainerResult provides a list of validation messages for each container.
|
||||
type ContainerResult struct {
|
||||
Name string
|
||||
Results ResultSet
|
||||
}
|
||||
+12
-67
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019 ReactiveOps
|
||||
// Copyright 2019 FairwindsOps Inc
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -15,79 +15,24 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
conf "github.com/reactiveops/polaris/pkg/config"
|
||||
"github.com/reactiveops/polaris/pkg/validator/messages"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"github.com/fairwindsops/polaris/pkg/config"
|
||||
"github.com/fairwindsops/polaris/pkg/kube"
|
||||
)
|
||||
|
||||
// PodValidation tracks validation failures associated with a Pod.
|
||||
type PodValidation struct {
|
||||
*ResourceValidation
|
||||
Pod *corev1.PodSpec
|
||||
}
|
||||
|
||||
// ValidatePod validates that each pod conforms to the Polaris config, returns a ResourceResult.
|
||||
func ValidatePod(podConf conf.Configuration, pod *corev1.PodSpec) PodResult {
|
||||
pv := PodValidation{
|
||||
Pod: pod,
|
||||
ResourceValidation: &ResourceValidation{},
|
||||
func ValidatePod(conf *config.Configuration, controller kube.GenericWorkload) (PodResult, error) {
|
||||
podResults, err := applyPodSchemaChecks(conf, controller)
|
||||
if err != nil {
|
||||
return PodResult{}, err
|
||||
}
|
||||
|
||||
pv.validateSecurity(&podConf.Security)
|
||||
pv.validateNetworking(&podConf.Networking)
|
||||
|
||||
pRes := PodResult{
|
||||
Messages: pv.messages(),
|
||||
Results: podResults,
|
||||
ContainerResults: []ContainerResult{},
|
||||
Summary: pv.summary(),
|
||||
podSpec: *pod,
|
||||
}
|
||||
|
||||
pv.validateContainers(pod.InitContainers, &pRes, &podConf, true)
|
||||
pv.validateContainers(pod.Containers, &pRes, &podConf, false)
|
||||
|
||||
for _, cRes := range pRes.ContainerResults {
|
||||
pRes.Summary.appendResults(*cRes.Summary)
|
||||
}
|
||||
|
||||
return pRes
|
||||
}
|
||||
|
||||
func (pv *PodValidation) validateContainers(containers []corev1.Container, pRes *PodResult, podConf *conf.Configuration, isInit bool) {
|
||||
for _, container := range containers {
|
||||
cRes := ValidateContainer(&container, pRes, podConf, isInit)
|
||||
pRes.ContainerResults = append(pRes.ContainerResults, cRes)
|
||||
}
|
||||
}
|
||||
|
||||
func (pv *PodValidation) validateSecurity(securityConf *conf.Security) {
|
||||
category := messages.CategorySecurity
|
||||
|
||||
if securityConf.HostIPCSet.IsActionable() {
|
||||
if pv.Pod.HostIPC {
|
||||
pv.addFailure(messages.HostIPCFailure, securityConf.HostIPCSet, category)
|
||||
} else {
|
||||
pv.addSuccess(messages.HostIPCSuccess, category)
|
||||
}
|
||||
}
|
||||
|
||||
if securityConf.HostPIDSet.IsActionable() {
|
||||
if pv.Pod.HostPID {
|
||||
pv.addFailure(messages.HostPIDFailure, securityConf.HostPIDSet, category)
|
||||
} else {
|
||||
pv.addSuccess(messages.HostPIDSuccess, category)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (pv *PodValidation) validateNetworking(networkConf *conf.Networking) {
|
||||
category := messages.CategoryNetworking
|
||||
|
||||
if networkConf.HostNetworkSet.IsActionable() {
|
||||
if pv.Pod.HostNetwork {
|
||||
pv.addFailure(messages.HostNetworkFailure, networkConf.HostNetworkSet, category)
|
||||
} else {
|
||||
pv.addSuccess(messages.HostNetworkSuccess, category)
|
||||
}
|
||||
pRes.ContainerResults, err = ValidateAllContainers(conf, controller)
|
||||
if err != nil {
|
||||
return pRes, err
|
||||
}
|
||||
return pRes, nil
|
||||
}
|
||||
|
||||
+186
-41
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019 ReactiveOps
|
||||
// Copyright 2019 FairwindsOps Inc
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -17,60 +17,205 @@ package validator
|
||||
import (
|
||||
"testing"
|
||||
|
||||
conf "github.com/reactiveops/polaris/pkg/config"
|
||||
"github.com/reactiveops/polaris/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
conf "github.com/fairwindsops/polaris/pkg/config"
|
||||
"github.com/fairwindsops/polaris/pkg/kube"
|
||||
"github.com/fairwindsops/polaris/test"
|
||||
)
|
||||
|
||||
func TestValidatePod(t *testing.T) {
|
||||
c := conf.Configuration{
|
||||
Security: conf.Security{
|
||||
HostIPCSet: conf.SeverityError,
|
||||
HostPIDSet: conf.SeverityError,
|
||||
},
|
||||
Networking: conf.Networking{
|
||||
HostNetworkSet: conf.SeverityWarning,
|
||||
HostPortSet: conf.SeverityError,
|
||||
Checks: map[string]conf.Severity{
|
||||
"hostIPCSet": conf.SeverityDanger,
|
||||
"hostPIDSet": conf.SeverityDanger,
|
||||
"hostNetworkSet": conf.SeverityWarning,
|
||||
"hostPortSet": conf.SeverityDanger,
|
||||
},
|
||||
}
|
||||
|
||||
k8s := test.SetupTestAPI()
|
||||
k8s, _ := test.SetupTestAPI()
|
||||
k8s = test.SetupAddControllers(k8s, "test")
|
||||
pod := test.MockPod()
|
||||
|
||||
expectedSum := ResultSummary{
|
||||
Totals: CountSummary{
|
||||
Successes: uint(8),
|
||||
Warnings: uint(0),
|
||||
Errors: uint(0),
|
||||
},
|
||||
ByCategory: make(map[string]*CountSummary),
|
||||
}
|
||||
expectedSum.ByCategory["Networking"] = &CountSummary{
|
||||
Successes: uint(2),
|
||||
Warnings: uint(0),
|
||||
Errors: uint(0),
|
||||
}
|
||||
expectedSum.ByCategory["Resources"] = &CountSummary{
|
||||
p := test.MockPod()
|
||||
deployment, err := kube.NewGenericWorkloadFromPod(p, nil)
|
||||
assert.NoError(t, err)
|
||||
expectedSum := CountSummary{
|
||||
Successes: uint(4),
|
||||
Warnings: uint(0),
|
||||
Errors: uint(0),
|
||||
}
|
||||
expectedSum.ByCategory["Security"] = &CountSummary{
|
||||
Successes: uint(2),
|
||||
Warnings: uint(0),
|
||||
Errors: uint(0),
|
||||
Dangers: uint(0),
|
||||
}
|
||||
|
||||
expectedMessages := []*ResultMessage{
|
||||
{Message: "Host IPC is not configured", Type: "success", Category: "Security"},
|
||||
{Message: "Host PID is not configured", Type: "success", Category: "Security"},
|
||||
{Message: "Host network is not configured", Type: "success", Category: "Networking"},
|
||||
expectedResults := ResultSet{
|
||||
"hostIPCSet": {ID: "hostIPCSet", Message: "Host IPC is not configured", Success: true, Severity: "danger", Category: "Security"},
|
||||
"hostNetworkSet": {ID: "hostNetworkSet", Message: "Host network is not configured", Success: true, Severity: "warning", Category: "Networking"},
|
||||
"hostPIDSet": {ID: "hostPIDSet", Message: "Host PID is not configured", Success: true, Severity: "danger", Category: "Security"},
|
||||
}
|
||||
|
||||
actualPodResult := ValidatePod(c, &pod.Spec)
|
||||
actualPodResult, err := ValidatePod(&c, deployment)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, len(actualPodResult.ContainerResults), 1, "should be equal")
|
||||
assert.EqualValues(t, actualPodResult.Summary, &expectedSum)
|
||||
assert.EqualValues(t, actualPodResult.Messages, expectedMessages)
|
||||
assert.Equal(t, 1, len(actualPodResult.ContainerResults), "should be equal")
|
||||
assert.EqualValues(t, expectedSum, actualPodResult.GetSummary())
|
||||
assert.EqualValues(t, expectedResults, actualPodResult.Results)
|
||||
}
|
||||
|
||||
func TestInvalidIPCPod(t *testing.T) {
|
||||
c := conf.Configuration{
|
||||
Checks: map[string]conf.Severity{
|
||||
"hostIPCSet": conf.SeverityDanger,
|
||||
"hostPIDSet": conf.SeverityDanger,
|
||||
"hostNetworkSet": conf.SeverityWarning,
|
||||
"hostPortSet": conf.SeverityDanger,
|
||||
},
|
||||
}
|
||||
|
||||
k8s, _ := test.SetupTestAPI()
|
||||
k8s = test.SetupAddControllers(k8s, "test")
|
||||
p := test.MockPod()
|
||||
p.Spec.HostIPC = true
|
||||
workload, err := kube.NewGenericWorkloadFromPod(p, nil)
|
||||
assert.NoError(t, err)
|
||||
expectedSum := CountSummary{
|
||||
Successes: uint(3),
|
||||
Warnings: uint(0),
|
||||
Dangers: uint(1),
|
||||
}
|
||||
expectedResults := ResultSet{
|
||||
"hostIPCSet": {ID: "hostIPCSet", Message: "Host IPC should not be configured", Success: false, Severity: "danger", Category: "Security"},
|
||||
"hostNetworkSet": {ID: "hostNetworkSet", Message: "Host network is not configured", Success: true, Severity: "warning", Category: "Networking"},
|
||||
"hostPIDSet": {ID: "hostPIDSet", Message: "Host PID is not configured", Success: true, Severity: "danger", Category: "Security"},
|
||||
}
|
||||
|
||||
actualPodResult, err := ValidatePod(&c, workload)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, 1, len(actualPodResult.ContainerResults), "should be equal")
|
||||
assert.EqualValues(t, expectedSum, actualPodResult.GetSummary())
|
||||
assert.EqualValues(t, expectedResults, actualPodResult.Results)
|
||||
}
|
||||
|
||||
func TestInvalidNeworkPod(t *testing.T) {
|
||||
c := conf.Configuration{
|
||||
Checks: map[string]conf.Severity{
|
||||
"hostNetworkSet": conf.SeverityWarning,
|
||||
"hostPortSet": conf.SeverityDanger,
|
||||
"hostIPCSet": conf.SeverityDanger,
|
||||
"hostPIDSet": conf.SeverityDanger,
|
||||
},
|
||||
}
|
||||
|
||||
k8s, _ := test.SetupTestAPI()
|
||||
k8s = test.SetupAddControllers(k8s, "test")
|
||||
p := test.MockPod()
|
||||
p.Spec.HostNetwork = true
|
||||
workload, err := kube.NewGenericWorkloadFromPod(p, nil)
|
||||
assert.NoError(t, err)
|
||||
expectedSum := CountSummary{
|
||||
Successes: uint(3),
|
||||
Warnings: uint(1),
|
||||
Dangers: uint(0),
|
||||
}
|
||||
|
||||
expectedResults := ResultSet{
|
||||
"hostNetworkSet": {ID: "hostNetworkSet", Message: "Host network should not be configured", Success: false, Severity: "warning", Category: "Networking"},
|
||||
"hostIPCSet": {ID: "hostIPCSet", Message: "Host IPC is not configured", Success: true, Severity: "danger", Category: "Security"},
|
||||
"hostPIDSet": {ID: "hostPIDSet", Message: "Host PID is not configured", Success: true, Severity: "danger", Category: "Security"},
|
||||
}
|
||||
|
||||
actualPodResult, err := ValidatePod(&c, workload)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, 1, len(actualPodResult.ContainerResults), "should be equal")
|
||||
assert.EqualValues(t, expectedSum, actualPodResult.GetSummary())
|
||||
assert.EqualValues(t, expectedResults, actualPodResult.Results)
|
||||
}
|
||||
|
||||
func TestInvalidPIDPod(t *testing.T) {
|
||||
c := conf.Configuration{
|
||||
Checks: map[string]conf.Severity{
|
||||
"hostIPCSet": conf.SeverityDanger,
|
||||
"hostPIDSet": conf.SeverityDanger,
|
||||
"hostNetworkSet": conf.SeverityWarning,
|
||||
"hostPortSet": conf.SeverityDanger,
|
||||
},
|
||||
}
|
||||
|
||||
k8s, _ := test.SetupTestAPI()
|
||||
k8s = test.SetupAddControllers(k8s, "test")
|
||||
p := test.MockPod()
|
||||
p.Spec.HostPID = true
|
||||
workload, err := kube.NewGenericWorkloadFromPod(p, nil)
|
||||
assert.NoError(t, err)
|
||||
expectedSum := CountSummary{
|
||||
Successes: uint(3),
|
||||
Warnings: uint(0),
|
||||
Dangers: uint(1),
|
||||
}
|
||||
|
||||
expectedResults := ResultSet{
|
||||
"hostPIDSet": {ID: "hostPIDSet", Message: "Host PID should not be configured", Success: false, Severity: "danger", Category: "Security"},
|
||||
"hostIPCSet": {ID: "hostIPCSet", Message: "Host IPC is not configured", Success: true, Severity: "danger", Category: "Security"},
|
||||
"hostNetworkSet": {ID: "hostNetworkSet", Message: "Host network is not configured", Success: true, Severity: "warning", Category: "Networking"},
|
||||
}
|
||||
|
||||
actualPodResult, err := ValidatePod(&c, workload)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, 1, len(actualPodResult.ContainerResults), "should be equal")
|
||||
assert.EqualValues(t, expectedSum, actualPodResult.GetSummary())
|
||||
assert.EqualValues(t, expectedResults, actualPodResult.Results)
|
||||
}
|
||||
|
||||
func TestExemption(t *testing.T) {
|
||||
c := conf.Configuration{
|
||||
Checks: map[string]conf.Severity{
|
||||
"hostIPCSet": conf.SeverityDanger,
|
||||
"hostNetworkSet": conf.SeverityWarning,
|
||||
"hostPIDSet": conf.SeverityDanger,
|
||||
"hostPortSet": conf.SeverityDanger,
|
||||
},
|
||||
Exemptions: []conf.Exemption{
|
||||
conf.Exemption{
|
||||
Rules: []string{"hostIPCSet"},
|
||||
ControllerNames: []string{"foo"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
k8s, _ := test.SetupTestAPI()
|
||||
k8s = test.SetupAddControllers(k8s, "test")
|
||||
p := test.MockPod()
|
||||
p.Spec.HostIPC = true
|
||||
p.ObjectMeta = metav1.ObjectMeta{
|
||||
Name: "foo",
|
||||
}
|
||||
workload, err := kube.NewGenericWorkloadFromPod(p, nil)
|
||||
assert.NoError(t, err)
|
||||
expectedSum := CountSummary{
|
||||
Successes: uint(3),
|
||||
Warnings: uint(0),
|
||||
Dangers: uint(0),
|
||||
}
|
||||
expectedResults := ResultSet{
|
||||
"hostNetworkSet": {ID: "hostNetworkSet", Message: "Host network is not configured", Success: true, Severity: "warning", Category: "Networking"},
|
||||
"hostPIDSet": {ID: "hostPIDSet", Message: "Host PID is not configured", Success: true, Severity: "danger", Category: "Security"},
|
||||
}
|
||||
|
||||
actualPodResult, err := ValidatePod(&c, workload)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, 1, len(actualPodResult.ContainerResults), "should be equal")
|
||||
assert.EqualValues(t, expectedSum, actualPodResult.GetSummary())
|
||||
assert.EqualValues(t, expectedResults, actualPodResult.Results)
|
||||
}
|
||||
|
||||
@@ -1,106 +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/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// ResourceValidation contains methods shared by PodValidation and ContainerValidation
|
||||
type ResourceValidation struct {
|
||||
Errors []*ResultMessage
|
||||
Warnings []*ResultMessage
|
||||
Successes []*ResultMessage
|
||||
}
|
||||
|
||||
func (rv *ResourceValidation) messages() []*ResultMessage {
|
||||
messages := []*ResultMessage{}
|
||||
messages = append(messages, rv.Errors...)
|
||||
messages = append(messages, rv.Warnings...)
|
||||
messages = append(messages, rv.Successes...)
|
||||
return messages
|
||||
}
|
||||
|
||||
func (rv *ResourceValidation) summary() *ResultSummary {
|
||||
counts := CountSummary{
|
||||
Errors: uint(len(rv.Errors)),
|
||||
Warnings: uint(len(rv.Warnings)),
|
||||
Successes: uint(len(rv.Successes)),
|
||||
}
|
||||
byCategory := CategorySummary{}
|
||||
for _, msg := range rv.messages() {
|
||||
if _, ok := byCategory[msg.Category]; !ok {
|
||||
byCategory[msg.Category] = &CountSummary{}
|
||||
}
|
||||
if msg.Type == MessageTypeError {
|
||||
byCategory[msg.Category].Errors++
|
||||
} else if msg.Type == MessageTypeWarning {
|
||||
byCategory[msg.Category].Warnings++
|
||||
} else if msg.Type == MessageTypeSuccess {
|
||||
byCategory[msg.Category].Successes++
|
||||
}
|
||||
}
|
||||
return &ResultSummary{
|
||||
Totals: counts,
|
||||
ByCategory: byCategory,
|
||||
}
|
||||
}
|
||||
|
||||
func (rv *ResourceValidation) addMessage(message ResultMessage) {
|
||||
if message.Type == MessageTypeError {
|
||||
rv.Errors = append(rv.Errors, &message)
|
||||
} else if message.Type == MessageTypeWarning {
|
||||
rv.Warnings = append(rv.Warnings, &message)
|
||||
} else if message.Type == MessageTypeSuccess {
|
||||
rv.Successes = append(rv.Successes, &message)
|
||||
} else {
|
||||
panic("Bad message type")
|
||||
}
|
||||
}
|
||||
|
||||
func (rv *ResourceValidation) addFailure(message string, severity conf.Severity, category string) {
|
||||
if severity == conf.SeverityError {
|
||||
rv.addError(message, category)
|
||||
} else if severity == conf.SeverityWarning {
|
||||
rv.addWarning(message, category)
|
||||
} else {
|
||||
logrus.Errorf("Invalid severity: %s", severity)
|
||||
}
|
||||
}
|
||||
|
||||
func (rv *ResourceValidation) addError(message string, category string) {
|
||||
rv.Errors = append(rv.Errors, &ResultMessage{
|
||||
Message: message,
|
||||
Type: MessageTypeError,
|
||||
Category: category,
|
||||
})
|
||||
}
|
||||
|
||||
func (rv *ResourceValidation) addWarning(message string, category string) {
|
||||
rv.Warnings = append(rv.Warnings, &ResultMessage{
|
||||
Message: message,
|
||||
Type: MessageTypeWarning,
|
||||
Category: category,
|
||||
})
|
||||
}
|
||||
|
||||
func (rv *ResourceValidation) addSuccess(message string, category string) {
|
||||
rv.Successes = append(rv.Successes, &ResultMessage{
|
||||
Message: message,
|
||||
Type: MessageTypeSuccess,
|
||||
Category: category,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
packr "github.com/gobuffalo/packr/v2"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/util/yaml"
|
||||
|
||||
"github.com/fairwindsops/polaris/pkg/config"
|
||||
"github.com/fairwindsops/polaris/pkg/kube"
|
||||
)
|
||||
|
||||
var (
|
||||
schemaBox = (*packr.Box)(nil)
|
||||
builtInChecks = map[string]config.SchemaCheck{}
|
||||
// We explicitly set the order to avoid thrash in the
|
||||
// tests as we migrate toward JSON schema
|
||||
checkOrder = []string{
|
||||
// Controller Checks
|
||||
"multipleReplicasForDeployment",
|
||||
// Pod checks
|
||||
"hostIPCSet",
|
||||
"hostPIDSet",
|
||||
"hostNetworkSet",
|
||||
// Container checks
|
||||
"memoryLimitsMissing",
|
||||
"memoryRequestsMissing",
|
||||
"cpuLimitsMissing",
|
||||
"cpuRequestsMissing",
|
||||
"readinessProbeMissing",
|
||||
"livenessProbeMissing",
|
||||
"pullPolicyNotAlways",
|
||||
"tagNotSpecified",
|
||||
"hostPortSet",
|
||||
"runAsRootAllowed",
|
||||
"runAsPrivileged",
|
||||
"notReadOnlyRootFilesystem",
|
||||
"privilegeEscalationAllowed",
|
||||
"dangerousCapabilities",
|
||||
"insecureCapabilities",
|
||||
"priorityClassNotSet",
|
||||
}
|
||||
)
|
||||
|
||||
func init() {
|
||||
schemaBox = packr.New("Schemas", "../../checks")
|
||||
for _, checkID := range checkOrder {
|
||||
contents, err := schemaBox.Find(checkID + ".yaml")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
check, err := parseCheck(contents)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
check.ID = checkID
|
||||
builtInChecks[checkID] = check
|
||||
}
|
||||
}
|
||||
|
||||
func parseCheck(rawBytes []byte) (config.SchemaCheck, error) {
|
||||
reader := bytes.NewReader(rawBytes)
|
||||
check := config.SchemaCheck{}
|
||||
d := yaml.NewYAMLOrJSONDecoder(reader, 4096)
|
||||
for {
|
||||
if err := d.Decode(&check); err != nil {
|
||||
if err == io.EOF {
|
||||
return check, nil
|
||||
}
|
||||
return check, fmt.Errorf("Decoding schema check failed: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func resolveCheck(conf *config.Configuration, checkID string, controller kube.GenericWorkload, target config.TargetKind, isInitContainer bool) (*config.SchemaCheck, error) {
|
||||
check, ok := conf.CustomChecks[checkID]
|
||||
if !ok {
|
||||
check, ok = builtInChecks[checkID]
|
||||
}
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Check %s not found", checkID)
|
||||
}
|
||||
if !conf.IsActionable(check.ID, controller.ObjectMeta.GetName()) {
|
||||
return nil, nil
|
||||
}
|
||||
if !check.IsActionable(target, controller.Kind, isInitContainer) {
|
||||
return nil, nil
|
||||
}
|
||||
return &check, nil
|
||||
}
|
||||
|
||||
func makeResult(conf *config.Configuration, check *config.SchemaCheck, passes bool) ResultMessage {
|
||||
result := ResultMessage{
|
||||
ID: check.ID,
|
||||
Severity: conf.Checks[check.ID],
|
||||
Category: check.Category,
|
||||
Success: passes,
|
||||
}
|
||||
if passes {
|
||||
result.Message = check.SuccessMessage
|
||||
} else {
|
||||
result.Message = check.FailureMessage
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func getExemptKey(checkID string) string {
|
||||
return fmt.Sprintf("polaris.fairwinds.com/%s-exempt", checkID)
|
||||
}
|
||||
|
||||
func applyPodSchemaChecks(conf *config.Configuration, controller kube.GenericWorkload) (ResultSet, error) {
|
||||
results := ResultSet{}
|
||||
checkIDs := getSortedKeys(conf.Checks)
|
||||
objectAnnotations := controller.ObjectMeta.GetAnnotations()
|
||||
for _, checkID := range checkIDs {
|
||||
exemptValue := objectAnnotations[getExemptKey(checkID)]
|
||||
if strings.ToLower(exemptValue) == "true" {
|
||||
continue
|
||||
}
|
||||
check, err := resolveCheck(conf, checkID, controller, config.TargetPod, false)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if check == nil {
|
||||
continue
|
||||
}
|
||||
passes, err := check.CheckPod(&controller.PodSpec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
results[check.ID] = makeResult(conf, check, passes)
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func applyControllerSchemaChecks(conf *config.Configuration, controller kube.GenericWorkload) (ResultSet, error) {
|
||||
results := ResultSet{}
|
||||
checkIDs := getSortedKeys(conf.Checks)
|
||||
objectAnnotations := controller.ObjectMeta.GetAnnotations()
|
||||
for _, checkID := range checkIDs {
|
||||
exemptValue := objectAnnotations[getExemptKey(checkID)]
|
||||
if strings.ToLower(exemptValue) == "true" {
|
||||
continue
|
||||
}
|
||||
check, err := resolveCheck(conf, checkID, controller, config.TargetController, false)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if check == nil {
|
||||
continue
|
||||
}
|
||||
passes, err := check.CheckController(controller.OriginalObjectJSON)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
results[check.ID] = makeResult(conf, check, passes)
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func applyContainerSchemaChecks(conf *config.Configuration, controller kube.GenericWorkload, container *corev1.Container, isInit bool) (ResultSet, error) {
|
||||
results := ResultSet{}
|
||||
checkIDs := getSortedKeys(conf.Checks)
|
||||
objectAnnotations := controller.ObjectMeta.GetAnnotations()
|
||||
for _, checkID := range checkIDs {
|
||||
exemptValue := objectAnnotations[getExemptKey(checkID)]
|
||||
if strings.ToLower(exemptValue) == "true" {
|
||||
continue
|
||||
}
|
||||
check, err := resolveCheck(conf, checkID, controller, config.TargetContainer, isInit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if check == nil {
|
||||
continue
|
||||
}
|
||||
var passes bool
|
||||
if check.SchemaTarget == config.TargetPod {
|
||||
podCopy := controller.PodSpec
|
||||
podCopy.InitContainers = []corev1.Container{}
|
||||
podCopy.Containers = []corev1.Container{*container}
|
||||
passes, err = check.CheckPod(&podCopy)
|
||||
} else {
|
||||
passes, err = check.CheckContainer(container)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
results[check.ID] = makeResult(conf, check, passes)
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func getSortedKeys(m map[string]config.Severity) []string {
|
||||
keys := make([]string, 0, len(m))
|
||||
for key := range m {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
return keys
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user