Compare commits

..
5 Commits
Author SHA1 Message Date
Robert Brennan df220c9465 update docs from template 2021-01-15 22:06:53 +00:00
Robert Brennan 9e0ed9ed33 Update docs (#472)
* move docs files

* update docs from template

* another update

* add insights callouts

* add ingress check to the docs

* remove old prefixes on check names
2021-01-15 21:21:51 +00:00
Robert Brennan 744e40a888 remove details links (#473) 2021-01-15 21:20:50 +00:00
Robert Brennan 6ca4e8a43d Update README.md (#464) 2021-01-15 21:20:31 +00:00
Robert Brennan 267509e73d Add architecture diagram (#458)
* Add files via upload

* Update README.md

* Update architecture.svg

* Delete architecture.svg

* Add files via upload

* Delete architecture.svg

* Add files via upload

* fix images
2021-01-15 21:20:13 +00:00
165 changed files with 1709 additions and 4367 deletions
+14 -25
View File
@@ -17,7 +17,6 @@ references:
echo 'export PUSH_ALL_VERSION_TAGS=true' >> ${BASH_ENV}
echo 'export GOPROXY=https://proxy.golang.org' >> ${BASH_ENV}
echo 'export GO111MODULE=on' >> ${BASH_ENV}
echo 'export GOFLAGS=-mod=mod' >> ${BASH_ENV}
install_k8s: &install_k8s
run:
@@ -59,6 +58,16 @@ references:
helm install cert-manager jetstack/cert-manager --namespace cert-manager --version 0.16.1 --set "installCRDs=true" --wait
echo "Install cert-manager successful"
# Test scripts
update_coverage: &update_coverage
run:
name: Update Coverage
command: |
if [[ -z $CIRCLE_PR_NUMBER ]]; then
bash <(curl -s https://codecov.io/bash)
else
echo "Skipping coverage for forked PR"
fi
test_binary_dashboard: &test_binary_dashboard
run:
name: Test Dashboard
@@ -148,36 +157,22 @@ jobs:
test:
working_directory: /go/src/github.com/fairwindsops/polaris/
docker:
- image: circleci/golang:1.16
- image: circleci/golang:1.13
steps:
- checkout
- *set_environment_variables
- run: go get -u golang.org/x/lint/golint
- run: go list ./... | grep -v vendor | xargs golint -set_exit_status
- run: go list ./... | grep -v vendor | xargs go vet
- run: go test ./... -coverprofile=coverage.txt -covermode=count
- run: go test ./pkg/... -coverprofile=coverage.txt -covermode=count
- run: go run main.go audit --audit-path ./deploy --set-exit-code-below-score 100 --set-exit-code-on-danger
- *update_coverage
- *test_binary_dashboard
insights:
docker:
- image: quay.io/reactiveops/ci-images:v11.0-stretch
steps:
- checkout
- setup_remote_docker
- run:
name: Adjust configs for latest image
command: |
sed -r "s|'(quay.io/fairwinds/polaris:).+'|'\1${CIRCLE_SHA1}'|" ./deploy/webhook.yaml > ./deploy/dashboard.yaml
sed -r "s|'(quay.io/fairwinds/polaris:).+'|'\1${CIRCLE_SHA1}'|" ./deploy/dashboard.yaml > ./deploy/webhook.yaml
- run:
name: Insights CI
command: curl -L https://insights.fairwinds.com/v0/insights-ci.sh | bash
release_binary:
working_directory: /go/src/github.com/fairwindsops/polaris/
docker:
- image: circleci/golang:1.16
- image: circleci/golang:1.13
steps:
- checkout
- setup_remote_docker
@@ -240,12 +235,6 @@ workflows:
filters:
branches:
ignore: /pull\/[0-9]+/
- insights:
requires:
- push
filters:
branches:
ignore: /pull\/[0-9]+/
- test_k8s:
requires:
- push
-15
View File
@@ -1,15 +0,0 @@
# The action uses an own Dockerfile on purpose because the root Dockerfile takes way too long to build for an action
FROM alpine:3.10
RUN apk add --no-cache \
bash \
ca-certificates \
curl \
wget \
tar \
jq
COPY get_polaris.sh /get_polaris.sh
ENTRYPOINT ["/get_polaris.sh"]
-22
View File
@@ -1,22 +0,0 @@
name: 'Install polaris'
description: 'Download a specific polaris version'
inputs:
version:
description: 'version of polaris'
required: true
default: 'latest'
runs:
using: 'docker'
image: './Dockerfile'
args:
- ${{ inputs.version }}
outputs:
version:
description: 'Version of polaris installed'
branding:
icon: 'download-cloud'
color: 'gray-dark'
@@ -1,19 +0,0 @@
#!/bin/bash
if [[ -z "$INPUT_VERSION" ]]; then
echo "Missing polaris version information"
exit 1
fi
polaris version | grep "$INPUT_VERSION" &> /dev/null
if [ $? == 0 ]; then
echo "Polaris $INPUT_VERSION is already installed! Exiting gracefully."
exit 0
else
echo "Installing polaris to path."
fi
TARGET_FILE="polaris.tar.gz"
curl -LJ -o $TARGET_FILE 'https://github.com/FairwindsOps/polaris/releases/download/'"$INPUT_VERSION"'/polaris_'"$INPUT_VERSION"'_linux_386.tar.gz'
mkdir polaris
tar -xzf $TARGET_FILE -C polaris
rm $TARGET_FILE
echo "polaris" >> $GITHUB_PATH
echo "::set-output name=version::$INPUT_VERSION"
-44
View File
@@ -1,44 +0,0 @@
version: 2
updates:
- package-ecosystem: gomod
directory: "/"
schedule:
interval: weekly
time: "11:00"
ignore:
- dependency-name: cloud.google.com/go
versions:
- ">= 0.57.a, < 0.58"
- dependency-name: github.com/go-logr/logr
versions:
- ">= 0.2.a, < 0.3"
- dependency-name: github.com/go-logr/zapr
versions:
- ">= 0.2.a, < 0.3"
- dependency-name: github.com/googleapis/gnostic
versions:
- ">= 0.4.a, < 0.5"
- dependency-name: github.com/googleapis/gnostic
versions:
- ">= 0.5.a, < 0.6"
- dependency-name: github.com/qri-io/jsonschema
versions:
- ">= 0.2.a, < 0.3"
- dependency-name: k8s.io/api
versions:
- ">= 0.19.a, < 0.20"
- dependency-name: k8s.io/apimachinery
versions:
- ">= 0.19.a, < 0.20"
- dependency-name: k8s.io/client-go
versions:
- ">= 0.19.a, < 0.20"
- dependency-name: sigs.k8s.io/controller-runtime
versions:
- ">= 0.5.a, < 0.6"
- dependency-name: sigs.k8s.io/controller-runtime
versions:
- ">= 0.8.a, < 0.9"
- dependency-name: k8s.io/apimachinery
versions:
- 0.20.4
-18
View File
@@ -1,18 +0,0 @@
daysUntilStale: 30
daysUntilClose: 7
onlyLabels: []
exemptLabels:
- pinned
- security
exemptProjects: false
exemptMilestones: true
exemptAssignees: false
staleLabel: stale
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
limitPerRun: 30
+47
View File
@@ -0,0 +1,47 @@
# This file is generated from FairwindsOps/documentation-template
# DO NOT EDIT MANUALLY
name: Build Website
on:
push:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14.x]
defaults:
run:
working-directory: ./docs-md
env:
CI: true
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- name: Build site
run: npm run build
- name: Check links
run: npm run check-links
- name: Push changes
run: |
username="GitHub Actions"
git config user.email "opensource@fairwinds.com"
git config user.name $username
HAS_CHANGE=$(git diff .)
if [ -n "${HAS_CHANGE}" ]; then
if [ "$(git log -1 --pretty=format:'%an')" == $username ]; then
echo "Build created a diff, but the last commit was a build."
exit 1
fi
git add ../docs/
git commit -m "[CI] rebuild website"
git push -u origin +master:website
fi
-27
View File
@@ -1,27 +0,0 @@
name: Test setup-polaris
on:
pull_request:
branches: [ master ]
jobs:
build-int:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup polaris
uses: ./.github/actions/setup-polaris
with:
version: 3.0.3
- name: Use command
run: polaris version
build-ext:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup polaris
uses: fairwindsops/polaris/.github/actions/setup-polaris@master
with:
version: 3.0.3
- name: Use command
run: polaris version
+2 -5
View File
@@ -7,7 +7,7 @@ changelog:
- '^docs:'
- '^test:'
builds:
- id: polaris
- id: watcher
env:
- CGO_ENABLED=0
- GO111MODULE=on
@@ -15,13 +15,10 @@ builds:
- amd64
- arm
- arm64
- 386
goarm:
- 6
- 7
archives:
- id: polaris
builds: ["polaris"]
name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}"
brews:
- name: polaris
github:
+1 -1
View File
@@ -1 +1 @@
* @rbren @makoscafee
* @rbren @makoscafee @jordandoig @baderbuddy
+2 -2
View File
@@ -1,4 +1,4 @@
FROM golang:1.16 AS build-env
FROM golang:1.13 AS build-env
WORKDIR /go/src/github.com/fairwindsops/polaris/
ENV GO111MODULE=on
@@ -15,7 +15,7 @@ RUN go get -u github.com/gobuffalo/packr/v2/packr2
COPY . .
RUN packr2 build -a -o polaris *.go
FROM alpine:3.14
FROM alpine:3.10
WORKDIR /usr/local/bin
RUN apk --no-cache add ca-certificates
+35 -27
View File
@@ -3,22 +3,19 @@
<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=4.0.8&color=239922">
<img src="https://img.shields.io/static/v1.svg?label=Version&message=3.0.0&color=239922">
</a>
<a href="https://goreportcard.com/report/github.com/FairwindsOps/polaris">
<img src="https://goreportcard.com/badge/github.com/FairwindsOps/polaris">
</a>
<a href="https://circleci.com/gh/FairwindsOps/polaris">
<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.
problems in the future. Polaris can be run in a few different modes:
Polaris can be run in three different modes:
* As a [dashboard](https://polaris.docs.fairwinds.com/dashboard), so you can audit what's running inside your cluster.
@@ -29,30 +26,41 @@ Polaris can be run in three different modes:
<img src="https://polaris.docs.fairwinds.com/img/architecture.svg" alt="Polaris Architecture" width="550"/>
</p>
**Want to learn more?** Reach out on [the Slack channel](https://fairwindscommunity.slack.com/messages/polaris) ([request invite](https://join.slack.com/t/fairwindscommunity/shared_invite/zt-e3c6vj4l-3lIH6dvKqzWII5fSSFDi1g)), send an email to `opensource@fairwinds.com`, or join us for [office hours on Zoom](https://fairwindscommunity.slack.com/messages/office-hours)
## Documentation
Check out the [documentation at docs.fairwinds.com](https://polaris.docs.fairwinds.com)
## Join the Fairwinds Open Source Community
The goal of the Fairwinds Community is to exchange ideas, influence the open source roadmap, and network with fellow Kubernetes users. [Chat with us on Slack](https://join.slack.com/t/fairwindscommunity/shared_invite/zt-e3c6vj4l-3lIH6dvKqzWII5fSSFDi1g) or [join the user group](https://www.fairwinds.com/open-source-software-user-group) to get involved!
## Other Projects from Fairwinds
Enjoying Polaris? Check out some of our other projects:
* [Goldilocks](https://github.com/FairwindsOps/Goldilocks) - Right-size your Kubernetes Deployments by compare your memory and CPU settings against actual usage
* [Pluto](https://github.com/FairwindsOps/Pluto) - Detect Kubernetes resources that have been deprecated or removed in future versions
* [Nova](https://github.com/FairwindsOps/Nova) - Check to see if any of your Helm charts have updates available
* [rbac-manager](https://github.com/FairwindsOps/rbac-manager) - Simplify the management of RBAC in your Kubernetes clusters
## Fairwinds Insights
## Integration with Fairwinds Insights
<p align="center">
<a href="https://www.fairwinds.com/polaris-user-insights-demo?utm_source=polaris&utm_medium=ad&utm_campaign=polarisad">
<img src="https://polaris.docs.fairwinds.com/img/insights-banner.png" alt="Fairwinds Insights" width="550"/>
</a>
<img src="https://polaris.docs.fairwinds.com/img/FW_Insights_Polaris.svg" alt="Fairwinds Insights" width="550"/>
</p>
[Fairwinds Insights](https://www.fairwinds.com/fairwinds-polaris-upgrade)
is a platform for auditing Kubernetes clusters and enforcing policy. If you'd like to:
* manage Polaris across a fleet of clusters
* track findings over time
* send results to services like Slack and Datadog
* add additional checks from tools like
[Trivy](https://github.com/aquasecurity/trivy),
[Goldilocks](https://github.com/FairwindsOps/goldilocks/), and
[OPA](https://www.openpolicyagent.org)
you can sign up for a [free account here](https://insights.fairwinds.com?source=polaris).
## Contributing
PRs welcome! Check out the [Contributing Guidelines](https://polaris.docs.fairwinds.com/contributing) and [Code of Conduct](https://polaris.docs.fairwinds.com/code-of-conduct) for more information.
## Further Information
A history of changes to this project can be viewed in the [Changelog](https://polaris.docs.fairwinds.com/changelog)
If you'd like to learn more about Polaris, or if you'd like to speak with
a Kubernetes expert, you can contact `info@fairwinds.com` or [visit our website](https://fairwinds.com)
---
<p align="center">
<img src="https://polaris.docs.fairwinds.com/img/dashboard-screenshot.png" alt="Polaris Dashboard" width="550"/>
</p>
If you're interested in running Polaris in multiple clusters,
tracking the results over time, integrating with Slack, Datadog, and Jira,
or unlocking other functionality, check out
[Fairwinds Insights](https://www.fairwinds.com/polaris-user-insights-demo?utm_source=polaris&utm_medium=polaris&utm_campaign=polaris), a platform for auditing and enforcing policy in Kubernetes clusters.
+9 -10
View File
@@ -14,13 +14,12 @@ schema:
properties:
add:
type: array
allOf:
- not:
contains:
const: ALL
- not:
contains:
const: SYS_ADMIN
- not:
contains:
const: NET_ADMIN
not:
contains:
const: ALL
not:
contains:
const: SYS_ADMIN
not:
contains:
const: NET_ADMIN
+17 -42
View File
@@ -5,52 +5,27 @@ target: Container
schema:
'$schema': http://json-schema.org/draft-07/schema
type: object
required:
- securityContext
properties:
securityContext:
type: object
required:
- capabilities
properties:
capabilities:
type: object
required:
- drop
properties:
drop:
type: array
oneOf:
- contains:
const: ALL
- allOf:
- contains:
const: NET_ADMIN
- contains:
const: CHOWN
- contains:
const: DAC_OVERRIDE
- contains:
const: FSETID
- contains:
const: FOWNER
- contains:
const: MKNOD
- contains:
const: NET_RAW
- contains:
const: SETGID
- contains:
const: SETUID
- contains:
const: SETFCAP
- contains:
const: SETPCAP
- contains:
const: NET_BIND_SERVICE
- contains:
const: SYS_CHROOT
- contains:
const: KILL
- contains:
const: AUDIT_WRITE
add:
enum:
- CHOWN
- DAC_OVERRIDE
- FSETID
- FOWNER
- MKNOD
- NET_RAW
- SETGID
- SETUID
- SETFCAP
- SETPCAP
- NET_BIND_SERVICE
- SYS_CHROOT
- KILL
- AUDIT_WRITE
-17
View File
@@ -1,17 +0,0 @@
successMessage: Label app.kubernetes.io/name matches metadata.name
failureMessage: Label app.kubernetes.io/name must match metadata.name
target: Controller
schema:
'$schema': http://json-schema.org/draft-07/schema
type: object
properties:
metadata:
type: object
required: ["labels"]
properties:
labels:
type: object
required: ["app.kubernetes.io/name"]
properties:
app.kubernetes.io/name:
const: "{{ .metadata.name }}"
-39
View File
@@ -1,39 +0,0 @@
successMessage: A PodDisruptionBudget is attached
failureMessage: Should have a PodDisruptionBudget
category: Reliability
target: Controller
controllers:
include:
- Deployment
schema:
'$schema': http://json-schema.org/draft-07/schema
type: object
properties:
metadata:
type: object
properties:
labels:
type: object
minProperties: 1
additionalSchemaStrings:
policy/PodDisruptionBudget: |
type: object
properties:
spec:
type: object
required: ["selector"]
properties:
selector:
type: object
required: ["matchLabels"]
properties:
matchLabels:
type: object
anyOf:
{{ range $key, $value := .metadata.labels }}
- properties:
"{{ $key }}":
type: string
const: {{ $value }}
required: ["{{ $key }}"]
{{ end }}
+7 -34
View File
@@ -2,42 +2,15 @@ successMessage: Filesystem is read only
failureMessage: Filesystem should be read only
category: Security
target: Container
schemaTarget: Pod
schema:
'$schema': http://json-schema.org/draft-07/schema
definitions:
goodSecurityContext:
type: object
anyOf:
- required:
- readOnlyRootFilesystem
properties:
readOnlyRootFilesystem:
const: true
notBadSecurityContext:
type: object
type: object
required:
- securityContext
properties:
securityContext:
required:
- readOnlyRootFilesystem
properties:
readOnlyRootFilesystem:
const: true
type: object
anyOf:
- required:
- securityContext
properties:
securityContext:
$ref: "#/definitions/goodSecurityContext"
containers:
type: array
items:
properties:
securityContext:
$ref: "#/definitions/notBadSecurityContext"
- properties:
containers:
type: array
items:
required:
- securityContext
properties:
securityContext:
$ref: "#/definitions/goodSecurityContext"
-18
View File
@@ -1,18 +0,0 @@
successMessage: disruptionsAllowed is greater than zero
failureMessage: disruptionsAllowed is not greater than zero
category: Reliability
target: policy/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
+5 -35
View File
@@ -2,42 +2,12 @@ successMessage: Privilege escalation not allowed
failureMessage: Privilege escalation should not be allowed
category: Security
target: Container
schemaTarget: Pod
schema:
'$schema': http://json-schema.org/draft-07/schema
definitions:
goodSecurityContext:
type: object
anyOf:
- required:
- allowPrivilegeEscalation
properties:
allowPrivilegeEscalation:
const: false
notBadSecurityContext:
type: object
type: object
properties:
securityContext:
properties:
allowPrivilegeEscalation:
const: false
type: object
anyOf:
- required:
- securityContext
properties:
securityContext:
$ref: "#/definitions/goodSecurityContext"
containers:
type: array
items:
properties:
securityContext:
$ref: "#/definitions/notBadSecurityContext"
- properties:
containers:
type: array
items:
required:
- securityContext
properties:
securityContext:
$ref: "#/definitions/goodSecurityContext"
not:
const: true
+3 -14
View File
@@ -2,23 +2,12 @@ successMessage: Not running as privileged
failureMessage: Should not be running as privileged
category: Security
target: Container
schemaTarget: Pod
schema:
'$schema': http://json-schema.org/draft-07/schema
definitions:
notBadSecurityContext:
type: object
type: object
properties:
securityContext:
properties:
privileged:
not:
const: true
type: object
properties:
securityContext:
$ref: "#/definitions/notBadSecurityContext"
containers:
type: array
items:
properties:
securityContext:
$ref: "#/definitions/notBadSecurityContext"
-19
View File
@@ -1,19 +0,0 @@
successMessage: Ingress has TLS configured
failureMessage: Ingress does not have TLS configured
category: Security
target: networking.k8s.io/Ingress
schema:
'$schema': http://json-schema.org/draft-07/schema
type: object
required:
- spec
properties:
spec:
type: object
required:
- tls
properties:
tls:
type: array
not:
const: null
+13 -67
View File
@@ -22,8 +22,8 @@ import (
"io/ioutil"
"net/http"
"os"
"os/exec"
conf "github.com/fairwindsops/polaris/pkg/config"
"github.com/fairwindsops/polaris/pkg/kube"
"github.com/fairwindsops/polaris/pkg/validator"
"github.com/sirupsen/logrus"
@@ -32,30 +32,22 @@ import (
)
var setExitCode bool
var onlyShowFailedTests bool
var minScore int
var auditOutputURL string
var auditOutputFile string
var auditOutputFormat string
var resourceToAudit string
var useColor bool
var helmChart string
var helmValues string
func init() {
rootCmd.AddCommand(auditCmd)
auditCmd.PersistentFlags().StringVar(&auditPath, "audit-path", "", "If specified, audits one or more YAML files instead of a cluster.")
auditCmd.PersistentFlags().BoolVar(&setExitCode, "set-exit-code-on-danger", false, "Set an exit code of 3 when the audit contains danger-level issues.")
auditCmd.PersistentFlags().BoolVar(&onlyShowFailedTests, "only-show-failed-tests", false, "If specified, audit output will only show failed tests.")
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, pretty, or score.")
auditCmd.PersistentFlags().BoolVar(&useColor, "color", true, "Whether to use color in pretty format.")
auditCmd.PersistentFlags().StringVarP(&auditOutputFormat, "format", "f", "json", "Output format for results - json, yaml, or score.")
auditCmd.PersistentFlags().StringVar(&displayName, "display-name", "", "An optional identifier for the audit.")
auditCmd.PersistentFlags().StringVar(&resourceToAudit, "resource", "", "Audit a specific resource, in the format namespace/kind/version/name, e.g. nginx-ingress/Deployment.apps/v1/default-backend.")
auditCmd.PersistentFlags().StringVar(&helmChart, "helm-chart", "", "Will fill out Helm template")
auditCmd.PersistentFlags().StringVar(&helmValues, "helm-values", "", "Optional flag to add helm values")
}
var auditCmd = &cobra.Command{
@@ -66,28 +58,8 @@ var auditCmd = &cobra.Command{
if displayName != "" {
config.DisplayName = displayName
}
if helmChart != "" {
var err error
auditPath, err = ProcessHelmTemplates(helmChart, helmValues)
if err != nil {
logrus.Infof("Couldn't process helm chart: %v", err)
os.Exit(1)
}
}
k, err := kube.CreateResourceProvider(context.TODO(), auditPath, resourceToAudit, config)
if err != nil {
logrus.Errorf("Error fetching Kubernetes resources %v", err)
os.Exit(1)
}
auditData, err := validator.RunAudit(config, k)
if err != nil {
logrus.Errorf("Error while running audit on resources: %v", err)
os.Exit(1)
}
outputAudit(auditData, auditOutputFile, auditOutputURL, auditOutputFormat, useColor, onlyShowFailedTests)
auditData := runAndReportAudit(cmd.Context(), config, auditPath, resourceToAudit, auditOutputFile, auditOutputURL, auditOutputFormat)
summary := auditData.GetSummary()
score := summary.GetScore()
@@ -101,55 +73,28 @@ var auditCmd = &cobra.Command{
},
}
// ProcessHelmTemplates turns helm into yaml to be processed by Polaris or the other tools.
func ProcessHelmTemplates(helmChart, helmValues string) (string, error) {
cmd := exec.Command("helm", "dependency", "update", helmChart)
output, err := cmd.CombinedOutput()
func runAndReportAudit(ctx context.Context, c conf.Configuration, auditPath, workload, outputFile, outputURL, outputFormat string) validator.AuditData {
// Create a kubernetes client resource provider
k, err := kube.CreateResourceProvider(ctx, auditPath, workload)
if err != nil {
logrus.Error(string(output))
return "", err
logrus.Errorf("Error fetching Kubernetes resources %v", err)
os.Exit(1)
}
dir, err := ioutil.TempDir("", "*")
if err != nil {
return "", err
}
params := []string{
"template", helmChart,
helmChart,
"--output-dir",
dir,
}
if helmValues != "" {
params = append(params, "--values", helmValues)
}
cmd = exec.Command("helm", params...)
output, err = cmd.CombinedOutput()
auditData, err := validator.RunAudit(ctx, c, k)
if err != nil {
logrus.Error(string(output))
return "", err
logrus.Errorf("Error while running audit on resources: %v", err)
os.Exit(1)
}
return dir, nil
}
func outputAudit(auditData validator.AuditData, outputFile, outputURL, outputFormat string, useColor bool, onlyShowFailedTests bool) {
if onlyShowFailedTests {
auditData = auditData.RemoveSuccessfulResults()
}
var outputBytes []byte
var err error
if outputFormat == "score" {
outputBytes = []byte(fmt.Sprintf("%d\n", auditData.GetSummary().GetScore()))
} else if outputFormat == "yaml" {
var jsonBytes []byte
jsonBytes, err = json.Marshal(auditData)
jsonBytes, err := json.Marshal(auditData)
if err == nil {
outputBytes, err = yaml.JSONToYAML(jsonBytes)
}
} else if outputFormat == "pretty" {
outputBytes = []byte(auditData.GetPrettyOutput(useColor))
} else {
outputBytes, err = json.MarshalIndent(auditData, "", " ")
}
@@ -203,4 +148,5 @@ func outputAudit(auditData validator.AuditData, outputFile, outputURL, outputFor
}
}
}
return auditData
}
+1 -3
View File
@@ -27,12 +27,10 @@ import (
var serverPort int
var basePath string
var loadAuditFile string
var listeningAddress string
func init() {
rootCmd.AddCommand(dashboardCmd)
dashboardCmd.PersistentFlags().IntVarP(&serverPort, "port", "p", 8080, "Port for the dashboard webserver.")
dashboardCmd.PersistentFlags().StringVar(&listeningAddress, "listening-address", "", "Listening Address for the dashboard webserver.")
dashboardCmd.PersistentFlags().StringVar(&basePath, "base-path", "/", "Path on which the dashboard is served.")
dashboardCmd.PersistentFlags().StringVar(&loadAuditFile, "load-audit-file", "", "Runs the dashboard with data saved from a past audit.")
dashboardCmd.PersistentFlags().StringVar(&auditPath, "audit-path", "", "If specified, audits one or more YAML files instead of a cluster.")
@@ -61,6 +59,6 @@ var dashboardCmd = &cobra.Command{
http.Handle("/", router)
logrus.Infof("Starting Polaris dashboard server on port %d", serverPort)
logrus.Fatal(http.ListenAndServe(fmt.Sprintf("%s:%d", listeningAddress, serverPort), nil))
logrus.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", serverPort), nil))
},
}
+1 -1
View File
@@ -24,7 +24,7 @@ import (
fwebhook "github.com/fairwindsops/polaris/pkg/webhook"
k8sConfig "sigs.k8s.io/controller-runtime/pkg/client/config"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/manager/signals"
"sigs.k8s.io/controller-runtime/pkg/runtime/signals"
)
var webhookPort int
+2
View File
@@ -0,0 +1,2 @@
coverage:
range: 50...80
+2 -12
View File
@@ -31,14 +31,6 @@ rules:
verbs:
- 'get'
- 'list'
- apiGroups:
- 'monitoring.coreos.com'
resources:
- 'prometheuses'
- 'alertmanagers'
verbs:
- 'get'
- 'list'
---
# Source: polaris/templates/rbac.yaml
apiVersion: rbac.authorization.k8s.io/v1
@@ -57,7 +49,7 @@ subjects:
namespace: polaris
---
# Source: polaris/templates/rbac.yaml
apiVersion: rbac.authorization.k8s.io/v1
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: polaris
@@ -117,9 +109,7 @@ spec:
- command:
- polaris
- dashboard
- --port
- "8080"
image: 'quay.io/fairwinds/polaris:4.0'
image: 'quay.io/fairwinds/polaris:3.0'
imagePullPolicy: 'Always'
name: dashboard
ports:
+2 -10
View File
@@ -31,14 +31,6 @@ rules:
verbs:
- 'get'
- 'list'
- apiGroups:
- 'monitoring.coreos.com'
resources:
- 'prometheuses'
- 'alertmanagers'
verbs:
- 'get'
- 'list'
---
# Source: polaris/templates/rbac.yaml
apiVersion: rbac.authorization.k8s.io/v1
@@ -57,7 +49,7 @@ subjects:
namespace: polaris
---
# Source: polaris/templates/rbac.yaml
apiVersion: rbac.authorization.k8s.io/v1
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: polaris
@@ -117,7 +109,7 @@ spec:
command:
- polaris
- webhook
image: 'quay.io/fairwinds/polaris:4.0'
image: 'quay.io/fairwinds/polaris:3.0'
imagePullPolicy: 'Always'
ports:
- containerPort: 9876
-7
View File
@@ -33,13 +33,6 @@ 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',
docsDir: 'docs-md',
editLinks: true,
editLinkText: "Help us improve this page",
logo: '/img/fairwinds-logo.svg',
Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

+3 -3
View File
@@ -17,13 +17,13 @@ n.callMethod.apply(n,arguments):n.queue.push(arguments)};
if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
n.queue=[];t=b.createElement(e);t.async=!0;
t.src=v;s=b.getElementsByTagName(e)[0];
s.parentNode.insertBefore(t,s)}(window,document,'script',
s.parentNode.insertBefore(t,s)}(window, document,'script',
'https://connect.facebook.net/en_US/fbevents.js');
fbq('init', '521127644762074');
fbq('init', '159554595936922');
fbq('track', 'PageView');
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
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-TM95WXQ');
})(window,document,'script','dataLayer','GTM-K5KK5H3');
-7
View File
@@ -23,13 +23,6 @@
color: $successColor;
}
blockquote {
border-left: 0.2rem solid $warningColor;
}
blockquote p {
color: $warningColor;
}
.theme-default-content:not(.custom),
.page-nav,
.page-edit,
+2 -5
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.0.0&color=239922">
</a>
<a href="https://goreportcard.com/report/github.com/FairwindsOps/polaris">
<img src="https://goreportcard.com/badge/github.com/FairwindsOps/polaris">
@@ -11,14 +11,11 @@
<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.
problems in the future. Polaris can be run in a few different modes:
Polaris can be run in three different modes:
* As a [dashboard](/dashboard), so you can audit what's running inside your cluster.
+1 -6
View File
@@ -1,8 +1,3 @@
---
meta:
- name: description
content: "Fairwinds Polaris | Documentation: Polaris can be run as an admission controller that acts as a validating webhook."
---
# Admission Controller
> Want to manage the Admission Controller across multiple clusters? Check out
> [Fairwinds Insights](https://www.fairwinds.com/fairwinds-polaris-upgrade)
@@ -33,7 +28,7 @@ kubectl apply -f https://github.com/fairwindsops/polaris/releases/latest/downloa
### Helm
```bash
helm repo add fairwinds-stable https://charts.fairwinds.com/stable
helm upgrade --install polaris fairwinds-stable/polaris --namespace polaris --create-namespace \
helm upgrade --install polaris fairwinds-stable/polaris --namespace polaris \
--set webhook.enable=true --set dashboard.enable=false
```
-65
View File
@@ -1,71 +1,6 @@
---
sidebarDepth: 0
meta:
- name: description
content: "Fairwinds Polaris | Changelog"
---
## 4.0.8
* Fix support for namespace checks
## 4.0.7
* Fix goreleaser format
## 4.0.6
* Change goreleaser format
* Fix `--helm-values` flag
## 4.0.5
* Bugfix for repeated objects on the dashboard
## 4.0.4
* Bugfix for validating webhook and non-pod checks
## 4.0.3
* Fixed bad interaction between `--set-exit-score-below` and `--only-show-failed-tests`
* Dependency updates
* Support for Helm chart scanning
## 4.0.2
* Goreleaser fix
## 4.0.1
* Goreleaser fix
## 4.0.0
* Add support for arbitrary resources, like Ingress or PodDisruptionBudget
* Add support check templating (see docs)
* Add support for multi-resource checks (see docs)
### Breaking Changes
* In custom checks, `jsonSchema` is now `schemaString`
* Check `pdbDisruptionsAllowedGreaterThanZero` is now called `pdbDisruptionsIsZero`
## 3.2.0
* Add `--format=pretty` option for CLI output
## 3.1.6
* Fix nil pointer issue with --only-output-failed-tests
## 3.1.5
* Fix UI display of Ingress checks
## 3.1.4
* Fixes for exemption annotations for the admission controller
## 3.1.3
* Fixes for `privilegeEscalationAllowed` and `insecureCapabilities` checks to take Kubernetes defaults into account
## 3.1.2
* Start checking deployment configuration using Fairwinds Insights
## 3.1.1
* Updated to alpine:3.13
## 3.1.0
* Added support for Ingress objects
* Fixes for exemptions, including support for exempting entire namespaces
## 3.0.0
* **Breaking** - fixed inconsistency in how controller-level checks are handled
Custom checks with `target: Controller` should remove `Object` from the top-level of the
-5
View File
@@ -1,8 +1,3 @@
---
meta:
- name: description
content: "Fairwinds Polaris | Ensure that CPU and memory settings are configured, so that Kubernetes can schedule your workload effectively"
---
# Efficiency
These checks ensure that CPU and memory settings are configured, so that
-6
View File
@@ -1,8 +1,3 @@
---
meta:
- name: description
content: "Fairwinds Polaris | Make sure your Kubernetes workloads are always available, and are running the correct image."
---
# Reliability
These checks help to make sure your workloads are always available,
@@ -16,7 +11,6 @@ key | default | description
`pullPolicyNotAlways` | `warning` | Fails when an image pull policy is not `always`.
`priorityClassNotSet` | `ignore` | Fails when a priorityClassName is not set for a pod.
`multipleReplicasForDeployment` | `ignore` | Fails when there is only one replica for a deployment.
`missingPodDisruptionBudget` | `ignore`
## Background
-5
View File
@@ -1,8 +1,3 @@
---
meta:
- name: description
content: "Fairwinds Polaris | Security Checks Documentation"
---
# Security
These checks are related to security concerns. Workloads that fail these
-56
View File
@@ -1,56 +0,0 @@
---
meta:
- name: description
content: "Fairwinds Polaris | Learn your CLI options"
---
#### 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")
--helm-chart string Will fill out Helm template
--helm-values string Optional flag to add helm values
-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
@@ -0,0 +1,66 @@
#### 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)
```
-5
View File
@@ -1,8 +1,3 @@
---
meta:
- name: description
content: "Fairwinds Polaris | Contribution Code of Conduct"
---
# Code of Conduct
## Our Pledge
+3 -21
View File
@@ -1,8 +1,3 @@
---
meta:
- name: description
content: "Fairwinds Polaris | Contribution Guidelines"
---
# Contributing
Issues, whether bugs, tasks, or feature requests are essential for keeping Polaris great. We believe it should be as easy as possible to contribute changes that get things working in your environment. There are a few guidelines that we need contributors to follow so that we can keep on top of things.
@@ -37,26 +32,12 @@ We label issues with the ["good first issue" tag](https://github.com/FairwindsOp
The following commands are all required to pass as part of Polaris testing:
```bash
```
go list ./... | grep -v vendor | xargs golint -set_exit_status
go list ./... | grep -v vendor | xargs go vet
go test ./pkg/... -v -coverprofile cover.out
```
### Webhook tests
```bash
kind create cluster --wait=90s --image kindest/node:v1.15.11 --name polaris-test
docker build -t quay.io/fairwinds/polaris:debug . # or use your own registry
docker push quay.io/fairwinds/polaris:debug
helm repo add jetstack https://charts.jetstack.io
kubectl create ns cert-manager
helm install cert-manager jetstack/cert-manager --namespace cert-manager --version 0.16.1 --set "installCRDs=true" --wait
POLARIS_IMAGE=quay.io/fairwinds/polaris:debug ./test/webhook_test.sh
```
to avoid the final cleanup for debugging purposes, you can run
```bash
SKIP_FINAL_CLEANUP=true IMAGE_TAG=debug ./test/webhook_test.sh
```
## Creating a New Issue
If you've encountered an issue that is not already reported, please create a [new issue](https://github.com/FairwindsOps/polaris/issues), choose `Bug Report`, `Feature Request` or `Misc.` and follow the instructions in the template.
@@ -69,7 +50,7 @@ Each new pull request should:
- Reference any related issues
- Add tests that show the issues have been solved
- Pass existing tests and linting
- Contain a clear indication of if they're ready for review, or a work in progress
- Contain a clear indication of if they're ready for review or a work in progress
- Be up to date and/or rebased on the master branch
## Creating a new release
@@ -120,3 +101,4 @@ The steps are:
3. Make sure CircleCI runs successfully for the new tag - this will push images to quay.io and create a release in GitHub
1. If CircleCI fails, check with Codeowners ASAP
4. Create and merge a PR for your changes to the Helm chart
-5
View File
@@ -1,8 +1,3 @@
---
meta:
- name: description
content: "Fairwinds Polaris | Documentation on setting checks by severity "
---
# Check Settings
Each check can be assigned a `severity`. Only checks with a severity of `danger` or `warning` will be validated. The results of these validations are visible on the dashboard. In the case of the validating webhook, only failures with a severity of `danger` will result in a change being rejected.
-5
View File
@@ -1,8 +1,3 @@
---
meta:
- name: description
content: "Fairwinds Polaris | Learn about Polaris defaults and how to customize configurations. "
---
# Configuration
The default Polaris configuration can be [seen here](https://github.com/FairwindsOps/polaris/blob/master/examples/config.yaml).
+9 -192
View File
@@ -1,29 +1,16 @@
---
meta:
- name: description
content: "Fairwinds Polaris | Documentation: Create your own checks, you can use JSON Schema"
---
# Custom Checks
If you'd like to create your own checks, you can use [JSON Schema](https://json-schema.org/).
This is how built-in Polaris checks are defined as well - you can see all the built-in checks
in the [checks folder](https://github.com/FairwindsOps/polaris/tree/master/checks) for examples.
If you write a check that could be useful for others, feel free to open a PR to add it in!
## Basic Example
For example, to disallow images from quay.io:
If you'd like to create your own checks, you can use [JSON Schema](https://json-schema.org/). For example,
to disallow images from quay.io:
```yaml
checks:
imageRegistry: warning
customChecks:
imageRegistry:
successMessage: Image comes from allowed registries
failureMessage: Image should not be from disallowed registry
category: Security
target: Container
category: Images
target: Container # target can be "Container" or "Pod"
schema:
'$schema': http://json-schema.org/draft-07/schema
type: object
@@ -34,181 +21,6 @@ customChecks:
pattern: ^quay.io
```
## Available Options
All custom checks should go under the `customChecks` field in your Polaris config, keyed by the
check ID. Note that you'll also have to set its severity in the `checks` section of your Polaris config.
* `successMessage` - the message to show when the check succeeds
* `failureMessage` - the message to show when the check fails
* `category` - one of `Security`, `Efficiency`, or `Reliability`
* `target` - specifies the type of resource to check. This can be:
* a group and kind, e.g. `apps/Deployment` or `networking.k8s.io/Ingress`
* `Controller`, to check _any_ resource that contains a pod spec (e.g. Deployments, CronJobs, StatefulSets), as well as naked Pods
* `Pod`, same as `Controller`, but the schema applies to the Pod spec rather than the top-level controller
* `Container` same as `Controller`, but the schema applies to all Container specs rather than the top-level controller
* `controllers` - if `target` is `Controller`, `Pod` or `Container`, you can use this to change which types of controllers are checked
* `controllers.include` - _only_ check these controllers
* `controllers.exclude` - check all controllers except these
* `containers` - if `target` is `Container`, you can use this to decide if `initContainers`, `containers`, or both should be checked
* `containers.exclude` - can be set to a list including `initContainer` or `container`
* `schema` - the JSON Schema to check against, as a YAML object
* `schemaString` - this JSON Schema to check against, as a YAML or JSON string. See [Templating](#templating) below
* Note: only _one_ of `schema` and `schemaString` can be specified.
* `additionalSchemas` - see [Multi-Resource Checks](#multi-resource-checks) below
* `additionalSchemaStrings` - see [Multi-Resource Checks](#multi-resource-checks) below
* Note: only _one_ of `additionalSchemas` and `additionalSchemaStrings` can be specified.
## Checking CPU and Memory
We extend JSON Schema with `resourceMinimum` and `resourceMaximum` fields to help compare memory and CPU resource
strings like `1000m` and `1G`. Here's an example check that memory and CPU falls within a certain range.
```yaml
customChecks:
resourceLimits:
containers:
exclude:
- initContainer
successMessage: Resource limits are within the required range
failureMessage: Resource limits should be within the required range
category: Resources
target: Container
schema:
'$schema': http://json-schema.org/draft-07/schema
type: object
required:
- resources
properties:
resources:
type: object
required:
- limits
properties:
limits:
type: object
required:
- memory
- cpu
properties:
memory:
type: string
resourceMinimum: 100M
resourceMaximum: 6G
cpu:
type: string
resourceMinimum: 100m
resourceMaximum: "2"
```
## Resource Presence
You can test for the presence of a resource in each Namespace. For example, to
ensure an AlertmanagerConfig is in every Namespace:
```yaml
successMessage: Namespace has monitoring configuration
failureMessage: Namespace should have monitoring configuration
category: Security
target: Namespace
schema: {}
additionalSchemas:
monitoring.coreos.com/AlertmanagerConfig: {}
```
## Templating
You can also utilize go templating in your JSON schema in order to match one field against another.
E.g. here is the built-in check to ensure that the `name` annotation matches the object's name:
```yaml
successMessage: Label app.kubernetes.io/name matches metadata.name
failureMessage: Label app.kubernetes.io/name must match metadata.name
target: Controller
schema:
'$schema': http://json-schema.org/draft-07/schema
type: object
properties:
metadata:
type: object
required: ["labels"]
properties:
labels:
type: object
required: ["app.kubernetes.io/name"]
properties:
app.kubernetes.io/name:
const: "{{ .metadata.name }}"
```
You can also use the full [Go template syntax](https://golang.org/pkg/text/template/), though
you may need to specify your schema as a string in order to use concepts like `range`. E.g.
this check ensures that at least one of the object's labels is present in `matchLabels`:
```yaml
schemaString: |
type: object
properties:
spec:
type: object
required: ["selector"]
properties:
selector:
type: object
required: ["matchLabels"]
properties:
matchLabels:
type: object
anyOf:
{{ range $key, $value := .metadata.labels }}
- properties:
"{{ $key }}":
type: string
const: {{ $value }}
required: ["{{ $key }}"]
{{ end }}
```
## Multi-Resource Checks
You can write checks that span multiple resources. This is helpful for ensuring e.g.
that every Deployment has a PDB or an HPA associated with it.
Here's the check to ensure that every Deployment has a PDB:
```yaml
successMessage: A PodDisruptionBudget is attached
failureMessage: Should have a PodDisruptionBudget
category: Reliability
target: Controller
controllers:
include:
- Deployment
schema:
'$schema': http://json-schema.org/draft-07/schema
type: object
properties:
metadata:
type: object
properties:
labels:
type: object
minProperties: 1
additionalSchemaStrings:
policy/PodDisruptionBudget: |
type: object
properties:
spec:
type: object
required: ["selector"]
properties:
selector:
type: object
required: ["matchLabels"]
properties:
matchLabels:
type: object
anyOf:
{{ range $key, $value := .metadata.labels }}
- properties:
"{{ $key }}":
type: string
const: {{ $value }}
required: ["{{ $key }}"]
{{ end }}
```
## JSON vs YAML
Schemas can also be specified as JSON strings instead of YAML, for easier copy/pasting:
```yaml
customChecks:
@@ -220,3 +32,8 @@ customChecks:
}
```
We extend JSON Schema with `resourceMinimum` and `resourceMaximum` fields to help compare memory and CPU resource
strings like `1000m` and `1G`. You can see an example in [the extended config](https://github.com/FairwindsOps/polaris/tree/master/examples/config-full.yaml)
There are additional examples in the [checks folder](https://github.com/FairwindsOps/polaris/tree/master/checks).
+4 -29
View File
@@ -1,17 +1,9 @@
---
meta:
- name: description
content: "Fairwinds Polaris | Exemptions"
---
# Exemptions
Sometimes a workload really does need to do things that Polaris considers insecure. For instance,
many of the `kube-system` workloads need to run as root, or need access to the host network. In these
cases, we can add **exemptions** to allow the workload to pass Polaris checks.
Exemptions can be added in a few different ways:
- Namespace: By editing the Polaris config.
- Controller: By annotating a controller, or editing the Polaris config.
- Container: By editing the Polaris config.
Exemptions can be added two ways: by annotating a controller, or editing the Polaris config.
## Annotations
To exempt a controller from all checks via annotations, use the annotation `polaris.fairwinds.com/exempt=true`, e.g.
@@ -26,36 +18,19 @@ kubectl annotate deployment my-deployment polaris.fairwinds.com/cpuRequestsMissi
## Config
To add exemptions via the config, you have to specify at least one or more of the following:
- A namespace
- A list of controller names
- A list of container names
You can also specify a list of particular rules. If no rules are specified then every rule is exempted.
Controller names and container names are matched as a prefix, so an empty string will match every controller or container respectively.
For example:
To exempt a controller via the config, you have to specify a namespace (optional), a list of controller names and a list of rules, e.g.
```yaml
exemptions:
# exemption valid for all rules on all containers in all controllers in default namespace
- namespace: default
# exemption valid for hostNetworkSet rule on all containers in dns-controller controller in kube-system namespace
# exemption valid for kube-system namespace
- namespace: kube-system
controllerNames:
- dns-controller
rules:
- hostNetworkSet
# exemption valid for hostNetworkSet rule on all containers in dns-controller controller in all namespaces
# exemption valid in all namespaces
- controllerNames:
- dns-controller
rules:
- hostNetworkSet
# exemption valid for hostNetworkSet rule on coredns container in all controllers in kube-system namespace
- namespace: kube-system
- containerNames:
- coredns
rules:
- hostNetworkSet
```
+1 -6
View File
@@ -1,8 +1,3 @@
---
meta:
- name: description
content: "Fairwinds Polaris | Ways to run Polaris | The Polaris dashboard can be installed on a cluster using kubectl or Helm"
---
# Dashboard
> Want to see Polaris results for all your clusters in a single dashboard? Check out
@@ -24,7 +19,7 @@ kubectl port-forward --namespace polaris svc/polaris-dashboard 8080:80
### Helm
```bash
helm repo add fairwinds-stable https://charts.fairwinds.com/stable
helm upgrade --install polaris fairwinds-stable/polaris --namespace polaris --create-namespace
helm upgrade --install polaris fairwinds-stable/polaris --namespace polaris
kubectl port-forward --namespace polaris svc/polaris-dashboard 8080:80
```
+2 -78
View File
@@ -1,8 +1,3 @@
---
meta:
- name: description
content: "Fairwinds Polaris | Documentation | Polaris can be used on the command line to audit local Kubernetes manifests stored in YAML files."
---
# Infrastructure as Code
> Want to see results for all your IaC repos in one place? Check out
> [Fairwinds Insights](https://www.fairwinds.com/fairwinds-polaris-upgrade)
@@ -26,9 +21,8 @@ polaris version
```
## Running in a CI pipeline
### 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.
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
polaris audit --audit-path ./deploy/ \
@@ -36,73 +30,3 @@ 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:
```bash
polaris audit --audit-path ./deploy/ \
--only-show-failed-tests true
```
### Audit Helm Charts
You can audit helm charts using the `--helm-chart` and `--helm-values` flags:
```
polaris audit \
--helm-chart ./deploy/chart \
--helm-values ./deploy/chart/values.yml
```
### As Github Action
#### Setup polaris action
This action downloads a version of [polaris](https://github.com/FairwindsOps/polaris) and adds it to the path. It makes the [polaris cli](https://polaris.docs.fairwinds.com/infrastructure-as-code) ready to use in following steps of the same job.
##### Inputs
###### `version`
The release version to fetch. This has to be in the form `<tag_name>`.
##### Outputs
###### `version`
The version number of the release tag.
##### Example usage
```yaml
uses: fairwindsops/polaris@master
with:
version: "3.0.3"
```
Example inside a job:
```yaml
steps:
- uses: actions/checkout@v2
- name: Setup polaris
uses: fairwindsops/polaris@master
with:
version: 3.0.3
- name: Use command
run: polaris version
```
-5
View File
@@ -1,5 +0,0 @@
---
meta:
- name: description
content: "Fairwinds Polaris | Documentation"
---
+111 -79
View File
@@ -2516,42 +2516,16 @@
}
},
"browserslist": {
"version": "4.16.6",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.6.tgz",
"integrity": "sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ==",
"version": "4.14.7",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.14.7.tgz",
"integrity": "sha512-BSVRLCeG3Xt/j/1cCGj1019Wbty0H+Yvu2AOuZSuoaUWn3RatbL33Cxk+Q4jRMRAbOm0p7SLravLjpnT6s0vzQ==",
"dev": true,
"requires": {
"caniuse-lite": "^1.0.30001219",
"colorette": "^1.2.2",
"electron-to-chromium": "^1.3.723",
"caniuse-lite": "^1.0.30001157",
"colorette": "^1.2.1",
"electron-to-chromium": "^1.3.591",
"escalade": "^3.1.1",
"node-releases": "^1.1.71"
},
"dependencies": {
"caniuse-lite": {
"version": "1.0.30001239",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001239.tgz",
"integrity": "sha512-cyBkXJDMeI4wthy8xJ2FvDU6+0dtcZSJW3voUF8+e9f1bBeuvyZfc3PNbkOETyhbR+dGCPzn9E7MA3iwzusOhQ==",
"dev": true
},
"colorette": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz",
"integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==",
"dev": true
},
"electron-to-chromium": {
"version": "1.3.752",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.752.tgz",
"integrity": "sha512-2Tg+7jSl3oPxgsBsWKh5H83QazTkmWG/cnNwJplmyZc7KcN61+I10oUgaXSVk/NwfvN3BdkKDR4FYuRBQQ2v0A==",
"dev": true
},
"node-releases": {
"version": "1.1.73",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.73.tgz",
"integrity": "sha512-uW7fodD6pyW2FZNZnp/Z3hvWKeEW1Y8R1+1CnErE8cXFXzl5blBOoVB41CvMer6P6Q0S5FXDwcHgFd1Wj0U9zg==",
"dev": true
}
"node-releases": "^1.1.66"
}
},
"buffer": {
@@ -2907,6 +2881,18 @@
"integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==",
"dev": true
},
"clipboard": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.6.tgz",
"integrity": "sha512-g5zbiixBRk/wyKakSwCKd7vQXDjFnAMGHoEyBogG/bw9kTD9GvdAvaoRR1ALcEzt3pVKxZR0pViekPMIS0QyGg==",
"dev": true,
"optional": true,
"requires": {
"good-listener": "^1.2.2",
"select": "^1.1.2",
"tiny-emitter": "^2.0.0"
}
},
"cliui": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
@@ -2988,9 +2974,9 @@
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
},
"color-string": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.5.tgz",
"integrity": "sha512-jgIoum0OfQfq9Whcfc2z/VhCNcmQjWbey6qBX0vqt7YICflUmBCh9E9CiQD5GSJ+Uehixm3NUwHVhqUAWRivZg==",
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.4.tgz",
"integrity": "sha512-57yF5yt8Xa3czSEW1jfQDE79Idk0+AkN/4KWad6tbdxUmAs3MvjxlWSWD4deYytcRfoZ9nhKyFl1kj5tBvidbw==",
"dev": true,
"requires": {
"color-name": "^1.0.0",
@@ -3806,6 +3792,13 @@
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
"dev": true
},
"delegate": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz",
"integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==",
"dev": true,
"optional": true
},
"depd": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
@@ -3873,9 +3866,9 @@
"dev": true
},
"dns-packet": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.4.tgz",
"integrity": "sha512-BQ6F4vycLXBvdrJZ6S3gZewt6rcrks9KBgM9vrhW+knGRqc8uEdT7fuCwloc7nny5xNoMJ17HGH0R/6fpo8ECA==",
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.1.tgz",
"integrity": "sha512-0UxfQkMhYAUaZI+xrNZOz/as5KgDU0M/fQ9b6SpkyLbk3GEswDi6PADJVaYJradtRVsRIlF1zLyOodbcTCDzUg==",
"dev": true,
"requires": {
"ip": "^1.1.0",
@@ -4019,25 +4012,31 @@
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=",
"dev": true
},
"electron-to-chromium": {
"version": "1.3.599",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.599.tgz",
"integrity": "sha512-u6VGpFsIzSCNrWJb1I72SUypz3EGoBaiEgygoMkd0IOcGR3WF3je5VTx9OIRI9Qd8UOMHinLImyJFkYHTq6nsg==",
"dev": true
},
"elliptic": {
"version": "6.5.4",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz",
"integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==",
"version": "6.5.3",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz",
"integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==",
"dev": true,
"requires": {
"bn.js": "^4.11.9",
"brorand": "^1.1.0",
"bn.js": "^4.4.0",
"brorand": "^1.0.1",
"hash.js": "^1.0.0",
"hmac-drbg": "^1.0.1",
"inherits": "^2.0.4",
"minimalistic-assert": "^1.0.1",
"minimalistic-crypto-utils": "^1.0.1"
"hmac-drbg": "^1.0.0",
"inherits": "^2.0.1",
"minimalistic-assert": "^1.0.0",
"minimalistic-crypto-utils": "^1.0.0"
},
"dependencies": {
"bn.js": {
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
"integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==",
"version": "4.11.9",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz",
"integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==",
"dev": true
}
}
@@ -4839,6 +4838,16 @@
"slash": "^2.0.0"
}
},
"good-listener": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz",
"integrity": "sha1-1TswzfkxPf+33JoNR3CWqm0UXFA=",
"dev": true,
"optional": true,
"requires": {
"delegate": "^3.1.2"
}
},
"got": {
"version": "9.6.0",
"resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz",
@@ -5338,9 +5347,9 @@
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"ini": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz",
"integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==",
"dev": true
},
"internal-ip": {
@@ -5913,9 +5922,9 @@
}
},
"lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"version": "4.17.20",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==",
"dev": true
},
"lodash._reinterpolate": {
@@ -6462,6 +6471,12 @@
}
}
},
"node-releases": {
"version": "1.1.67",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.67.tgz",
"integrity": "sha512-V5QF9noGFl3EymEwUYzO+3NTDpGfQB4ve6Qfnzf3UNydMhjQRVPR1DZTuvWiLzaFJYw2fmDwAfnRNEVb64hSIg==",
"dev": true
},
"nopt": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz",
@@ -6893,9 +6908,9 @@
"dev": true
},
"path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
"dev": true
},
"path-to-regexp": {
@@ -7008,9 +7023,9 @@
"integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs="
},
"postcss": {
"version": "7.0.36",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.36.tgz",
"integrity": "sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw==",
"version": "7.0.35",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz",
"integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==",
"dev": true,
"requires": {
"chalk": "^2.4.2",
@@ -7644,10 +7659,13 @@
"dev": true
},
"prismjs": {
"version": "1.24.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.24.0.tgz",
"integrity": "sha512-SqV5GRsNqnzCL8k5dfAjCNhUrF3pR0A9lTDSCUZeh/LIshheXJEaP0hwLz2t4XHivd2J/v2HR+gRnigzeKe3cQ==",
"dev": true
"version": "1.22.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.22.0.tgz",
"integrity": "sha512-lLJ/Wt9yy0AiSYBf212kK3mM5L8ycwlyTlSxHBAneXLR0nzFMlZ5y7riFPF3E33zXOF2IH95xdY5jIyZbM9z/w==",
"dev": true,
"requires": {
"clipboard": "^2.0.0"
}
},
"process": {
"version": "0.11.10",
@@ -8269,6 +8287,13 @@
}
}
},
"select": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz",
"integrity": "sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0=",
"dev": true,
"optional": true
},
"select-hose": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz",
@@ -8809,9 +8834,9 @@
}
},
"ssri": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.2.tgz",
"integrity": "sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q==",
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz",
"integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==",
"dev": true,
"requires": {
"figgy-pudding": "^3.5.1"
@@ -9272,6 +9297,13 @@
"integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=",
"dev": true
},
"tiny-emitter": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
"integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==",
"dev": true,
"optional": true
},
"to-arraybuffer": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz",
@@ -9723,9 +9755,9 @@
}
},
"url-parse": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.3.tgz",
"integrity": "sha512-IIORyIQD9rvj0A4CLWsHkBBJuNqWpFQe224b6j9t/ABmquIS0qDU2pY6kl6AuOrL5OkCXHMCFNe1jBcuAggjvQ==",
"version": "1.4.7",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.7.tgz",
"integrity": "sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg==",
"dev": true,
"requires": {
"querystringify": "^2.1.1",
@@ -10566,9 +10598,9 @@
}
},
"ws": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/ws/-/ws-6.2.2.tgz",
"integrity": "sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw==",
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz",
"integrity": "sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==",
"dev": true,
"requires": {
"async-limiter": "~1.0.0"
@@ -10587,9 +10619,9 @@
"dev": true
},
"y18n": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz",
"integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==",
"dev": true
},
"yallist": {
+1 -2
View File
@@ -26,8 +26,7 @@
"scripts": {
"build": "npm run build:readme && npm run build:docs",
"build:docs": "vuepress build -d ../dist/",
"build:metadata": "cat main-metadata.md > README.md || true",
"build:readme": "npm run build:metadata && cat ../README.md | grep -v 'ocumentation' | sed \"s/https:\\/\\/\\w\\+.docs.fairwinds.com//g\" >> README.md",
"build:readme": "cat ../README.md | grep -v 'ocumentation' | sed \"s/https:\\/\\/\\w\\+.docs.fairwinds.com//g\" > README.md",
"check-links": "vuepress check-md",
"serve": "npm run build:readme && vuepress dev --port 3003",
"vuepress": "vuepress"
-5
View File
@@ -6,10 +6,6 @@ checks:
pullPolicyNotAlways: warning
readinessProbeMissing: warning
livenessProbeMissing: warning
metadataAndNameMismatched: ignore
pdbDisruptionsIsZero: warning
missingPodDisruptionBudget: ignore
# efficiency
cpuRequestsMissing: warning
cpuLimitsMissing: warning
@@ -26,7 +22,6 @@ checks:
insecureCapabilities: warning
hostNetworkSet: warning
hostPortSet: warning
tlsSettingsMissing: warning
exemptions:
- namespace: kube-system
-14
View File
@@ -1,14 +0,0 @@
options:
organization: fairwinds-opensource
baseBranch: master
# These images will be scanned for vulnerabilities.
images:
docker:
- quay.io/fairwinds/polaris:$CI_SHA1
# These manifests will be scanned for configuration issues.
manifests:
yaml:
- ./deploy/dashboard.yaml
- ./deploy/webhook.yaml
+27 -12
View File
@@ -1,25 +1,40 @@
module github.com/fairwindsops/polaris
go 1.15
go 1.13
require (
github.com/fatih/color v1.12.0
cloud.google.com/go v0.65.0 // indirect
github.com/Azure/go-autorest/autorest v0.11.4 // indirect
github.com/Azure/go-autorest/autorest/adal v0.9.2 // indirect
github.com/gobuffalo/packr/v2 v2.8.1
github.com/google/go-cmp v0.5.2 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/uuid v1.1.3 // indirect
github.com/gophercloud/gophercloud v0.12.0 // indirect
github.com/gorilla/mux v1.8.0
github.com/imdario/mergo v0.3.11 // indirect
github.com/karrick/godirwalk v1.16.1 // indirect
github.com/kr/pretty v0.2.0 // indirect
github.com/prometheus/common v0.13.0 // indirect
github.com/qri-io/jsonpointer v0.1.1 // indirect
github.com/qri-io/jsonschema v0.1.1
github.com/rogpeppe/go-internal v1.6.2 // indirect
github.com/sirupsen/logrus v1.8.1
github.com/spf13/cobra v1.2.1
github.com/sirupsen/logrus v1.7.0
github.com/spf13/cobra v1.1.1
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.7.0
github.com/thoas/go-funk v0.9.0
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
k8s.io/api v0.22.0
k8s.io/apimachinery v0.22.0
k8s.io/client-go v0.22.0
sigs.k8s.io/controller-runtime v0.9.6
github.com/stretchr/testify v1.6.1
gitlab.com/golang-commonmark/linkify v0.0.0-20200225224916-64bca66f6ad3 // indirect
gitlab.com/golang-commonmark/markdown v0.0.0-20191127184510-91b5b3c99c19
go.uber.org/zap v1.16.0 // indirect
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a // indirect
golang.org/x/sys v0.0.0-20200824131525-c12d262b63d8 // indirect
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e // indirect
gomodules.xyz/jsonpatch/v2 v2.1.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776
k8s.io/api v0.18.8
k8s.io/apimachinery v0.18.8
k8s.io/client-go v0.18.6
k8s.io/klog/v2 v2.1.0 // indirect
k8s.io/utils v0.0.0-20200821003339-5e75c0163111 // indirect
sigs.k8s.io/controller-runtime v0.6.4
sigs.k8s.io/yaml v1.2.0
)
+359 -307
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -20,7 +20,7 @@ import (
const (
// Version represents the current release version of Polaris
Version = "4.0.8"
Version = "3.0.0"
)
func main() {
-60
View File
@@ -1,60 +0,0 @@
package config
import (
"github.com/gobuffalo/packr/v2"
"github.com/sirupsen/logrus"
)
var (
// BuiltInChecks contains the checks that come pre-installed w/ Polaris
BuiltInChecks = map[string]SchemaCheck{}
schemaBox = (*packr.Box)(nil)
// We explicitly set the order to avoid thrash in the
// tests as we migrate toward JSON schema
checkOrder = []string{
// Controller Checks
"multipleReplicasForDeployment",
// Pod checks
"hostIPCSet",
"hostPIDSet",
"hostNetworkSet",
// Container checks
"memoryLimitsMissing",
"memoryRequestsMissing",
"cpuLimitsMissing",
"cpuRequestsMissing",
"readinessProbeMissing",
"livenessProbeMissing",
"pullPolicyNotAlways",
"tagNotSpecified",
"hostPortSet",
"runAsRootAllowed",
"runAsPrivileged",
"notReadOnlyRootFilesystem",
"privilegeEscalationAllowed",
"dangerousCapabilities",
"insecureCapabilities",
"priorityClassNotSet",
// Other checks
"tlsSettingsMissing",
"pdbDisruptionsIsZero",
"metadataAndNameMismatched",
"missingPodDisruptionBudget",
}
)
func init() {
schemaBox = packr.New("Schemas", "../../checks")
for _, checkID := range checkOrder {
contents, err := schemaBox.Find(checkID + ".yaml")
if err != nil {
panic(err)
}
check, err := ParseCheck(checkID, contents)
if err != nil {
logrus.Errorf("Error while parsing check %s", checkID)
panic(err)
}
BuiltInChecks[checkID] = check
}
}
+6 -7
View File
@@ -23,7 +23,7 @@ import (
"net/http"
"strings"
"github.com/gobuffalo/packr/v2"
packr "github.com/gobuffalo/packr/v2"
"k8s.io/apimachinery/pkg/util/yaml"
)
@@ -40,8 +40,7 @@ type Configuration struct {
type Exemption struct {
Rules []string `json:"rules"`
ControllerNames []string `json:"controllerNames"`
ContainerNames []string `json:"containerNames"`
Namespace string `json:"namespace"`
Namespace string `json:"namespace"`
}
var configBox = (*packr.Box)(nil)
@@ -60,14 +59,14 @@ func ParseFile(path string) (Configuration, error) {
if path == "" {
rawBytes, err = getConfigBox().Find("config.yaml")
} else if strings.HasPrefix(path, "https://") || strings.HasPrefix(path, "http://") {
// path is a url
//path is a url
response, err2 := http.Get(path)
if err2 != nil {
return Configuration{}, err2
}
rawBytes, err = ioutil.ReadAll(response.Body)
} else {
// path is local
//path is local
rawBytes, err = ioutil.ReadFile(path)
}
if err != nil {
@@ -103,8 +102,8 @@ func Parse(rawBytes []byte) (Configuration, error) {
}
// Validate checks if a config is valid
func (conf Configuration) Validate() error {
if len(conf.Checks) == 0 {
func (c Configuration) Validate() error {
if len(c.Checks) == 0 {
return errors.New("No checks were enabled")
}
return nil
+6 -12
View File
@@ -16,7 +16,6 @@ package config
import (
"context"
"fmt"
"io"
"log"
"net/http"
@@ -143,9 +142,7 @@ func TestConfigNoServerError(t *testing.T) {
func TestConfigWithCustomChecks(t *testing.T) {
valid := map[string]interface{}{
"securityContext": map[string]interface{}{
"foo": "bar",
},
"securityContext": map[string]interface{}{},
}
invalid := map[string]interface{}{
"notSecurityContext": map[string]interface{}{},
@@ -154,23 +151,20 @@ func TestConfigWithCustomChecks(t *testing.T) {
parsedConf, err := Parse([]byte(confCustomChecks))
assert.NoError(t, err, "Expected no error when parsing YAML config")
assert.Equal(t, 1, len(parsedConf.CustomChecks))
check, err := parsedConf.CustomChecks["foo"].TemplateForResource(map[string]interface{}{})
isValid, _, err := check.CheckObject(valid)
isValid, err := parsedConf.CustomChecks["foo"].CheckObject(valid)
assert.NoError(t, err)
assert.Equal(t, true, isValid)
isValid, _, err = check.CheckObject(invalid)
isValid, err = parsedConf.CustomChecks["foo"].CheckObject(invalid)
assert.NoError(t, err)
assert.Equal(t, false, isValid)
parsedConf, err = Parse([]byte(confCustomChecksWithJSONSchema))
assert.NoError(t, err, "Expected no error when parsing YAML config")
assert.Equal(t, 1, len(parsedConf.CustomChecks))
isValid, problems, err := parsedConf.CustomChecks["foo"].CheckObject(valid)
isValid, err = parsedConf.CustomChecks["foo"].CheckObject(valid)
assert.NoError(t, err)
if !assert.Equal(t, true, isValid) {
fmt.Println(problems[0].PropertyPath, problems[0].InvalidValue, problems[0].Message)
}
isValid, _, err = check.CheckObject(invalid)
assert.Equal(t, true, isValid)
isValid, err = parsedConf.CustomChecks["foo"].CheckObject(invalid)
assert.NoError(t, err)
assert.Equal(t, false, isValid)
}
+15 -28
View File
@@ -2,53 +2,40 @@ 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 string, objMeta metav1.Object, containerName string) bool {
func (conf Configuration) IsActionable(ruleID, namespace, controllerName string) bool {
if severity, ok := conf.Checks[ruleID]; !ok || !severity.IsActionable() {
return false
}
if conf.DisallowExemptions {
return true
}
for _, exemption := range conf.Exemptions {
if exemption.Namespace != "" && exemption.Namespace != objMeta.GetNamespace() {
for _, example := range conf.Exemptions {
if example.Namespace != "" && example.Namespace != namespace {
continue
}
checkIfRuleMatches := false
for _, rule := range exemption.Rules {
for _, rule := range example.Rules {
if rule != ruleID {
continue
}
checkIfRuleMatches = true
break
}
if len(exemption.Rules) == 0 || checkIfRuleMatches {
if !isExemptionCheckMatched(exemption.ControllerNames, objMeta.GetName()) {
continue
for _, controller := range example.ControllerNames {
if strings.HasPrefix(controllerName, controller) {
return false
}
}
if isExemptionCheckMatched(exemption.ContainerNames, containerName) {
return false
}
if len(example.Rules) == 0 {
for _, controller := range example.ControllerNames {
if strings.HasPrefix(controllerName, controller) {
return false
}
}
}
}
return true
}
func isExemptionCheckMatched(arr []string, predicate string) bool {
if len(arr) == 0 {
return true
}
for _, container := range arr {
if strings.HasPrefix(predicate, container) {
return true
}
}
return false
}
+46 -231
View File
@@ -18,248 +18,63 @@ 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 = `
var confExemptRuleTest = `
checks:
multipleReplicasForDeployment: warning
priorityClassNotSet: warning
pullPolicyNotAlways: warning
ANY: warning
OTHER: warning
exemptions:
- namespace: prometheus
rules:
- multipleReplicasForDeployment
- controllerNames:
- controller2
rules:
- multipleReplicasForDeployment
- namespace: kube-system
controllerNames:
- controller3
rules:
- multipleReplicasForDeployment
- containerNames:
- container41
- container42
rules:
- multipleReplicasForDeployment
- namespace: kube-system
containerNames:
- container51
- container52
rules:
- multipleReplicasForDeployment
- controllerNames:
- controller6
containerNames:
- container61
- container62
- test
rules:
- multipleReplicasForDeployment
- namespace: kube-system
controllerNames:
- controller7
containerNames:
- container71
- container72
rules:
- multipleReplicasForDeployment
- priorityClassNotSet
- namespace: polaris
- ANY
`
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
var confExemptTest = `
checks:
ANY: warning
exemptions:
- controllerNames:
- test
`
var confNamespaceTest = `
checks:
ANY: warning
exemptions:
- namespace: kube-system
controllerNames:
- test
`
func TestInclusiveExemption(t *testing.T) {
parsedConf, _ := Parse([]byte(confExemptTest))
applicable := parsedConf.IsActionable("ANY", "test", "test")
applicableOtherController := parsedConf.IsActionable("ANY","test", "other")
assert.False(t, applicable, "Expected all checks to be exempted when their controller is specified.")
assert.True(t, applicableOtherController, "Expected checks to only be exempted when their controller is specified.")
}
func TestNamespaceExemptionForSpecifiedRules(t *testing.T) {
parsedConf, err := Parse([]byte(confContainerTest))
assert.NoError(t, err)
func TestIndividualRuleException(t *testing.T) {
parsedConf, _ := Parse([]byte(confExemptRuleTest))
applicable := parsedConf.IsActionable("ANY", "test", "test")
applicableOtherRule := parsedConf.IsActionable("OTHER","test", "test")
applicableOtherRuleOtherController := parsedConf.IsActionable("OTHER","test", "other")
applicableRuleOtherController := parsedConf.IsActionable("ANY","test", "other")
actionable := parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("prometheus", ""), "")
assert.False(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("prometheus", "controller1"), "container11")
assert.False(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("prometheus", ""), "container11")
assert.False(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("prometheus", "controller1"), "")
assert.False(t, actionable)
actionable = parsedConf.IsActionable("pullPolicyNotAlways", createMeta("prometheus", "controller1"), "")
assert.True(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("kube-system", ""), "")
assert.True(t, actionable)
assert.False(t, applicable, "Expected all checks to be exempted when their controller and rule are specified.")
assert.True(t, applicableOtherRule, "Expected checks to only be exempted when their controller and rule are specified.")
assert.True(t, applicableOtherRuleOtherController, "Expected checks to only be exempted when their controller and rule are specified.")
assert.True(t, applicableRuleOtherController, "Expected checks to only be exempted when their controller and rule are specified.")
}
func TestNamespaceExemptionForAllRules(t *testing.T) {
parsedConf, err := Parse([]byte(confContainerTest))
assert.NoError(t, err)
func TestNamespaceExemption(t *testing.T) {
parsedConf, _ := Parse([]byte(confNamespaceTest))
applicable := parsedConf.IsActionable("ANY", "kube-system", "test")
applicableOtherController := parsedConf.IsActionable("ANY","default", "test")
actionable := parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("polaris", ""), "")
assert.False(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("polaris", "controller1"), "container11")
assert.False(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("polaris", ""), "container11")
assert.False(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("polaris", "controller1"), "")
assert.False(t, actionable)
actionable = parsedConf.IsActionable("pullPolicyNotAlways", createMeta("polaris", "controller1"), "")
assert.False(t, actionable)
}
func TestControllerExemption(t *testing.T) {
parsedConf, err := Parse([]byte(confContainerTest))
assert.NoError(t, err)
actionable := parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("", "controller2"), "")
assert.False(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("", "controller2"), "container21")
assert.False(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("prometheus", "controller2"), "container21")
assert.False(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("prometheus", "controller2"), "")
assert.False(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("", "controller3"), "")
assert.True(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("kube-system", "controller3"), "")
assert.False(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("kube-system", "controller3"), "container31")
assert.False(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("kube-system", "controller4"), "")
assert.True(t, actionable)
}
func TestOnlyContainerExemption(t *testing.T) {
parsedConf, err := Parse([]byte(confContainerTest))
assert.NoError(t, err)
actionable := parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("", ""), "container41")
assert.False(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("", ""), "container42")
assert.False(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("", "controller4"), "container41")
assert.False(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("kube-system", ""), "container41")
assert.False(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("kube-system", "controller4"), "container41")
assert.False(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("", ""), "container51")
assert.True(t, actionable)
}
func TestNamespaceAndContainerExemption(t *testing.T) {
parsedConf, err := Parse([]byte(confContainerTest))
assert.NoError(t, err)
actionable := parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("kube-system", ""), "container51")
assert.False(t, actionable)
actionable = parsedConf.IsActionable("priorityClassNotSet", createMeta("kube-system", ""), "container51")
assert.True(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("kube-system", "controller5"), "container51")
assert.False(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("kube-system", "controller5"), "")
assert.True(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("insights-agent", ""), "container51")
assert.True(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("", ""), "container51")
assert.True(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("", "controller5"), "container51")
assert.True(t, actionable)
}
func TestControllerAndContainerExemption(t *testing.T) {
parsedConf, err := Parse([]byte(confContainerTest))
assert.NoError(t, err)
actionable := parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("", "controller6"), "container61")
assert.False(t, actionable)
actionable = parsedConf.IsActionable("priorityClassNotSet", createMeta("", "controller6"), "container61")
assert.True(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("kube-system", "controller6"), "container61")
assert.False(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("kube-system", "controller6"), "")
assert.True(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("", "controller7"), "container61")
assert.True(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("", ""), "container61")
assert.True(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("kube-system", ""), "container61")
assert.True(t, actionable)
}
func TestContainerExemption(t *testing.T) {
parsedConf, err := Parse([]byte(confContainerTest))
assert.NoError(t, err)
actionable := parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("", ""), "container71")
assert.True(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("kube-system", ""), "container71")
assert.True(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("", "controller7"), "container71")
assert.True(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("kube-system", "controller7"), "")
assert.True(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("kube-system", "controller7"), "container71")
assert.False(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("insights-agent", "controller7"), "container71")
assert.True(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("kube-system", "controller6"), "container71")
assert.True(t, actionable)
actionable = parsedConf.IsActionable("multipleReplicasForDeployment", createMeta("kube-system", "controller7"), "container61")
assert.True(t, actionable)
actionable = parsedConf.IsActionable("priorityClassNotSet", createMeta("kube-system", "controller7"), "container71")
assert.False(t, actionable)
actionable = parsedConf.IsActionable("pullPolicyNotAlways", createMeta("kube-system", "controller8"), "container71")
assert.True(t, actionable)
}
assert.False(t, applicable, "Expected all checks to be exempted when their namespace and controller is specified.")
assert.True(t, applicableOtherController, "Expected checks to only be exempted when their namespace and controller is specified.")
}
+27 -158
View File
@@ -1,86 +1,43 @@
package config
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"strings"
"text/template"
"github.com/qri-io/jsonschema"
"github.com/thoas/go-funk"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
k8sYaml "k8s.io/apimachinery/pkg/util/yaml"
)
// TargetKind represents the part of the config to be validated
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"
)
// 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" json:"id"`
Category string `yaml:"category" json:"category"`
SuccessMessage string `yaml:"successMessage" json:"successMessage"`
FailureMessage string `yaml:"failureMessage" json:"failureMessage"`
Controllers includeExcludeList `yaml:"controllers" json:"controllers"`
Containers includeExcludeList `yaml:"containers" json:"containers"`
Target TargetKind `yaml:"target" json:"target"`
SchemaTarget TargetKind `yaml:"schemaTarget" json:"schemaTarget"`
Schema map[string]interface{} `yaml:"schema" json:"schema"`
SchemaString string `yaml:"schemaString" json:"schemaString"`
Validator jsonschema.RootSchema `yaml:"-" json:"-"`
AdditionalSchemas map[string]map[string]interface{} `yaml:"additionalSchemas" json:"additionalSchemas"`
AdditionalSchemaStrings map[string]string `yaml:"additionalSchemaStrings" json:"additionalSchemaStrings"`
AdditionalValidators map[string]jsonschema.RootSchema `yaml:"-" json:"-"`
ID string `yaml:"id"`
Category string `yaml:"category"`
SuccessMessage string `yaml:"successMessage"`
FailureMessage string `yaml:"failureMessage"`
Controllers includeExcludeList `yaml:"controllers"`
Containers includeExcludeList `yaml:"containers"`
Target TargetKind `yaml:"target"`
SchemaTarget TargetKind `yaml:"schemaTarget"`
Schema jsonschema.RootSchema `yaml:"schema"`
JSONSchema string `yaml:"jsonSchema"`
}
type resourceMinimum string
type resourceMaximum string
func unmarshalYAMLOrJSON(raw []byte, dest interface{}) error {
reader := bytes.NewReader(raw)
d := k8sYaml.NewYAMLOrJSONDecoder(reader, 4096)
for {
if err := d.Decode(dest); err != nil {
if err == io.EOF {
break
}
return fmt.Errorf("Decoding schema check failed: %v", err)
}
}
return nil
}
// ParseCheck parses a check from a byte array
func ParseCheck(id string, rawBytes []byte) (SchemaCheck, error) {
check := SchemaCheck{}
err := unmarshalYAMLOrJSON(rawBytes, &check)
if err != nil {
return check, err
}
check.Initialize(id)
return check, nil
}
func init() {
jsonschema.RegisterValidator("resourceMinimum", newResourceMinimum)
jsonschema.RegisterValidator("resourceMaximum", newResourceMaximum)
@@ -160,136 +117,48 @@ func validateRange(path string, limit interface{}, data interface{}, isMinimum b
// Initialize sets up the schema
func (check *SchemaCheck) Initialize(id string) error {
check.ID = id
if check.SchemaString == "" {
jsonBytes, err := json.Marshal(check.Schema)
if err != nil {
if check.JSONSchema != "" {
if err := json.Unmarshal([]byte(check.JSONSchema), &check.Schema); err != nil {
return err
}
check.SchemaString = string(jsonBytes)
}
if check.AdditionalSchemaStrings == nil {
check.AdditionalSchemaStrings = make(map[string]string)
}
for kind, schema := range check.AdditionalSchemas {
jsonBytes, err := json.Marshal(schema)
if err != nil {
return err
}
check.AdditionalSchemaStrings[kind] = string(jsonBytes)
}
check.Schema = map[string]interface{}{}
check.AdditionalSchemas = map[string]map[string]interface{}{}
return nil
}
// TemplateForResource fills out a check's templated fields given a particular resource
func (check SchemaCheck) TemplateForResource(res interface{}) (*SchemaCheck, error) {
newCheck := check // Make a copy of the check, since we're going to modify the schema
templateStrings := map[string]string{
"": newCheck.SchemaString,
}
for kind, schema := range newCheck.AdditionalSchemaStrings {
templateStrings[kind] = schema
}
newCheck.SchemaString = ""
newCheck.AdditionalSchemaStrings = map[string]string{}
for kind, tmplString := range templateStrings {
tmpl := template.New(newCheck.ID)
tmpl, err := tmpl.Parse(tmplString)
if err != nil {
return nil, err
}
w := bytes.Buffer{}
err = tmpl.Execute(&w, res)
if err != nil {
return nil, err
}
if kind == "" {
newCheck.SchemaString = w.String()
} else {
newCheck.AdditionalSchemaStrings[kind] = w.String()
}
}
newCheck.AdditionalValidators = map[string]jsonschema.RootSchema{}
for kind, schemaStr := range newCheck.AdditionalSchemaStrings {
val := jsonschema.RootSchema{}
err := unmarshalYAMLOrJSON([]byte(schemaStr), &val)
if err != nil {
return nil, err
}
newCheck.AdditionalValidators[kind] = val
}
err := unmarshalYAMLOrJSON([]byte(newCheck.SchemaString), &newCheck.Validator)
if err != nil {
return nil, err
}
return &newCheck, err
}
// CheckPod checks a pod spec against the schema
func (check SchemaCheck) CheckPod(pod *corev1.PodSpec) (bool, []jsonschema.ValError, error) {
func (check SchemaCheck) CheckPod(pod *corev1.PodSpec) (bool, error) {
return check.CheckObject(pod)
}
// CheckController checks a controler's spec against the schema
func (check SchemaCheck) CheckController(bytes []byte) (bool, []jsonschema.ValError, error) {
errs, err := check.Validator.ValidateBytes(bytes)
return len(errs) == 0, errs, err
func (check SchemaCheck) CheckController(bytes []byte) (bool, error) {
errs, err := check.Schema.ValidateBytes(bytes)
return len(errs) == 0, err
}
// CheckContainer checks a container spec against the schema
func (check SchemaCheck) CheckContainer(container *corev1.Container) (bool, []jsonschema.ValError, error) {
func (check SchemaCheck) CheckContainer(container *corev1.Container) (bool, error) {
return check.CheckObject(container)
}
// CheckObject checks arbitrary data against the schema
func (check SchemaCheck) CheckObject(obj interface{}) (bool, []jsonschema.ValError, error) {
func (check SchemaCheck) CheckObject(obj interface{}) (bool, error) {
bytes, err := json.Marshal(obj)
if err != nil {
return false, nil, err
return false, err
}
errs, err := check.Validator.ValidateBytes(bytes)
return len(errs) == 0, errs, err
}
// CheckAdditionalObjects looks for an object that passes the specified additional schema
func (check SchemaCheck) CheckAdditionalObjects(groupkind string, objects []interface{}) (bool, error) {
val, ok := check.AdditionalValidators[groupkind]
if !ok {
return false, errors.New("No validator found for " + groupkind)
}
for _, obj := range objects {
bytes, err := json.Marshal(obj)
if err != nil {
return false, err
}
errs, err := val.ValidateBytes(bytes)
if err != nil {
return false, err
}
if len(errs) == 0 {
return true, nil
}
}
return false, nil
errs, err := check.Schema.ValidateBytes(bytes)
return len(errs) == 0, err
}
// IsActionable decides if this check applies to a particular target
func (check SchemaCheck) IsActionable(target TargetKind, 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) {
func (check SchemaCheck) IsActionable(target TargetKind, namespace, controllerType string, isInit bool) bool {
if check.Target != target {
return false
}
isIncluded := len(check.Controllers.Include) == 0
for _, inclusion := range check.Controllers.Include {
if inclusion == kind {
if inclusion == controllerType {
isIncluded = true
break
}
@@ -298,7 +167,7 @@ func (check SchemaCheck) IsActionable(target TargetKind, kind string, isInit boo
return false
}
for _, exclusion := range check.Controllers.Exclude {
if exclusion == kind {
if exclusion == controllerType {
return false
}
}
+2 -4
View File
@@ -13,10 +13,8 @@
padding-right: 40px;
box-shadow: none;
}
.card.transparent {
background-color: transparent;
border: none;
box-shadow: none;
.card.insights img {
max-width: 400px;
}
.card.insights a {
background-color: #20162D;
Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 34 KiB

+5 -10
View File
@@ -1,19 +1,14 @@
$(function () {
var data = [
polarisSummary.Successes,
polarisSummary.Warnings,
polarisSummary.Dangers,
];
var sum = data.reduce(function(total, cur) { return total + cur }, 0.0)
if (sum === 0.0) {
data = [1, 0, 0];
}
var clusterChart = new Chart("clusterScoreChart", {
type: 'doughnut',
data: {
labels: ["Passing", "Warning", "Error"],
datasets: [{
data: data,
data: [
polarisSummary.Successes,
polarisSummary.Warnings,
polarisSummary.Dangers,
],
backgroundColor: ['#8BD2DC', '#f26c21', '#a11f4c'],
}]
},
+5 -7
View File
@@ -141,7 +141,7 @@ func getConfigForQuery(base config.Configuration, query url.Values) config.Confi
}
func stripUnselectedNamespaces(data *validator.AuditData, selectedNamespaces []string) {
newResults := []validator.Result{}
newResults := []validator.ControllerResult{}
for _, res := range data.Results {
if stringInSlice(res.Namespace, selectedNamespaces) {
newResults = append(newResults, res)
@@ -173,15 +173,14 @@ func GetRouter(c config.Configuration, auditPath string, port int, basePath stri
router.HandleFunc("/results.json", func(w http.ResponseWriter, r *http.Request) {
adjustedConf := getConfigForQuery(c, r.URL.Query())
if auditData == nil {
k, err := kube.CreateResourceProvider(r.Context(), auditPath, "", c)
k, err := kube.CreateResourceProvider(r.Context(), auditPath, "")
if err != nil {
logrus.Errorf("Error fetching Kubernetes resources %v", err)
http.Error(w, "Error fetching Kubernetes resources", http.StatusInternalServerError)
return
}
var auditDataObj validator.AuditData
auditDataObj, err = validator.RunAudit(adjustedConf, k)
auditDataObj, err := validator.RunAudit(r.Context(), adjustedConf, k)
if err != nil {
http.Error(w, "Error Fetching Deployments", http.StatusInternalServerError)
return
@@ -206,15 +205,14 @@ func GetRouter(c config.Configuration, auditPath string, port int, basePath stri
adjustedConf := getConfigForQuery(c, r.URL.Query())
if auditData == nil {
k, err := kube.CreateResourceProvider(r.Context(), auditPath, "", c)
k, err := kube.CreateResourceProvider(r.Context(), auditPath, "")
if err != nil {
logrus.Errorf("Error fetching Kubernetes resources %v", err)
http.Error(w, "Error fetching Kubernetes resources", http.StatusInternalServerError)
return
}
var auditData validator.AuditData
auditData, err = validator.RunAudit(adjustedConf, k)
auditData, err := validator.RunAudit(r.Context(), adjustedConf, k)
if err != nil {
logrus.Errorf("Error getting audit data: %v", err)
http.Error(w, "Error running audit", 500)
+2 -2
View File
@@ -25,7 +25,7 @@ import (
func getWarningWidth(counts validator.CountSummary, fullWidth int) uint {
denom := counts.Successes + counts.Warnings + counts.Dangers
if denom == 0 {
return uint(1 * float64(fullWidth))
return uint(0)
}
res := float64(counts.Successes+counts.Warnings) / float64(denom) * float64(fullWidth)
return uint(res)
@@ -34,7 +34,7 @@ func getWarningWidth(counts validator.CountSummary, fullWidth int) uint {
func getSuccessWidth(counts validator.CountSummary, fullWidth int) uint {
denom := counts.Successes + counts.Warnings + counts.Dangers
if denom == 0 {
return uint(1 * float64(fullWidth))
return uint(0)
}
res := float64(counts.Successes) / float64(denom) * float64(fullWidth)
return uint(res)
-241
View File
@@ -1,241 +0,0 @@
package dashboard
import (
"testing"
"github.com/fairwindsops/polaris/pkg/config"
"github.com/fairwindsops/polaris/pkg/validator"
"github.com/stretchr/testify/assert"
)
func TestWarningWidth(t *testing.T) {
input1 := validator.CountSummary{
Successes: 0,
Warnings: 0,
Dangers: 0,
}
input2 := 6
expectedOutput := uint(0x6)
actual := getWarningWidth(input1, input2)
assert.Equal(t, expectedOutput, actual)
input1 = validator.CountSummary{
Successes: 10,
Warnings: 3,
Dangers: 1,
}
input2 = 3
expectedOutput = uint(0x2)
actual = getWarningWidth(input1, input2)
assert.Equal(t, expectedOutput, actual)
}
func TestSuccessWidth(t *testing.T) {
input1 := validator.CountSummary{
Successes: 0,
Warnings: 0,
Dangers: 0,
}
input2 := 6
expectedOutput := uint(0x6)
actual := getSuccessWidth(input1, input2)
assert.Equal(t, expectedOutput, actual)
input1 = validator.CountSummary{
Successes: 8,
Warnings: 6,
Dangers: 4,
}
input2 = 7
expectedOutput = uint(0x3)
actual = getSuccessWidth(input1, input2)
assert.Equal(t, expectedOutput, actual)
}
func TestGetGrade(t *testing.T) {
input := validator.CountSummary{
Successes: 10,
Warnings: 3,
Dangers: 1,
}
expectedOutput := "B-"
actual := getGrade(input)
assert.Equal(t, expectedOutput, actual)
assert.NotEqual(t, "A+", actual)
}
func TestGetWeatherIcon(t *testing.T) {
input := validator.CountSummary{
Successes: 10,
Warnings: 3,
Dangers: 1,
}
expectedOutput := "fa-cloud-sun"
actual := getWeatherIcon(input)
assert.Equal(t, expectedOutput, actual)
assert.NotEqual(t, "fa-cloud-showers-heavy", actual)
}
func TestGetResultClass(t *testing.T) {
input := validator.ResultMessage{
ID: "",
Message: "",
Details: []string(nil),
Success: false,
Severity: "",
Category: "",
}
expectedOutput := " failure"
actual := getResultClass(input)
assert.Equal(t, expectedOutput, actual)
assert.NotEqual(t, " success", actual)
input = validator.ResultMessage{
ID: "",
Message: "",
Details: []string(nil),
Success: true,
Severity: "",
Category: "",
}
expectedOutput = " success"
actual = getResultClass(input)
assert.Equal(t, expectedOutput, actual)
assert.NotEqual(t, " failure", actual)
}
func TestGetWeatherText(t *testing.T) {
input := validator.CountSummary{
Successes: 10,
Warnings: 3,
Dangers: 1,
}
expectedOutput := "Mostly smooth sailing"
actual := getWeatherText(input)
assert.Equal(t, expectedOutput, actual)
assert.NotEqual(t, "Storms ahead, be careful", actual)
}
func TestGetIcon(t *testing.T) {
input := validator.ResultMessage{
ID: "",
Message: "",
Details: []string(nil),
Success: false,
Severity: "",
Category: "",
}
expectedOutput := "fas fa-times"
actual := getIcon(input)
assert.Equal(t, expectedOutput, actual)
assert.NotEqual(t, "fas fa-check", actual)
input = validator.ResultMessage{
ID: "",
Message: "",
Details: []string(nil),
Success: true,
Severity: "",
Category: "",
}
expectedOutput = "fas fa-check"
actual = getIcon(input)
assert.Equal(t, expectedOutput, actual)
assert.NotEqual(t, "fas fa-times", actual)
input = validator.ResultMessage{
ID: "",
Message: "",
Details: []string(nil),
Success: false,
Severity: config.SeverityWarning,
Category: "",
}
expectedOutput = "fas fa-exclamation"
actual = getIcon(input)
assert.Equal(t, expectedOutput, actual)
assert.NotEqual(t, "fas fa-times", actual)
}
func TestGetCategoryLink(t *testing.T) {
input := "Efficiency"
expectedOutput := "https://polaris.docs.fairwinds.com/checks/efficiency"
actual := getCategoryLink(input)
assert.Equal(t, expectedOutput, actual)
assert.NotEqual(t, "ttps://polaris.docs.fairwinds.com/checks/reliability", actual)
}
func TestGetCategoryInfo(t *testing.T) {
input := "Security"
expectedOutput :=
`
Kubernetes provides a great deal of configurability when it comes to the
security of your workloads. A key principle here involves limiting the level
of access any individual workload has. Polaris has validations for a number of
best practices, mostly focused on ensuring that unnecessary access has not
been granted to an application workload.
`
actual := getCategoryInfo(input)
assert.Equal(t, expectedOutput, actual)
assert.NotEqual(t, "fas fa-times", actual)
input = "Reliability"
expectedOutput =
`
Kubernetes is built to reliabily run highly available applications.
Polaris includes a number of checks to ensure that you are maximizing
the reliability potential of Kubernetes.
`
actual = getCategoryInfo(input)
assert.Equal(t, expectedOutput, actual)
assert.NotEqual(t, "fas fa-times", actual)
}
func TestStringInSlice(t *testing.T) {
input1 := "a"
input2 := []string{"a", "b", "cde"}
expectedOutput := true
actual := stringInSlice(input1, input2)
assert.Equal(t, expectedOutput, actual)
assert.NotEqual(t, false, actual)
input1 = "f"
input2 = []string{"a", "b", "cde"}
expectedOutput = false
actual = stringInSlice(input1, input2)
assert.Equal(t, expectedOutput, actual)
assert.NotEqual(t, true, actual)
}
+29 -51
View File
@@ -83,12 +83,10 @@
</div>
</div>
<div class="card transparent">
<a class="align-self-center"
target="_blank"
href="https://www.fairwinds.com/polaris-user-insights-demo?utm_source=polaris&utm_medium=ad&utm_campaign=polarisad">
<img src="static/images/FW_Insights_Polaris.svg" />
</a>
<div id="insights" class="card insights py-2">
<img class="align-self-center" src="static/images/FW_Insights_Polaris.svg" />
<span>Fairwinds, the company behind Polaris, now offers Fairwinds Insights, a multi-cluster, multi-user Kubernetes configuration validation and policy enforcement platform. Fairwinds Insights can help you avoid errors that lead to wasted time, compute costs, and increased risk.</span>
<a class="col-auto py-1 px-3 mt-2 align-self-center" href="https://fairwinds.com/insights?source=polaris" target="_blank">Try Insights</a>
</div>
<div id="categories" class="card category">
@@ -122,7 +120,7 @@
</div>
<div class="result-messages expandable-content">
<form id="namespaceFiltersForm" class="namespace-list">
{{ range $namespace, $results := .AuditData.GetResultsByNamespace }}
{{ range $namespace, $ctrlResults := .AuditData.GetResultsByNamespace }}
<div class="namespace-row">
<input type="checkbox" name="{{ $namespace }}" id="namespace-{{ $namespace }}">
<label for="namespace-{{ $namespace }}">{{ $namespace }}</label>
@@ -134,21 +132,17 @@
</div>
</div>
{{ range $namespace, $results := .FilteredAuditData.GetResultsByNamespace }}
{{ range $namespace, $ctrlResults := .FilteredAuditData.GetResultsByNamespace }}
<div id="{{ $namespace }}" class="card namespace">
{{ if eq $namespace "" }}
<h3>Cluster Resources</h3>
{{ else }}
<h3>Namespace: <strong>{{ $namespace }}</strong></h3>
{{ end }}
<div class="expandable-table">
{{ range $index, $result := $results }}
{{ range $ctrlResults }}
<div class="resource-info">
<div class="status-bar">
<div class="status">
<div class="failing">
<div class="warning" style="width: {{ getWarningWidth $result.GetSummary 200 }}px;">
<div class="passing" style="width: {{ getSuccessWidth $result.GetSummary 200 }}px;"></div>
<div class="warning" style="width: {{ getWarningWidth .PodResult.GetSummary 200 }}px;">
<div class="passing" style="width: {{ getSuccessWidth .PodResult.GetSummary 200 }}px;"></div>
</div>
</div>
</div>
@@ -159,11 +153,7 @@
<strong>{{ .Name }}</strong></div>
<div class="result-messages expandable-content">
<h4>Spec:
{{ if eq 0 (len .Results.GetSortedResults) }}
<i>no checks applied</i>
{{ end }}
</h4>
<h4>Controller Spec:</h4>
<ul class="message-list">
{{ range $message := .Results.GetSortedResults }}
<li class="{{ getResultClass . }}">
@@ -177,15 +167,26 @@
</ul>
</div>
{{ if .PodResult }}
<div class="result-messages expandable-content">
<h4>Pod Spec:</h4>
<ul class="message-list">
{{ range $message := .PodResult.Results.GetSortedResults }}
<li class="{{ getResultClass . }}">
<i class="message-icon {{ getIcon $message }}"></i>
<span class="message">{{ .Message }}</span>
<a class="more-info" href="{{ getCategoryLink .Category }}" target="_blank">
<i class="far fa-question-circle"></i>
</a>
</li>
{{ end }}
</ul>
</div>
{{ range .PodResult.ContainerResults }}
<div class="result-messages expandable-content">
<h4>Pod Spec:
{{ if eq 0 (len .PodResult.Results.GetSortedResults) }}
<i>no checks applied</i>
{{ end }}
</h4>
<h4>Container: {{ .Name }}</h4>
<ul class="message-list">
{{ range $message := .PodResult.Results.GetSortedResults }}
{{ range $message := .Results.GetSortedResults }}
<li class="{{ getResultClass . }}">
<i class="message-icon {{ getIcon $message }}"></i>
<span class="message">{{ .Message }}</span>
@@ -196,30 +197,7 @@
{{ end }}
</ul>
</div>
{{ end }}
{{ if .PodResult }}
{{ range .PodResult.ContainerResults }}
<div class="result-messages expandable-content">
<h4>Container {{ .Name }}:
{{ if eq 0 (len .Results.GetSortedResults) }}
<i>no checks applied</i>
{{ end }}
</h4>
<ul class="message-list">
{{ range $message := .Results.GetSortedResults }}
<li class="{{ getResultClass . }}">
<i class="message-icon {{ getIcon $message }}"></i>
<span class="message">{{ .Message }}</span>
<a class="more-info" href="{{ getCategoryLink .Category }}" target="_blank">
<i class="far fa-question-circle"></i>
</a>
</li>
{{ end }}
</ul>
</div>
{{ end }} {{/* end range .PodResult.ContainerResults */}}
{{ end }} {{/* end if .PodResult */}}
{{ end }} {{/* end range .PodResult.ContainerResults */}}
</div>
{{ end }} {{/* end range .Results.GetSortedResults */}}
</div>
+1 -1
View File
@@ -23,7 +23,7 @@
<hr class="col-11">
<div class="align-self-center d-flex flex-column justify-content-center">
<img class="row mb-4 fw-logo" src="static/images/purple_logo_fairwinds.svg" alt="Fairwinds" />
<a class="row justify-content-center" href="https://www.fairwinds.com/polaris-user-insights-demo?utm_source=polaris&utm_medium=polaris&utm_campaign=polaris" target="_blank">&copy; 2020 Fairwinds Ops Inc.</a>
<a class="row justify-content-center" href="https://fairwinds.com?source=polaris" target="_blank">&copy; 2020 Fairwinds Ops Inc.</a>
</div>
</div>
{{end}}
+1 -1
View File
@@ -2,7 +2,7 @@
<div class="navbar">
<div class="fw-nav">
<div class="container p-2">
<a href="https://www.fairwinds.com/polaris-user-insights-demo?utm_source=polaris&utm_medium=polaris&utm_campaign=polaris" target="_blank">
<a href="https://fairwinds.com?source=polaris" target="_blank">
<img class="fw-logo" src="static/images/white_logo_fairwinds.svg" alt="Fairwinds" />
</a>
<div class="right-section p-0 d-flex justify-content-between">
+58 -158
View File
@@ -12,15 +12,11 @@ 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"
"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"
@@ -37,74 +33,7 @@ type ResourceProvider struct {
SourceType string
Nodes []corev1.Node
Namespaces []corev1.Namespace
Resources resourceKindMap
}
type resourceKindMap map[string][]GenericResource
func (rkm resourceKindMap) addResource(r GenericResource) {
gvk := r.Resource.GroupVersionKind()
key := gvk.Group + "/" + gvk.Kind
rkm[key] = append(rkm[key], 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 string) string {
if k == "Ingress" {
return "networking.k8s.io/Ingress"
} else if k == "PodDisruptionBudget" {
return "policy/PodDisruptionBudget"
}
return k
}
func parseGroupKind(gk string) schema.GroupKind {
i := strings.Index(gk, "/")
if i == -1 {
return schema.GroupKind{Kind: gk}
}
group := gk[:i]
kind := gk[i+1:]
return schema.GroupKind{Group: group, Kind: kind}
}
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),
}
Controllers []GenericWorkload
}
type k8sResource struct {
@@ -114,18 +43,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, c conf.Configuration) (*ResourceProvider, error) {
func CreateResourceProvider(ctx context.Context, directory, workload string) (*ResourceProvider, error) {
if workload != "" {
return CreateResourceProviderFromResource(ctx, workload)
return CreateResourceProviderFromWorkload(ctx, workload)
}
if directory != "" {
return CreateResourceProviderFromPath(directory)
}
return CreateResourceProviderFromCluster(ctx, c)
return CreateResourceProviderFromCluster(ctx)
}
// CreateResourceProviderFromResource creates a new ResourceProvider that just contains one workload
func CreateResourceProviderFromResource(ctx context.Context, workload string) (*ResourceProvider, error) {
// CreateResourceProviderFromWorkload creates a new ResourceProvider that just contains one workload
func CreateResourceProviderFromWorkload(ctx context.Context, workload string) (*ResourceProvider, error) {
kubeConf, configError := config.GetConfig()
if configError != nil {
logrus.Errorf("Error fetching KubeConfig: %v", configError)
@@ -141,7 +70,14 @@ func CreateResourceProviderFromResource(ctx context.Context, workload string) (*
logrus.Errorf("Error fetching Cluster API version: %v", err)
return nil, err
}
resources := newResourceProvider(serverVersion.Major+"."+serverVersion.Minor, "Resource", workload)
resources := ResourceProvider{
ServerVersion: serverVersion.Major + "." + serverVersion.Minor,
SourceType: "Workload",
SourceName: workload,
CreationTime: time.Now(),
Nodes: []corev1.Node{},
Namespaces: []corev1.Namespace{},
}
parts := strings.Split(workload, "/")
if len(parts) != 4 {
@@ -168,24 +104,31 @@ func CreateResourceProviderFromResource(ctx context.Context, workload string) (*
logrus.Errorf("Could not find workload %s: %v", workload, err)
return nil, err
}
workloadObj, err := NewGenericResourceFromUnstructured(*obj)
workloadObj, err := NewGenericWorkloadFromUnstructured(kind, obj)
if err != nil {
logrus.Errorf("Could not parse workload %s: %v", workload, err)
return nil, err
}
resources.Resources.addResource(workloadObj)
resources.Controllers = []GenericWorkload{workloadObj}
return &resources, nil
}
// CreateResourceProviderFromPath returns a new ResourceProvider using the YAML files in a directory
func CreateResourceProviderFromPath(directory string) (*ResourceProvider, error) {
resources := newResourceProvider("unknown", "Path", directory)
resources := ResourceProvider{
ServerVersion: "unknown",
SourceType: "Path",
SourceName: directory,
Nodes: []corev1.Node{},
Namespaces: []corev1.Namespace{},
Controllers: []GenericWorkload{},
}
if directory == "-" {
fi, err := os.Stdin.Stat()
if err == nil && fi.Mode()&os.ModeNamedPipe == os.ModeNamedPipe {
if err := resources.addResourcesFromReader(os.Stdin); err != nil {
if err := addResourcesFromReader(os.Stdin, &resources); err != nil {
return nil, err
}
return &resources, nil
@@ -201,7 +144,7 @@ func CreateResourceProviderFromPath(directory string) (*ResourceProvider, error)
logrus.Errorf("Error reading file: %v", path)
return err
}
return resources.addResourcesFromYaml(string(contents))
return addResourcesFromYaml(string(contents), &resources)
}
err := filepath.Walk(directory, visitFile)
@@ -212,7 +155,7 @@ func CreateResourceProviderFromPath(directory string) (*ResourceProvider, error)
}
// CreateResourceProviderFromCluster creates a new ResourceProvider using live data from a cluster
func CreateResourceProviderFromCluster(ctx context.Context, c conf.Configuration) (*ResourceProvider, error) {
func CreateResourceProviderFromCluster(ctx context.Context) (*ResourceProvider, error) {
kubeConf, configError := config.GetConfig()
if configError != nil {
logrus.Errorf("Error fetching KubeConfig: %v", configError)
@@ -228,18 +171,17 @@ func CreateResourceProviderFromCluster(ctx context.Context, c conf.Configuration
logrus.Errorf("Error connecting to dynamic interface: %v", err)
return nil, err
}
return CreateResourceProviderFromAPI(ctx, api, kubeConf.Host, &dynamicInterface, c)
return CreateResourceProviderFromAPI(ctx, api, kubeConf.Host, &dynamicInterface)
}
// CreateResourceProviderFromAPI creates a new ResourceProvider from an existing k8s interface
func CreateResourceProviderFromAPI(ctx context.Context, kube kubernetes.Interface, clusterName string, dynamic *dynamic.Interface, c conf.Configuration) (*ResourceProvider, error) {
func CreateResourceProviderFromAPI(ctx context.Context, kube kubernetes.Interface, clusterName string, dynamic *dynamic.Interface) (*ResourceProvider, error) {
listOpts := metav1.ListOptions{}
serverVersion, err := kube.Discovery().ServerVersion()
if err != nil {
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 {
@@ -263,51 +205,6 @@ func CreateResourceProviderFromAPI(ctx context.Context, kube kubernetes.Interfac
return nil, err
}
restMapper := restmapper.NewDiscoveryRESTMapper(resources)
allChecks := []conf.SchemaCheck{}
for _, check := range c.CustomChecks {
allChecks = append(allChecks, check)
}
for _, check := range conf.BuiltInChecks {
allChecks = append(allChecks, check)
}
var additionalKinds []conf.TargetKind
for _, check := range allChecks {
neededKinds := []conf.TargetKind{check.Target}
for key := range check.AdditionalSchemas {
neededKinds = append(neededKinds, conf.TargetKind(key))
}
for key := range check.AdditionalSchemaStrings {
neededKinds = append(neededKinds, conf.TargetKind(key))
}
for _, kind := range neededKinds {
if !funk.Contains(conf.HandledTargets, kind) && !funk.Contains(additionalKinds, kind) {
additionalKinds = append(additionalKinds, kind)
}
}
}
for _, kind := range additionalKinds {
groupKind := parseGroupKind(maybeTransformKindIntoGroupKind(string(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{}
@@ -316,15 +213,22 @@ func CreateResourceProviderFromAPI(ctx context.Context, kube kubernetes.Interfac
logrus.Errorf("Error loading controllers from pods: %v", err)
return nil, err
}
provider.Nodes = nodes.Items
provider.Namespaces = namespaces.Items
provider.Resources.addResources(controllers)
return &provider, nil
api := ResourceProvider{
ServerVersion: serverVersion.Major + "." + serverVersion.Minor,
SourceType: "Cluster",
SourceName: clusterName,
CreationTime: time.Now(),
Nodes: nodes.Items,
Namespaces: namespaces.Items,
Controllers: controllers,
}
return &api, 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) ([]GenericResource, error) {
interfaces := []GenericResource{}
func LoadControllers(ctx context.Context, pods []corev1.Pod, dynamicClientPointer *dynamic.Interface, restMapperPointer *meta.RESTMapper, objectCache map[string]unstructured.Unstructured) ([]GenericWorkload, error) {
interfaces := []GenericWorkload{}
deduped := map[string]corev1.Pod{}
for _, pod := range pods {
owners := pod.ObjectMeta.OwnerReferences
@@ -335,7 +239,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 := ResolveControllerFromPod(ctx, pod, dynamicClientPointer, restMapperPointer, objectCache)
workload, err := NewGenericWorkload(ctx, pod, dynamicClientPointer, restMapperPointer, objectCache)
if err != nil {
return nil, err
}
@@ -346,8 +250,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 []GenericResource) []GenericResource {
controllerMap := make(map[string]GenericResource)
func deduplicateControllers(inputControllers []GenericWorkload) []GenericWorkload {
controllerMap := make(map[string]GenericWorkload)
for _, controller := range inputControllers {
key := controller.ObjectMeta.GetNamespace() + "/" + controller.Kind + "/" + controller.ObjectMeta.GetName()
oldController, ok := controllerMap[key]
@@ -355,34 +259,32 @@ func deduplicateControllers(inputControllers []GenericResource) []GenericResourc
controllerMap[key] = controller
}
}
results := make([]GenericResource, len(controllerMap))
idx := 0
results := make([]GenericWorkload, 0)
for _, controller := range controllerMap {
results[idx] = controller
idx++
results = append(results, controller)
}
return results
}
func (resources *ResourceProvider) addResourcesFromReader(reader io.Reader) error {
func addResourcesFromReader(reader io.Reader, resources *ResourceProvider) error {
contents, err := ioutil.ReadAll(reader)
if err != nil {
logrus.Errorf("Error reading from %v: %v", reader, err)
return err
}
if err := resources.addResourcesFromYaml(string(contents)); err != nil {
if err := addResourcesFromYaml(string(contents), resources); err != nil {
return err
}
return nil
}
func (resources *ResourceProvider) addResourcesFromYaml(contents string) error {
func addResourcesFromYaml(contents string, resources *ResourceProvider) error {
specs := regexp.MustCompile("[\r\n]-+[\r\n]").Split(string(contents), -1)
for _, spec := range specs {
if strings.TrimSpace(spec) == "" {
continue
}
err := resources.addResourceFromString(spec)
err := addResourceFromString(spec, resources)
if err != nil {
logrus.Errorf("Error parsing YAML: (%v)", err)
return err
@@ -391,7 +293,7 @@ func (resources *ResourceProvider) addResourcesFromYaml(contents string) error {
return nil
}
func (resources *ResourceProvider) addResourceFromString(contents string) error {
func addResourceFromString(contents string, resources *ResourceProvider) error {
contentBytes := []byte(contents)
decoder := k8sYaml.NewYAMLOrJSONDecoder(bytes.NewReader(contentBytes), 1000)
resource := k8sResource{}
@@ -406,25 +308,23 @@ func (resources *ResourceProvider) addResourceFromString(contents string) error
ns := corev1.Namespace{}
err = decoder.Decode(&ns)
resources.Namespaces = append(resources.Namespaces, ns)
}
if resource.Kind == "Pod" {
} else if resource.Kind == "Pod" {
pod := corev1.Pod{}
err = decoder.Decode(&pod)
if err != nil {
return err
}
workload, err := NewGenericResourceFromPod(pod, pod)
workload, err := NewGenericWorkloadFromPod(pod, pod)
if err != nil {
return err
}
resources.Resources.addResource(workload)
resources.Controllers = append(resources.Controllers, workload)
} else {
newResource, err := NewGenericResourceFromBytes(contentBytes)
if err != nil {
newController, err := GetWorkloadFromBytes(contentBytes)
if err != nil || newController == nil {
return err
}
resources.Resources.addResource(newResource)
resources.Controllers = append(resources.Controllers, *newController)
}
return err
}
+30 -27
View File
@@ -7,34 +7,32 @@ import (
"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) {
provider, err := CreateResourceProviderFromPath("./test_files/test_1")
resources, err := CreateResourceProviderFromPath("./test_files/test_1")
assert.Equal(t, nil, err, "Error should be nil")
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, "Path", resources.SourceType, "Should have type Path")
assert.Equal(t, "./test_files/test_1", resources.SourceName, "Should have filename as name")
assert.Equal(t, "unknown", resources.ServerVersion, "Server version should be unknown")
assert.IsType(t, time.Now(), resources.CreationTime, "Creation time should be set")
assert.Equal(t, 0, len(provider.Nodes), "Should not have any nodes")
assert.Equal(t, 0, len(resources.Nodes), "Should not have any nodes")
assert.Equal(t, 1, len(provider.Namespaces), "Should have a namespace")
assert.Equal(t, "two", provider.Namespaces[0].ObjectMeta.Name)
assert.Equal(t, 1, len(resources.Namespaces), "Should have a namespace")
assert.Equal(t, "two", resources.Namespaces[0].ObjectMeta.Name)
assert.Equal(t, 9, len(resources.Controllers), "Should have eight controllers")
namespaceCount := map[string]int{}
for _, resources := range provider.Resources {
for _, controller := range resources {
namespaceCount[controller.ObjectMeta.GetNamespace()]++
}
for _, controller := range resources.Controllers {
namespaceCount[controller.ObjectMeta.GetNamespace()]++
}
assert.Equal(t, 11, provider.Resources.GetLength())
assert.Equal(t, 10, namespaceCount[""])
assert.Equal(t, 8, namespaceCount[""])
assert.Equal(t, 1, namespaceCount["two"])
}
@@ -50,8 +48,8 @@ func TestGetMultipleResourceFromSingleFile(t *testing.T) {
assert.Equal(t, 0, len(resources.Nodes), "Should not have any nodes")
assert.Equal(t, 1, len(resources.Resources["extensions/Deployment"]), "Should have one controller")
assert.Equal(t, "dashboard", resources.Resources["extensions/Deployment"][0].PodSpec.Containers[0].Name)
assert.Equal(t, 1, len(resources.Controllers), "Should have one controller")
assert.Equal(t, "dashboard", resources.Controllers[0].PodSpec.Containers[0].Name)
assert.Equal(t, 2, len(resources.Namespaces), "Should have a namespace")
assert.Equal(t, "polaris", resources.Namespaces[0].ObjectMeta.Name)
@@ -67,14 +65,21 @@ func TestAddResourcesFromReader(t *testing.T) {
contents, err := ioutil.ReadFile("./test_files/test_2/multi.yaml")
assert.NoError(t, err)
reader := bytes.NewBuffer(contents)
resources := newResourceProvider("unknown", "Path", "-")
err = resources.addResourcesFromReader(reader)
resources := &ResourceProvider{
ServerVersion: "unknown",
SourceType: "Path",
SourceName: "-",
Nodes: []corev1.Node{},
Namespaces: []corev1.Namespace{},
Controllers: []GenericWorkload{},
}
err = addResourcesFromReader(reader, resources)
assert.NoError(t, err)
assert.Equal(t, 0, len(resources.Nodes), "Should not have any nodes")
assert.Equal(t, 1, len(resources.Resources["extensions/Deployment"]), "Should have one controller")
assert.Equal(t, "dashboard", resources.Resources["extensions/Deployment"][0].PodSpec.Containers[0].Name)
assert.Equal(t, 1, len(resources.Controllers), "Should have one controller")
assert.Equal(t, "dashboard", resources.Controllers[0].PodSpec.Containers[0].Name)
assert.Equal(t, 2, len(resources.Namespaces), "Should have a namespace")
assert.Equal(t, "polaris", resources.Namespaces[0].ObjectMeta.Name)
@@ -83,7 +88,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, conf.Configuration{})
resources, err := CreateResourceProviderFromAPI(context.Background(), k8s, "test", &dynamicInterface)
assert.Equal(t, nil, err, "Error should be nil")
assert.Equal(t, "Cluster", resources.SourceType, "Should have type Path")
@@ -91,7 +96,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, 5, len(resources.Resources), "Should have 5 controllers")
assert.Equal(t, 5, len(resources.Controllers), "Should have 5 controllers")
expectedNames := map[string]bool{
"deploy": false,
@@ -100,10 +105,8 @@ func TestGetResourceFromAPI(t *testing.T) {
"statefulset": false,
"daemonset": false,
}
for _, controllers := range resources.Resources {
for _, ctrl := range controllers {
expectedNames[ctrl.ObjectMeta.GetName()] = true
}
for _, ctrl := range resources.Controllers {
expectedNames[ctrl.ObjectMeta.GetName()] = true
}
for name, val := range expectedNames {
assert.Equal(t, true, val, name)
+88 -72
View File
@@ -1,6 +1,7 @@
package kube
import (
"bytes"
"context"
"encoding/json"
"fmt"
@@ -12,62 +13,61 @@ 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"
)
// GenericResource is a base implementation with some free methods for inherited structs
type GenericResource struct {
// GenericWorkload is a base implementation with some free methods for inherited structs
type GenericWorkload struct {
Kind string
PodSpec kubeAPICoreV1.PodSpec
ObjectMeta kubeAPIMetaV1.Object
Resource unstructured.Unstructured
PodSpec *kubeAPICoreV1.PodSpec
OriginalObjectJSON []byte
}
// NewGenericResourceFromUnstructured creates a workload from an unstructured.Unstructured
func NewGenericResourceFromUnstructured(unst unstructured.Unstructured) (GenericResource, error) {
workload := GenericResource{
Kind: unst.GetKind(),
Resource: unst,
// NewGenericWorkloadFromUnstructured creates a workload from an unstructured.Unstructured
func NewGenericWorkloadFromUnstructured(kind string, unst *unstructured.Unstructured) (GenericWorkload, error) {
workload := GenericWorkload{
Kind: kind,
}
objMeta, err := meta.Accessor(&unst)
objMeta, err := meta.Accessor(unst)
if err != nil {
return workload, err
}
workload.ObjectMeta = objMeta
b, err := json.Marshal(&unst)
b, err := json.Marshal(unst)
if err != nil {
return workload, err
}
workload.OriginalObjectJSON = b
m := make(map[string]interface{})
err = json.Unmarshal(b, &m)
if err != nil {
return workload, err
}
podSpecMap := GetPodSpec(m)
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
b, err = json.Marshal(podSpecMap)
if err != nil {
return workload, err
}
podSpec := kubeAPICoreV1.PodSpec{}
err = json.Unmarshal(b, &podSpec)
if err != nil {
return workload, err
}
workload.PodSpec = podSpec
return workload, nil
}
// NewGenericResourceFromPod builds a new workload for a given Pod without looking at parents
func NewGenericResourceFromPod(podResource kubeAPICoreV1.Pod, originalObject interface{}) (GenericResource, error) {
workload := GenericResource{
// NewGenericWorkloadFromPod builds a new workload for a given Pod without looking at parents
func NewGenericWorkloadFromPod(podResource kubeAPICoreV1.Pod, originalObject interface{}) (GenericWorkload, error) {
workload := GenericWorkload{
Kind: "Pod",
PodSpec: &podResource.Spec,
PodSpec: podResource.Spec,
ObjectMeta: podResource.ObjectMeta.GetObjectMeta(),
}
if originalObject != nil {
@@ -76,52 +76,30 @@ func NewGenericResourceFromPod(podResource kubeAPICoreV1.Pod, originalObject int
return workload, err
}
workload.OriginalObjectJSON = bytes
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(&workload.Resource)
if err != nil {
logrus.Error("Couldn't create meta accessor for unstructred ", err)
return workload, err
}
workload.ObjectMeta = objMeta
}
return workload, nil
}
// 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)
// 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)
if err != nil {
return workload, err
}
if len(workload.OriginalObjectJSON) == 0 {
return NewGenericResourceFromPod(podResource, podResource)
return NewGenericWorkloadFromPod(podResource, podResource)
}
return workload, err
}
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)
func newGenericWorkload(ctx context.Context, podResource kubeAPICoreV1.Pod, dynamicClient *dynamic.Interface, restMapper *meta.RESTMapper, objectCache map[string]unstructured.Unstructured) (GenericWorkload, error) {
workload, err := NewGenericWorkloadFromPod(podResource, nil)
if err != nil {
return podWorkload, err
return workload, err
}
topKind := "Pod"
topMeta := podWorkload.ObjectMeta
owners := podResource.ObjectMeta.GetOwnerReferences()
// If an owner exists then set the name to the workload.
// This allows us to handle CRDs creating Workloads or DeploymentConfigs in OpenShift.
owners := workload.ObjectMeta.GetOwnerReferences()
lastKey := ""
for len(owners) > 0 {
if len(owners) > 1 {
@@ -131,12 +109,12 @@ func resolveControllerFromPod(ctx context.Context, podResource kubeAPICoreV1.Pod
if firstOwner.Kind == "Node" {
break
}
topKind = firstOwner.Kind
key := fmt.Sprintf("%s/%s/%s", firstOwner.Kind, topMeta.GetNamespace(), firstOwner.Name)
workload.Kind = firstOwner.Kind
key := fmt.Sprintf("%s/%s/%s", firstOwner.Kind, workload.ObjectMeta.GetNamespace(), firstOwner.Name)
lastKey = key
abstractObject, ok := objectCache[key]
if !ok {
err := cacheAllObjectsOfKind(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
@@ -151,22 +129,26 @@ func resolveControllerFromPod(ctx context.Context, podResource kubeAPICoreV1.Pod
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 GenericResource{}, err
return workload, err
}
topMeta = objMeta
workload.ObjectMeta = objMeta
owners = abstractObject.GetOwnerReferences()
}
if lastKey != "" {
unst := objectCache[lastKey]
return NewGenericResourceFromUnstructured(unst)
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
}
workload, err := NewGenericResourceFromPod(podResource, podResource)
if err != nil {
return workload, err
}
workload.Kind = topKind
workload.ObjectMeta = topMeta
return workload, nil
}
@@ -174,13 +156,13 @@ func cacheAllObjectsOfKind(ctx context.Context, apiVersion, kind string, dynamic
fqKind := schema.FromAPIVersionAndKind(apiVersion, kind)
mapping, err := (*restMapper).RESTMapping(fqKind.GroupKind(), fqKind.Version)
if err != nil {
logrus.Warnf("Error retrieving mapping of API %s and Kind %s because of error: %v", apiVersion, kind, err)
logrus.Warnf("Error retrieving mapping of API %s and Kind %s because of error: %v ", apiVersion, kind, err)
return err
}
objects, err := (*dynamicClient).Resource(mapping.Resource).Namespace("").List(ctx, kubeAPIMetaV1.ListOptions{})
if err != nil {
logrus.Warnf("Error retrieving parent object API %s and Kind %s because of error: %v", mapping.Resource.Version, mapping.Resource.Resource, err)
logrus.Warnf("Error retrieving parent object API %s and Kind %s because of error: %v ", mapping.Resource.Version, mapping.Resource.Resource, err)
return err
}
for idx, object := range objects.Items {
@@ -212,3 +194,37 @@ 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
}
-89
View File
@@ -1,89 +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 (
"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{
"pdbDisruptionsIsZero": conf.SeverityWarning,
},
}
pdb := unstructured.Unstructured{}
res, err := kube.NewGenericResourceFromUnstructured(pdb)
res.Kind = "PodDisruptionBudget"
actualResult, err := applyNonControllerSchemaChecks(&c, nil, res)
if err != nil {
panic(err)
}
results := actualResult.Results["pdbDisruptionsIsZero"]
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, nil, 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)
}
+60
View File
@@ -0,0 +1,60 @@
// 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 (
"context"
"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(ctx context.Context, conf *config.Configuration, controller kube.GenericWorkload, container *corev1.Container, isInit bool) (ContainerResult, error) {
results, err := applyContainerSchemaChecks(ctx, conf, controller, container, isInit)
if err != nil {
return ContainerResult{}, err
}
cRes := ContainerResult{
Name: container.Name,
Results: results,
}
return cRes, nil
}
// ValidateAllContainers validates both init and regular containers
func ValidateAllContainers(ctx context.Context, conf *config.Configuration, controller kube.GenericWorkload) ([]ContainerResult, error) {
results := []ContainerResult{}
pod := controller.PodSpec
for _, container := range pod.InitContainers {
result, err := ValidateContainer(ctx, conf, controller, &container, true)
if err != nil {
return nil, err
}
results = append(results, result)
}
for _, container := range pod.Containers {
result, err := ValidateContainer(ctx, conf, controller, &container, false)
if err != nil {
return nil, err
}
results = append(results, result)
}
return results, nil
}
+28 -31
View File
@@ -15,6 +15,7 @@
package validator
import (
"context"
"fmt"
"testing"
@@ -50,8 +51,8 @@ exemptions:
- foo
`
func getEmptyWorkload(t *testing.T, name string) kube.GenericResource {
workload, err := kube.NewGenericResourceFromPod(corev1.Pod{
func getEmptyWorkload(t *testing.T, name string) kube.GenericWorkload {
workload, err := kube.NewGenericWorkloadFromPod(corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
@@ -64,28 +65,24 @@ 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.GenericResource, expectedDangers []ResultMessage, expectedWarnings []ResultMessage, expectedSuccesses []ResultMessage) {
func testValidateWithWorkload(t *testing.T, container *corev1.Container, resourceConf *string, workload kube.GenericWorkload, expectedDangers []ResultMessage, expectedWarnings []ResultMessage, expectedSuccesses []ResultMessage) {
parsedConf, err := conf.Parse([]byte(*resourceConf))
assert.NoError(t, err, "Expected no error when parsing config")
var results ResultSet
results, err = applyContainerSchemaChecks(&parsedConf, nil, workload, container, false)
results, err := applyContainerSchemaChecks(context.Background(), &parsedConf, workload, container, false)
if err != nil {
panic(err)
}
summary := results.GetSummary()
if assert.Equal(t, uint(len(expectedWarnings)), summary.Warnings) {
assert.ElementsMatch(t, expectedWarnings, results.GetWarnings())
}
assert.Equal(t, uint(len(expectedWarnings)), summary.Warnings)
assert.ElementsMatch(t, expectedWarnings, results.GetWarnings())
if assert.Equal(t, uint(len(expectedDangers)), summary.Dangers) {
assert.ElementsMatch(t, expectedDangers, results.GetDangers())
}
assert.Equal(t, uint(len(expectedDangers)), summary.Dangers)
assert.ElementsMatch(t, expectedDangers, results.GetDangers())
if assert.Equal(t, uint(len(expectedSuccesses)), summary.Successes) {
assert.ElementsMatch(t, expectedSuccesses, results.GetSuccesses())
}
assert.Equal(t, uint(len(expectedSuccesses)), summary.Successes)
assert.ElementsMatch(t, expectedSuccesses, results.GetSuccesses())
}
func TestValidateResourcesEmptyConfig(t *testing.T) {
@@ -93,7 +90,7 @@ func TestValidateResourcesEmptyConfig(t *testing.T) {
Name: "Empty",
}
results, err := applyContainerSchemaChecks(&conf.Configuration{}, nil, getEmptyWorkload(t, ""), container, false)
results, err := applyContainerSchemaChecks(context.Background(), &conf.Configuration{}, getEmptyWorkload(t, ""), container, false)
if err != nil {
panic(err)
}
@@ -190,7 +187,7 @@ func TestValidateHealthChecks(t *testing.T) {
for idx, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
controller := getEmptyWorkload(t, "")
results, err := applyContainerSchemaChecks(&conf.Configuration{Checks: tt.probes}, nil, controller, tt.container, tt.isInit)
results, err := applyContainerSchemaChecks(context.Background(), &conf.Configuration{Checks: tt.probes}, controller, tt.container, tt.isInit)
if err != nil {
panic(err)
}
@@ -304,7 +301,7 @@ func TestValidateImage(t *testing.T) {
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
controller := getEmptyWorkload(t, "")
results, err := applyContainerSchemaChecks(&conf.Configuration{Checks: tt.image}, nil, controller, tt.container, false)
results, err := applyContainerSchemaChecks(context.Background(), &conf.Configuration{Checks: tt.image}, controller, tt.container, false)
if err != nil {
panic(err)
}
@@ -421,7 +418,7 @@ func TestValidateNetworking(t *testing.T) {
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
controller := getEmptyWorkload(t, "")
results, err := applyContainerSchemaChecks(&conf.Configuration{Checks: tt.networkConf}, nil, controller, tt.container, false)
results, err := applyContainerSchemaChecks(context.Background(), &conf.Configuration{Checks: tt.networkConf}, controller, tt.container, false)
if err != nil {
panic(err)
}
@@ -558,14 +555,14 @@ func TestValidateSecurity(t *testing.T) {
Category: "Security",
}, {
ID: "privilegeEscalationAllowed",
Message: "Privilege escalation should not be allowed",
Success: false,
Message: "Privilege escalation not allowed",
Success: true,
Severity: "danger",
Category: "Security",
}, {
ID: "insecureCapabilities",
Message: "Container should not have insecure capabilities",
Success: false,
Message: "Container does not have any insecure capabilities",
Success: true,
Severity: "warning",
Category: "Security",
}, {
@@ -742,8 +739,8 @@ func TestValidateSecurity(t *testing.T) {
Category: "Security",
}, {
ID: "insecureCapabilities",
Message: "Container should not have insecure capabilities",
Success: false,
Message: "Container does not have any insecure capabilities",
Success: true,
Severity: "warning",
Category: "Security",
}},
@@ -761,8 +758,8 @@ func TestValidateSecurity(t *testing.T) {
Category: "Security",
}, {
ID: "insecureCapabilities",
Message: "Container should not have insecure capabilities",
Success: false,
Message: "Container does not have any insecure capabilities",
Success: true,
Severity: "danger",
Category: "Security",
}, {
@@ -924,9 +921,9 @@ func TestValidateSecurity(t *testing.T) {
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
workload, err := kube.NewGenericResourceFromPod(corev1.Pod{Spec: *tt.pod}, nil)
workload, err := kube.NewGenericWorkloadFromPod(corev1.Pod{Spec: *tt.pod}, nil)
assert.NoError(t, err)
results, err := applyContainerSchemaChecks(&conf.Configuration{Checks: tt.securityConf}, nil, workload, tt.container, false)
results, err := applyContainerSchemaChecks(context.Background(), &conf.Configuration{Checks: tt.securityConf}, workload, tt.container, false)
if err != nil {
panic(err)
}
@@ -1069,9 +1066,9 @@ func TestValidateRunAsRoot(t *testing.T) {
}
for idx, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
workload, err := kube.NewGenericResourceFromPod(corev1.Pod{Spec: *tt.pod}, nil)
workload, err := kube.NewGenericWorkloadFromPod(corev1.Pod{Spec: *tt.pod}, nil)
assert.NoError(t, err)
results, err := applyContainerSchemaChecks(&config, nil, workload, tt.container, false)
results, err := applyContainerSchemaChecks(context.Background(), &config, workload, tt.container, false)
if err != nil {
panic(err)
}
@@ -1171,7 +1168,7 @@ func TestValidateResourcesEmptyContainerCPURequestsExempt(t *testing.T) {
expectedSuccesses := []ResultMessage{}
workload, err := kube.NewGenericResourceFromPod(corev1.Pod{
workload, err := kube.NewGenericWorkloadFromPod(corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
Annotations: map[string]string{
+77
View File
@@ -0,0 +1,77 @@
// 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 (
"context"
"strings"
"github.com/sirupsen/logrus"
conf "github.com/fairwindsops/polaris/pkg/config"
"github.com/fairwindsops/polaris/pkg/kube"
)
const exemptionAnnotationKey = "polaris.fairwinds.com/exempt"
// ValidateController validates a single controller, returns a ControllerResult.
func ValidateController(ctx context.Context, conf *conf.Configuration, controller kube.GenericWorkload) (ControllerResult, error) {
podResult, err := ValidatePod(ctx, conf, controller)
if err != nil {
return ControllerResult{}, err
}
controllerResult, err := applyControllerSchemaChecks(ctx, conf, controller)
if err != nil {
return ControllerResult{}, err
}
result := ControllerResult{
Kind: controller.Kind,
Name: controller.ObjectMeta.GetName(),
Namespace: controller.ObjectMeta.GetNamespace(),
Results: 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(ctx context.Context, config *conf.Configuration, kubeResources *kube.ResourceProvider) ([]ControllerResult, error) {
controllersToAudit := kubeResources.Controllers
results := []ControllerResult{}
for _, controller := range controllersToAudit {
if !config.DisallowExemptions && hasExemptionAnnotation(controller) {
continue
}
result, err := ValidateController(ctx, config, controller)
if err != nil {
logrus.Warn("An error occured validating controller:", err)
return nil, err
}
results = append(results, result)
}
return results, nil
}
func hasExemptionAnnotation(ctrl kube.GenericWorkload) bool {
annot := ctrl.ObjectMeta.GetAnnotations()
val := annot[exemptionAnnotationKey]
return strings.ToLower(val) == "true"
}
+44 -74
View File
@@ -34,7 +34,7 @@ func TestValidateController(t *testing.T) {
"hostPIDSet": conf.SeverityDanger,
},
}
deployment, err := kube.NewGenericResourceFromPod(test.MockPod(), nil)
deployment, err := kube.NewGenericWorkloadFromPod(test.MockPod(), nil)
assert.NoError(t, err)
deployment.Kind = "Deployment"
expectedSum := CountSummary{
@@ -48,8 +48,7 @@ func TestValidateController(t *testing.T) {
"hostPIDSet": {ID: "hostPIDSet", Message: "Host PID is not configured", Success: true, Severity: "danger", Category: "Security"},
}
var actualResult Result
actualResult, err = applyControllerSchemaChecks(&c, nil, deployment)
actualResult, err := ValidateController(context.Background(), &c, deployment)
if err != nil {
panic(err)
}
@@ -72,31 +71,33 @@ func TestControllerLevelChecks(t *testing.T) {
Severity: "danger",
Category: "Reliability",
}
for _, controller := range res.Resources["Deployment"] {
actualResult, err := applyControllerSchemaChecks(&c, nil, 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,
}
for _, controller := range res.Controllers {
if controller.Kind == "Deployment" {
actualResult, err := ValidateController(context.Background(), &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())
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, 11, res.Resources.GetLength())
assert.Equal(t, 9, len(res.Controllers), "Should have eight controllers")
testResources(res)
replicaSpec := map[string]interface{}{"replicas": 2}
@@ -106,12 +107,11 @@ func TestControllerLevelChecks(t *testing.T) {
d1, p1 := test.MockDeploy("test", "test-deployment")
d2, p2 := test.MockDeploy("test", "test-deployment-2")
two := int32(2)
d2.Spec.Replicas = &two
d2.Object["spec"] = replicaSpec
k8s, dynamicClient := test.SetupTestAPI(&d1, &p1, &d2, &p2)
res, err = kube.CreateResourceProviderFromAPI(context.Background(), k8s, "test", &dynamicClient, conf.Configuration{})
res, err = kube.CreateResourceProviderFromAPI(context.Background(), k8s, "test", &dynamicClient)
assert.Equal(t, err, nil, "error should be nil")
assert.Equal(t, 2, res.Resources.GetLength(), "Should have two controllers")
assert.Equal(t, 2, len(res.Controllers), "Should have two controllers")
testResources(res)
}
@@ -124,7 +124,7 @@ func TestSkipHealthChecks(t *testing.T) {
}
pod := test.MockPod()
pod.Spec.InitContainers = []corev1.Container{test.MockContainer("test")}
deployment, err := kube.NewGenericResourceFromPod(pod, nil)
deployment, err := kube.NewGenericWorkloadFromPod(pod, nil)
assert.NoError(t, err)
deployment.Kind = "Deployment"
expectedSum := CountSummary{
@@ -136,8 +136,7 @@ func TestSkipHealthChecks(t *testing.T) {
"readinessProbeMissing": {ID: "readinessProbeMissing", Message: "Readiness probe should be configured", Success: false, Severity: "danger", Category: "Reliability"},
"livenessProbeMissing": {ID: "livenessProbeMissing", Message: "Liveness probe should be configured", Success: false, Severity: "warning", Category: "Reliability"},
}
var actualResult Result
actualResult, err = applyControllerSchemaChecks(&c, nil, deployment)
actualResult, err := ValidateController(context.Background(), &c, deployment)
if err != nil {
panic(err)
}
@@ -147,7 +146,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.NewGenericResourceFromPod(test.MockPod(), nil)
job, err := kube.NewGenericWorkloadFromPod(test.MockPod(), nil)
assert.NoError(t, err)
job.Kind = "Job"
expectedSum = CountSummary{
@@ -156,7 +155,7 @@ func TestSkipHealthChecks(t *testing.T) {
Dangers: uint(0),
}
expectedResults = ResultSet{}
actualResult, err = applyControllerSchemaChecks(&c, nil, job)
actualResult, err = ValidateController(context.Background(), &c, job)
if err != nil {
panic(err)
}
@@ -165,7 +164,7 @@ func TestSkipHealthChecks(t *testing.T) {
assert.EqualValues(t, expectedSum, actualResult.GetSummary())
assert.EqualValues(t, expectedResults, actualResult.PodResult.ContainerResults[0].Results)
cronjob, err := kube.NewGenericResourceFromPod(test.MockPod(), nil)
cronjob, err := kube.NewGenericWorkloadFromPod(test.MockPod(), nil)
assert.NoError(t, err)
cronjob.Kind = "CronJob"
expectedSum = CountSummary{
@@ -174,7 +173,7 @@ func TestSkipHealthChecks(t *testing.T) {
Dangers: uint(0),
}
expectedResults = ResultSet{}
actualResult, err = applyControllerSchemaChecks(&c, nil, cronjob)
actualResult, err = ValidateController(context.Background(), &c, cronjob)
if err != nil {
panic(err)
}
@@ -191,26 +190,20 @@ func TestControllerExemptions(t *testing.T) {
"livenessProbeMissing": conf.SeverityWarning,
},
}
pod := test.MockPod()
workload, err := kube.NewGenericWorkloadFromPod(pod, nil)
assert.NoError(t, err)
workload.Kind = "Deployment"
resources := &kube.ResourceProvider{
Controllers: []kube.GenericWorkload{workload},
}
expectedSum := CountSummary{
Successes: uint(0),
Warnings: uint(1),
Dangers: uint(1),
}
expectedExemptSum := CountSummary{
Successes: uint(0),
Warnings: uint(0),
Dangers: uint(0),
}
pod := test.MockPod()
pod.ObjectMeta.Namespace = "foo"
workload, err := kube.NewGenericResourceFromPod(pod, nil)
assert.NoError(t, err)
workload.Kind = "Deployment"
resources := []kube.GenericResource{workload}
var actualResults []Result
actualResults, err = ApplyAllSchemaChecksToAllResources(&c, nil, resources)
actualResults, err := ValidateControllers(context.Background(), &c, resources)
if err != nil {
panic(err)
}
@@ -218,35 +211,12 @@ func TestControllerExemptions(t *testing.T) {
assert.Equal(t, "Deployment", actualResults[0].Kind)
assert.EqualValues(t, expectedSum, actualResults[0].GetSummary())
c.Exemptions = []conf.Exemption{{
Namespace: "foo",
}}
actualResults, err = ApplyAllSchemaChecksToAllResources(&c, nil, resources)
if err != nil {
panic(err)
}
assert.Equal(t, 1, len(actualResults))
assert.Equal(t, "Deployment", actualResults[0].Kind)
assert.EqualValues(t, expectedExemptSum, actualResults[0].GetSummary())
c.Exemptions = nil
resources[0].ObjectMeta.SetAnnotations(map[string]string{
resources.Controllers[0].ObjectMeta.SetAnnotations(map[string]string{
exemptionAnnotationKey: "true",
})
actualResults, err = ApplyAllSchemaChecksToAllResources(&c, nil, resources)
actualResults, err = ValidateControllers(context.Background(), &c, resources)
if err != nil {
panic(err)
}
assert.Equal(t, 1, len(actualResults))
assert.Equal(t, "Deployment", actualResults[0].Kind)
assert.EqualValues(t, expectedExemptSum, actualResults[0].GetSummary())
c.DisallowExemptions = true
actualResults, err = ApplyAllSchemaChecksToAllResources(&c, nil, resources)
if err != nil {
panic(err)
}
assert.Equal(t, 1, len(actualResults))
assert.Equal(t, "Deployment", actualResults[0].Kind)
assert.EqualValues(t, expectedSum, actualResults[0].GetSummary())
assert.Equal(t, 0, len(actualResults))
}
+5 -4
View File
@@ -2,6 +2,7 @@ package validator
import (
"bytes"
"context"
"fmt"
"io"
"io/ioutil"
@@ -16,13 +17,13 @@ import (
)
// RunAudit runs a full Polaris audit and returns an AuditData object
func RunAudit(config conf.Configuration, kubeResources *kube.ResourceProvider) (AuditData, error) {
func RunAudit(ctx context.Context, config conf.Configuration, kubeResources *kube.ResourceProvider) (AuditData, error) {
displayName := config.DisplayName
if displayName == "" {
displayName = kubeResources.SourceName
}
results, err := ApplyAllSchemaChecksToResourceProvider(&config, kubeResources)
results, err := ValidateControllers(ctx, &config, kubeResources)
if err != nil {
return AuditData{}, err
}
@@ -36,12 +37,12 @@ func RunAudit(config conf.Configuration, kubeResources *kube.ResourceProvider) (
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: kubeResources.Resources.GetNumberOfControllers(),
Controllers: len(results),
},
Results: results,
}
auditData.Score = auditData.GetSummary().GetScore()
return auditData, nil
}
+8 -12
View File
@@ -11,6 +11,11 @@ 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,
@@ -18,22 +23,14 @@ 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),
Dangers: uint(3),
}
score := uint(0)
var actualAudit AuditData
actualAudit, err = RunAudit(c, resources)
actualAudit, err := RunAudit(context.Background(), c, resources)
assert.Equal(t, err, nil, "error should be nil")
assert.Equal(t, score, actualAudit.Score, "")
assert.EqualValues(t, sum, actualAudit.GetSummary())
assert.Equal(t, actualAudit.SourceType, "Cluster", "should be from a cluster")
assert.Equal(t, actualAudit.SourceName, "test", "should be from a cluster")
@@ -57,9 +54,8 @@ func TestGetTemplateData(t *testing.T) {
continue
}
found = true
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, 1, len(result.PodResult.ContainerResults))
assert.Equal(t, expected.results, len(result.PodResult.ContainerResults[0].Results))
}
assert.Equal(t, found, true)
}
+4 -140
View File
@@ -15,12 +15,8 @@
package validator
import (
"fmt"
"time"
"github.com/fatih/color"
"github.com/thoas/go-funk"
"github.com/fairwindsops/polaris/pkg/config"
)
@@ -29,17 +25,6 @@ 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
@@ -48,17 +33,7 @@ type AuditData struct {
SourceName string
DisplayName string
ClusterInfo ClusterInfo
Results []Result
Score uint
}
// RemoveSuccessfulResults remove all test that have passed.
func (res AuditData) RemoveSuccessfulResults() AuditData {
resCopy := res
resCopy.Results = funk.Map(res.Results, func(auditDataResult Result) Result {
return auditDataResult.removeSuccessfulResults()
}).([]Result)
return resCopy
Results []ControllerResult
}
// ClusterInfo contains Polaris results as well as some high-level stats
@@ -74,7 +49,6 @@ type ClusterInfo struct {
type ResultMessage struct {
ID string
Message string
Details []string
Success bool
Severity config.Severity
Category string
@@ -83,36 +57,16 @@ type ResultMessage struct {
// ResultSet contiains the results for a set of checks
type ResultSet map[string]ResultMessage
func (res ResultSet) removeSuccessfulResults() ResultSet {
newResults := ResultSet{}
for k, resultMessage := range res {
if !resultMessage.Success {
newResults[k] = resultMessage
}
}
return newResults
}
// Result provides results for a Kubernetes object
type Result struct {
// ControllerResult provides results for a controller
type ControllerResult struct {
Name string
Namespace string
Kind string
Results ResultSet
PodResult *PodResult
PodResult PodResult
CreatedTime time.Time
}
func (res Result) removeSuccessfulResults() Result {
resCopy := res
resCopy.Results = res.Results.removeSuccessfulResults()
if res.PodResult != nil {
podCopy := res.PodResult.removeSuccessfulResults()
resCopy.PodResult = &podCopy
}
return resCopy
}
// PodResult provides a list of validation messages for each pod.
type PodResult struct {
Name string
@@ -120,98 +74,8 @@ type PodResult struct {
ContainerResults []ContainerResult
}
func (res PodResult) removeSuccessfulResults() PodResult {
resCopy := PodResult{}
resCopy.Results = res.Results.removeSuccessfulResults()
resCopy.ContainerResults = funk.Map(res.ContainerResults, func(containerResult ContainerResult) ContainerResult {
return containerResult.removeSuccessfulResults()
}).([]ContainerResult)
return resCopy
}
// ContainerResult provides a list of validation messages for each container.
type ContainerResult struct {
Name string
Results ResultSet
}
func (res ContainerResult) removeSuccessfulResults() ContainerResult {
resCopy := res
resCopy.Results = res.Results.removeSuccessfulResults()
return resCopy
}
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", res.Kind, res.Name))
if res.Namespace != "" {
str += titleColor.Sprint(fmt.Sprintf("in namespace %s", res.Namespace))
}
str += "\n"
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
}
+40
View File
@@ -0,0 +1,40 @@
// 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 (
"context"
"github.com/fairwindsops/polaris/pkg/config"
"github.com/fairwindsops/polaris/pkg/kube"
)
// ValidatePod validates that each pod conforms to the Polaris config, returns a ResourceResult.
func ValidatePod(ctx context.Context, conf *config.Configuration, controller kube.GenericWorkload) (PodResult, error) {
podResults, err := applyPodSchemaChecks(ctx, conf, controller)
if err != nil {
return PodResult{}, err
}
pRes := PodResult{
Results: podResults,
ContainerResults: []ContainerResult{},
}
pRes.ContainerResults, err = ValidateAllContainers(ctx, conf, controller)
if err != nil {
return pRes, err
}
return pRes, nil
}
+23 -22
View File
@@ -15,6 +15,7 @@
package validator
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
@@ -36,7 +37,7 @@ func TestValidatePod(t *testing.T) {
}
p := test.MockPod()
deployment, err := kube.NewGenericResourceFromPod(p, nil)
deployment, err := kube.NewGenericWorkloadFromPod(p, nil)
assert.NoError(t, err)
expectedSum := CountSummary{
Successes: uint(4),
@@ -50,14 +51,14 @@ func TestValidatePod(t *testing.T) {
"hostPIDSet": {ID: "hostPIDSet", Message: "Host PID is not configured", Success: true, Severity: "danger", Category: "Security"},
}
actualPodResult, err := applyControllerSchemaChecks(&c, nil, deployment)
actualPodResult, err := ValidatePod(context.Background(), &c, deployment)
if err != nil {
panic(err)
}
assert.Equal(t, 1, len(actualPodResult.PodResult.ContainerResults), "should be equal")
assert.Equal(t, 1, len(actualPodResult.ContainerResults), "should be equal")
assert.EqualValues(t, expectedSum, actualPodResult.GetSummary())
assert.EqualValues(t, expectedResults, actualPodResult.PodResult.Results)
assert.EqualValues(t, expectedResults, actualPodResult.Results)
}
func TestInvalidIPCPod(t *testing.T) {
@@ -72,7 +73,7 @@ func TestInvalidIPCPod(t *testing.T) {
p := test.MockPod()
p.Spec.HostIPC = true
workload, err := kube.NewGenericResourceFromPod(p, nil)
workload, err := kube.NewGenericWorkloadFromPod(p, nil)
assert.NoError(t, err)
expectedSum := CountSummary{
Successes: uint(3),
@@ -85,17 +86,17 @@ func TestInvalidIPCPod(t *testing.T) {
"hostPIDSet": {ID: "hostPIDSet", Message: "Host PID is not configured", Success: true, Severity: "danger", Category: "Security"},
}
actualPodResult, err := applyControllerSchemaChecks(&c, nil, workload)
actualPodResult, err := ValidatePod(context.Background(), &c, workload)
if err != nil {
panic(err)
}
assert.Equal(t, 1, len(actualPodResult.PodResult.ContainerResults), "should be equal")
assert.Equal(t, 1, len(actualPodResult.ContainerResults), "should be equal")
assert.EqualValues(t, expectedSum, actualPodResult.GetSummary())
assert.EqualValues(t, expectedResults, actualPodResult.PodResult.Results)
assert.EqualValues(t, expectedResults, actualPodResult.Results)
}
func TestInvalidNetworkPod(t *testing.T) {
func TestInvalidNeworkPod(t *testing.T) {
c := conf.Configuration{
Checks: map[string]conf.Severity{
"hostNetworkSet": conf.SeverityWarning,
@@ -107,7 +108,7 @@ func TestInvalidNetworkPod(t *testing.T) {
p := test.MockPod()
p.Spec.HostNetwork = true
workload, err := kube.NewGenericResourceFromPod(p, nil)
workload, err := kube.NewGenericWorkloadFromPod(p, nil)
assert.NoError(t, err)
expectedSum := CountSummary{
Successes: uint(3),
@@ -121,14 +122,14 @@ func TestInvalidNetworkPod(t *testing.T) {
"hostPIDSet": {ID: "hostPIDSet", Message: "Host PID is not configured", Success: true, Severity: "danger", Category: "Security"},
}
actualPodResult, err := applyControllerSchemaChecks(&c, nil, workload)
actualPodResult, err := ValidatePod(context.Background(), &c, workload)
if err != nil {
panic(err)
}
assert.Equal(t, 1, len(actualPodResult.PodResult.ContainerResults), "should be equal")
assert.Equal(t, 1, len(actualPodResult.ContainerResults), "should be equal")
assert.EqualValues(t, expectedSum, actualPodResult.GetSummary())
assert.EqualValues(t, expectedResults, actualPodResult.PodResult.Results)
assert.EqualValues(t, expectedResults, actualPodResult.Results)
}
func TestInvalidPIDPod(t *testing.T) {
@@ -143,7 +144,7 @@ func TestInvalidPIDPod(t *testing.T) {
p := test.MockPod()
p.Spec.HostPID = true
workload, err := kube.NewGenericResourceFromPod(p, nil)
workload, err := kube.NewGenericWorkloadFromPod(p, nil)
assert.NoError(t, err)
expectedSum := CountSummary{
Successes: uint(3),
@@ -157,14 +158,14 @@ func TestInvalidPIDPod(t *testing.T) {
"hostNetworkSet": {ID: "hostNetworkSet", Message: "Host network is not configured", Success: true, Severity: "warning", Category: "Security"},
}
actualPodResult, err := applyControllerSchemaChecks(&c, nil, workload)
actualPodResult, err := ValidatePod(context.Background(), &c, workload)
if err != nil {
panic(err)
}
assert.Equal(t, 1, len(actualPodResult.PodResult.ContainerResults), "should be equal")
assert.Equal(t, 1, len(actualPodResult.ContainerResults), "should be equal")
assert.EqualValues(t, expectedSum, actualPodResult.GetSummary())
assert.EqualValues(t, expectedResults, actualPodResult.PodResult.Results)
assert.EqualValues(t, expectedResults, actualPodResult.Results)
}
func TestExemption(t *testing.T) {
@@ -176,7 +177,7 @@ func TestExemption(t *testing.T) {
"hostPortSet": conf.SeverityDanger,
},
Exemptions: []conf.Exemption{
{
conf.Exemption{
Rules: []string{"hostIPCSet"},
ControllerNames: []string{"foo"},
},
@@ -188,7 +189,7 @@ func TestExemption(t *testing.T) {
p.ObjectMeta = metav1.ObjectMeta{
Name: "foo",
}
workload, err := kube.NewGenericResourceFromPod(p, nil)
workload, err := kube.NewGenericWorkloadFromPod(p, nil)
assert.NoError(t, err)
expectedSum := CountSummary{
Successes: uint(3),
@@ -200,12 +201,12 @@ func TestExemption(t *testing.T) {
"hostPIDSet": {ID: "hostPIDSet", Message: "Host PID is not configured", Success: true, Severity: "danger", Category: "Security"},
}
actualPodResult, err := applyControllerSchemaChecks(&c, nil, workload)
actualPodResult, err := ValidatePod(context.Background(), &c, workload)
if err != nil {
panic(err)
}
assert.Equal(t, 1, len(actualPodResult.PodResult.ContainerResults), "should be equal")
assert.Equal(t, 1, len(actualPodResult.ContainerResults), "should be equal")
assert.EqualValues(t, expectedSum, actualPodResult.GetSummary())
assert.EqualValues(t, expectedResults, actualPodResult.PodResult.Results)
assert.EqualValues(t, expectedResults, actualPodResult.Results)
}
+142 -226
View File
@@ -1,68 +1,106 @@
package validator
import (
"bytes"
"context"
"fmt"
"io"
"sort"
"strings"
"github.com/qri-io/jsonschema"
"github.com/thoas/go-funk"
packr "github.com/gobuffalo/packr/v2"
corev1 "k8s.io/api/core/v1"
metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/yaml"
"github.com/fairwindsops/polaris/pkg/config"
"github.com/fairwindsops/polaris/pkg/kube"
)
type schemaTestCase struct {
Target config.TargetKind
Resource kube.GenericResource
IsInitContianer bool
Container *corev1.Container
ResourceProvider *kube.ResourceProvider
var (
schemaBox = (*packr.Box)(nil)
builtInChecks = map[string]config.SchemaCheck{}
// We explicitly set the order to avoid thrash in the
// tests as we migrate toward JSON schema
checkOrder = []string{
// Controller Checks
"multipleReplicasForDeployment",
// Pod checks
"hostIPCSet",
"hostPIDSet",
"hostNetworkSet",
// Container checks
"memoryLimitsMissing",
"memoryRequestsMissing",
"cpuLimitsMissing",
"cpuRequestsMissing",
"readinessProbeMissing",
"livenessProbeMissing",
"pullPolicyNotAlways",
"tagNotSpecified",
"hostPortSet",
"runAsRootAllowed",
"runAsPrivileged",
"notReadOnlyRootFilesystem",
"privilegeEscalationAllowed",
"dangerousCapabilities",
"insecureCapabilities",
"priorityClassNotSet",
}
)
func init() {
schemaBox = packr.New("Schemas", "../../checks")
for _, checkID := range checkOrder {
contents, err := schemaBox.Find(checkID + ".yaml")
if err != nil {
panic(err)
}
check, err := parseCheck(contents)
if err != nil {
panic(err)
}
check.ID = checkID
builtInChecks[checkID] = check
}
}
func resolveCheck(conf *config.Configuration, checkID string, test schemaTestCase) (*config.SchemaCheck, error) {
if !conf.DisallowExemptions && hasExemptionAnnotation(test.Resource.ObjectMeta, checkID) {
return nil, nil
func parseCheck(rawBytes []byte) (config.SchemaCheck, error) {
reader := bytes.NewReader(rawBytes)
check := config.SchemaCheck{}
d := yaml.NewYAMLOrJSONDecoder(reader, 4096)
for {
if err := d.Decode(&check); err != nil {
if err == io.EOF {
return check, nil
}
return check, fmt.Errorf("Decoding schema check failed: %v", err)
}
}
}
func resolveCheck(conf *config.Configuration, checkID string, controller kube.GenericWorkload, target config.TargetKind, isInitContainer bool) (*config.SchemaCheck, error) {
check, ok := conf.CustomChecks[checkID]
if !ok {
check, ok = config.BuiltInChecks[checkID]
check, ok = builtInChecks[checkID]
}
if !ok {
return nil, fmt.Errorf("Check %s not found", checkID)
}
containerName := ""
if test.Container != nil {
containerName = test.Container.Name
}
if !conf.IsActionable(check.ID, test.Resource.ObjectMeta, containerName) {
if !conf.IsActionable(check.ID, controller.ObjectMeta.GetNamespace(), controller.ObjectMeta.GetName()) {
return nil, nil
}
if !check.IsActionable(test.Target, test.Resource.Kind, test.IsInitContianer) {
if !check.IsActionable(target, controller.ObjectMeta.GetNamespace(), controller.Kind, isInitContainer) {
return nil, nil
}
checkPtr, err := check.TemplateForResource(test.Resource.Resource.Object)
if err != nil {
return nil, err
}
return checkPtr, nil
return &check, nil
}
func makeResult(conf *config.Configuration, check *config.SchemaCheck, passes bool, issues []jsonschema.ValError) ResultMessage {
details := []string{}
for _, issue := range issues {
details = append(details, issue.Message)
}
func makeResult(conf *config.Configuration, check *config.SchemaCheck, passes bool) ResultMessage {
result := ResultMessage{
ID: check.ID,
Severity: conf.Checks[check.ID],
Category: check.Category,
Success: passes,
// FIXME: need to fix the tests before adding this back
//Details: details,
}
if passes {
result.Message = check.SuccessMessage
@@ -72,212 +110,90 @@ func makeResult(conf *config.Configuration, check *config.SchemaCheck, passes bo
return result
}
const exemptionAnnotationKey = "polaris.fairwinds.com/exempt"
const exemptionAnnotationPattern = "polaris.fairwinds.com/%s-exempt"
func hasExemptionAnnotation(objMeta metaV1.Object, checkID string) bool {
annot := objMeta.GetAnnotations()
val := annot[exemptionAnnotationKey]
if strings.ToLower(val) == "true" {
return true
}
checkKey := fmt.Sprintf(exemptionAnnotationPattern, checkID)
val = annot[checkKey]
if strings.ToLower(val) == "true" {
return true
}
return false
func getExemptKey(checkID string) string {
return fmt.Sprintf("polaris.fairwinds.com/%s-exempt", checkID)
}
// ApplyAllSchemaChecksToResourceProvider applies all available checks to a ResourceProvider
func ApplyAllSchemaChecksToResourceProvider(conf *config.Configuration, resourceProvider *kube.ResourceProvider) ([]Result, error) {
results := []Result{}
for _, resources := range resourceProvider.Resources {
kindResults, err := ApplyAllSchemaChecksToAllResources(conf, resourceProvider, resources)
if err != nil {
return results, err
}
results = append(results, kindResults...)
}
return results, nil
}
// ApplyAllSchemaChecksToAllResources applies available checks to a list of resources
func ApplyAllSchemaChecksToAllResources(conf *config.Configuration, resourceProvider *kube.ResourceProvider, resources []kube.GenericResource) ([]Result, error) {
results := []Result{}
for _, resource := range resources {
result, err := ApplyAllSchemaChecks(conf, resourceProvider, resource)
if err != nil {
return results, err
}
results = append(results, result)
}
return results, nil
}
// ApplyAllSchemaChecks applies available checks to a single resource
func ApplyAllSchemaChecks(conf *config.Configuration, resourceProvider *kube.ResourceProvider, resource kube.GenericResource) (Result, error) {
if resource.PodSpec == nil {
return applyNonControllerSchemaChecks(conf, resourceProvider, resource)
}
return applyControllerSchemaChecks(conf, resourceProvider, resource)
}
func applyNonControllerSchemaChecks(conf *config.Configuration, resourceProvider *kube.ResourceProvider, resource kube.GenericResource) (Result, error) {
finalResult := Result{
Kind: resource.Kind,
Name: resource.ObjectMeta.GetName(),
Namespace: resource.ObjectMeta.GetNamespace(),
}
resultSet, err := applyTopLevelSchemaChecks(conf, resourceProvider, resource, false)
finalResult.Results = resultSet
return finalResult, err
}
func applyControllerSchemaChecks(conf *config.Configuration, resourceProvider *kube.ResourceProvider, resource kube.GenericResource) (Result, error) {
finalResult := Result{
Kind: resource.Kind,
Name: resource.ObjectMeta.GetName(),
Namespace: resource.ObjectMeta.GetNamespace(),
}
resultSet, err := applyTopLevelSchemaChecks(conf, resourceProvider, resource, true)
if err != nil {
return finalResult, err
}
finalResult.Results = resultSet
podRS, err := applyPodSchemaChecks(conf, resourceProvider, 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, resourceProvider, 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, resourceProvider, 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, resources *kube.ResourceProvider, res kube.GenericResource, isController bool) (ResultSet, error) {
test := schemaTestCase{
ResourceProvider: resources,
Resource: res,
}
if isController {
test.Target = config.TargetController
}
return applySchemaChecks(conf, test)
}
func applyPodSchemaChecks(conf *config.Configuration, resources *kube.ResourceProvider, controller kube.GenericResource) (ResultSet, error) {
test := schemaTestCase{
Target: config.TargetPod,
ResourceProvider: resources,
Resource: controller,
}
return applySchemaChecks(conf, test)
}
func applyContainerSchemaChecks(conf *config.Configuration, resources *kube.ResourceProvider, controller kube.GenericResource, container *corev1.Container, isInit bool) (ResultSet, error) {
test := schemaTestCase{
Target: config.TargetContainer,
ResourceProvider: resources,
Resource: controller,
Container: container,
IsInitContianer: isInit,
}
return applySchemaChecks(conf, test)
}
func applySchemaChecks(conf *config.Configuration, test schemaTestCase) (ResultSet, error) {
func applyPodSchemaChecks(ctx context.Context, conf *config.Configuration, controller kube.GenericWorkload) (ResultSet, error) {
results := ResultSet{}
checkIDs := getSortedKeys(conf.Checks)
objectAnnotations := controller.ObjectMeta.GetAnnotations()
for _, checkID := range checkIDs {
result, err := applySchemaCheck(conf, checkID, test)
if err != nil {
return results, err
exemptValue := objectAnnotations[getExemptKey(checkID)]
if strings.ToLower(exemptValue) == "true" {
continue
}
if result != nil {
results[checkID] = *result
}
}
return results, nil
}
check, err := resolveCheck(conf, checkID, controller, config.TargetPod, false)
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
var issues []jsonschema.ValError
if check.SchemaTarget != "" {
if check.SchemaTarget == config.TargetPod && check.Target == config.TargetContainer {
podCopy := *test.Resource.PodSpec
podCopy.InitContainers = []corev1.Container{}
podCopy.Containers = []corev1.Container{*test.Container}
passes, issues, err = check.CheckPod(&podCopy)
} else {
return nil, fmt.Errorf("Unknown combination of target (%s) and schema target (%s)", check.Target, check.SchemaTarget)
if err != nil {
return nil, err
} else if check == nil {
continue
}
} else if check.Target == config.TargetPod {
passes, issues, err = check.CheckPod(test.Resource.PodSpec)
} else if check.Target == config.TargetContainer {
passes, issues, err = check.CheckContainer(test.Container)
} else {
passes, issues, err = check.CheckObject(test.Resource.Resource.Object)
}
if err != nil {
return nil, err
}
for groupkind := range check.AdditionalValidators {
if !passes {
break
}
resources := test.ResourceProvider.Resources[groupkind]
namespace := test.Resource.ObjectMeta.GetNamespace()
if test.Resource.Kind == "Namespace" {
namespace = test.Resource.ObjectMeta.GetName()
}
resources = funk.Filter(resources, func(res kube.GenericResource) bool {
return res.ObjectMeta.GetNamespace() == namespace
}).([]kube.GenericResource)
objects := funk.Map(resources, func(res kube.GenericResource) interface{} {
return res.Resource.Object
}).([]interface{})
passes, err = check.CheckAdditionalObjects(groupkind, objects)
passes, err := check.CheckPod(&controller.PodSpec)
if err != nil {
return nil, err
}
results[check.ID] = makeResult(conf, check, passes)
}
result := makeResult(conf, check, passes, issues)
return &result, nil
return results, nil
}
func applyControllerSchemaChecks(ctx context.Context, conf *config.Configuration, controller kube.GenericWorkload) (ResultSet, error) {
results := ResultSet{}
checkIDs := getSortedKeys(conf.Checks)
objectAnnotations := controller.ObjectMeta.GetAnnotations()
for _, checkID := range checkIDs {
exemptValue := objectAnnotations[getExemptKey(checkID)]
if strings.ToLower(exemptValue) == "true" {
continue
}
check, err := resolveCheck(conf, checkID, controller, config.TargetController, false)
if err != nil {
return nil, err
} else if check == nil {
continue
}
passes, err := check.CheckController(controller.OriginalObjectJSON)
if err != nil {
return nil, err
}
results[check.ID] = makeResult(conf, check, passes)
}
return results, nil
}
func applyContainerSchemaChecks(ctx context.Context, conf *config.Configuration, controller kube.GenericWorkload, container *corev1.Container, isInit bool) (ResultSet, error) {
results := ResultSet{}
checkIDs := getSortedKeys(conf.Checks)
objectAnnotations := controller.ObjectMeta.GetAnnotations()
for _, checkID := range checkIDs {
exemptValue := objectAnnotations[getExemptKey(checkID)]
if strings.ToLower(exemptValue) == "true" {
continue
}
check, err := resolveCheck(conf, checkID, controller, config.TargetContainer, isInit)
if err != nil {
return nil, err
} else if check == nil {
continue
}
var passes bool
if check.SchemaTarget == config.TargetPod {
podCopy := controller.PodSpec
podCopy.InitContainers = []corev1.Container{}
podCopy.Containers = []corev1.Container{*container}
passes, err = check.CheckPod(&podCopy)
} else {
passes, err = check.CheckContainer(container)
}
if err != nil {
return nil, err
}
results[check.ID] = makeResult(conf, check, passes)
}
return results, nil
}
func getSortedKeys(m map[string]config.Severity) []string {
+3 -3
View File
@@ -1,6 +1,7 @@
package validator
import (
"context"
"testing"
conf "github.com/fairwindsops/polaris/pkg/config"
@@ -143,15 +144,14 @@ func TestValidateResourcesInit(t *testing.T) {
parsedConf, err := conf.Parse([]byte(resourceConfRanges))
assert.NoError(t, err, "Expected no error when parsing config")
var results ResultSet
results, err = applyContainerSchemaChecks(&parsedConf, nil, controller, emptyContainer, false)
results, err := applyContainerSchemaChecks(context.Background(), &parsedConf, controller, emptyContainer, false)
if err != nil {
panic(err)
}
assert.Equal(t, uint(1), results.GetSummary().Dangers)
assert.Equal(t, uint(1), results.GetSummary().Warnings)
results, err = applyContainerSchemaChecks(&parsedConf, nil, controller, emptyContainer, true)
results, err = applyContainerSchemaChecks(context.Background(), &parsedConf, controller, emptyContainer, true)
if err != nil {
panic(err)
}
+15 -22
View File
@@ -11,7 +11,7 @@ import (
type CountSummary struct {
Successes uint
Warnings uint
Dangers uint
Dangers uint
}
// CountSummaryByCategory is a map from category to CountSummary
@@ -21,10 +21,9 @@ type CountSummaryByCategory map[string]CountSummary
func (cs CountSummary) GetScore() uint {
total := (cs.Successes * 2) + cs.Warnings + (cs.Dangers * 2)
if total == 0 {
return uint(100)
return 0 // Prevent divide by 0.
}
score := uint((float64(cs.Successes*2) / float64(total)) * 100)
return score
return uint((float64(cs.Successes*2) / float64(total)) * 100)
}
// AddSummary adds two CountSummaries together
@@ -104,29 +103,25 @@ func (p PodResult) GetSummaryByCategory() CountSummaryByCategory {
return summaries
}
// GetSummary summarizes a Result
func (c Result) GetSummary() CountSummary {
// GetSummary summarizes a ControllerResult
func (c ControllerResult) GetSummary() CountSummary {
summary := c.Results.GetSummary()
if c.PodResult != nil {
summary.AddSummary(c.PodResult.GetSummary())
}
summary.AddSummary(c.PodResult.GetSummary())
return summary
}
// GetSummaryByCategory summarizes a Result
func (c Result) GetSummaryByCategory() CountSummaryByCategory {
// GetSummaryByCategory summarizes a ControllerResult
func (c ControllerResult) GetSummaryByCategory() CountSummaryByCategory {
summary := c.Results.GetSummaryByCategory()
if c.PodResult != nil {
summary.AddSummary(c.PodResult.GetSummaryByCategory())
}
summary.AddSummary(c.PodResult.GetSummaryByCategory())
return summary
}
// GetSummary summarizes AuditData
func (a AuditData) GetSummary() CountSummary {
summary := CountSummary{}
for _, res := range a.Results {
summary.AddSummary(res.GetSummary())
for _, ctrlResult := range a.Results {
summary.AddSummary(ctrlResult.GetSummary())
}
return summary
}
@@ -135,20 +130,18 @@ func (a AuditData) GetSummary() CountSummary {
func (a AuditData) GetSummaryByCategory() CountSummaryByCategory {
summaries := CountSummaryByCategory{}
for _, ctrlResult := range a.Results {
if ctrlResult.PodResult != nil {
summaries.AddSummary(ctrlResult.GetSummaryByCategory())
}
summaries.AddSummary(ctrlResult.GetSummaryByCategory())
}
return summaries
}
// GetResultsByNamespace organizes results by namespace
func (a AuditData) GetResultsByNamespace() map[string][]*Result {
allResults := map[string][]*Result{}
func (a AuditData) GetResultsByNamespace() map[string][]*ControllerResult {
allResults := map[string][]*ControllerResult{}
for idx, ctrlResult := range a.Results {
nsResults, ok := allResults[ctrlResult.Namespace]
if !ok {
nsResults = []*Result{}
nsResults = []*ControllerResult{}
}
nsResults = append(nsResults, &a.Results[idx])
allResults[ctrlResult.Namespace] = nsResults
+48 -28
View File
@@ -16,6 +16,8 @@ package webhook
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
@@ -54,11 +56,37 @@ func NewWebhook(mgr manager.Manager, validator Validator) {
mgr.GetWebhookServer().Register(path, &webhook.Admission{Handler: &validator})
}
func (v *Validator) handleInternal(req admission.Request) (*validator.Result, error) {
var controller kube.GenericResource
// GetObjectFromRawRequest returns the pod object and the controller's object from the raw json bytes.
func GetObjectFromRawRequest(raw []byte) (corev1.Pod, interface{}, error) {
pod := corev1.Pod{}
var originalObject interface{}
decoded := map[string]interface{}{}
err := json.Unmarshal(raw, &decoded)
if err != nil {
return pod, originalObject, err
}
podMap := kube.GetPodSpec(decoded)
if podMap == nil {
return pod, originalObject, errors.New("Object does not contain pods")
}
encoded, err := json.Marshal(podMap)
if err != nil {
return pod, originalObject, err
}
err = json.Unmarshal(encoded, &pod.Spec)
if err != nil {
return pod, originalObject, err
}
originalObject = decoded
return pod, originalObject, err
}
func (v *Validator) handleInternal(ctx context.Context, req admission.Request) (*validator.PodResult, error) {
pod := corev1.Pod{}
var originalObject interface{}
var err error
if req.AdmissionRequest.Kind.Kind == "Pod" {
pod := corev1.Pod{}
err := v.decoder.Decode(req, &pod)
if err != nil {
return nil, err
@@ -67,64 +95,56 @@ func (v *Validator) handleInternal(req admission.Request) (*validator.Result, er
logrus.Infof("Allowing owned pod %s/%s to pass through webhook", pod.ObjectMeta.Namespace, pod.ObjectMeta.Name)
return nil, nil
}
controller, err = kube.NewGenericResourceFromPod(pod, pod)
originalObject = pod
} else {
controller, err = kube.NewGenericResourceFromBytes(req.Object.Raw)
pod, originalObject, err = GetObjectFromRawRequest(req.Object.Raw)
}
controller, err := kube.NewGenericWorkloadFromPod(pod, originalObject)
if err != nil {
return nil, err
}
// TODO: consider enabling multi-resource checks
controllerResult, err := validator.ApplyAllSchemaChecks(&v.Config, nil, controller)
controller.Kind = req.AdmissionRequest.Kind.Kind
controllerResult, err := validator.ValidateController(ctx, &v.Config, controller)
if err != nil {
return nil, err
}
return &controllerResult, nil
return &controllerResult.PodResult, nil
}
// Handle for Validator to run validation checks.
func (v *Validator) Handle(ctx context.Context, req admission.Request) admission.Response {
logrus.Info("Starting request")
result, err := v.handleInternal(req)
podResult, err := v.handleInternal(ctx, req)
if err != nil {
logrus.Errorf("Error validating request: %v", err)
return admission.Errored(http.StatusBadRequest, err)
}
allowed := true
reason := ""
if result != nil {
numDangers := result.GetSummary().Dangers
if podResult != nil {
numDangers := podResult.GetSummary().Dangers
if numDangers > 0 {
allowed = false
reason = getFailureReason(*result)
reason = getFailureReason(*podResult)
}
logrus.Infof("%d validation errors found when validating %s", numDangers, result.Name)
logrus.Infof("%d validation errors found when validating %s", numDangers, podResult.Name)
}
return admission.ValidationResponse(allowed, reason)
}
func getFailureReason(result validator.Result) string {
func getFailureReason(podResult validator.PodResult) string {
reason := "\nPolaris prevented this deployment due to configuration problems:\n"
for _, message := range result.Results {
for _, message := range podResult.Results {
if !message.Success && message.Severity == config.SeverityDanger {
reason += fmt.Sprintf("- %s: %s\n", result.Kind, message.Message)
reason += fmt.Sprintf("- Pod: %s\n", message.Message)
}
}
podResult := result.PodResult
if podResult != nil {
for _, message := range podResult.Results {
for _, containerResult := range podResult.ContainerResults {
for _, message := range containerResult.Results {
if !message.Success && message.Severity == config.SeverityDanger {
reason += fmt.Sprintf("- Pod: %s\n", message.Message)
}
}
for _, containerResult := range podResult.ContainerResults {
for _, message := range containerResult.Results {
if !message.Success && message.Severity == config.SeverityDanger {
reason += fmt.Sprintf("- Container %s: %s\n", containerResult.Name, message.Message)
}
reason += fmt.Sprintf("- Container %s: %s\n", containerResult.Name, message.Message)
}
}
}
-13
View File
@@ -1,13 +0,0 @@
apiVersion: v1
kind: Pod
metadata:
name: nginx
labels:
app.kubernetes.io/name: nginx
spec:
containers:
- name: nginx
image: nginx
resources:
limits:
memory: 128Mi
-14
View File
@@ -1,14 +0,0 @@
apiVersion: v1
kind: Pod
metadata:
name: nginx
labels:
app.kubernetes.io/name: nginx
spec:
containers:
- name: nginx
image: nginx
resources:
limits:
memory: 256Mi
cpu: 100m
@@ -1,13 +0,0 @@
apiVersion: v1
kind: Pod
metadata:
name: nginx
labels:
app.kubernetes.io/name: nginx
spec:
containers:
- name: nginx
image: nginx
resources:
requests:
memory: 128Mi
@@ -1,14 +0,0 @@
apiVersion: v1
kind: Pod
metadata:
name: nginx
labels:
app.kubernetes.io/name: nginx
spec:
containers:
- name: nginx
image: nginx
resources:
requests:
memory: 256Mi
cpu: 100m
@@ -1,13 +0,0 @@
apiVersion: v1
kind: Pod
metadata:
name: nginx
labels:
app.kubernetes.io/name: nginx
spec:
containers:
- name: nginx
image: nginx
securityContext:
capabilities:
add: ["ALL"]
@@ -1,14 +0,0 @@
apiVersion: v1
kind: Pod
metadata:
name: nginx
labels:
app.kubernetes.io/name: nginx
spec:
containers:
- name: nginx
image: nginx
securityContext:
capabilities:
add:
- NET_ADMIN
@@ -1,14 +0,0 @@
apiVersion: v1
kind: Pod
metadata:
name: nginx
labels:
app.kubernetes.io/name: nginx
spec:
containers:
- name: nginx
image: nginx
securityContext:
capabilities:
add:
-11
View File
@@ -1,11 +0,0 @@
apiVersion: v1
kind: Pod
metadata:
name: nginx
labels:
app.kubernetes.io/name: nginx
spec:
hostIPC: true
containers:
- name: nginx
image: nginx
-12
View File
@@ -1,12 +0,0 @@
apiVersion: v1
kind: Pod
metadata:
name: nginx
labels:
app.kubernetes.io/name: nginx
spec:
hostIPC: false
containers:
- name: nginx
image: nginx
-11
View File
@@ -1,11 +0,0 @@
apiVersion: v1
kind: Pod
metadata:
name: nginx
labels:
app.kubernetes.io/name: nginx
spec:
hostNetwork: true
containers:
- name: nginx
image: nginx
-11
View File
@@ -1,11 +0,0 @@
apiVersion: v1
kind: Pod
metadata:
name: nginx
labels:
app.kubernetes.io/name: nginx
spec:
hostNetwork: false
containers:
- name: nginx
image: nginx
-11
View File
@@ -1,11 +0,0 @@
apiVersion: v1
kind: Pod
metadata:
name: nginx
labels:
app.kubernetes.io/name: nginx
spec:
hostPID: true
containers:
- name: nginx
image: nginx

Some files were not shown because too many files have changed in this diff Show More