Compare commits

...
12 Commits
Author SHA1 Message Date
Robert Brennan ad197f34c2 Merge branch 'master' into rb/cos 2021-04-07 12:22:13 -04:00
Terraform User e802c28801 Managed by Terraform 2021-04-07 09:59:48 -06:00
Robert Brennan 68ca5a8d13 Update CODEOWNERS 2021-04-07 11:55:41 -04:00
Robert Brennan 14d503580b Update documentation from template (#521) 2021-04-05 11:47:39 -04:00
Terraform User 0fc109c199 Managed by Terraform 2021-03-30 12:20:28 -06:00
Jordan DoigandRobert Brennan 63fd576d3e Add support for arbitrary Kinds (#505)
* Add basic flow

* Add arbitrary validator

* Pipe config through to resource provider

* Set arbitraries on resource provider

* Add arbitrary validation to fullaudit

* Add conf argument

* Fix resource setting from string

* PR updates

* Fix nil map error

* Delete lingering print, add pdb check, start implementing validator test

* move ingress to arbitrary

* fix compile

* refactor a bunch

* add tls tests

* tests passing

* resource provider helper

* refactor tests

* fix exemptions

* fix check test

* fix up resource creation from API

* fix init containers

* fix cronjob test

* fix pod tests

* combine controllers and-noncontrollers in resource provider

* delint

* add ingress backward compat

* fix tests

* reenable test

* rename a fn

* remove unused fn

* remove if

Co-authored-by: Robert Brennan <contact@rbren.io>
2021-03-26 08:29:59 -04:00
Robert Brennan f0c8ee256e Update documentation from template (#518) 2021-03-17 13:55:36 -04:00
dependabot-preview[bot]andlnx01 d5cb68084e Bump github.com/fatih/color from 1.7.0 to 1.10.0 (#515)
Bumps [github.com/fatih/color](https://github.com/fatih/color) from 1.7.0 to 1.10.0.
- [Release notes](https://github.com/fatih/color/releases)
- [Commits](https://github.com/fatih/color/compare/v1.7.0...v1.10.0)

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

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2021-03-17 12:51:17 -04:00
dependabot-preview[bot]andlnx01 f7d2309608 Bump github.com/sirupsen/logrus from 1.8.0 to 1.8.1 (#514)
Bumps [github.com/sirupsen/logrus](https://github.com/sirupsen/logrus) from 1.8.0 to 1.8.1.
- [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.8.0...v1.8.1)

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

Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2021-03-17 12:51:10 -04:00
Robert Brennan d800e8d629 empty commit 2021-03-16 13:30:47 +00:00
Robert Brennan eebc9537a2 update for 3.2.0 (#513) 2021-03-12 13:12:16 -05:00
Robert Brennan 1fed099b53 Pretty printer for audit (#512)
* pretty output

* add colors

* better nocolor

* fix up main title

* delint

* remove double negative

* update docs
2021-03-12 12:20:00 -05:00
41 changed files with 898 additions and 810 deletions
+1 -1
View File
@@ -1 +1 @@
* @rbren @makoscafee @jordandoig @baderbuddy @shaswa
* @rbren @makoscafee @baderbuddy @shaswa
+1 -1
View File
@@ -3,7 +3,7 @@
<br>
<h3>Best Practices for Kubernetes Workload Configuration</h3>
<a href="https://github.com/FairwindsOps/polaris">
<img src="https://img.shields.io/static/v1.svg?label=Version&message=3.1.6&color=239922">
<img src="https://img.shields.io/static/v1.svg?label=Version&message=3.2.0&color=239922">
</a>
<a href="https://goreportcard.com/report/github.com/FairwindsOps/polaris">
<img src="https://goreportcard.com/badge/github.com/FairwindsOps/polaris">
@@ -0,0 +1,18 @@
successMessage: disruptionsAllowed is greater than zero
failureMessage: disruptionsAllowed is not greater than zero
category: Reliability
target: PodDisruptionBudget
schema:
'$schema': http://json-schema.org/draft-07/schema
type: object
required:
- status
properties:
status:
type: object
required:
- disruptionsAllowed
properties:
disruptionsAllowed:
type: integer
minimum: 1
+1 -1
View File
@@ -1,7 +1,7 @@
successMessage: Ingress has TLS configured
failureMessage: Ingress does not have TLS configured
category: Security
target: Ingress
target: networking.k8s.io/Ingress
schema:
'$schema': http://json-schema.org/draft-07/schema
type: object
+8 -4
View File
@@ -38,6 +38,7 @@ var auditOutputURL string
var auditOutputFile string
var auditOutputFormat string
var resourceToAudit string
var useColor bool
func init() {
rootCmd.AddCommand(auditCmd)
@@ -47,7 +48,8 @@ func init() {
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().StringVarP(&auditOutputFormat, "format", "f", "json", "Output format for results - json, yaml, pretty, or score.")
auditCmd.PersistentFlags().BoolVar(&useColor, "color", true, "Whether to use color in pretty format.")
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.")
}
@@ -61,7 +63,7 @@ var auditCmd = &cobra.Command{
config.DisplayName = displayName
}
auditData := runAndReportAudit(cmd.Context(), config, auditPath, resourceToAudit, auditOutputFile, auditOutputURL, auditOutputFormat)
auditData := runAndReportAudit(cmd.Context(), config, auditPath, resourceToAudit, auditOutputFile, auditOutputURL, auditOutputFormat, useColor)
summary := auditData.GetSummary()
score := summary.GetScore()
@@ -75,9 +77,9 @@ var auditCmd = &cobra.Command{
},
}
func runAndReportAudit(ctx context.Context, c conf.Configuration, auditPath, workload, outputFile, outputURL, outputFormat string) validator.AuditData {
func runAndReportAudit(ctx context.Context, c conf.Configuration, auditPath, workload, outputFile, outputURL, outputFormat string, useColor bool) validator.AuditData {
// Create a kubernetes client resource provider
k, err := kube.CreateResourceProvider(ctx, auditPath, workload)
k, err := kube.CreateResourceProvider(ctx, auditPath, workload, c)
if err != nil {
logrus.Errorf("Error fetching Kubernetes resources %v", err)
os.Exit(1)
@@ -98,6 +100,8 @@ func runAndReportAudit(ctx context.Context, c conf.Configuration, auditPath, wor
if err == nil {
outputBytes, err = yaml.JSONToYAML(jsonBytes)
}
} else if outputFormat == "pretty" {
outputBytes = []byte(auditData.GetPrettyOutput(useColor))
} else {
outputBytes, err = json.MarshalIndent(auditData, "", " ")
}
+7
View File
@@ -33,6 +33,13 @@ module.exports = {
"/infrastructure-as-code",
],
},
{
title: "Usage",
collapsable: false,
children: [
"/cli",
],
},
{
title: "Customization",
collapsable: false,
+1 -1
View File
@@ -42,7 +42,7 @@ const baseConfig = {
],
themeConfig: {
docsRepo: "",
docsDir: 'docs-md',
docsDir: 'docs',
editLinks: true,
editLinkText: "Help us improve this page",
logo: '/img/fairwinds-logo.svg',
+1 -1
View File
@@ -26,4 +26,4 @@ fbq('track', 'PageView');
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-K5KK5H3');
})(window,document,'script','dataLayer','GTM-TM95WXQ');
+5 -2
View File
@@ -3,7 +3,7 @@
<br>
<h3>Best Practices for Kubernetes Workload Configuration</h3>
<a href="https://github.com/FairwindsOps/polaris">
<img src="https://img.shields.io/static/v1.svg?label=Version&message=3.0.0&color=239922">
<img src="https://img.shields.io/static/v1.svg?label=Version&message=3.1.6&color=239922">
</a>
<a href="https://goreportcard.com/report/github.com/FairwindsOps/polaris">
<img src="https://goreportcard.com/badge/github.com/FairwindsOps/polaris">
@@ -11,11 +11,14 @@
<a href="https://circleci.com/gh/FairwindsOps/polaris.svg">
<img src="https://circleci.com/gh/FairwindsOps/polaris.svg?style=svg">
</a>
<a href="https://insights.fairwinds.com/gh/FairwindsOps/polaris">
<img src="https://insights.fairwinds.com/v0/gh/FairwindsOps/polaris/badge.svg">
</a>
</div>
Fairwinds' Polaris keeps your clusters sailing smoothly. It runs a variety of checks to ensure that
Kubernetes pods and controllers are configured using best practices, helping you avoid
problems in the future. Polaris can be run in a few different modes:
problems in the future.
Polaris can be run in three different modes:
* As a [dashboard](/dashboard), so you can audit what's running inside your cluster.
+3
View File
@@ -2,6 +2,9 @@
sidebarDepth: 0
---
## 3.2.0
* Add `--format=pretty` option for CLI output
## 3.1.6
* Fix nil pointer issue with --only-output-failed-tests
+50
View File
@@ -0,0 +1,50 @@
#### CLI Options
```
# top-level commands
audit
Runs a one-time audit.
dashboard
Runs the webserver for Polaris dashboard.
help
Prints help, if you give it a command then it will print help for that command. Same as -h
version
Prints the version of Polaris
webhook
Runs the webhook webserver
# global flags
-c, --config string Location of Polaris configuration file.
--disallow-exemptions Disallow any exemptions from configuration file.
--kubeconfig string Paths to a kubeconfig. Only required if out-of-cluster.
--log-level string Logrus log level. (default "info")
# dashboard flags
--audit-path string If specified, audits one or more YAML files instead of a cluster.
--base-path string Path on which the dashboard is served. (default "/")
--display-name string An optional identifier for the audit.
-h, --help help for dashboard
--listening-address string Listening Address for the dashboard webserver.
--load-audit-file string Runs the dashboard with data saved from a past audit.
-p, --port int Port for the dashboard webserver. (default 8080)
# audit flags
--audit-path string If specified, audits one or more YAML files instead of a cluster.
--color Whether to use color in pretty format. (default true)
--display-name string An optional identifier for the audit.
-f, --format string Output format for results - json, yaml, pretty, or score. (default "json")
-h, --help help for audit
--only-show-failed-tests If specified, audit output will only show failed tests.
--output-file string Destination file for audit results.
--output-url string Destination URL to send audit results.
--resource string Audit a specific resource, in the format namespace/kind/version/name, e.g. nginx-ingress/Deployment.apps/v1/default-backend.
--set-exit-code-below-score int Set an exit code of 4 when the score is below this threshold (1-100).
--set-exit-code-on-danger Set an exit code of 3 when the audit contains danger-level issues.
# webhook flags
--disable-webhook-config-installer disable the installer in the webhook server, so it won't install webhook configuration resources during bootstrapping.
-h, --help help for webhook
-p, --port int Port for the dashboard webserver. (default 9876)
```
-66
View File
@@ -1,66 +0,0 @@
#### CLI Options
```
# top-level commands
audit
Runs a one-time audit.
dashboard
Runs the webserver for Polaris dashboard.
help
Prints help, if you give it a command then it will print help for that command. Same as -h
version
Prints the version of Polaris
webhook
Runs the webhook webserver
# high-level flags
-c, --config string
Location of Polaris configuration file
--disallow-exemptions
Disallow any exemptions from configuration file.
-h, --help
Help for Polaris (same as help command)
--kubeconfig string
Path to a kubeconfig. Only required if out-of-cluster.
--log-level string
Logrus log level (default "info")
--master string
The address of the Kubernetes API server. Overrides any value in kubeconfig. Only required if out-of-cluster.
# dashboard flags
--audit-path string
If specified, audits one or more YAML files instead of a cluster
--base-path string
Path on which the dashboard is served (default "/")
--display-name string
An optional identifier for the audit
--load-audit-file string
Runs the dashboard with data saved from a past audit.
-p, --port int
Port for the dashboard webserver (default 8080)
# audit flags
--audit-path string
If specified, audits one or more YAML files instead of a cluster
--resource string
If specified, audit a specific resource, in the format namespace/kind/version/name, e.g. nginx-ingress/Deployment.apps/v1/default-backend
--display-name string
An optional identifier for the audit
--format string
Output format for results - json, yaml, or score (default "json")
--output-file string
Destination file for audit results
--output-url string
Destination URL to send audit results
--set-exit-code-below-score int
Set an exit code of 4 when the score is below this threshold (1-100)
--set-exit-code-on-danger
Set an exit code of 3 when the audit contains danger-level issues.
# webhook flags
--disable-webhook-config-installer
disable the installer in the webhook server, so it won't install webhook configuration resources during bootstrapping
-p, --port int
Port for the webhook webserver (default 9876)
```
+17 -1
View File
@@ -22,7 +22,7 @@ polaris version
## Running in a CI pipeline
### Set minimum score for an exit code
### Set minimum score for an exit code
You can tell the CLI to set an exit code if it detects certain issues with your YAML files.
For example, to fail if polaris detects *any* danger-level issues, or if the score drops below 90%:
```bash
@@ -31,6 +31,22 @@ polaris audit --audit-path ./deploy/ \
--set-exit-code-below-score 90
```
### Pretty-print results
By default, results are output as JSON. You can get human-readable output with
the `--format=pretty` flag:
```bash
polaris audit --audit-path ./deploy/ \
--format=pretty
```
You can also disable colors and emoji:
```bash
polaris audit --audit-path ./deploy/ \
--format=pretty \
--color=false
```
### Output only showing failed tests
The CLI to gives you ability to display results containing only failed tests.
For example:
+3 -1
View File
@@ -6,6 +6,7 @@ require (
cloud.google.com/go v0.74.0 // indirect
github.com/Azure/go-autorest/autorest v0.11.15 // indirect
github.com/Azure/go-autorest/autorest/adal v0.9.10 // indirect
github.com/fatih/color v1.10.0
github.com/gobuffalo/packr/v2 v2.8.1
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/uuid v1.1.3 // indirect
@@ -17,10 +18,11 @@ require (
github.com/prometheus/client_golang v1.9.0 // indirect
github.com/qri-io/jsonschema v0.1.1
github.com/rogpeppe/go-internal v1.6.2 // indirect
github.com/sirupsen/logrus v1.8.0
github.com/sirupsen/logrus v1.8.1
github.com/spf13/cobra v1.1.3
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.7.0
github.com/thoas/go-funk v0.7.0
go.uber.org/zap v1.16.0 // indirect
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad // indirect
golang.org/x/net v0.0.0-20201224014010-6772e930b67b // indirect
+10
View File
@@ -166,7 +166,9 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7
github.com/evanphx/json-patch v4.5.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/evanphx/json-patch v4.9.0+incompatible h1:kLcOMZeuLAJvL2BPWLMIj5oaZQobrkAqrL+WFZwQses=
github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTgWiPvpYe4Xau31I0PRk=
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
@@ -427,9 +429,13 @@ github.com/markbates/oncer v1.0.0 h1:E83IaVAHygyndzPimgUYJjbshhDTALZyXxvk9FOlQRY
github.com/markbates/oncer v1.0.0/go.mod h1:Z59JA581E9GP6w96jai+TGqafHPW+cPfRxz2aSZ0mcI=
github.com/markbates/safe v1.0.1 h1:yjZkbvRM6IzKj9tlu/zMJLS0n/V351OZWRnF3QfaUxI=
github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0=
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
@@ -571,6 +577,7 @@ github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.8.0 h1:nfhvjKcUMhBMVqbKHJlk5RPrrfYr/NMo3692g0dwfWU=
github.com/sirupsen/logrus v1.8.0/go.mod h1:4GuYW9TZmE769R5STWrRakJc4UqQ3+QQ95fyz7ENv1A=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
@@ -610,6 +617,8 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/thoas/go-funk v0.7.0 h1:GmirKrs6j6zJbhJIficOsz2aAI7700KsU/5YrdHRM1Y=
github.com/thoas/go-funk v0.7.0/go.mod h1:+IWnUfUmFO1+WVYQWQtIJHeRRdaIyyYglZN7xzUPe4Q=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
@@ -807,6 +816,7 @@ golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+1 -1
View File
@@ -20,7 +20,7 @@ import (
const (
// Version represents the current release version of Polaris
Version = "3.1.6"
Version = "3.2.0"
)
func main() {
+5 -3
View File
@@ -2,10 +2,12 @@ package config
import (
"strings"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// IsActionable determines whether a check is actionable given the current configuration
func (conf Configuration) IsActionable(ruleID, namespace, controllerName, containerName string) bool {
func (conf Configuration) IsActionable(ruleID string, objMeta metav1.Object, containerName string) bool {
if severity, ok := conf.Checks[ruleID]; !ok || !severity.IsActionable() {
return false
}
@@ -13,7 +15,7 @@ func (conf Configuration) IsActionable(ruleID, namespace, controllerName, contai
return true
}
for _, exemption := range conf.Exemptions {
if exemption.Namespace != "" && exemption.Namespace != namespace {
if exemption.Namespace != "" && exemption.Namespace != objMeta.GetNamespace() {
continue
}
@@ -27,7 +29,7 @@ func (conf Configuration) IsActionable(ruleID, namespace, controllerName, contai
}
if len(exemption.Rules) == 0 || checkIfRuleMatches {
if !isExemptionCheckMatched(exemption.ControllerNames, controllerName) {
if !isExemptionCheckMatched(exemption.ControllerNames, objMeta.GetName()) {
continue
}
if isExemptionCheckMatched(exemption.ContainerNames, containerName) {
+63 -49
View File
@@ -18,6 +18,9 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
var confContainerTest = `
@@ -68,26 +71,37 @@ exemptions:
- namespace: polaris
`
func createMeta(namespace, name string) metav1.Object {
unst := unstructured.Unstructured{}
obj, err := meta.Accessor(&unst)
if err != nil {
panic(err)
}
obj.SetName(name)
obj.SetNamespace(namespace)
return obj
}
func TestNamespaceExemptionForSpecifiedRules(t *testing.T) {
parsedConf, err := Parse([]byte(confContainerTest))
assert.NoError(t, err)
actionable := parsedConf.IsActionable("multipleReplicasForDeployment", "prometheus", "", "")
actionable := parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("prometheus", ""), "")
assert.False(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", "prometheus", "controller1", "container11")
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("prometheus", "controller1"), "container11")
assert.False(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", "prometheus", "", "container11")
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("prometheus", ""), "container11")
assert.False(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", "prometheus", "controller1", "")
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("prometheus", "controller1"), "")
assert.False(t, actionable)
actionable = parsedConf.IsActionable("pullPolicyNotAlways", "prometheus", "controller1", "")
actionable = parsedConf.IsActionable("pullPolicyNotAlways", createMeta("prometheus", "controller1"), "")
assert.True(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", "kube-system", "", "")
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("kube-system", ""), "")
assert.True(t, actionable)
}
@@ -95,19 +109,19 @@ func TestNamespaceExemptionForAllRules(t *testing.T) {
parsedConf, err := Parse([]byte(confContainerTest))
assert.NoError(t, err)
actionable := parsedConf.IsActionable("multipleReplicasForDeployment", "polaris", "", "")
actionable := parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("polaris", ""), "")
assert.False(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", "polaris", "controller1", "container11")
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("polaris", "controller1"), "container11")
assert.False(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", "polaris", "", "container11")
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("polaris", ""), "container11")
assert.False(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", "polaris", "controller1", "")
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("polaris", "controller1"), "")
assert.False(t, actionable)
actionable = parsedConf.IsActionable("pullPolicyNotAlways", "polaris", "controller1", "")
actionable = parsedConf.IsActionable("pullPolicyNotAlways", createMeta("polaris", "controller1"), "")
assert.False(t, actionable)
}
@@ -115,28 +129,28 @@ func TestControllerExemption(t *testing.T) {
parsedConf, err := Parse([]byte(confContainerTest))
assert.NoError(t, err)
actionable := parsedConf.IsActionable("multipleReplicasForDeployment", "", "controller2", "")
actionable := parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("", "controller2"), "")
assert.False(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", "", "controller2", "container21")
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("", "controller2"), "container21")
assert.False(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", "prometheus", "controller2", "container21")
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("prometheus", "controller2"), "container21")
assert.False(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", "prometheus", "controller2", "")
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("prometheus", "controller2"), "")
assert.False(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", "", "controller3", "")
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("", "controller3"), "")
assert.True(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", "kube-system", "controller3", "")
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("kube-system", "controller3"), "")
assert.False(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", "kube-system", "controller3", "container31")
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("kube-system", "controller3"), "container31")
assert.False(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", "kube-system", "controller4", "")
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("kube-system", "controller4"), "")
assert.True(t, actionable)
}
@@ -144,22 +158,22 @@ func TestOnlyContainerExemption(t *testing.T) {
parsedConf, err := Parse([]byte(confContainerTest))
assert.NoError(t, err)
actionable := parsedConf.IsActionable("multipleReplicasForDeployment", "", "", "container41")
actionable := parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("", ""), "container41")
assert.False(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", "", "", "container42")
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("", ""), "container42")
assert.False(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", "", "controller4", "container41")
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("", "controller4"), "container41")
assert.False(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", "kube-system", "", "container41")
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("kube-system", ""), "container41")
assert.False(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", "kube-system", "controller4", "container41")
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("kube-system", "controller4"), "container41")
assert.False(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", "", "", "container51")
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("", ""), "container51")
assert.True(t, actionable)
}
@@ -167,25 +181,25 @@ func TestNamespaceAndContainerExemption(t *testing.T) {
parsedConf, err := Parse([]byte(confContainerTest))
assert.NoError(t, err)
actionable := parsedConf.IsActionable("multipleReplicasForDeployment", "kube-system", "", "container51")
actionable := parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("kube-system", ""), "container51")
assert.False(t, actionable)
actionable = parsedConf.IsActionable("priorityClassNotSet", "kube-system", "", "container51")
actionable = parsedConf.IsActionable("priorityClassNotSet", createMeta("kube-system", ""), "container51")
assert.True(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", "kube-system", "controller5", "container51")
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("kube-system", "controller5"), "container51")
assert.False(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", "kube-system", "controller5", "")
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("kube-system", "controller5"), "")
assert.True(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", "insights-agent", "", "container51")
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("insights-agent", ""), "container51")
assert.True(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", "", "", "container51")
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("", ""), "container51")
assert.True(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", "", "controller5", "container51")
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("", "controller5"), "container51")
assert.True(t, actionable)
}
@@ -193,25 +207,25 @@ func TestControllerAndContainerExemption(t *testing.T) {
parsedConf, err := Parse([]byte(confContainerTest))
assert.NoError(t, err)
actionable := parsedConf.IsActionable("multipleReplicasForDeployment", "", "controller6", "container61")
actionable := parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("", "controller6"), "container61")
assert.False(t, actionable)
actionable = parsedConf.IsActionable("priorityClassNotSet", "", "controller6", "container61")
actionable = parsedConf.IsActionable("priorityClassNotSet", createMeta("", "controller6"), "container61")
assert.True(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", "kube-system", "controller6", "container61")
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("kube-system", "controller6"), "container61")
assert.False(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", "kube-system", "controller6", "")
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("kube-system", "controller6"), "")
assert.True(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", "", "controller7", "container61")
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("", "controller7"), "container61")
assert.True(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", "", "", "container61")
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("", ""), "container61")
assert.True(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", "kube-system", "", "container61")
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("kube-system", ""), "container61")
assert.True(t, actionable)
}
@@ -219,33 +233,33 @@ func TestContainerExemption(t *testing.T) {
parsedConf, err := Parse([]byte(confContainerTest))
assert.NoError(t, err)
actionable := parsedConf.IsActionable("multipleReplicasForDeployment", "", "", "container71")
actionable := parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("", ""), "container71")
assert.True(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", "kube-system", "", "container71")
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("kube-system", ""), "container71")
assert.True(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", "", "controller7", "container71")
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("", "controller7"), "container71")
assert.True(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", "kube-system", "controller7", "")
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("kube-system", "controller7"), "")
assert.True(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", "kube-system", "controller7", "container71")
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("kube-system", "controller7"), "container71")
assert.False(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", "insights-agent", "controller7", "container71")
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("insights-agent", "controller7"), "container71")
assert.True(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", "kube-system", "controller6", "container71")
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("kube-system", "controller6"), "container71")
assert.True(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", "kube-system", "controller7", "container61")
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("kube-system", "controller7"), "container61")
assert.True(t, actionable)
actionable = parsedConf.IsActionable("priorityClassNotSet", "kube-system", "controller7", "container71")
actionable = parsedConf.IsActionable("priorityClassNotSet", createMeta("kube-system", "controller7"), "container71")
assert.False(t, actionable)
actionable = parsedConf.IsActionable("pullPolicyNotAlways", "kube-system", "controller8", "container71")
actionable = parsedConf.IsActionable("pullPolicyNotAlways", createMeta("kube-system", "controller8"), "container71")
assert.True(t, actionable)
}
+19 -8
View File
@@ -3,8 +3,10 @@ package config
import (
"encoding/json"
"fmt"
"strings"
"github.com/qri-io/jsonschema"
"github.com/thoas/go-funk"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
)
@@ -13,16 +15,21 @@ import (
type TargetKind string
const (
// TargetController points to the controller's spec
TargetController TargetKind = "Controller"
// TargetContainer points to the container spec
TargetContainer TargetKind = "Container"
// TargetPod points to the pod spec
TargetPod TargetKind = "Pod"
// TargetController points to the controller's spec
TargetController TargetKind = "Controller"
// TargetIngress points to the ingress spec
TargetIngress TargetKind = "Ingress"
)
// HandledTargets is a list of target names that are explicitly handled
var HandledTargets = []TargetKind{
TargetController,
TargetContainer,
TargetPod,
}
// SchemaCheck is a Polaris check that runs using JSON Schema
type SchemaCheck struct {
ID string `yaml:"id"`
@@ -154,13 +161,17 @@ func (check SchemaCheck) CheckObject(obj interface{}) (bool, error) {
}
// IsActionable decides if this check applies to a particular target
func (check SchemaCheck) IsActionable(target TargetKind, controllerType string, isInit bool) bool {
if check.Target != target {
func (check SchemaCheck) IsActionable(target TargetKind, kind string, isInit bool) bool {
if funk.Contains(HandledTargets, target) {
if check.Target != target {
return false
}
} else if string(check.Target) != kind && !strings.HasSuffix(string(check.Target), "/"+kind) {
return false
}
isIncluded := len(check.Controllers.Include) == 0
for _, inclusion := range check.Controllers.Include {
if inclusion == controllerType {
if inclusion == kind {
isIncluded = true
break
}
@@ -169,7 +180,7 @@ func (check SchemaCheck) IsActionable(target TargetKind, controllerType string,
return false
}
for _, exclusion := range check.Controllers.Exclude {
if exclusion == controllerType {
if exclusion == kind {
return false
}
}
+2 -2
View File
@@ -173,7 +173,7 @@ 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(r.Context(), auditPath, "")
k, err := kube.CreateResourceProvider(r.Context(), auditPath, "", c)
if err != nil {
logrus.Errorf("Error fetching Kubernetes resources %v", err)
http.Error(w, "Error fetching Kubernetes resources", http.StatusInternalServerError)
@@ -206,7 +206,7 @@ func GetRouter(c config.Configuration, auditPath string, port int, basePath stri
adjustedConf := getConfigForQuery(c, r.URL.Query())
if auditData == nil {
k, err := kube.CreateResourceProvider(r.Context(), auditPath, "")
k, err := kube.CreateResourceProvider(r.Context(), auditPath, "", c)
if err != nil {
logrus.Errorf("Error fetching Kubernetes resources %v", err)
http.Error(w, "Error fetching Kubernetes resources", http.StatusInternalServerError)
+58 -85
View File
@@ -1,7 +1,6 @@
package kube
import (
"bytes"
"context"
"encoding/json"
"fmt"
@@ -13,22 +12,23 @@ import (
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 {
// GenericResource is a base implementation with some free methods for inherited structs
type GenericResource struct {
Kind string
PodSpec kubeAPICoreV1.PodSpec
ObjectMeta kubeAPIMetaV1.Object
Resource unstructured.Unstructured
PodSpec *kubeAPICoreV1.PodSpec
OriginalObjectJSON []byte
}
// NewGenericWorkloadFromUnstructured creates a workload from an unstructured.Unstructured
func NewGenericWorkloadFromUnstructured(kind string, unst *unstructured.Unstructured) (GenericWorkload, error) {
workload := GenericWorkload{
Kind: kind,
// NewGenericResourceFromUnstructured creates a workload from an unstructured.Unstructured
func NewGenericResourceFromUnstructured(unst *unstructured.Unstructured) (GenericResource, error) {
workload := GenericResource{
Kind: unst.GetKind(),
Resource: *unst,
}
objMeta, err := meta.Accessor(unst)
@@ -49,25 +49,27 @@ func NewGenericWorkloadFromUnstructured(kind string, unst *unstructured.Unstruct
return workload, err
}
podSpecMap := GetPodSpec(m)
b, err = json.Marshal(podSpecMap)
if err != nil {
return workload, err
if podSpecMap != nil {
b, err = json.Marshal(podSpecMap)
if err != nil {
return workload, err
}
podSpec := kubeAPICoreV1.PodSpec{}
err = json.Unmarshal(b, &podSpec)
if err != nil {
return workload, err
}
workload.PodSpec = &podSpec
}
podSpec := kubeAPICoreV1.PodSpec{}
err = json.Unmarshal(b, &podSpec)
if err != nil {
return workload, err
}
workload.PodSpec = podSpec
return workload, nil
}
// NewGenericWorkloadFromPod builds a new workload for a given Pod without looking at parents
func NewGenericWorkloadFromPod(podResource kubeAPICoreV1.Pod, originalObject interface{}) (GenericWorkload, error) {
workload := GenericWorkload{
// NewGenericResourceFromPod builds a new workload for a given Pod without looking at parents
func NewGenericResourceFromPod(podResource kubeAPICoreV1.Pod, originalObject interface{}) (GenericResource, error) {
workload := GenericResource{
Kind: "Pod",
PodSpec: podResource.Spec,
PodSpec: &podResource.Spec,
ObjectMeta: podResource.ObjectMeta.GetObjectMeta(),
}
if originalObject != nil {
@@ -77,13 +79,12 @@ func NewGenericWorkloadFromPod(podResource kubeAPICoreV1.Pod, originalObject int
}
workload.OriginalObjectJSON = bytes
var unst unstructured.Unstructured
err = json.Unmarshal(bytes, &unst.Object)
err = json.Unmarshal(bytes, &workload.Resource.Object)
if err != nil {
logrus.Error("Couldn't marshal JSON for pod ", err)
return workload, err
}
objMeta, err := meta.Accessor(&unst)
objMeta, err := meta.Accessor(&workload.Resource)
if err != nil {
logrus.Error("Couldn't create meta accessor for unstructred ", err)
return workload, err
@@ -93,26 +94,36 @@ func NewGenericWorkloadFromPod(podResource kubeAPICoreV1.Pod, originalObject int
return workload, nil
}
// NewGenericWorkload builds a new workload for a given Pod
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)
// NewGenericResourceFromBytes parses a generic kubernetes resource
func NewGenericResourceFromBytes(contentBytes []byte) (GenericResource, error) {
unst := unstructured.Unstructured{}
err := yaml.Unmarshal(contentBytes, &unst.Object)
if err != nil {
return GenericResource{}, err
}
return NewGenericResourceFromUnstructured(&unst)
}
// ResolveControllerFromPod builds a new workload for a given Pod
func ResolveControllerFromPod(ctx context.Context, podResource kubeAPICoreV1.Pod, dynamicClient *dynamic.Interface, restMapper *meta.RESTMapper, objectCache map[string]unstructured.Unstructured) (GenericResource, error) {
workload, err := resolveControllerFromPod(ctx, podResource, dynamicClient, restMapper, objectCache)
if err != nil {
return workload, err
}
if len(workload.OriginalObjectJSON) == 0 {
return NewGenericWorkloadFromPod(podResource, podResource)
return NewGenericResourceFromPod(podResource, podResource)
}
return workload, err
}
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)
func resolveControllerFromPod(ctx context.Context, podResource kubeAPICoreV1.Pod, dynamicClient *dynamic.Interface, restMapper *meta.RESTMapper, objectCache map[string]unstructured.Unstructured) (GenericResource, error) {
podWorkload, err := NewGenericResourceFromPod(podResource, nil)
if err != nil {
return workload, err
return podWorkload, err
}
// If an owner exists then set the name to the workload.
// This allows us to handle CRDs creating Workloads or DeploymentConfigs in OpenShift.
owners := workload.ObjectMeta.GetOwnerReferences()
topKind := "Pod"
topMeta := podWorkload.ObjectMeta
owners := podResource.ObjectMeta.GetOwnerReferences()
lastKey := ""
for len(owners) > 0 {
if len(owners) > 1 {
@@ -122,12 +133,12 @@ func newGenericWorkload(ctx context.Context, podResource kubeAPICoreV1.Pod, dyna
if firstOwner.Kind == "Node" {
break
}
workload.Kind = firstOwner.Kind
key := fmt.Sprintf("%s/%s/%s", firstOwner.Kind, workload.ObjectMeta.GetNamespace(), firstOwner.Name)
topKind = firstOwner.Kind
key := fmt.Sprintf("%s/%s/%s", firstOwner.Kind, topMeta.GetNamespace(), firstOwner.Name)
lastKey = key
abstractObject, ok := objectCache[key]
if !ok {
err = cacheAllObjectsOfKind(ctx, firstOwner.APIVersion, firstOwner.Kind, dynamicClient, restMapper, objectCache)
err := cacheAllObjectsOfKind(ctx, firstOwner.APIVersion, firstOwner.Kind, dynamicClient, restMapper, objectCache)
if err != nil {
logrus.Warnf("Error caching objects of Kind %s %v", firstOwner.Kind, err)
break
@@ -142,26 +153,22 @@ func newGenericWorkload(ctx context.Context, podResource kubeAPICoreV1.Pod, dyna
objMeta, err := meta.Accessor(&abstractObject)
if err != nil {
logrus.Warnf("Error retrieving parent metadata %s of API %s and Kind %s because of error: %v ", firstOwner.Name, firstOwner.APIVersion, firstOwner.Kind, err)
return workload, err
return GenericResource{}, err
}
workload.ObjectMeta = objMeta
topMeta = objMeta
owners = abstractObject.GetOwnerReferences()
}
if lastKey != "" {
unst := objectCache[lastKey]
bytes, err := json.Marshal(&unst)
if err != nil {
return workload, err
}
workload.OriginalObjectJSON = bytes
} else {
bytes, err := json.Marshal(podResource)
if err != nil {
return workload, err
}
workload.OriginalObjectJSON = bytes
return NewGenericResourceFromUnstructured(&unst)
}
workload, err := NewGenericResourceFromPod(podResource, podResource)
if err != nil {
return workload, err
}
workload.Kind = topKind
workload.ObjectMeta = topMeta
return workload, nil
}
@@ -207,37 +214,3 @@ func GetPodSpec(yaml map[string]interface{}) interface{} {
}
return nil
}
// GetWorkloadFromBytes parses a GenericWorkload
func GetWorkloadFromBytes(contentBytes []byte) (*GenericWorkload, error) {
yamlNode := make(map[string]interface{})
err := yaml.Unmarshal(contentBytes, &yamlNode)
if err != nil {
logrus.Errorf("Invalid YAML: %s", string(contentBytes))
return nil, err
}
finalDoc := make(map[string]interface{})
finalDoc["metadata"] = yamlNode["metadata"]
finalDoc["apiVersion"] = "v1"
finalDoc["kind"] = "Pod"
podSpec := GetPodSpec(yamlNode)
if podSpec == nil {
return nil, nil
}
finalDoc["spec"] = podSpec
marshaledYaml, err := yaml.Marshal(finalDoc)
if err != nil {
logrus.Errorf("Could not marshal yaml: %v", err)
return nil, err
}
decoder := k8sYaml.NewYAMLOrJSONDecoder(bytes.NewReader(marshaledYaml), 1000)
pod := kubeAPICoreV1.Pod{}
err = decoder.Decode(&pod)
newController, err := NewGenericWorkloadFromPod(pod, yamlNode)
if err != nil {
return nil, err
}
newController.Kind = yamlNode["kind"].(string)
return &newController, nil
}
+127 -70
View File
@@ -12,12 +12,15 @@ import (
"strings"
"time"
conf "github.com/fairwindsops/polaris/pkg/config"
"github.com/sirupsen/logrus"
"github.com/thoas/go-funk"
corev1 "k8s.io/api/core/v1"
v1beta1 "k8s.io/api/extensions/v1beta1"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "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"
"k8s.io/client-go/kubernetes"
@@ -34,8 +37,59 @@ type ResourceProvider struct {
SourceType string
Nodes []corev1.Node
Namespaces []corev1.Namespace
Controllers []GenericWorkload
Ingresses []v1beta1.Ingress
Resources resourceKindMap
}
type resourceKindMap map[string][]GenericResource
func (rkm resourceKindMap) addResource(r GenericResource) {
rkm[r.Kind] = append(rkm[r.Kind], r)
}
func (rkm resourceKindMap) addResources(rs []GenericResource) {
for _, r := range rs {
rkm.addResource(r)
}
}
func (rkm resourceKindMap) GetLength() int {
total := 0
for _, rs := range rkm {
total += len(rs)
}
return total
}
func (rkm resourceKindMap) GetNumberOfControllers() int {
total := 0
for _, rs := range rkm {
for _, r := range rs {
if r.PodSpec != nil {
total++
}
}
}
return total
}
// This is here for backward compatibility reasons
func maybeTransformKindIntoGroupKind(k conf.TargetKind) string {
if k == "Ingress" {
return "networking.k8s.io/Ingress"
}
return ""
}
func newResourceProvider(version, sourceType, sourceName string) ResourceProvider {
return ResourceProvider{
ServerVersion: version,
SourceType: sourceType,
SourceName: sourceName,
CreationTime: time.Now(),
Nodes: make([]corev1.Node, 0),
Namespaces: make([]corev1.Namespace, 0),
Resources: make(map[string][]GenericResource),
}
}
type k8sResource struct {
@@ -45,18 +99,18 @@ type k8sResource struct {
var podSpecFields = []string{"jobTemplate", "spec", "template"}
// CreateResourceProvider returns a new ResourceProvider object to interact with k8s resources
func CreateResourceProvider(ctx context.Context, directory, workload string) (*ResourceProvider, error) {
func CreateResourceProvider(ctx context.Context, directory, workload string, c conf.Configuration) (*ResourceProvider, error) {
if workload != "" {
return CreateResourceProviderFromWorkload(ctx, workload)
return CreateResourceProviderFromResource(ctx, workload)
}
if directory != "" {
return CreateResourceProviderFromPath(directory)
}
return CreateResourceProviderFromCluster(ctx)
return CreateResourceProviderFromCluster(ctx, c)
}
// CreateResourceProviderFromWorkload creates a new ResourceProvider that just contains one workload
func CreateResourceProviderFromWorkload(ctx context.Context, workload string) (*ResourceProvider, error) {
// CreateResourceProviderFromResource creates a new ResourceProvider that just contains one workload
func CreateResourceProviderFromResource(ctx context.Context, workload string) (*ResourceProvider, error) {
kubeConf, configError := config.GetConfig()
if configError != nil {
logrus.Errorf("Error fetching KubeConfig: %v", configError)
@@ -72,14 +126,7 @@ func CreateResourceProviderFromWorkload(ctx context.Context, workload string) (*
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{},
}
resources := newResourceProvider(serverVersion.Major+"."+serverVersion.Minor, "Resource", workload)
parts := strings.Split(workload, "/")
if len(parts) != 4 {
@@ -106,31 +153,24 @@ func CreateResourceProviderFromWorkload(ctx context.Context, workload string) (*
logrus.Errorf("Could not find workload %s: %v", workload, err)
return nil, err
}
workloadObj, err := NewGenericWorkloadFromUnstructured(kind, obj)
workloadObj, err := NewGenericResourceFromUnstructured(obj)
if err != nil {
logrus.Errorf("Could not parse workload %s: %v", workload, err)
return nil, err
}
resources.Controllers = []GenericWorkload{workloadObj}
resources.Resources.addResource(workloadObj)
return &resources, nil
}
// CreateResourceProviderFromPath returns a new ResourceProvider using the YAML files in a directory
func CreateResourceProviderFromPath(directory string) (*ResourceProvider, error) {
resources := ResourceProvider{
ServerVersion: "unknown",
SourceType: "Path",
SourceName: directory,
Nodes: []corev1.Node{},
Namespaces: []corev1.Namespace{},
Controllers: []GenericWorkload{},
}
resources := newResourceProvider("unknown", "Path", directory)
if directory == "-" {
fi, err := os.Stdin.Stat()
if err == nil && fi.Mode()&os.ModeNamedPipe == os.ModeNamedPipe {
if err := addResourcesFromReader(os.Stdin, &resources); err != nil {
if err := resources.addResourcesFromReader(os.Stdin); err != nil {
return nil, err
}
return &resources, nil
@@ -146,7 +186,7 @@ func CreateResourceProviderFromPath(directory string) (*ResourceProvider, error)
logrus.Errorf("Error reading file: %v", path)
return err
}
return addResourcesFromYaml(string(contents), &resources)
return resources.addResourcesFromYaml(string(contents))
}
err := filepath.Walk(directory, visitFile)
@@ -157,7 +197,7 @@ func CreateResourceProviderFromPath(directory string) (*ResourceProvider, error)
}
// CreateResourceProviderFromCluster creates a new ResourceProvider using live data from a cluster
func CreateResourceProviderFromCluster(ctx context.Context) (*ResourceProvider, error) {
func CreateResourceProviderFromCluster(ctx context.Context, c conf.Configuration) (*ResourceProvider, error) {
kubeConf, configError := config.GetConfig()
if configError != nil {
logrus.Errorf("Error fetching KubeConfig: %v", configError)
@@ -173,17 +213,18 @@ func CreateResourceProviderFromCluster(ctx context.Context) (*ResourceProvider,
logrus.Errorf("Error connecting to dynamic interface: %v", err)
return nil, err
}
return CreateResourceProviderFromAPI(ctx, api, kubeConf.Host, &dynamicInterface)
return CreateResourceProviderFromAPI(ctx, api, kubeConf.Host, &dynamicInterface, c)
}
// CreateResourceProviderFromAPI creates a new ResourceProvider from an existing k8s interface
func CreateResourceProviderFromAPI(ctx context.Context, kube kubernetes.Interface, clusterName string, dynamic *dynamic.Interface) (*ResourceProvider, error) {
func CreateResourceProviderFromAPI(ctx context.Context, kube kubernetes.Interface, clusterName string, dynamic *dynamic.Interface, c conf.Configuration) (*ResourceProvider, error) {
listOpts := metav1.ListOptions{}
serverVersion, err := kube.Discovery().ServerVersion()
if err != nil {
logrus.Errorf("Error fetching Cluster API version: %v", err)
return nil, err
}
provider := newResourceProvider(serverVersion.Major+"."+serverVersion.Minor, "Cluster", clusterName)
nodes, err := kube.CoreV1().Nodes().List(ctx, listOpts)
if err != nil {
@@ -200,11 +241,6 @@ func CreateResourceProviderFromAPI(ctx context.Context, kube kubernetes.Interfac
logrus.Errorf("Error fetching Pods: %v", err)
return nil, err
}
ingressList, err := kube.ExtensionsV1beta1().Ingresses("").List(ctx, listOpts)
if err != nil {
logrus.Errorf("Error fetching Ingresses: %v", err)
return nil, err
}
resources, err := restmapper.GetAPIGroupResources(kube.Discovery())
if err != nil {
@@ -213,6 +249,35 @@ func CreateResourceProviderFromAPI(ctx context.Context, kube kubernetes.Interfac
}
restMapper := restmapper.NewDiscoveryRESTMapper(resources)
var additionalKinds []conf.TargetKind
for _, check := range c.CustomChecks {
if !funk.Contains(conf.HandledTargets, check.Target) {
additionalKinds = append(additionalKinds, check.Target)
}
}
for _, kind := range additionalKinds {
groupKind := schema.ParseGroupKind(maybeTransformKindIntoGroupKind(kind))
mapping, err := (restMapper).RESTMapping(groupKind)
if err != nil {
logrus.Warnf("Error retrieving mapping of Kind %s because of error: %v", kind, err)
return nil, err
}
objects, err := (*dynamic).Resource(mapping.Resource).Namespace("").List(ctx, metav1.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 nil, err
}
for _, obj := range objects.Items {
res, err := NewGenericResourceFromUnstructured(&obj)
if err != nil {
return nil, err
}
provider.Resources.addResource(res)
}
}
objectCache := map[string]unstructured.Unstructured{}
controllers, err := LoadControllers(ctx, pods.Items, dynamic, &restMapper, objectCache)
@@ -220,23 +285,15 @@ func CreateResourceProviderFromAPI(ctx context.Context, kube kubernetes.Interfac
logrus.Errorf("Error loading controllers from pods: %v", err)
return nil, err
}
api := ResourceProvider{
ServerVersion: serverVersion.Major + "." + serverVersion.Minor,
SourceType: "Cluster",
SourceName: clusterName,
CreationTime: time.Now(),
Nodes: nodes.Items,
Namespaces: namespaces.Items,
Controllers: controllers,
Ingresses: ingressList.Items,
}
return &api, nil
provider.Nodes = nodes.Items
provider.Namespaces = namespaces.Items
provider.Resources.addResources(controllers)
return &provider, nil
}
// LoadControllers loads a list of controllers from the kubeResources Pods
func LoadControllers(ctx context.Context, pods []corev1.Pod, dynamicClientPointer *dynamic.Interface, restMapperPointer *meta.RESTMapper, objectCache map[string]unstructured.Unstructured) ([]GenericWorkload, error) {
interfaces := []GenericWorkload{}
func LoadControllers(ctx context.Context, pods []corev1.Pod, dynamicClientPointer *dynamic.Interface, restMapperPointer *meta.RESTMapper, objectCache map[string]unstructured.Unstructured) ([]GenericResource, error) {
interfaces := []GenericResource{}
deduped := map[string]corev1.Pod{}
for _, pod := range pods {
owners := pod.ObjectMeta.OwnerReferences
@@ -247,7 +304,7 @@ func LoadControllers(ctx context.Context, pods []corev1.Pod, dynamicClientPointe
deduped[pod.ObjectMeta.Namespace+"/"+owners[0].Kind+"/"+owners[0].Name] = pod
}
for _, pod := range deduped {
workload, err := NewGenericWorkload(ctx, pod, dynamicClientPointer, restMapperPointer, objectCache)
workload, err := ResolveControllerFromPod(ctx, pod, dynamicClientPointer, restMapperPointer, objectCache)
if err != nil {
return nil, err
}
@@ -258,8 +315,8 @@ func LoadControllers(ctx context.Context, pods []corev1.Pod, dynamicClientPointe
// Because the controllers with an Owner take on the name of the Owner, this eliminates any duplicates.
// In cases like CronJobs older children can hang around, so this takes the most recent.
func deduplicateControllers(inputControllers []GenericWorkload) []GenericWorkload {
controllerMap := make(map[string]GenericWorkload)
func deduplicateControllers(inputControllers []GenericResource) []GenericResource {
controllerMap := make(map[string]GenericResource)
for _, controller := range inputControllers {
key := controller.ObjectMeta.GetNamespace() + "/" + controller.Kind + "/" + controller.ObjectMeta.GetName()
oldController, ok := controllerMap[key]
@@ -267,32 +324,34 @@ func deduplicateControllers(inputControllers []GenericWorkload) []GenericWorkloa
controllerMap[key] = controller
}
}
results := make([]GenericWorkload, 0)
results := make([]GenericResource, len(controllerMap))
idx := 0
for _, controller := range controllerMap {
results = append(results, controller)
results[idx] = controller
idx++
}
return results
}
func addResourcesFromReader(reader io.Reader, resources *ResourceProvider) error {
func (resources *ResourceProvider) addResourcesFromReader(reader io.Reader) 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 {
if err := resources.addResourcesFromYaml(string(contents)); err != nil {
return err
}
return nil
}
func addResourcesFromYaml(contents string, resources *ResourceProvider) error {
func (resources *ResourceProvider) addResourcesFromYaml(contents string) 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)
err := resources.addResourceFromString(spec)
if err != nil {
logrus.Errorf("Error parsing YAML: (%v)", err)
return err
@@ -301,7 +360,7 @@ func addResourcesFromYaml(contents string, resources *ResourceProvider) error {
return nil
}
func addResourceFromString(contents string, resources *ResourceProvider) error {
func (resources *ResourceProvider) addResourceFromString(contents string) error {
contentBytes := []byte(contents)
decoder := k8sYaml.NewYAMLOrJSONDecoder(bytes.NewReader(contentBytes), 1000)
resource := k8sResource{}
@@ -316,27 +375,25 @@ func addResourceFromString(contents string, resources *ResourceProvider) error {
ns := corev1.Namespace{}
err = decoder.Decode(&ns)
resources.Namespaces = append(resources.Namespaces, ns)
} else if resource.Kind == "Pod" {
}
if resource.Kind == "Pod" {
pod := corev1.Pod{}
err = decoder.Decode(&pod)
if err != nil {
return err
}
workload, err := NewGenericWorkloadFromPod(pod, pod)
workload, err := NewGenericResourceFromPod(pod, pod)
if err != nil {
return err
}
resources.Controllers = append(resources.Controllers, workload)
} else if resource.Kind == "Ingress" {
ingress := v1beta1.Ingress{}
err = decoder.Decode(&ingress)
resources.Ingresses = append(resources.Ingresses, ingress)
resources.Resources.addResource(workload)
} else {
newController, err := GetWorkloadFromBytes(contentBytes)
if err != nil || newController == nil {
newResource, err := NewGenericResourceFromBytes(contentBytes)
if err != nil {
return err
}
resources.Controllers = append(resources.Controllers, *newController)
resources.Resources.addResource(newResource)
}
return err
}
+29 -31
View File
@@ -3,36 +3,40 @@ package kube
import (
"bytes"
"context"
"fmt"
"io/ioutil"
"testing"
"time"
conf "github.com/fairwindsops/polaris/pkg/config"
"github.com/fairwindsops/polaris/test"
"github.com/stretchr/testify/assert"
corev1 "k8s.io/api/core/v1"
)
func TestGetResourcesFromPath(t *testing.T) {
resources, err := CreateResourceProviderFromPath("./test_files/test_1")
provider, err := CreateResourceProviderFromPath("./test_files/test_1")
assert.Equal(t, nil, err, "Error should be nil")
assert.Equal(t, "Path", resources.SourceType, "Should have type Path")
assert.Equal(t, "./test_files/test_1", resources.SourceName, "Should have filename as name")
assert.Equal(t, "unknown", resources.ServerVersion, "Server version should be unknown")
assert.IsType(t, time.Now(), resources.CreationTime, "Creation time should be set")
assert.Equal(t, "Path", provider.SourceType, "Should have type Path")
assert.Equal(t, "./test_files/test_1", provider.SourceName, "Should have filename as name")
assert.Equal(t, "unknown", provider.ServerVersion, "Server version should be unknown")
assert.IsType(t, time.Now(), provider.CreationTime, "Creation time should be set")
assert.Equal(t, 0, len(resources.Nodes), "Should not have any nodes")
assert.Equal(t, 0, len(provider.Nodes), "Should not have any nodes")
assert.Equal(t, 1, len(resources.Namespaces), "Should have a namespace")
assert.Equal(t, "two", resources.Namespaces[0].ObjectMeta.Name)
assert.Equal(t, 1, len(provider.Namespaces), "Should have a namespace")
assert.Equal(t, "two", provider.Namespaces[0].ObjectMeta.Name)
assert.Equal(t, 9, len(resources.Controllers), "Should have eight controllers")
namespaceCount := map[string]int{}
for _, controller := range resources.Controllers {
namespaceCount[controller.ObjectMeta.GetNamespace()]++
for kind, resources := range provider.Resources {
fmt.Println("found", kind, len(resources))
for _, controller := range resources {
namespaceCount[controller.ObjectMeta.GetNamespace()]++
}
}
assert.Equal(t, 8, namespaceCount[""])
assert.Equal(t, 11, provider.Resources.GetLength())
assert.Equal(t, 10, namespaceCount[""])
assert.Equal(t, 1, namespaceCount["two"])
}
@@ -48,8 +52,8 @@ func TestGetMultipleResourceFromSingleFile(t *testing.T) {
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, 1, len(resources.Resources["Deployment"]), "Should have one controller")
assert.Equal(t, "dashboard", resources.Resources["Deployment"][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)
@@ -65,21 +69,14 @@ 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)
resources := newResourceProvider("unknown", "Path", "-")
err = resources.addResourcesFromReader(reader)
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, 1, len(resources.Resources["Deployment"]), "Should have one controller")
assert.Equal(t, "dashboard", resources.Resources["Deployment"][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)
@@ -88,7 +85,7 @@ func TestAddResourcesFromReader(t *testing.T) {
func TestGetResourceFromAPI(t *testing.T) {
k8s, dynamicInterface := test.SetupTestAPI(test.GetMockControllers("test")...)
resources, err := CreateResourceProviderFromAPI(context.Background(), k8s, "test", &dynamicInterface)
resources, err := CreateResourceProviderFromAPI(context.Background(), k8s, "test", &dynamicInterface, conf.Configuration{})
assert.Equal(t, nil, err, "Error should be nil")
assert.Equal(t, "Cluster", resources.SourceType, "Should have type Path")
@@ -96,8 +93,7 @@ func TestGetResourceFromAPI(t *testing.T) {
assert.IsType(t, time.Now(), resources.CreationTime, "Creation time should be set")
assert.Equal(t, 0, len(resources.Nodes), "Should not have any nodes")
assert.Equal(t, 0, len(resources.Ingresses), "Should not have any ingresses")
assert.Equal(t, 5, len(resources.Controllers), "Should have 5 controllers")
assert.Equal(t, 5, len(resources.Resources), "Should have 5 controllers")
expectedNames := map[string]bool{
"deploy": false,
@@ -106,8 +102,10 @@ func TestGetResourceFromAPI(t *testing.T) {
"statefulset": false,
"daemonset": false,
}
for _, ctrl := range resources.Controllers {
expectedNames[ctrl.ObjectMeta.GetName()] = true
for _, controllers := range resources.Resources {
for _, ctrl := range controllers {
expectedNames[ctrl.ObjectMeta.GetName()] = true
}
}
for name, val := range expectedNames {
assert.Equal(t, true, val, name)
+89
View File
@@ -0,0 +1,89 @@
// Copyright 2019 FairwindsOps Inc
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package validator
import (
"encoding/json"
"testing"
conf "github.com/fairwindsops/polaris/pkg/config"
"github.com/fairwindsops/polaris/pkg/kube"
"github.com/stretchr/testify/assert"
network "k8s.io/api/networking/v1beta1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
func TestValidatePDB(t *testing.T) {
c := conf.Configuration{
Checks: map[string]conf.Severity{
"pdbDisruptionsAllowedGreaterThanZero": conf.SeverityWarning,
},
}
pdb := unstructured.Unstructured{}
res, err := kube.NewGenericResourceFromUnstructured(&pdb)
res.Kind = "PodDisruptionBudget"
actualResult, err := applyNonControllerSchemaChecks(&c, res)
if err != nil {
panic(err)
}
results := actualResult.Results["pdbDisruptionsAllowedGreaterThanZero"]
assert.False(t, results.Success)
assert.Equal(t, conf.SeverityWarning, results.Severity)
assert.Equal(t, "Reliability", results.Category)
assert.EqualValues(t, "disruptionsAllowed is not greater than zero", results.Message)
}
func TestValidateIngress(t *testing.T) {
c := conf.Configuration{
Checks: map[string]conf.Severity{
"tlsSettingsMissing": conf.SeverityWarning,
},
}
tls := network.IngressTLS{
Hosts: []string{"test"},
SecretName: "secret",
}
ingress := network.Ingress{}
ingress.Spec.TLS = []network.IngressTLS{tls}
b, err := json.Marshal(ingress)
if err != nil {
panic(err)
}
unst := unstructured.Unstructured{}
err = json.Unmarshal(b, &unst.Object)
if err != nil {
panic(err)
}
res, err := kube.NewGenericResourceFromUnstructured(&unst)
if err != nil {
panic(err)
}
res.Kind = "Ingress"
actualResult, err := applyNonControllerSchemaChecks(&c, res)
if err != nil {
panic(err)
}
results := actualResult.Results["tlsSettingsMissing"]
assert.True(t, results.Success)
assert.Equal(t, conf.SeverityWarning, results.Severity)
assert.Equal(t, "Security", results.Category)
assert.EqualValues(t, "Ingress has TLS configured", results.Message)
}
-58
View File
@@ -1,58 +0,0 @@
// Copyright 2019 FairwindsOps Inc
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package validator
import (
"github.com/fairwindsops/polaris/pkg/config"
"github.com/fairwindsops/polaris/pkg/kube"
corev1 "k8s.io/api/core/v1"
)
// ValidateContainer validates a single container from a given controller
func ValidateContainer(conf *config.Configuration, controller kube.GenericWorkload, container *corev1.Container, isInit bool) (ContainerResult, error) {
results, err := applyContainerSchemaChecks(conf, controller, container, isInit)
if err != nil {
return ContainerResult{}, err
}
cRes := ContainerResult{
Name: container.Name,
Results: results,
}
return cRes, nil
}
// ValidateAllContainers validates both init and regular containers
func ValidateAllContainers(conf *config.Configuration, controller kube.GenericWorkload) ([]ContainerResult, error) {
results := []ContainerResult{}
pod := controller.PodSpec
for _, container := range pod.InitContainers {
result, err := ValidateContainer(conf, controller, &container, true)
if err != nil {
return nil, err
}
results = append(results, result)
}
for _, container := range pod.Containers {
result, err := ValidateContainer(conf, controller, &container, false)
if err != nil {
return nil, err
}
results = append(results, result)
}
return results, nil
}
+6 -6
View File
@@ -50,8 +50,8 @@ exemptions:
- foo
`
func getEmptyWorkload(t *testing.T, name string) kube.GenericWorkload {
workload, err := kube.NewGenericWorkloadFromPod(corev1.Pod{
func getEmptyWorkload(t *testing.T, name string) kube.GenericResource {
workload, err := kube.NewGenericResourceFromPod(corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
@@ -64,7 +64,7 @@ func testValidate(t *testing.T, container *corev1.Container, resourceConf *strin
testValidateWithWorkload(t, container, resourceConf, getEmptyWorkload(t, controllerName), expectedDangers, expectedWarnings, expectedSuccesses)
}
func testValidateWithWorkload(t *testing.T, container *corev1.Container, resourceConf *string, workload kube.GenericWorkload, expectedDangers []ResultMessage, expectedWarnings []ResultMessage, expectedSuccesses []ResultMessage) {
func testValidateWithWorkload(t *testing.T, container *corev1.Container, resourceConf *string, workload kube.GenericResource, expectedDangers []ResultMessage, expectedWarnings []ResultMessage, expectedSuccesses []ResultMessage) {
parsedConf, err := conf.Parse([]byte(*resourceConf))
assert.NoError(t, err, "Expected no error when parsing config")
@@ -921,7 +921,7 @@ func TestValidateSecurity(t *testing.T) {
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
workload, err := kube.NewGenericWorkloadFromPod(corev1.Pod{Spec: *tt.pod}, nil)
workload, err := kube.NewGenericResourceFromPod(corev1.Pod{Spec: *tt.pod}, nil)
assert.NoError(t, err)
results, err := applyContainerSchemaChecks(&conf.Configuration{Checks: tt.securityConf}, workload, tt.container, false)
if err != nil {
@@ -1066,7 +1066,7 @@ func TestValidateRunAsRoot(t *testing.T) {
}
for idx, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
workload, err := kube.NewGenericWorkloadFromPod(corev1.Pod{Spec: *tt.pod}, nil)
workload, err := kube.NewGenericResourceFromPod(corev1.Pod{Spec: *tt.pod}, nil)
assert.NoError(t, err)
results, err := applyContainerSchemaChecks(&config, workload, tt.container, false)
if err != nil {
@@ -1168,7 +1168,7 @@ func TestValidateResourcesEmptyContainerCPURequestsExempt(t *testing.T) {
expectedSuccesses := []ResultMessage{}
workload, err := kube.NewGenericWorkloadFromPod(corev1.Pod{
workload, err := kube.NewGenericResourceFromPod(corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
Annotations: map[string]string{
-64
View File
@@ -1,64 +0,0 @@
// Copyright 2019 FairwindsOps Inc
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package validator
import (
"github.com/sirupsen/logrus"
conf "github.com/fairwindsops/polaris/pkg/config"
"github.com/fairwindsops/polaris/pkg/kube"
)
// ValidateController validates a single controller, returns a Result.
func ValidateController(conf *conf.Configuration, controller kube.GenericWorkload) (Result, error) {
podResult, err := ValidatePod(conf, controller)
if err != nil {
return Result{}, err
}
var controllerResult ResultSet
controllerResult, err = applyControllerSchemaChecks(conf, controller)
if err != nil {
return Result{}, err
}
result := Result{
Kind: controller.Kind,
Name: controller.ObjectMeta.GetName(),
Namespace: controller.ObjectMeta.GetNamespace(),
Results: controllerResult,
PodResult: &podResult,
}
return result, nil
}
// ValidateControllers validates that each deployment conforms to the Polaris config,
// builds a list of ResourceResults organized by namespace.
func ValidateControllers(config *conf.Configuration, kubeResources *kube.ResourceProvider) ([]Result, error) {
controllersToAudit := kubeResources.Controllers
results := []Result{}
for _, controller := range controllersToAudit {
result, err := ValidateController(config, controller)
if err != nil {
logrus.Warn("An error occurred validating controller:", err)
return nil, err
}
results = append(results, result)
}
return results, nil
}
+37 -41
View File
@@ -34,7 +34,7 @@ func TestValidateController(t *testing.T) {
"hostPIDSet": conf.SeverityDanger,
},
}
deployment, err := kube.NewGenericWorkloadFromPod(test.MockPod(), nil)
deployment, err := kube.NewGenericResourceFromPod(test.MockPod(), nil)
assert.NoError(t, err)
deployment.Kind = "Deployment"
expectedSum := CountSummary{
@@ -49,7 +49,7 @@ func TestValidateController(t *testing.T) {
}
var actualResult Result
actualResult, err = ValidateController(&c, deployment)
actualResult, err = applyControllerSchemaChecks(&c, deployment)
if err != nil {
panic(err)
}
@@ -72,33 +72,31 @@ func TestControllerLevelChecks(t *testing.T) {
Severity: "danger",
Category: "Reliability",
}
for _, controller := range res.Controllers {
if controller.Kind == "Deployment" {
actualResult, err := ValidateController(&c, controller)
if err != nil {
panic(err)
}
if controller.ObjectMeta.GetName() == "test-deployment-2" {
expectedResult.Success = true
expectedResult.Message = "Multiple replicas are scheduled"
} else if controller.ObjectMeta.GetName() == "test-deployment" {
expectedResult.Success = false
expectedResult.Message = "Only one replica is scheduled"
}
expectedResults := ResultSet{
"multipleReplicasForDeployment": expectedResult,
}
assert.Equal(t, "Deployment", actualResult.Kind)
assert.Equal(t, 1, len(actualResult.Results), "should be equal")
assert.EqualValues(t, expectedResults, actualResult.Results, controller.ObjectMeta.GetName())
for _, controller := range res.Resources["Deployment"] {
actualResult, err := applyControllerSchemaChecks(&c, controller)
if err != nil {
panic(err)
}
if controller.ObjectMeta.GetName() == "test-deployment-2" {
expectedResult.Success = true
expectedResult.Message = "Multiple replicas are scheduled"
} else if controller.ObjectMeta.GetName() == "test-deployment" {
expectedResult.Success = false
expectedResult.Message = "Only one replica is scheduled"
}
expectedResults := ResultSet{
"multipleReplicasForDeployment": expectedResult,
}
assert.Equal(t, "Deployment", actualResult.Kind)
assert.Equal(t, 1, len(actualResult.Results), "should be equal")
assert.EqualValues(t, expectedResults, actualResult.Results, controller.ObjectMeta.GetName())
}
}
res, err := kube.CreateResourceProviderFromPath("../kube/test_files/test_1")
assert.Equal(t, nil, err, "Error should be nil")
assert.Equal(t, 9, len(res.Controllers), "Should have eight controllers")
assert.Equal(t, 11, res.Resources.GetLength())
testResources(res)
replicaSpec := map[string]interface{}{"replicas": 2}
@@ -111,9 +109,9 @@ func TestControllerLevelChecks(t *testing.T) {
two := int32(2)
d2.Spec.Replicas = &two
k8s, dynamicClient := test.SetupTestAPI(&d1, &p1, &d2, &p2)
res, err = kube.CreateResourceProviderFromAPI(context.Background(), k8s, "test", &dynamicClient)
res, err = kube.CreateResourceProviderFromAPI(context.Background(), k8s, "test", &dynamicClient, conf.Configuration{})
assert.Equal(t, err, nil, "error should be nil")
assert.Equal(t, 2, len(res.Controllers), "Should have two controllers")
assert.Equal(t, 2, res.Resources.GetLength(), "Should have two controllers")
testResources(res)
}
@@ -126,7 +124,7 @@ func TestSkipHealthChecks(t *testing.T) {
}
pod := test.MockPod()
pod.Spec.InitContainers = []corev1.Container{test.MockContainer("test")}
deployment, err := kube.NewGenericWorkloadFromPod(pod, nil)
deployment, err := kube.NewGenericResourceFromPod(pod, nil)
assert.NoError(t, err)
deployment.Kind = "Deployment"
expectedSum := CountSummary{
@@ -139,7 +137,7 @@ func TestSkipHealthChecks(t *testing.T) {
"livenessProbeMissing": {ID: "livenessProbeMissing", Message: "Liveness probe should be configured", Success: false, Severity: "warning", Category: "Reliability"},
}
var actualResult Result
actualResult, err = ValidateController(&c, deployment)
actualResult, err = applyControllerSchemaChecks(&c, deployment)
if err != nil {
panic(err)
}
@@ -149,7 +147,7 @@ func TestSkipHealthChecks(t *testing.T) {
assert.EqualValues(t, ResultSet{}, actualResult.PodResult.ContainerResults[0].Results)
assert.EqualValues(t, expectedResults, actualResult.PodResult.ContainerResults[1].Results)
job, err := kube.NewGenericWorkloadFromPod(test.MockPod(), nil)
job, err := kube.NewGenericResourceFromPod(test.MockPod(), nil)
assert.NoError(t, err)
job.Kind = "Job"
expectedSum = CountSummary{
@@ -158,7 +156,7 @@ func TestSkipHealthChecks(t *testing.T) {
Dangers: uint(0),
}
expectedResults = ResultSet{}
actualResult, err = ValidateController(&c, job)
actualResult, err = applyControllerSchemaChecks(&c, job)
if err != nil {
panic(err)
}
@@ -167,7 +165,7 @@ func TestSkipHealthChecks(t *testing.T) {
assert.EqualValues(t, expectedSum, actualResult.GetSummary())
assert.EqualValues(t, expectedResults, actualResult.PodResult.ContainerResults[0].Results)
cronjob, err := kube.NewGenericWorkloadFromPod(test.MockPod(), nil)
cronjob, err := kube.NewGenericResourceFromPod(test.MockPod(), nil)
assert.NoError(t, err)
cronjob.Kind = "CronJob"
expectedSum = CountSummary{
@@ -176,7 +174,7 @@ func TestSkipHealthChecks(t *testing.T) {
Dangers: uint(0),
}
expectedResults = ResultSet{}
actualResult, err = ValidateController(&c, cronjob)
actualResult, err = applyControllerSchemaChecks(&c, cronjob)
if err != nil {
panic(err)
}
@@ -203,18 +201,16 @@ func TestControllerExemptions(t *testing.T) {
Warnings: uint(0),
Dangers: uint(0),
}
var actualResults []Result
pod := test.MockPod()
pod.ObjectMeta.Namespace = "foo"
workload, err := kube.NewGenericWorkloadFromPod(pod, nil)
workload, err := kube.NewGenericResourceFromPod(pod, nil)
assert.NoError(t, err)
workload.Kind = "Deployment"
resources := &kube.ResourceProvider{
Controllers: []kube.GenericWorkload{workload},
}
resources := []kube.GenericResource{workload}
actualResults, err = ValidateControllers(&c, resources)
var actualResults []Result
actualResults, err = ApplyAllSchemaChecksToAllResources(&c, resources)
if err != nil {
panic(err)
}
@@ -225,7 +221,7 @@ func TestControllerExemptions(t *testing.T) {
c.Exemptions = []conf.Exemption{{
Namespace: "foo",
}}
actualResults, err = ValidateControllers(&c, resources)
actualResults, err = ApplyAllSchemaChecksToAllResources(&c, resources)
if err != nil {
panic(err)
}
@@ -234,10 +230,10 @@ func TestControllerExemptions(t *testing.T) {
assert.EqualValues(t, expectedExemptSum, actualResults[0].GetSummary())
c.Exemptions = nil
resources.Controllers[0].ObjectMeta.SetAnnotations(map[string]string{
resources[0].ObjectMeta.SetAnnotations(map[string]string{
exemptionAnnotationKey: "true",
})
actualResults, err = ValidateControllers(&c, resources)
actualResults, err = ApplyAllSchemaChecksToAllResources(&c, resources)
if err != nil {
panic(err)
}
@@ -246,7 +242,7 @@ func TestControllerExemptions(t *testing.T) {
assert.EqualValues(t, expectedExemptSum, actualResults[0].GetSummary())
c.DisallowExemptions = true
actualResults, err = ValidateControllers(&c, resources)
actualResults, err = ApplyAllSchemaChecksToAllResources(&c, resources)
if err != nil {
panic(err)
}
+8 -12
View File
@@ -22,17 +22,14 @@ func RunAudit(config conf.Configuration, kubeResources *kube.ResourceProvider, o
displayName = kubeResources.SourceName
}
results, err := ValidateControllers(&config, kubeResources)
if err != nil {
return AuditData{}, err
results := []Result{}
for _, resources := range kubeResources.Resources {
kindResults, err := ApplyAllSchemaChecksToAllResources(&config, resources)
if err != nil {
return AuditData{}, err
}
results = append(results, kindResults...)
}
controllerCount := len(results)
ingressResults, err := ValidateIngresses(&config, kubeResources)
if err != nil {
return AuditData{}, err
}
results = append(results, ingressResults...)
auditData := AuditData{
PolarisOutputVersion: PolarisOutputVersion,
@@ -43,9 +40,8 @@ func RunAudit(config conf.Configuration, kubeResources *kube.ResourceProvider, o
ClusterInfo: ClusterInfo{
Version: kubeResources.ServerVersion,
Nodes: len(kubeResources.Nodes),
Pods: len(kubeResources.Controllers), // TODO validate that this is still valuable
Namespaces: len(kubeResources.Namespaces),
Controllers: controllerCount,
Controllers: kubeResources.Resources.GetNumberOfControllers(),
},
Results: results,
}
+8 -7
View File
@@ -11,11 +11,6 @@ import (
)
func TestGetTemplateData(t *testing.T) {
k8s, dynamicClient := test.SetupTestAPI(test.GetMockControllers("test")...)
resources, err := kube.CreateResourceProviderFromAPI(context.Background(), k8s, "test", &dynamicClient)
assert.Equal(t, err, nil, "error should be nil")
assert.Equal(t, 5, len(resources.Controllers))
c := conf.Configuration{
Checks: map[string]conf.Severity{
"readinessProbeMissing": conf.SeverityDanger,
@@ -23,6 +18,11 @@ func TestGetTemplateData(t *testing.T) {
},
}
k8s, dynamicClient := test.SetupTestAPI(test.GetMockControllers("test")...)
resources, err := kube.CreateResourceProviderFromAPI(context.Background(), k8s, "test", &dynamicClient, c)
assert.Equal(t, err, nil, "error should be nil")
assert.Equal(t, 5, len(resources.Resources))
sum := CountSummary{
Successes: uint(0),
Warnings: uint(3),
@@ -57,8 +57,9 @@ func TestGetTemplateData(t *testing.T) {
continue
}
found = true
assert.Equal(t, 1, len(result.PodResult.ContainerResults))
assert.Equal(t, expected.results, len(result.PodResult.ContainerResults[0].Results))
if assert.Equal(t, 1, len(result.PodResult.ContainerResults), "bad container results for "+result.Kind) {
assert.Equal(t, expected.results, len(result.PodResult.ContainerResults[0].Results))
}
}
assert.Equal(t, found, true)
}
-51
View File
@@ -1,51 +0,0 @@
// Copyright 2019 FairwindsOps Inc
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package validator
import (
conf "github.com/fairwindsops/polaris/pkg/config"
"github.com/fairwindsops/polaris/pkg/kube"
"k8s.io/api/extensions/v1beta1"
)
// ValidateIngresses validates all the ingresses in a ResourceProvider
func ValidateIngresses(config *conf.Configuration, kubeResources *kube.ResourceProvider) ([]Result, error) {
var results []Result
for _, ingress := range kubeResources.Ingresses {
result, err := ValidateIngress(config, ingress)
if err != nil {
return []Result{}, err
}
results = append(results, result)
}
return results, nil
}
// ValidateIngress validates a single ingress
func ValidateIngress(config *conf.Configuration, ingress v1beta1.Ingress) (Result, error) {
results, err := applyIngressSchemaChecks(config, ingress)
if err != nil {
return Result{}, err
}
result := Result{
Kind: "Ingress",
Name: ingress.ObjectMeta.GetName(),
Namespace: ingress.ObjectMeta.GetNamespace(),
Results: results,
}
return result, nil
}
-64
View File
@@ -1,64 +0,0 @@
// Copyright 2019 FairwindsOps Inc
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package validator
import (
"testing"
conf "github.com/fairwindsops/polaris/pkg/config"
"github.com/fairwindsops/polaris/test"
"github.com/stretchr/testify/assert"
extv1beta1 "k8s.io/api/extensions/v1beta1"
)
func TestValidateIngress(t *testing.T) {
c := conf.Configuration{
Checks: map[string]conf.Severity{
"tlsSettingsMissing": conf.SeverityWarning,
},
}
ingress := test.MockIngress()
var actualResult Result
actualResult, err := ValidateIngress(&c, ingress)
if err != nil {
panic(err)
}
results := actualResult.Results["tlsSettingsMissing"]
assert.False(t, results.Success)
assert.Equal(t, conf.Severity("warning"), results.Severity)
assert.Equal(t, "Security", results.Category)
assert.EqualValues(t, "Ingress does not have TLS configured", results.Message)
tls := extv1beta1.IngressTLS{
Hosts: []string{"test"},
SecretName: "secret",
}
ingress.Spec.TLS = []extv1beta1.IngressTLS{tls}
actualResult, err = ValidateIngress(&c, ingress)
if err != nil {
panic(err)
}
results = actualResult.Results["tlsSettingsMissing"]
assert.True(t, results.Success)
assert.Equal(t, conf.Severity("warning"), results.Severity)
assert.Equal(t, "Security", results.Category)
assert.EqualValues(t, "Ingress has TLS configured", results.Message)
}
+85
View File
@@ -15,8 +15,11 @@
package validator
import (
"fmt"
"time"
"github.com/fatih/color"
"github.com/fairwindsops/polaris/pkg/config"
)
@@ -25,6 +28,17 @@ const (
PolarisOutputVersion = "1.0"
)
var (
successMessage = "🎉 Success"
dangerMessage = "❌ Danger"
warningMessage = "😬 Warning"
)
var (
titleColor = color.New(color.FgBlue).Add(color.Bold)
checkColor = color.New(color.FgCyan)
)
// AuditData contains all the data from a full Polaris audit
type AuditData struct {
PolarisOutputVersion string
@@ -114,3 +128,74 @@ type ContainerResult struct {
func (res *ContainerResult) removeSuccessfulResults() {
res.Results.removeSuccessfulResults()
}
func fillString(id string, l int) string {
for len(id) < l {
id += " "
}
return id
}
// GetPrettyOutput returns a human-readable string
func (res AuditData) GetPrettyOutput(useColor bool) string {
color.NoColor = !useColor
str := titleColor.Sprint(fmt.Sprintf("\n\nPolaris audited %s %s at %s\n", res.SourceType, res.SourceName, res.AuditTime))
str += color.CyanString(fmt.Sprintf(" Nodes: %d | Namespaces: %d | Controllers: %d\n", res.ClusterInfo.Nodes, res.ClusterInfo.Namespaces, res.ClusterInfo.Controllers))
str += color.GreenString(fmt.Sprintf(" Final score: %d\n", res.Score))
str += "\n"
for _, result := range res.Results {
str += result.GetPrettyOutput() + "\n"
}
color.NoColor = false
return str
}
// GetPrettyOutput returns a human-readable string
func (res Result) GetPrettyOutput() string {
str := titleColor.Sprint(fmt.Sprintf("%s %s in namespace %s\n", res.Kind, res.Name, res.Namespace))
str += res.Results.GetPrettyOutput()
if res.PodResult != nil {
str += res.PodResult.GetPrettyOutput()
}
return str
}
// GetPrettyOutput returns a human-readable string
func (res PodResult) GetPrettyOutput() string {
str := res.Results.GetPrettyOutput()
for _, cont := range res.ContainerResults {
str += cont.GetPrettyOutput() + "\n"
}
return str
}
// GetPrettyOutput returns a human-readable string
func (res ContainerResult) GetPrettyOutput() string {
str := titleColor.Sprint(fmt.Sprintf(" Container %s\n", res.Name))
str += res.Results.GetPrettyOutput()
return str
}
const minIDLength = 40
// GetPrettyOutput returns a human-readable string
func (res ResultSet) GetPrettyOutput() string {
indent := " "
str := ""
for _, msg := range res {
status := color.GreenString(successMessage)
if !msg.Success {
if msg.Severity == config.SeverityWarning {
status = color.YellowString(warningMessage)
} else {
status = color.RedString(dangerMessage)
}
}
if color.NoColor {
status = status[2:] // remove emoji
}
str += fmt.Sprintf("%s%s %s\n", indent, checkColor.Sprint(fillString(msg.ID, minIDLength-len(indent))), status)
str += fmt.Sprintf("%s %s - %s\n", indent, msg.Category, msg.Message)
}
return str
}
-38
View File
@@ -1,38 +0,0 @@
// Copyright 2019 FairwindsOps Inc
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package validator
import (
"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)
if err != nil {
return PodResult{}, err
}
pRes := PodResult{
Results: podResults,
ContainerResults: []ContainerResult{},
}
pRes.ContainerResults, err = ValidateAllContainers(conf, controller)
if err != nil {
return pRes, err
}
return pRes, nil
}
+20 -25
View File
@@ -36,7 +36,7 @@ func TestValidatePod(t *testing.T) {
}
p := test.MockPod()
deployment, err := kube.NewGenericWorkloadFromPod(p, nil)
deployment, err := kube.NewGenericResourceFromPod(p, nil)
assert.NoError(t, err)
expectedSum := CountSummary{
Successes: uint(4),
@@ -50,15 +50,14 @@ func TestValidatePod(t *testing.T) {
"hostPIDSet": {ID: "hostPIDSet", Message: "Host PID is not configured", Success: true, Severity: "danger", Category: "Security"},
}
var actualPodResult PodResult
actualPodResult, err = ValidatePod(&c, deployment)
actualPodResult, err := applyControllerSchemaChecks(&c, deployment)
if err != nil {
panic(err)
}
assert.Equal(t, 1, len(actualPodResult.ContainerResults), "should be equal")
assert.Equal(t, 1, len(actualPodResult.PodResult.ContainerResults), "should be equal")
assert.EqualValues(t, expectedSum, actualPodResult.GetSummary())
assert.EqualValues(t, expectedResults, actualPodResult.Results)
assert.EqualValues(t, expectedResults, actualPodResult.PodResult.Results)
}
func TestInvalidIPCPod(t *testing.T) {
@@ -73,7 +72,7 @@ func TestInvalidIPCPod(t *testing.T) {
p := test.MockPod()
p.Spec.HostIPC = true
workload, err := kube.NewGenericWorkloadFromPod(p, nil)
workload, err := kube.NewGenericResourceFromPod(p, nil)
assert.NoError(t, err)
expectedSum := CountSummary{
Successes: uint(3),
@@ -86,15 +85,14 @@ func TestInvalidIPCPod(t *testing.T) {
"hostPIDSet": {ID: "hostPIDSet", Message: "Host PID is not configured", Success: true, Severity: "danger", Category: "Security"},
}
var actualPodResult PodResult
actualPodResult, err = ValidatePod(&c, workload)
actualPodResult, err := applyControllerSchemaChecks(&c, workload)
if err != nil {
panic(err)
}
assert.Equal(t, 1, len(actualPodResult.ContainerResults), "should be equal")
assert.Equal(t, 1, len(actualPodResult.PodResult.ContainerResults), "should be equal")
assert.EqualValues(t, expectedSum, actualPodResult.GetSummary())
assert.EqualValues(t, expectedResults, actualPodResult.Results)
assert.EqualValues(t, expectedResults, actualPodResult.PodResult.Results)
}
func TestInvalidNetworkPod(t *testing.T) {
@@ -109,7 +107,7 @@ func TestInvalidNetworkPod(t *testing.T) {
p := test.MockPod()
p.Spec.HostNetwork = true
workload, err := kube.NewGenericWorkloadFromPod(p, nil)
workload, err := kube.NewGenericResourceFromPod(p, nil)
assert.NoError(t, err)
expectedSum := CountSummary{
Successes: uint(3),
@@ -123,15 +121,14 @@ func TestInvalidNetworkPod(t *testing.T) {
"hostPIDSet": {ID: "hostPIDSet", Message: "Host PID is not configured", Success: true, Severity: "danger", Category: "Security"},
}
var actualPodResult PodResult
actualPodResult, err = ValidatePod(&c, workload)
actualPodResult, err := applyControllerSchemaChecks(&c, workload)
if err != nil {
panic(err)
}
assert.Equal(t, 1, len(actualPodResult.ContainerResults), "should be equal")
assert.Equal(t, 1, len(actualPodResult.PodResult.ContainerResults), "should be equal")
assert.EqualValues(t, expectedSum, actualPodResult.GetSummary())
assert.EqualValues(t, expectedResults, actualPodResult.Results)
assert.EqualValues(t, expectedResults, actualPodResult.PodResult.Results)
}
func TestInvalidPIDPod(t *testing.T) {
@@ -146,7 +143,7 @@ func TestInvalidPIDPod(t *testing.T) {
p := test.MockPod()
p.Spec.HostPID = true
workload, err := kube.NewGenericWorkloadFromPod(p, nil)
workload, err := kube.NewGenericResourceFromPod(p, nil)
assert.NoError(t, err)
expectedSum := CountSummary{
Successes: uint(3),
@@ -160,15 +157,14 @@ func TestInvalidPIDPod(t *testing.T) {
"hostNetworkSet": {ID: "hostNetworkSet", Message: "Host network is not configured", Success: true, Severity: "warning", Category: "Security"},
}
var actualPodResult PodResult
actualPodResult, err = ValidatePod(&c, workload)
actualPodResult, err := applyControllerSchemaChecks(&c, workload)
if err != nil {
panic(err)
}
assert.Equal(t, 1, len(actualPodResult.ContainerResults), "should be equal")
assert.Equal(t, 1, len(actualPodResult.PodResult.ContainerResults), "should be equal")
assert.EqualValues(t, expectedSum, actualPodResult.GetSummary())
assert.EqualValues(t, expectedResults, actualPodResult.Results)
assert.EqualValues(t, expectedResults, actualPodResult.PodResult.Results)
}
func TestExemption(t *testing.T) {
@@ -192,7 +188,7 @@ func TestExemption(t *testing.T) {
p.ObjectMeta = metav1.ObjectMeta{
Name: "foo",
}
workload, err := kube.NewGenericWorkloadFromPod(p, nil)
workload, err := kube.NewGenericResourceFromPod(p, nil)
assert.NoError(t, err)
expectedSum := CountSummary{
Successes: uint(3),
@@ -204,13 +200,12 @@ func TestExemption(t *testing.T) {
"hostPIDSet": {ID: "hostPIDSet", Message: "Host PID is not configured", Success: true, Severity: "danger", Category: "Security"},
}
var actualPodResult PodResult
actualPodResult, err = ValidatePod(&c, workload)
actualPodResult, err := applyControllerSchemaChecks(&c, workload)
if err != nil {
panic(err)
}
assert.Equal(t, 1, len(actualPodResult.ContainerResults), "should be equal")
assert.Equal(t, 1, len(actualPodResult.PodResult.ContainerResults), "should be equal")
assert.EqualValues(t, expectedSum, actualPodResult.GetSummary())
assert.EqualValues(t, expectedResults, actualPodResult.Results)
assert.EqualValues(t, expectedResults, actualPodResult.PodResult.Results)
}
+151 -82
View File
@@ -9,7 +9,6 @@ import (
"github.com/gobuffalo/packr/v2"
corev1 "k8s.io/api/core/v1"
"k8s.io/api/extensions/v1beta1"
metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/yaml"
@@ -46,11 +45,19 @@ var (
"dangerousCapabilities",
"insecureCapabilities",
"priorityClassNotSet",
// Ingress checks
// Other checks
"tlsSettingsMissing",
"pdbDisruptionsAllowedGreaterThanZero",
}
)
type schemaTestCase struct {
Target config.TargetKind
Resource kube.GenericResource
IsInitContianer bool
Container *corev1.Container
}
func init() {
schemaBox = packr.New("Schemas", "../../checks")
for _, checkID := range checkOrder {
@@ -81,7 +88,10 @@ func parseCheck(rawBytes []byte) (config.SchemaCheck, error) {
}
}
func resolveCheck(conf *config.Configuration, checkID, kind string, target config.TargetKind, meta metaV1.Object, containerName string, isInitContainer bool) (*config.SchemaCheck, error) {
func resolveCheck(conf *config.Configuration, checkID string, test schemaTestCase) (*config.SchemaCheck, error) {
if !conf.DisallowExemptions && hasExemptionAnnotation(test.Resource.ObjectMeta, checkID) {
return nil, nil
}
check, ok := conf.CustomChecks[checkID]
if !ok {
check, ok = builtInChecks[checkID]
@@ -90,12 +100,14 @@ func resolveCheck(conf *config.Configuration, checkID, kind string, target confi
return nil, fmt.Errorf("Check %s not found", checkID)
}
namespace := meta.GetNamespace()
name := meta.GetName()
if !conf.IsActionable(check.ID, namespace, name, containerName) {
containerName := ""
if test.Container != nil {
containerName = test.Container.Name
}
if !conf.IsActionable(check.ID, test.Resource.ObjectMeta, containerName) {
return nil, nil
}
if !check.IsActionable(target, kind, isInitContainer) {
if !check.IsActionable(test.Target, test.Resource.Kind, test.IsInitContianer) {
return nil, nil
}
return &check, nil
@@ -119,8 +131,8 @@ func makeResult(conf *config.Configuration, check *config.SchemaCheck, passes bo
const exemptionAnnotationKey = "polaris.fairwinds.com/exempt"
const exemptionAnnotationPattern = "polaris.fairwinds.com/%s-exempt"
func hasExemptionAnnotation(ctrl kube.GenericWorkload, checkID string) bool {
annot := ctrl.ObjectMeta.GetAnnotations()
func hasExemptionAnnotation(objMeta metaV1.Object, checkID string) bool {
annot := objMeta.GetAnnotations()
val := annot[exemptionAnnotationKey]
if strings.ToLower(val) == "true" {
return true
@@ -133,101 +145,158 @@ func hasExemptionAnnotation(ctrl kube.GenericWorkload, checkID string) bool {
return false
}
func applyPodSchemaChecks(conf *config.Configuration, controller kube.GenericWorkload) (ResultSet, error) {
results := ResultSet{}
checkIDs := getSortedKeys(conf.Checks)
for _, checkID := range checkIDs {
if !conf.DisallowExemptions && hasExemptionAnnotation(controller, checkID) {
continue
}
check, err := resolveCheck(conf, checkID, controller.Kind, config.TargetPod, controller.ObjectMeta, "", false)
// ApplyAllSchemaChecksToAllResources applies available checks to a list of resources
func ApplyAllSchemaChecksToAllResources(conf *config.Configuration, resources []kube.GenericResource) ([]Result, error) {
results := []Result{}
for _, resource := range resources {
result, err := ApplyAllSchemaChecks(conf, resource)
if err != nil {
return nil, err
} else if check == nil {
continue
return results, err
}
passes, err := check.CheckPod(&controller.PodSpec)
if err != nil {
return nil, err
}
results[check.ID] = makeResult(conf, check, passes)
results = append(results, result)
}
return results, nil
}
func applyControllerSchemaChecks(conf *config.Configuration, controller kube.GenericWorkload) (ResultSet, error) {
// ApplyAllSchemaChecks applies available checks to a single resource
func ApplyAllSchemaChecks(conf *config.Configuration, resource kube.GenericResource) (Result, error) {
if resource.PodSpec == nil {
return applyNonControllerSchemaChecks(conf, resource)
}
return applyControllerSchemaChecks(conf, resource)
}
func applyNonControllerSchemaChecks(conf *config.Configuration, resource kube.GenericResource) (Result, error) {
finalResult := Result{
Kind: resource.Kind,
Name: resource.ObjectMeta.GetName(),
Namespace: resource.ObjectMeta.GetNamespace(),
}
resultSet, err := applyTopLevelSchemaChecks(conf, resource, false)
finalResult.Results = resultSet
return finalResult, err
}
func applyControllerSchemaChecks(conf *config.Configuration, resource kube.GenericResource) (Result, error) {
finalResult := Result{
Kind: resource.Kind,
Name: resource.ObjectMeta.GetName(),
Namespace: resource.ObjectMeta.GetNamespace(),
}
resultSet, err := applyTopLevelSchemaChecks(conf, resource, true)
if err != nil {
return finalResult, err
}
finalResult.Results = resultSet
podRS, err := applyPodSchemaChecks(conf, resource)
if err != nil {
return finalResult, err
}
podRes := PodResult{
Results: podRS,
ContainerResults: []ContainerResult{},
}
finalResult.PodResult = &podRes
for _, container := range resource.PodSpec.InitContainers {
results, err := applyContainerSchemaChecks(conf, resource, &container, true)
if err != nil {
return finalResult, err
}
cRes := ContainerResult{
Name: container.Name,
Results: results,
}
podRes.ContainerResults = append(podRes.ContainerResults, cRes)
}
for _, container := range resource.PodSpec.Containers {
results, err := applyContainerSchemaChecks(conf, resource, &container, false)
if err != nil {
return finalResult, err
}
cRes := ContainerResult{
Name: container.Name,
Results: results,
}
podRes.ContainerResults = append(podRes.ContainerResults, cRes)
}
return finalResult, nil
}
func applyTopLevelSchemaChecks(conf *config.Configuration, res kube.GenericResource, isController bool) (ResultSet, error) {
test := schemaTestCase{
Resource: res,
}
if isController {
test.Target = config.TargetController
}
return applySchemaChecks(conf, test)
}
func applyPodSchemaChecks(conf *config.Configuration, controller kube.GenericResource) (ResultSet, error) {
test := schemaTestCase{
Target: config.TargetPod,
Resource: controller,
}
return applySchemaChecks(conf, test)
}
func applyContainerSchemaChecks(conf *config.Configuration, controller kube.GenericResource, container *corev1.Container, isInit bool) (ResultSet, error) {
test := schemaTestCase{
Target: config.TargetContainer,
Resource: controller,
Container: container,
IsInitContianer: isInit,
}
return applySchemaChecks(conf, test)
}
func applySchemaChecks(conf *config.Configuration, test schemaTestCase) (ResultSet, error) {
results := ResultSet{}
checkIDs := getSortedKeys(conf.Checks)
for _, checkID := range checkIDs {
if !conf.DisallowExemptions && hasExemptionAnnotation(controller, checkID) {
continue
}
check, err := resolveCheck(conf, checkID, controller.Kind, config.TargetController, controller.ObjectMeta, "", false)
result, err := applySchemaCheck(conf, checkID, test)
if err != nil {
return nil, err
} else if check == nil {
continue
return results, err
}
passes, err := check.CheckController(controller.OriginalObjectJSON)
if err != nil {
return nil, err
if result != nil {
results[checkID] = *result
}
results[check.ID] = makeResult(conf, check, passes)
}
return results, nil
}
func applyContainerSchemaChecks(conf *config.Configuration, controller kube.GenericWorkload, container *corev1.Container, isInit bool) (ResultSet, error) {
results := ResultSet{}
checkIDs := getSortedKeys(conf.Checks)
for _, checkID := range checkIDs {
if !conf.DisallowExemptions && hasExemptionAnnotation(controller, checkID) {
continue
}
check, err := resolveCheck(conf, checkID, controller.Kind, config.TargetContainer, controller.ObjectMeta, container.Name, isInit)
if err != nil {
return nil, err
} else if check == nil {
continue
}
var passes bool
if check.SchemaTarget == config.TargetPod {
podCopy := controller.PodSpec
func applySchemaCheck(conf *config.Configuration, checkID string, test schemaTestCase) (*ResultMessage, error) {
check, err := resolveCheck(conf, checkID, test)
if err != nil {
return nil, err
} else if check == nil {
return nil, nil
}
var passes bool
if check.SchemaTarget != "" {
if check.SchemaTarget == config.TargetPod && check.Target == config.TargetContainer {
podCopy := *test.Resource.PodSpec
podCopy.InitContainers = []corev1.Container{}
podCopy.Containers = []corev1.Container{*container}
podCopy.Containers = []corev1.Container{*test.Container}
passes, err = check.CheckPod(&podCopy)
} else {
passes, err = check.CheckContainer(container)
return nil, fmt.Errorf("Unknown combination of target (%s) and schema target (%s)", check.Target, check.SchemaTarget)
}
if err != nil {
return nil, err
}
results[check.ID] = makeResult(conf, check, passes)
} else if check.Target == config.TargetPod {
passes, err = check.CheckPod(test.Resource.PodSpec)
} else if check.Target == config.TargetContainer {
passes, err = check.CheckContainer(test.Container)
} else {
passes, err = check.CheckObject(test.Resource.Resource.Object)
}
return results, nil
}
func applyIngressSchemaChecks(conf *config.Configuration, ingress v1beta1.Ingress) (ResultSet, error) {
results := ResultSet{}
checkIDs := getSortedKeys(conf.Checks)
for _, checkID := range checkIDs {
check, err := resolveCheck(conf, checkID, ingress.Kind, config.TargetIngress, ingress.ObjectMeta.GetObjectMeta(), "", false)
if err != nil {
return nil, err
} else if check == nil {
continue
}
passes, err := check.CheckObject(ingress)
if err != nil {
return nil, err
}
results[check.ID] = makeResult(conf, check, passes)
if err != nil {
return nil, err
}
return results, nil
result := makeResult(conf, check, passes)
return &result, nil
}
func getSortedKeys(m map[string]config.Severity) []string {
+2 -2
View File
@@ -99,13 +99,13 @@ func (v *Validator) handleInternal(req admission.Request) (*validator.PodResult,
} else {
pod, originalObject, err = GetObjectFromRawRequest(req.Object.Raw)
}
controller, err := kube.NewGenericWorkloadFromPod(pod, originalObject)
controller, err := kube.NewGenericResourceFromPod(pod, originalObject)
if err != nil {
return nil, err
}
controller.Kind = req.AdmissionRequest.Kind.Kind
var controllerResult validator.Result
controllerResult, err = validator.ValidateController(&v.Config, controller)
controllerResult, err = validator.ApplyAllSchemaChecks(&v.Config, controller)
if err != nil {
return nil, err
}
@@ -0,0 +1,17 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: minimal-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
rules:
- http:
paths:
- path: /testpath
pathType: Prefix
backend:
service:
name: test
port:
number: 80
@@ -0,0 +1,21 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: minimal-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
tls:
- secretName: example
hosts:
- example.com
rules:
- http:
paths:
- path: /testpath
pathType: Prefix
backend:
service:
name: test
port:
number: 80
+19 -12
View File
@@ -1,6 +1,7 @@
package test
import (
"fmt"
"io/ioutil"
"path/filepath"
"runtime"
@@ -52,22 +53,28 @@ func init() {
func TestChecks(t *testing.T) {
for _, tc := range testCases {
workload, err := kube.GetWorkloadFromBytes(tc.input)
assert.NoError(t, err)
res, err := kube.NewGenericResourceFromBytes(tc.input)
if err != nil {
fmt.Println("error parsing", string(tc.input))
panic(err)
}
c, err := config.Parse([]byte("checks:\n " + tc.check + ": danger"))
assert.NoError(t, err)
var result validator.Result
result, err = validator.ValidateController(&c, *workload)
result, err := validator.ApplyAllSchemaChecks(&c, res)
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)
total := summary.Successes + summary.Dangers
msg := fmt.Sprintf("Check %s ran %d times instead of 1", tc.check, total)
if assert.Equal(t, uint(1), total, msg) {
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)
}
}
}
}
+5 -20
View File
@@ -19,7 +19,7 @@ import (
"k8s.io/client-go/kubernetes/fake"
)
func newUnstructured(apiVersion, kind, namespace, name string, spec interface{}) unstructured.Unstructured {
func newUnstructured(apiVersion, kind, namespace, name string, spec map[string]interface{}) unstructured.Unstructured {
return unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": apiVersion,
@@ -69,7 +69,7 @@ func MockIngress() extv1beta1.Ingress {
}
// MockController creates a mock controller and pod
func MockController(apiVersion, kind, namespace, name string, spec interface{}, podSpec corev1.PodSpec, dest interface{}) corev1.Pod {
func MockController(apiVersion, kind, namespace, name string, spec map[string]interface{}, podSpec corev1.PodSpec, dest interface{}) corev1.Pod {
unst := newUnstructured(apiVersion, kind, namespace, name, spec)
pod := corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
@@ -143,25 +143,10 @@ func MockJob(namespace, name string) (batchv1.Job, corev1.Pod) {
func MockCronJob(namespace, name string) (batchv1beta1.CronJob, corev1.Pod) {
cj := batchv1beta1.CronJob{}
p := MockPod()
b, err := json.Marshal(p.Spec)
if err != nil {
panic(err)
}
pSpec := map[string]interface{}{}
err = json.Unmarshal(b, &pSpec)
if err != nil {
panic(err)
}
spec := map[string]interface{}{
"job_template": map[string]interface{}{
"spec": map[string]interface{}{
"template": map[string]interface{}{
"spec": pSpec,
},
},
},
}
spec := map[string]interface{}{}
pod := MockController("batch/v1beta1", "CronJob", namespace, name, spec, p.Spec, &cj)
cj.Spec.JobTemplate.Spec.Template.Spec = pod.Spec
return cj, pod
}