Compare commits

...
Author SHA1 Message Date
Bader Boland 567e16d056 Fix compile errors 2020-10-23 08:30:00 -04:00
Bader Boland f6e6f7ce2f Start work on using controller-utils 2020-10-23 08:24:48 -04:00
dependabot-preview[bot]andlnx01 9cce9fcec7 Bump github.com/sirupsen/logrus from 1.6.0 to 1.7.0 (#420)
Bumps [github.com/sirupsen/logrus](https://github.com/sirupsen/logrus) from 1.6.0 to 1.7.0.
- [Release notes](https://github.com/sirupsen/logrus/releases)
- [Changelog](https://github.com/sirupsen/logrus/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sirupsen/logrus/compare/v1.6.0...v1.7.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-10-05 15:05:22 -04:00
Robert Brennan f05d3caa0b Update images.md (#423) 2020-10-05 13:07:34 -04:00
dependabot-preview[bot]andlnx01 3c1533790f Bump sigs.k8s.io/controller-runtime from 0.6.2 to 0.6.3 (#419)
Bumps [sigs.k8s.io/controller-runtime](https://github.com/kubernetes-sigs/controller-runtime) from 0.6.2 to 0.6.3.
- [Release notes](https://github.com/kubernetes-sigs/controller-runtime/releases)
- [Commits](https://github.com/kubernetes-sigs/controller-runtime/compare/v0.6.2...v0.6.3)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-09-21 10:33:07 -04:00
boostrack 20746518bc "install" option as docker container (#410)
* Update usage.md

option for running as docker container locally

* Update usage.md
2020-09-21 10:09:09 -04:00
dependabot-preview[bot]andlnx01 75808b1dc1 Bump sigs.k8s.io/controller-runtime from 0.6.1 to 0.6.2 (#411)
Bumps [sigs.k8s.io/controller-runtime](https://github.com/kubernetes-sigs/controller-runtime) from 0.6.1 to 0.6.2.
- [Release notes](https://github.com/kubernetes-sigs/controller-runtime/releases)
- [Commits](https://github.com/kubernetes-sigs/controller-runtime/compare/v0.6.1...v0.6.2)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-09-18 08:32:24 -04:00
baderbuddy 7c9f01639b Update dependencies (#400)
* Start working on updating dependencies:

* Fix webhook

* Rollback jsonschema update

* Checkin new config

* Fix run as root

* Update versions of kind

* Fix typo in kind URL

* Fix kind config

* Add csr permissions

* Fix weird image thing

* Fixed certificates

* Add to logging

* Approve cert manually

* Fix approval

* Add cert script

* Fix deployment

* Add requests/limits

* Wait if certificate doesn't exist yet

* Add check for file size

* Add variable

* Try a different imagE

* Fix command

* Update certificate logic

* Add healthz

* Don't check cert size

* Remove stat

* Fix vet

* Put in change that makes no sense

* Fix cert names

* Roll back

* Try changing config

* Add logging for each request

* Cleanup code some

* Remove bad deployments

* Fix client injection

* Update timeout

* Add logging

* Fixed e2e webhook tests

* Add permissions for approval

* Fix permissions for CSR

* Remove logging code

* Remove refresh certs file

* Fix merge issues

* Update deployments

* Try beta of admission controller config

* Target 1.15 for testing

* Add beta versions of resourceS

* Lower webhook timeout

* Refactor out a method

* Fix up PR issues

* Fix more tabs

* Remove unnecessary messageS

* Fix go.sum

* Fix go.sum
2020-09-11 08:53:14 -04:00
dependabot-preview[bot]andlnx01 6cb9999864 Bump go.uber.org/zap from 1.15.0 to 1.16.0 (#406)
Bumps [go.uber.org/zap](https://github.com/uber-go/zap) from 1.15.0 to 1.16.0.
- [Release notes](https://github.com/uber-go/zap/releases)
- [Changelog](https://github.com/uber-go/zap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/uber-go/zap/compare/v1.15.0...v1.16.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-09-11 08:28:56 -04:00
dependabot-preview[bot]andlnx01 a621249787 Bump github.com/rogpeppe/go-internal from 1.6.1 to 1.6.2 (#405)
Bumps [github.com/rogpeppe/go-internal](https://github.com/rogpeppe/go-internal) from 1.6.1 to 1.6.2.
- [Release notes](https://github.com/rogpeppe/go-internal/releases)
- [Commits](https://github.com/rogpeppe/go-internal/compare/v1.6.1...v1.6.2)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-09-11 08:28:25 -04:00
dependabot-preview[bot]andlnx01 679f135b62 Bump cloud.google.com/go from 0.63.0 to 0.65.0 (#402)
Bumps [cloud.google.com/go](https://github.com/googleapis/google-cloud-go) from 0.63.0 to 0.65.0.
- [Release notes](https://github.com/googleapis/google-cloud-go/releases)
- [Changelog](https://github.com/googleapis/google-cloud-go/blob/master/CHANGES.md)
- [Commits](https://github.com/googleapis/google-cloud-go/compare/v0.63.0...v0.65.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-09-11 08:27:54 -04:00
dependabot-preview[bot]andlnx01 5c456ddb86 Bump github.com/evanphx/json-patch (#399)
Bumps [github.com/evanphx/json-patch](https://github.com/evanphx/json-patch) from 4.5.0+incompatible to 4.9.0+incompatible.
- [Release notes](https://github.com/evanphx/json-patch/releases)
- [Commits](https://github.com/evanphx/json-patch/compare/v4.5.0...v4.9.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-09-01 11:47:41 -04:00
dependabot-preview[bot]andlnx01 c1c07bcea2 Bump google.golang.org/grpc from 1.31.0 to 1.31.1 (#401)
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.31.0 to 1.31.1.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.31.0...v1.31.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-09-01 11:47:12 -04:00
dependabot-preview[bot]andlnx01 c703aa4039 Bump github.com/prometheus/common from 0.12.0 to 0.13.0 (#398)
Bumps [github.com/prometheus/common](https://github.com/prometheus/common) from 0.12.0 to 0.13.0.
- [Release notes](https://github.com/prometheus/common/releases)
- [Commits](https://github.com/prometheus/common/compare/v0.12.0...v0.13.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-09-01 11:46:59 -04:00
dependabot-preview[bot]andlnx01 1051bc5922 Bump github.com/gorilla/mux from 1.7.4 to 1.8.0 (#396)
Bumps [github.com/gorilla/mux](https://github.com/gorilla/mux) from 1.7.4 to 1.8.0.
- [Release notes](https://github.com/gorilla/mux/releases)
- [Commits](https://github.com/gorilla/mux/compare/v1.7.4...v1.8.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-09-01 11:46:38 -04:00
dependabot-preview[bot]andlnx01 155299a8d5 Bump github.com/karrick/godirwalk from 1.15.6 to 1.16.1 (#393)
Bumps [github.com/karrick/godirwalk](https://github.com/karrick/godirwalk) from 1.15.6 to 1.16.1.
- [Release notes](https://github.com/karrick/godirwalk/releases)
- [Changelog](https://github.com/karrick/godirwalk/blob/master/debug_release.go)
- [Commits](https://github.com/karrick/godirwalk/compare/v1.15.6...v1.16.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-08-19 14:01:21 -04:00
dependabot-preview[bot]andlnx01 f0b63e3cb6 Bump github.com/imdario/mergo from 0.3.10 to 0.3.11 (#392)
Bumps [github.com/imdario/mergo](https://github.com/imdario/mergo) from 0.3.10 to 0.3.11.
- [Release notes](https://github.com/imdario/mergo/releases)
- [Commits](https://github.com/imdario/mergo/compare/v0.3.10...v0.3.11)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-08-19 14:01:09 -04:00
dependabot-preview[bot]andlnx01 479ab1dae6 Bump github.com/prometheus/common from 0.11.1 to 0.12.0 (#394)
Bumps [github.com/prometheus/common](https://github.com/prometheus/common) from 0.11.1 to 0.12.0.
- [Release notes](https://github.com/prometheus/common/releases)
- [Commits](https://github.com/prometheus/common/compare/v0.11.1...v0.12.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-08-19 14:00:56 -04:00
dependabot-preview[bot]andlnx01 b5058cd906 Bump github.com/grpc-ecosystem/grpc-gateway from 1.14.6 to 1.14.7 (#395)
Bumps [github.com/grpc-ecosystem/grpc-gateway](https://github.com/grpc-ecosystem/grpc-gateway) from 1.14.6 to 1.14.7.
- [Release notes](https://github.com/grpc-ecosystem/grpc-gateway/releases)
- [Changelog](https://github.com/grpc-ecosystem/grpc-gateway/blob/master/CHANGELOG.md)
- [Commits](https://github.com/grpc-ecosystem/grpc-gateway/compare/v1.14.6...v1.14.7)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-08-19 14:00:42 -04:00
dependabot-preview[bot]andlnx01 c1115e556b Bump github.com/pborman/uuid from 1.2.0 to 1.2.1 (#391)
Bumps [github.com/pborman/uuid](https://github.com/pborman/uuid) from 1.2.0 to 1.2.1.
- [Release notes](https://github.com/pborman/uuid/releases)
- [Commits](https://github.com/pborman/uuid/compare/v1.2...v1.2.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-08-19 14:00:28 -04:00
dependabot-preview[bot]andlnx01 617d3d28b5 Bump cloud.google.com/go from 0.61.0 to 0.63.0 (#389)
Bumps [cloud.google.com/go](https://github.com/googleapis/google-cloud-go) from 0.61.0 to 0.63.0.
- [Release notes](https://github.com/googleapis/google-cloud-go/releases)
- [Changelog](https://github.com/googleapis/google-cloud-go/blob/master/CHANGES.md)
- [Commits](https://github.com/googleapis/google-cloud-go/compare/v0.61.0...v0.63.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-08-13 15:35:35 -04:00
dependabot-preview[bot]andlnx01 582a631014 Bump github.com/prometheus/common from 0.10.0 to 0.11.1 (#387)
Bumps [github.com/prometheus/common](https://github.com/prometheus/common) from 0.10.0 to 0.11.1.
- [Release notes](https://github.com/prometheus/common/releases)
- [Commits](https://github.com/prometheus/common/compare/v0.10.0...v0.11.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-08-11 14:02:17 -04:00
dependabot-preview[bot]andlnx01 f4c2ae451f Bump github.com/google/gofuzz from 1.1.0 to 1.2.0 (#388)
Bumps [github.com/google/gofuzz](https://github.com/google/gofuzz) from 1.1.0 to 1.2.0.
- [Release notes](https://github.com/google/gofuzz/releases)
- [Commits](https://github.com/google/gofuzz/compare/v1.1.0...v1.2.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-08-11 14:01:45 -04:00
dependabot-preview[bot]andlnx01 b76b2c3df4 Bump github.com/rogpeppe/go-internal from 1.6.0 to 1.6.1 (#384)
Bumps [github.com/rogpeppe/go-internal](https://github.com/rogpeppe/go-internal) from 1.6.0 to 1.6.1.
- [Release notes](https://github.com/rogpeppe/go-internal/releases)
- [Commits](https://github.com/rogpeppe/go-internal/compare/v1.6.0...v1.6.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-08-11 14:01:35 -04:00
dependabot-preview[bot]andlnx01 0bbf43146c Bump google.golang.org/grpc from 1.30.0 to 1.31.0 (#383)
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.30.0 to 1.31.0.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.30.0...v1.31.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-08-11 14:01:23 -04:00
dirien c33452bba0 Update footer.gohtml (#386)
* Update footer.gohtml

update the date to 2020 in the footer of the dashboard

* Update main.go

* Update CHANGELOG.md
2020-08-09 11:53:02 -04:00
dependabot-preview[bot]andlnx01 300c8d9d94 Bump github.com/census-instrumentation/opencensus-proto (#381)
Bumps [github.com/census-instrumentation/opencensus-proto](https://github.com/census-instrumentation/opencensus-proto) from 0.2.1 to 0.3.0.
- [Release notes](https://github.com/census-instrumentation/opencensus-proto/releases)
- [Commits](https://github.com/census-instrumentation/opencensus-proto/compare/v0.2.1...v0.3.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-07-30 14:46:39 -04:00
Robert Brennan 7b1f046586 update version (#380)
* update version

* update changelog
2020-07-22 14:01:49 -04:00
Robert Brennan 4e290e192d Update config.yaml (#378)
* Update config.yaml

* Update config-full.yaml

* update docs
2020-07-22 13:48:30 -04:00
Robert Brennan 8d562f2490 fix for parent tree climbing (#379) 2020-07-22 13:31:49 -04:00
Robert Brennan 47150155a0 add ability to audit a single workload (#368)
* add ability to audit a single workload

* delint

* fix help text

* update readme
2020-07-22 13:28:02 -04:00
dependabot-preview[bot]andlnx01 b3d323d742 Bump github.com/imdario/mergo from 0.3.9 to 0.3.10 (#376)
Bumps [github.com/imdario/mergo](https://github.com/imdario/mergo) from 0.3.9 to 0.3.10.
- [Release notes](https://github.com/imdario/mergo/releases)
- [Commits](https://github.com/imdario/mergo/compare/v0.3.9...v0.3.10)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-07-21 10:37:11 -04:00
dependabot-preview[bot]andlnx01 6060cf243f Bump cloud.google.com/go from 0.60.0 to 0.61.0 (#375)
Bumps [cloud.google.com/go](https://github.com/googleapis/google-cloud-go) from 0.60.0 to 0.61.0.
- [Release notes](https://github.com/googleapis/google-cloud-go/releases)
- [Changelog](https://github.com/googleapis/google-cloud-go/blob/master/CHANGES.md)
- [Commits](https://github.com/googleapis/google-cloud-go/compare/v0.60.0...v0.61.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-07-21 10:36:56 -04:00
Justin BurrandJustin Burr e044a540ed Fix CLI flags documentation. (#374)
Co-authored-by: Justin Burr <juburr@users.noreply.github.com>
2020-07-17 10:46:37 -04:00
dependabot-preview[bot]andlnx01 f6a4bffe88 Bump google.golang.org/api from 0.28.0 to 0.29.0 (#373)
Bumps [google.golang.org/api](https://github.com/googleapis/google-api-go-client) from 0.28.0 to 0.29.0.
- [Release notes](https://github.com/googleapis/google-api-go-client/releases)
- [Changelog](https://github.com/googleapis/google-api-go-client/blob/master/CHANGES.md)
- [Commits](https://github.com/googleapis/google-api-go-client/compare/v0.28.0...v0.29.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-07-15 15:42:54 -04:00
Robert Brennan 343e24886d bump to 1.1.1 (#372) 2020-07-10 10:03:12 -04:00
Robert Brennan 997d4bfb2b Handle orphaned pods (#371)
* handle orphaned pods

* set originalResource when creating Pod resources
2020-07-10 09:43:33 -04:00
dependabot-preview[bot]andlnx01 9ded3269c6 Bump cloud.google.com/go from 0.59.0 to 0.60.0 (#369)
Bumps [cloud.google.com/go](https://github.com/googleapis/google-cloud-go) from 0.59.0 to 0.60.0.
- [Release notes](https://github.com/googleapis/google-cloud-go/releases)
- [Changelog](https://github.com/googleapis/google-cloud-go/blob/master/CHANGES.md)
- [Commits](https://github.com/googleapis/google-cloud-go/compare/v0.59.0...v0.60.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-07-09 15:24:00 -04:00
dependabot-preview[bot]andlnx01 ca65a446bd Bump cloud.google.com/go from 0.58.0 to 0.59.0 (#366)
Bumps [cloud.google.com/go](https://github.com/googleapis/google-cloud-go) from 0.58.0 to 0.59.0.
- [Release notes](https://github.com/googleapis/google-cloud-go/releases)
- [Changelog](https://github.com/googleapis/google-cloud-go/blob/master/CHANGES.md)
- [Commits](https://github.com/googleapis/google-cloud-go/compare/v0.58.0...v0.59.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-06-29 14:00:39 -04:00
Samuel Barabas 4e9aa21cea Show controller check results on dashboard (#364)
As I mentioned in https://github.com/FairwindsOps/polaris/issues/351,
custom check results for `target: Controller` have not been shown on the
Dashboard.
2020-06-29 13:48:02 -04:00
dependabot-preview[bot]andlnx01 ed014ecf8e Bump github.com/prometheus/client_golang from 1.7.0 to 1.7.1 (#365)
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.7.0 to 1.7.1.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/master/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.7.0...v1.7.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-06-29 13:46:39 -04:00
dependabot-preview[bot]andlnx01 2e11c04664 Bump google.golang.org/grpc from 1.29.1 to 1.30.0 (#367)
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.29.1 to 1.30.0.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.29.1...v1.30.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-06-29 13:46:06 -04:00
Robert Brennan 80662bd203 update to version 1.1.0 (#362) 2020-06-23 08:50:28 -04:00
Robert Brennan 5705f81911 throw error when severity isn't set for custom check (#360)
* throw error when severity isn't set for custom check

* fix syntax
2020-06-22 16:53:15 -04:00
Robert Brennan b4e3d40f4b Add priority class check, some test infra (#342)
* add check for priority-class

* add test message

* lint
2020-06-22 16:34:48 -04:00
dependabot-preview[bot]andlnx01 cea0ca7fa8 Bump golang.org/x/text from 0.3.2 to 0.3.3 (#355)
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.3.2 to 0.3.3.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.3.2...v0.3.3)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-06-22 16:08:17 -04:00
Robert Brennan 69bed16e4f don't error out when resolving parent resource (#358) 2020-06-22 16:05:23 -04:00
Huang Huang 5b173cf4a7 Support read yaml contents of workload from stdin (#353) 2020-06-22 14:19:51 -04:00
hgoscenski-vail 0a0720a26c Adds option to exempt an entire controller from checks via config file (#350)
This adds the ability to exempt a controller from all checks similar to
the annotation for "exempt" which exempts all checks.

I added the tests to go with this as well as for the IsActionable
function.
2020-06-22 14:18:23 -04:00
Robert Brennan fa3504c350 add custom checks to config (#340) 2020-06-22 14:16:15 -04:00
dependabot-preview[bot]andlnx01 4020000d68 Bump go.opencensus.io from 0.22.3 to 0.22.4 (#354)
Bumps [go.opencensus.io](https://github.com/census-instrumentation/opencensus-go) from 0.22.3 to 0.22.4.
- [Release notes](https://github.com/census-instrumentation/opencensus-go/releases)
- [Commits](https://github.com/census-instrumentation/opencensus-go/compare/v0.22.3...v0.22.4)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-06-22 14:15:56 -04:00
dependabot-preview[bot]andlnx01 ab7eeccfb0 Bump github.com/prometheus/client_golang from 1.6.0 to 1.7.0 (#356)
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.6.0 to 1.7.0.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/master/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.6.0...v1.7.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-06-22 14:15:33 -04:00
dependabot-preview[bot]andlnx01 78a16fa0ea Bump google.golang.org/api from 0.26.0 to 0.28.0 (#357)
Bumps [google.golang.org/api](https://github.com/googleapis/google-api-go-client) from 0.26.0 to 0.28.0.
- [Release notes](https://github.com/googleapis/google-api-go-client/releases)
- [Changelog](https://github.com/googleapis/google-api-go-client/blob/master/CHANGES.md)
- [Commits](https://github.com/googleapis/google-api-go-client/compare/v0.26.0...v0.28.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-06-22 14:15:23 -04:00
Huang Huang b26c4be8d6 Support audit files which use \r or \r\n as newline character (#343) 2020-06-16 10:46:28 -04:00
dependabot-preview[bot]andlnx01 aada33f0e9 Bump cloud.google.com/go from 0.56.0 to 0.58.0 (#347)
Bumps [cloud.google.com/go](https://github.com/googleapis/google-cloud-go) from 0.56.0 to 0.58.0.
- [Release notes](https://github.com/googleapis/google-cloud-go/releases)
- [Changelog](https://github.com/googleapis/google-cloud-go/blob/master/CHANGES.md)
- [Commits](https://github.com/googleapis/google-cloud-go/compare/v0.56.0...v0.58.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-06-15 10:39:10 -04:00
dependabot-preview[bot]andlnx01 7b88c48a6e Bump github.com/prometheus/procfs from 0.1.0 to 0.1.3 (#345)
Bumps [github.com/prometheus/procfs](https://github.com/prometheus/procfs) from 0.1.0 to 0.1.3.
- [Release notes](https://github.com/prometheus/procfs/releases)
- [Commits](https://github.com/prometheus/procfs/compare/v0.1.0...v0.1.3)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-06-15 10:28:24 -04:00
dependabot-preview[bot]andlnx01 ded260ee19 Bump github.com/json-iterator/go from 1.1.9 to 1.1.10 (#346)
Bumps [github.com/json-iterator/go](https://github.com/json-iterator/go) from 1.1.9 to 1.1.10.
- [Release notes](https://github.com/json-iterator/go/releases)
- [Commits](https://github.com/json-iterator/go/compare/v1.1.9...v1.1.10)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-06-15 10:17:07 -04:00
Robert Brennan 3e9c270ac7 fix zero-state (#341)
* fix zero-state

* fix lint error

* fix denom checks
2020-06-10 16:53:40 -04:00
baderbuddy bf6e41566d Don't assume all objects have pods. (#329)
* Don't assume all objects have pods.

* Test service is ignored

* Fix test

* Fix comment on test
2020-06-10 10:22:59 -04:00
dependabot-preview[bot]andlnx01 ededabf00f Bump contrib.go.opencensus.io/exporter/ocagent from 0.4.12 to 0.7.0 (#335)
Bumps [contrib.go.opencensus.io/exporter/ocagent](https://github.com/census-ecosystem/opencensus-go-exporter-ocagent) from 0.4.12 to 0.7.0.
- [Release notes](https://github.com/census-ecosystem/opencensus-go-exporter-ocagent/releases)
- [Commits](https://github.com/census-ecosystem/opencensus-go-exporter-ocagent/compare/v0.4.12...v0.7.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-06-08 07:55:38 -04:00
dependabot-preview[bot]andlnx01 3df9d35c57 Bump google.golang.org/api from 0.25.0 to 0.26.0 (#334)
Bumps [google.golang.org/api](https://github.com/googleapis/google-api-go-client) from 0.25.0 to 0.26.0.
- [Release notes](https://github.com/googleapis/google-api-go-client/releases)
- [Changelog](https://github.com/googleapis/google-api-go-client/blob/master/CHANGES.md)
- [Commits](https://github.com/googleapis/google-api-go-client/compare/v0.25.0...v0.26.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-06-08 07:55:26 -04:00
dependabot-preview[bot]andlnx01 a90c3b0f12 Bump github.com/stretchr/testify from 1.5.1 to 1.6.1 (#333)
Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.5.1 to 1.6.1.
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/compare/v1.5.1...v1.6.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-06-08 07:55:15 -04:00
dependabot-preview[bot]andlnx01 d19b0769b4 Bump gopkg.in/yaml.v2 from 2.2.8 to 2.3.0 (#332)
Bumps [gopkg.in/yaml.v2](https://github.com/go-yaml/yaml) from 2.2.8 to 2.3.0.
- [Release notes](https://github.com/go-yaml/yaml/releases)
- [Commits](https://github.com/go-yaml/yaml/compare/v2.2.8...v2.3.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-06-08 07:55:06 -04:00
dependabot-preview[bot]andlnx01 97f25cd7d1 Bump github.com/prometheus/procfs from 0.0.11 to 0.1.0 (#331)
Bumps [github.com/prometheus/procfs](https://github.com/prometheus/procfs) from 0.0.11 to 0.1.0.
- [Release notes](https://github.com/prometheus/procfs/releases)
- [Commits](https://github.com/prometheus/procfs/compare/v0.0.11...v0.1.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-06-08 07:54:55 -04:00
Jordan Doig 03331147ca Merge pull request #320 from FairwindsOps/jd/search-filter
Filter by Namespace on Dashboard
2020-06-04 12:39:28 -06:00
jordandoig d44138bead Update filter section styles 2020-06-03 20:26:25 -06:00
jordandoig eb21b030c2 Add filters javascript 2020-06-03 20:19:18 -06:00
jordandoig 3061756a55 Filter by namespaces in query, style expandable filter section, check checkboxes on page load 2020-06-03 20:19:18 -06:00
jordandoig 2964280976 Add namespace filters section 2020-06-03 20:19:18 -06:00
Robert Brennan f657071a7f fix goreleaser (#326)
* fix goreleaser

* update rok8s
2020-06-03 17:26:37 -04:00
Robert Brennan 2cdc4775e0 bump to 1.0.3 (#325) 2020-06-03 16:56:24 -04:00
Robert Brennan af5b86e28c catch cache error (#324) 2020-06-03 15:55:13 -04:00
Robert Brennan 1021f6b1d7 fix basepath link (#319) 2020-06-03 15:42:06 -04:00
Robert Brennan 0111b7ec2b update documentation (#318) 2020-06-03 14:43:15 -04:00
Robert Brennan 33524757aa set minor version in polaris tag (#323) 2020-06-03 14:42:36 -04:00
dependabot-preview[bot]andlnx01 eea4a8e2c3 Bump google.golang.org/api from 0.5.0 to 0.25.0 (#317)
Bumps [google.golang.org/api](https://github.com/googleapis/google-api-go-client) from 0.5.0 to 0.25.0.
- [Release notes](https://github.com/googleapis/google-api-go-client/releases)
- [Changelog](https://github.com/googleapis/google-api-go-client/blob/master/CHANGES.md)
- [Commits](https://github.com/googleapis/google-api-go-client/compare/v0.5.0...v0.25.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-06-01 08:45:51 -04:00
dependabot-preview[bot]andlnx01 fb98d3112d Bump github.com/gobuffalo/mapi from 1.2.0 to 1.2.1 (#316)
Bumps [github.com/gobuffalo/mapi](https://github.com/gobuffalo/mapi) from 1.2.0 to 1.2.1.
- [Release notes](https://github.com/gobuffalo/mapi/releases)
- [Commits](https://github.com/gobuffalo/mapi/compare/v1.2.0...v1.2.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-06-01 08:45:42 -04:00
dependabot-preview[bot]andlnx01 97c81ccf60 Bump github.com/golang/protobuf from 1.4.0 to 1.4.2 (#315)
Bumps [github.com/golang/protobuf](https://github.com/golang/protobuf) from 1.4.0 to 1.4.2.
- [Release notes](https://github.com/golang/protobuf/releases)
- [Commits](https://github.com/golang/protobuf/compare/v1.4.0...v1.4.2)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-06-01 08:45:33 -04:00
dependabot-preview[bot]andlnx01 e9b79fbdd4 Bump k8s.io/klog from 0.3.0 to 0.4.0 (#314)
Bumps [k8s.io/klog](https://github.com/kubernetes/klog) from 0.3.0 to 0.4.0.
- [Release notes](https://github.com/kubernetes/klog/releases)
- [Changelog](https://github.com/kubernetes/klog/blob/master/RELEASE.md)
- [Commits](https://github.com/kubernetes/klog/compare/v0.3.0...v0.4.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-06-01 08:45:23 -04:00
dependabot-preview[bot]andlnx01 217e3563d6 Bump github.com/imdario/mergo from 0.3.7 to 0.3.9 (#312)
Bumps [github.com/imdario/mergo](https://github.com/imdario/mergo) from 0.3.7 to 0.3.9.
- [Release notes](https://github.com/imdario/mergo/releases)
- [Commits](https://github.com/imdario/mergo/compare/v0.3.7...v0.3.9)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-05-28 08:41:39 -04:00
dependabot-preview[bot]andlnx01 deb33bb05f Bump github.com/gogo/protobuf from 1.2.1 to 1.3.1 (#310)
Bumps [github.com/gogo/protobuf](https://github.com/gogo/protobuf) from 1.2.1 to 1.3.1.
- [Release notes](https://github.com/gogo/protobuf/releases)
- [Commits](https://github.com/gogo/protobuf/compare/v1.2.1...v1.3.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-05-28 08:41:10 -04:00
dependabot-preview[bot]andlnx01 e605eef755 Bump github.com/grpc-ecosystem/grpc-gateway from 1.9.0 to 1.14.6 (#306)
Bumps [github.com/grpc-ecosystem/grpc-gateway](https://github.com/grpc-ecosystem/grpc-gateway) from 1.9.0 to 1.14.6.
- [Release notes](https://github.com/grpc-ecosystem/grpc-gateway/releases)
- [Changelog](https://github.com/grpc-ecosystem/grpc-gateway/blob/master/CHANGELOG.md)
- [Commits](https://github.com/grpc-ecosystem/grpc-gateway/compare/v1.9.0...v1.14.6)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2020-05-28 08:40:59 -04:00
Robert Brennan c304036f97 Revert "go mod tidy"
This reverts commit 3eb2b6e2c9.
2020-05-21 11:14:48 +00:00
Robert Brennan c1061b50e0 Revert "fix go.sum"
This reverts commit 3b07496a4d.
2020-05-21 11:14:36 +00:00
Robert Brennan 3b07496a4d fix go.sum 2020-05-21 11:08:09 +00:00
Robert Brennan 3eb2b6e2c9 go mod tidy 2020-05-21 10:42:00 +00:00
Robert Brennan 5f5da069fd update deps 2020-05-21 10:38:58 +00:00
Robert Brennan ceeffb97a0 update to 1.0.2 2020-05-21 10:29:59 +00:00
Robert Brennan 27faab919c handle case where RBAC is missing for a CRD (#305)
* handle case where RBAC is missing for a CRD

* handle error
2020-05-21 06:27:15 -04:00
Robert Brennan f4fc7c1b5d add ARM support (#303) 2020-05-20 10:40:08 -04:00
Robert Brennan 54d796b1f3 update go.sum 2020-05-20 14:04:49 +00:00
Robert Brennan 9065e3b2ae fix dependencies (#302)
* fix dependencies

* revert packr2 change
2020-05-20 09:59:04 -04:00
Robert Brennan bd14ab8bc1 Release 1.0.0 (#300)
* Release 1.0.0

* update deploy files

* remove docs for old capabilities

* update images

* update image

* update docs

* remove capabilities language

* add CLI changes to changelog

* reorg changelog
2020-05-20 09:00:41 -04:00
Robert Brennan 2ac6a2b540 Change error to danger (#299)
* rename 'error' to 'danger'

* update dashboard

* fix docs

* update deploy configs
2020-05-19 08:41:07 -04:00
Robert Brennan 9d81c393de Pack config.yaml in Docker (#298)
* update examples

* pack config.yaml in Docker
2020-05-18 17:16:16 -04:00
Robert Brennan b557786325 Stop webhook from preventing scaling events (#293)
* test deployment scaling

* stop watching pods in the webhook

* fix check

* add pod check back

* skip webhook for owned pods
2020-05-18 15:17:21 -04:00
Robert Brennan 5572bb8118 fix up goreleaser (#297)
* fix up goreleaser

* revert version number change
2020-05-18 15:17:08 -04:00
Robert Brennan ac9412920f sort results on dashboard (#295)
This reverts commit 0c671d0f62.
2020-05-18 15:01:12 -04:00
baderbuddy d50d9c81f8 Add the capability for controller level checks (#285)
* Add controller level checks

* Add check for multipleReplicas

* Fixed spec

* Add controller level check

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

After

Width:  |  Height:  |  Size: 186 KiB

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

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