Compare commits

..
8 Commits
368 changed files with 16342 additions and 16353 deletions
+163 -45
View File
@@ -1,24 +1,12 @@
version: 2.1
orbs:
rok8s: fairwinds/rok8s-scripts@12
oss-docs: fairwinds/oss-docs@0
rok8s: fairwinds/rok8s-scripts@11
executors:
vm:
machine:
enabled: true
commands:
install_goreleaser_dependencies:
description: Installs dependencies for CI scripts
steps:
- run: apk update
# gettext provides envsubst
- run: apk add gettext
# Register other docker platforms, to build arm64.
# This shouldn't be needed, why TBD.
- run: docker run --privileged --rm tonistiigi/binfmt --install all
image: cimg/base:stable-20.04
references:
set_environment_variables: &set_environment_variables
@@ -44,20 +32,20 @@ references:
sudo apt-get install -yqq jq git
echo "Installing KIND"
curl -sLO https://github.com/kubernetes-sigs/kind/releases/download/v0.19.0/kind-linux-amd64
curl -sLO https://github.com/kubernetes-sigs/kind/releases/download/v0.14.0/kind-linux-amd64
chmod 0755 kind-linux-amd64
sudo mv kind-linux-amd64 /usr/local/bin/kind
kind version
echo "Installing Kubectl"
curl -sLO https://storage.googleapis.com/kubernetes-release/release/v1.25.10/bin/linux/amd64/kubectl
curl -sLO https://storage.googleapis.com/kubernetes-release/release/v1.21.12/bin/linux/amd64/kubectl
chmod 0755 kubectl
sudo mv kubectl /usr/local/bin/
kubectl version --client
echo "Creating Kubernetes Cluster with Kind"
kind create cluster --wait=90s --image kindest/node:v1.25.9@sha256:c08d6c52820aa42e533b70bce0c2901183326d86dcdcbedecc9343681db45161
kind create cluster --wait=90s --image kindest/node:v1.21.12
docker ps -a
kubectl version
@@ -68,17 +56,18 @@ references:
./get_helm.sh
echo "Installing cert-manager"
kubectl create namespace cert-manager
helm repo add jetstack https://charts.jetstack.io
helm repo update
echo "Helm install"
helm install cert-manager jetstack/cert-manager --namespace cert-manager --version v1.12.1 --set "installCRDs=true" --wait --create-namespace
helm install cert-manager jetstack/cert-manager --namespace cert-manager --version 0.16.1 --set "installCRDs=true" --wait
echo "Install cert-manager successful"
test_binary_dashboard: &test_binary_dashboard
run:
name: Test Dashboard
command: |
go run main.go dashboard --port 3000 --audit-path ./pkg/config/examples &
go run main.go dashboard --port 3000 --audit-path ./examples &
sleep 30
curl -f http://localhost:3000 > /dev/null
curl -f http://localhost:3000/health > /dev/null
@@ -97,86 +86,215 @@ references:
echo "Skipping Kubernetes tests for forked PR"
fi
docker_build: &docker_build
run:
name: Docker login and build
command: |
docker-pull -f .circleci/build.config
docker-build -f .circleci/build.config
docker_build_and_push: &docker_build_and_push
run:
name: Docker login, build, and push
command: |
docker-pull -f .circleci/build.config
docker-build -f .circleci/build.config
docker login quay.io -u="${fairwinds_quay_user}" -p="${fairwinds_quay_token}"
docker-push -f .circleci/build.config
enable_experimental_features: &enable_experimental_docker_features
run:
name: enable experimental features
command: |
set -ex
apk --update add openssh
ssh remote-docker \<<EOF
sudo bash -c 'echo "{\"experimental\": true}" > /etc/docker/daemon.json'
sudo systemctl restart docker
EOF
install_vault_alpine: &install_vault_alpine
run:
name: install hashicorp vault
command: |
apk --update add curl yq
cd /tmp
curl -LO https://releases.hashicorp.com/vault/1.13.2/vault_1.13.2_linux_amd64.zip
sha256sum vault_1.13.2_linux_amd64.zip | grep f7930279de8381de7c532164b4a4408895d9606c0d24e2e9d2f9acb5dfe99b3c
unzip vault_1.13.2_linux_amd64.zip
curl -LO https://releases.hashicorp.com/vault/1.9.3/vault_1.9.3_linux_amd64.zip
unzip vault_1.9.3_linux_amd64.zip
mv vault /usr/bin/vault
jobs:
build:
docker:
- image: quay.io/reactiveops/ci-images:v11.0-stretch
steps:
- checkout
- setup_remote_docker
- *set_environment_variables
- *docker_build
push:
docker:
- image: quay.io/reactiveops/ci-images:v11.0-stretch
steps:
- checkout
- setup_remote_docker
- *set_environment_variables
- *docker_build_and_push
test_k8s:
working_directory: ~/polaris
resource_class: medium
executor: vm
steps:
- checkout
- *set_environment_variables
- *install_k8s
- *test_k8s
test:
working_directory: /go/src/github.com/fairwindsops/polaris/
docker:
- image: cimg/go:1.19
- image: circleci/golang:1.17
steps:
- checkout
- *set_environment_variables
- run: go vet ./...
- 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
- *test_binary_dashboard
build_and_push:
insights:
docker:
- image: quay.io/reactiveops/ci-images:v11.0-stretch
steps:
- checkout
- setup_remote_docker
- 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/
resource_class: large
shell: /bin/bash
docker:
# The goreleaser image tag determins the version of Go.
# Manually check goreleaser images for their version of Go.
# Ref: https://hub.docker.com/r/goreleaser/goreleaser/tags
- image: goreleaser/goreleaser:v1.18.2
- image: goreleaser/goreleaser:v1.3.0
steps:
- checkout
- setup_remote_docker:
version: 20.10.11
version: 20.10.6
- *enable_experimental_docker_features
- *install_vault_alpine
- rok8s/get_vault_env:
vault_path: repo/global/env
- rok8s/get_vault_env:
vault_path: repo/polaris/env
- *set_environment_variables
- run: docker login quay.io -u="${FAIRWINDS_QUAY_USER}" -p="${FAIRWINDS_QUAY_TOKEN}"
- install_goreleaser_dependencies
- run: scripts/goreleaser.sh
- run: go get -u github.com/gobuffalo/packr/v2/packr2
- run: git checkout -- . # FIXME: the go get makes the directory dirty
- run: packr2
- run: echo 'export GORELEASER_CURRENT_TAG="${CIRCLE_TAG}"' >> $BASH_ENV
- run: goreleaser
release_images:
working_directory: /go/src/github.com/fairwindsops/polaris/
docker:
- image: quay.io/reactiveops/ci-images:v11.0-stretch
steps:
- checkout
- setup_remote_docker
- *set_environment_variables
- *docker_build_and_push
publish_docs:
docker:
- image: cimg/node:15.5.1
steps:
- checkout
- run:
name: Build Docs Site
command: |
set -e
cd ./docs
npm install
npm run check-links
npm run build
- run:
name: Install Tools
command: |
cd /tmp
echo "Installing AWS CLI"
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip
sudo ./aws/install
echo "Installing Hashicorp Vault"
curl -LO https://releases.hashicorp.com/vault/1.9.3/vault_1.9.3_linux_amd64.zip
unzip vault_1.9.3_linux_amd64.zip
sudo mv vault /usr/bin/vault
sudo chmod +x /usr/bin/vault
vault --version
echo "Installing yq"
curl -LO https://github.com/mikefarah/yq/releases/download/v4.16.2/yq_linux_amd64.tar.gz
tar -zxvf yq_linux_amd64.tar.gz
sudo mv yq_linux_amd64 /usr/bin/yq
sudo chmod +x /usr/bin/yq
yq --version
- rok8s/get_vault_env:
vault_path: repo/polaris/env
- run:
name: Publish Docs Site to S3
command: |
cd ./dist
aws s3 sync ./ s3://polaris.docs.fairwinds.com --delete
workflows:
version: 2
release:
build:
jobs:
- test:
filters:
tags:
only: /.*/
- build_and_push:
- test
- build:
requires:
- test
- push:
context: org-global
requires:
- build
filters:
branches:
ignore: /pull\/[0-9]+/
- insights:
requires:
- push
filters:
branches:
ignore: /pull\/[0-9]+/
tags:
ignore: /^testing-.*/
- test_k8s:
requires:
- build_and_push
- push
filters:
branches:
ignore: /pull\/[0-9]+/
release:
jobs:
- release_binary:
context: org-global
filters:
branches:
ignore: /.*/
tags:
ignore: /^testing-.*/
- oss-docs/publish-docs:
repository: polaris
- release_images:
requires:
- release_binary
context: org-global
filters:
branches:
ignore: /.*/
tags:
ignore: /^testing-.*/
- publish_docs:
filters:
branches:
ignore: /.*/
+1 -1
View File
@@ -2,7 +2,7 @@
name: Feature request
about: Suggest an idea for this project
title: ''
labels: [triage, enhancement]
labels: ''
assignees: ''
---
+1 -1
View File
@@ -2,7 +2,7 @@
name: Other
about: For misc. tasks like research or continued conversation
title: ''
labels: [triage]
labels: ''
assignees: ''
---
+1 -1
View File
@@ -1,6 +1,6 @@
# The action uses an own Dockerfile on purpose because the root Dockerfile takes way too long to build for an action
FROM alpine:3.17
FROM alpine:3.10
RUN apk add --no-cache \
bash \
+1 -1
View File
@@ -17,4 +17,4 @@ mkdir polaris
tar -xzf $TARGET_FILE -C polaris
rm $TARGET_FILE
echo "polaris" >> $GITHUB_PATH
echo "version=$INPUT_VERSION" >> $GITHUB_OUTPUT
echo "::set-output name=version::$INPUT_VERSION"
-1
View File
@@ -1,4 +1,3 @@
This PR fixes #
## Checklist
+2 -2
View File
@@ -7,7 +7,7 @@ jobs:
build-int:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2
- name: Setup polaris
uses: ./.github/actions/setup-polaris
with:
@@ -18,7 +18,7 @@ jobs:
build-ext:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2
- name: Setup polaris
uses: fairwindsops/polaris/.github/actions/setup-polaris@master
with:
+2 -5
View File
@@ -1,7 +1,4 @@
# goreleaser is run via a wrapper that creates .goreleaser.yml from .goreleaser.yml.envsubst
.goreleaser.yml
# dist
# # Binaries for programs and plugins
# Binaries for programs and plugins
.go-version
*.exe
*.exe~
@@ -23,6 +20,7 @@ Tiltfile
main
.DS_Store
*-packr.go
dist
.vscode
@@ -30,4 +28,3 @@ dist
node_modules
/dist
docs/README.md
+51
View File
@@ -0,0 +1,51 @@
checksum:
name_template: 'checksums.txt'
changelog:
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'
builds:
- id: polaris
ldflags:
- -X main.Version={{.Version}} -X main.Commit={{.Commit}} -s -w
env:
- CGO_ENABLED=0
- GO111MODULE=on
goos:
- linux
- darwin
- windows
goarch:
- amd64
- arm
- arm64
goarm:
- 6
- 7
archives:
- id: polaris
builds: ["polaris"]
name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}"
signs:
- cmd: cosign
args: ["sign-blob", "--key=hashivault://cosign", "-output-signature=${signature}", "${artifact}"]
artifacts: checksum
release:
prerelease: auto
footer: |
You can verify the signature of the checksums.txt file using [cosign](https://github.com/sigstore/cosign).
```
cosign verify-blob checksums.txt --signature=checksums.txt.sig --key https://artifacts.fairwinds.com/cosign.pub
```
brews:
- name: polaris
tap:
owner: FairwindsOps
name: homebrew-tap
folder: Formula
description: Open Source Best Practices for Kubernetes
test: |
system "#{bin}/polaris version"
-112
View File
@@ -1,112 +0,0 @@
checksum:
name_template: 'checksums.txt'
changelog:
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'
env:
- GOBIN={{ .Env.TMPDIR }}/go-bin
before:
hooks:
- go mod download
builds:
- id: polaris
ldflags:
- -X main.Version={{.Version}} -X main.Commit={{.Commit}} -s -w
env:
- CGO_ENABLED=0
- GO111MODULE=on
goos:
- linux
- darwin
- windows
goarch:
- amd64
- arm
- arm64
goarm:
- 6
- 7
archives:
- id: polaris
builds: ["polaris"]
name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}"
signs:
- cmd: cosign
args: ["sign-blob", "--key=hashivault://cosign", "-output-signature=${signature}", "${artifact}"]
artifacts: checksum
release:
# This is replaced using `envsubst`, depending on the git branch.
disable: ${skip_release}
prerelease: auto
footer: |
You can verify the signature of the checksums.txt file using [cosign](https://github.com/sigstore/cosign).
```
cosign verify-blob checksums.txt --signature=checksums.txt.sig --key https://artifacts.fairwinds.com/cosign.pub
```
brews:
- name: polaris
# This is replaced using `envsubst`, depending on the git branch.
skip_upload: ${skip_release}
tap:
owner: FairwindsOps
name: homebrew-tap
folder: Formula
description: Open Source Best Practices for Kubernetes
test: |
system "#{bin}/polaris version"
dockers:
# There are multiple images to match the `--platform` docker build flag with
# combinations of `GOOS`, `GOARCH`, and `GOARM`
- image_templates:
- "quay.io/fairwinds/polaris:{{ .FullCommit }}-amd64"
use: buildx
build_flag_templates:
- "--platform=linux/amd64"
- image_templates:
- "quay.io/fairwinds/polaris:{{ .FullCommit }}-arm64"
use: buildx
goarch: arm64
goos: linux
build_flag_templates:
- "--platform=linux/arm64"
docker_manifests:
# Create DOcker manifests that make multiple architectures available within a tag,
# and provide partial-version tags like 2, and 2.2.
- name_template: quay.io/fairwinds/polaris:{{ .FullCommit }}
image_templates:
- "quay.io/fairwinds/polaris:{{ .FullCommit }}-amd64"
- "quay.io/fairwinds/polaris:{{ .FullCommit }}-arm64"
- name_template: quay.io/fairwinds/polaris:{{ .Env.feature_docker_tag }}
# This is replaced using `envsubst`, depending on the git branch.
skip_push: ${skip_feature_docker_tags}
image_templates:
- "quay.io/fairwinds/polaris:{{ .FullCommit }}-amd64"
- "quay.io/fairwinds/polaris:{{ .FullCommit }}-arm64"
- name_template: quay.io/fairwinds/polaris:latest
# This is replaced using `envsubst`, depending on the git branch.
skip_push: ${skip_release}
image_templates:
- "quay.io/fairwinds/polaris:{{ .FullCommit }}-amd64"
- "quay.io/fairwinds/polaris:{{ .FullCommit }}-arm64"
- name_template: quay.io/fairwinds/polaris:{{ .Tag }}
# This is replaced using `envsubst`, depending on the git branch.
skip_push: ${skip_release}
image_templates:
- "quay.io/fairwinds/polaris:{{ .FullCommit }}-amd64"
- "quay.io/fairwinds/polaris:{{ .FullCommit }}-arm64"
- name_template: quay.io/fairwinds/polaris:{{ .Major }}
# This is replaced using `envsubst`, depending on the git branch.
skip_push: ${skip_release}
image_templates:
- "quay.io/fairwinds/polaris:{{ .FullCommit }}-amd64"
- "quay.io/fairwinds/polaris:{{ .FullCommit }}-arm64"
- name_template: quay.io/fairwinds/polaris:{{ .Major }}.{{ .Minor }}
# This is replaced using `envsubst`, depending on the git branch.
skip_push: ${skip_release}
image_templates:
- "quay.io/fairwinds/polaris:{{ .FullCommit }}-amd64"
- "quay.io/fairwinds/polaris:{{ .FullCommit }}-arm64"
+1 -1
View File
@@ -1,2 +1,2 @@
## DO NOT EDIT - Managed by Terraform
* @sudermanjr
* @rbren @makoscafee
+17 -11
View File
@@ -1,21 +1,27 @@
FROM alpine:3.19
FROM golang:1.17 AS build-env
WORKDIR /go/src/github.com/fairwindsops/polaris/
LABEL org.opencontainers.image.authors="FairwindsOps, Inc." \
org.opencontainers.image.vendor="FairwindsOps, Inc." \
org.opencontainers.image.title="polaris" \
org.opencontainers.image.description="Polaris is a cli tool to help discover deprecated apiVersions in Kubernetes" \
org.opencontainers.image.documentation="https://polaris.docs.fairwinds.com/" \
org.opencontainers.image.source="https://github.com/FairwindsOps/polaris" \
org.opencontainers.image.url="https://github.com/FairwindsOps/polaris" \
org.opencontainers.image.licenses="Apache License 2.0"
ENV GO111MODULE=on
ENV GOPROXY=https://proxy.golang.org
ENV CGO_ENABLED=0
ENV GOOS=linux
ENV GOARCH=amd64
COPY go.mod .
COPY go.sum .
RUN go mod download
RUN go get -u github.com/gobuffalo/packr/v2/packr2
COPY . .
RUN packr2 build -a -o polaris *.go
FROM alpine:3.16.0
WORKDIR /usr/local/bin
RUN apk -U upgrade
RUN apk --no-cache add ca-certificates
RUN addgroup -S polaris && adduser -u 1200 -S polaris -G polaris
USER 1200
COPY polaris .
COPY --from=build-env /go/src/github.com/fairwindsops/polaris/polaris .
WORKDIR /opt/app
+13 -11
View File
@@ -1,7 +1,7 @@
<div align="center" class="no-border">
<img src="https://polaris.docs.fairwinds.com/img/polaris-logo.png" alt="Polaris Logo">
<br>
<h3>Polaris is an open source policy engine for Kubernetes</h3>
<h3>Best Practices for Kubernetes Workload Configuration</h3>
<a href="https://github.com/FairwindsOps/polaris/releases">
<img src="https://img.shields.io/github/v/release/FairwindsOps/polaris">
</a>
@@ -16,12 +16,15 @@
</a>
</div>
Polaris is an open source policy engine for Kubernetes that validates and remediates resource configuration. It includes 30+ built in configuration policies, as well as the ability to build custom policies with JSON Schema. When run on the command line or as a mutating webhook, Polaris can automatically remediate issues based on policy criteria.
Fairwinds' Polaris keeps your clusters sailing smoothly. It runs a variety of checks to ensure that
Kubernetes pods and controllers are configured using best practices, helping you avoid
problems in the future.
Polaris can be run in three different modes:
* As a [dashboard](https://polaris.docs.fairwinds.com/dashboard) - Validate Kubernetes resources against policy-as-code.
* As an [admission controller](https://polaris.docs.fairwinds.com/admission-controller) - Automatically reject or modify workloads that don't adhere to your organization's policies.
* As a [command-line tool](https://polaris.docs.fairwinds.com/infrastructure-as-code) - Incorporate policy-as-code into the CI/CD process to test local YAML files.
* As a [dashboard](https://polaris.docs.fairwinds.com/dashboard), so you can audit what's running inside your cluster.
* As an [admission controller](https://polaris.docs.fairwinds.com/admission-controller), so you can automatically reject workloads that don't adhere to your organization's policies.
* As a [command-line tool](https://polaris.docs.fairwinds.com/infrastructure-as-code), so you can test local YAML files, e.g. as part of a CI/CD process.
<p align="center">
<img src="https://polaris.docs.fairwinds.com/img/architecture.svg" alt="Polaris Architecture" width="550"/>
</p>
@@ -34,13 +37,12 @@ Check out the [documentation at docs.fairwinds.com](https://polaris.docs.fairwin
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-2na8gtwb4-DGQ4qgmQbczQyB2NlFlYQQ)
[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!
<a href="https://insights.fairwinds.com/auth/register/">
<img src="https://www.fairwinds.com/hubfs/Doc_Banners/Fairwinds_OSS_User_Group_740x125_v6.png"
alt="Love Fairwinds Open Source? Automate Fairwinds Open Source for free with Fairwinds Insights. Click to learn more" />
<a href="https://www.fairwinds.com/t-shirt-offer?utm_source=polaris&utm_medium=polaris&utm_campaign=polaris-tshirt">
<img src="https://www.fairwinds.com/hubfs/Doc_Banners/Fairwinds_OSS_User_Group_740x125_v6.png" alt="Love Fairwinds Open Source? Share your business email and job title and we'll send you a free Fairwinds t-shirt!" />
</a>
## Other Projects from Fairwinds
@@ -56,9 +58,9 @@ Or [check out the full list](https://www.fairwinds.com/open-source-software?utm_
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://fairwinds.com/pricing),
[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.
<a href="https://fairwinds.com/pricing">
<a href="https://www.fairwinds.com/polaris-user-insights-demo?utm_source=polaris&utm_medium=ad&utm_campaign=polarisad">
<img src="https://www.fairwinds.com/hubfs/Doc_Banners/Fairwinds_Polaris_Ad.png" alt="Fairwinds Insights" />
</a>
@@ -26,7 +26,12 @@ schema:
not:
const: ''
mutations:
- op: add
path: /resources/limits
value: {}
- op: add
path: /resources/limits/cpu
value: 100m
comment: "TODO: Set this to the maximum amount of CPU you want your workload to use"
comments:
- find: "cpu: 100m"
comment: "TODO: Set this to the amount of CPU you want to reserve for your workload"
@@ -26,7 +26,12 @@ schema:
not:
const: ''
mutations:
- op: add
path: /resources/requests
value: {}
- op: add
path: /resources/requests/cpu
value: 100m
comment: "TODO: Set this to the amount of CPU you want to reserve for your workload"
comments:
- find: "cpu: 100m"
comment: "TODO: Set this to the amount of CPU you want to reserve for your workload"
@@ -24,8 +24,3 @@ schema:
- not:
contains:
pattern: '^(?i)NET_ADMIN$'
mutations:
- op: remove
path: /securityContext/capabilities/add
@@ -12,7 +12,4 @@ schema:
items:
properties:
hostPort:
const: 0
mutations:
- op: remove
path: /ports/*/hostPort
const: 0
@@ -54,7 +54,3 @@ schema:
pattern: '^(?i)KILL$'
- contains:
pattern: '^(?i)AUDIT_WRITE$'
mutations:
- op: replace
path: /securityContext/capabilities
value: {"drop": ["ALL"]}
@@ -23,4 +23,6 @@ mutations:
- op: add
path: /livenessProbe
value: {"exec": { "command": [ "cat", "/tmp/healthy" ] }, "initialDelaySeconds": 5, "periodSeconds": 5 }
comment: "TODO: Change the livenessProbe setting to reflect your application's health"
comments:
- find: "livenessProbe:"
comment: "TODO: Change livenessProbe setting to reflect your health endpoints"
@@ -26,7 +26,12 @@ schema:
not:
const: ''
mutations:
- op: add
path: /resources/limits
value: {}
- op: add
path: /resources/limits/memory
value: "512Mi"
comment: "TODO: Set this to the maximum amount of memory you want your workload to use"
comments:
- find: "memory: 512Mi"
comment: "TODO: Set this to the amount of Memory you want to reserve for your workload"
@@ -26,7 +26,12 @@ schema:
not:
const: ''
mutations:
- op: add
path: /resources/requests
value: {}
- op: add
path: /resources/requests/memory
value: "512Mi"
comment: "TODO: Set this to the amount of Memory you want to reserve for your workload"
comments:
- find: "memory: 512Mi"
comment: "TODO: Set this to the amount of Memory you want to reserve for your workload"
@@ -1,6 +1,5 @@
successMessage: Label app.kubernetes.io/instance matches metadata.name
failureMessage: Label app.kubernetes.io/instance must match metadata.name
category: Reliability
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
@@ -12,7 +11,7 @@ schema:
properties:
labels:
type: object
required: ["app.kubernetes.io/instance"]
required: ["app.kubernetes.io/name"]
properties:
app.kubernetes.io/instance:
app.kubernetes.io/name:
const: "{{ .metadata.name }}"
@@ -4,27 +4,17 @@ category: Reliability
target: Controller
controllers:
include:
- Deployment
- Deployment
schema:
"$schema": http://json-schema.org/draft-07/schema#
'$schema': http://json-schema.org/draft-07/schema
type: object
required: [spec]
properties:
spec:
metadata:
type: object
required: [template]
properties:
template:
labels:
type: object
required: [metadata]
properties:
metadata:
type: object
required: [labels]
properties:
labels:
type: object
minProperties: 1
minProperties: 1
additionalSchemaStrings:
policy/PodDisruptionBudget: |
type: object
@@ -40,7 +30,7 @@ additionalSchemaStrings:
matchLabels:
type: object
anyOf:
{{ range $key, $value := .spec.template.metadata.labels }}
{{ range $key, $value := .metadata.labels }}
- properties:
"{{ $key }}":
type: string
@@ -40,9 +40,4 @@ schema:
- securityContext
properties:
securityContext:
$ref: "#/definitions/goodSecurityContext"
mutations:
- op: add
path: /securityContext/readOnlyRootFilesystem
value: true
$ref: "#/definitions/goodSecurityContext"
@@ -1,6 +1,6 @@
successMessage: Priority class has been set
failureMessage: Priority class should be set
category: Reliability
category: Security
target: PodSpec
schema:
'$schema': http://json-schema.org/draft-07/schema
@@ -40,9 +40,4 @@ schema:
- securityContext
properties:
securityContext:
$ref: "#/definitions/goodSecurityContext"
mutations:
- op: add
path: /securityContext/allowPrivilegeEscalation
value: false
$ref: "#/definitions/goodSecurityContext"
@@ -23,4 +23,6 @@ mutations:
- op: add
path: /readinessProbe
value: {"exec": { "command": [ "cat", "/tmp/healthy" ] }, "initialDelaySeconds": 5, "periodSeconds": 5 }
comment: "TODO: Change the readinessProbe setting to reflect your application's readiness to serve traffic"
comments:
- find: "readinessProbe:"
comment: "TODO: Change livenessProbe setting to reflect your health endpoints"
@@ -51,6 +51,6 @@ schema:
securityContext:
$ref: "#/definitions/goodSecurityContext"
mutations:
- op: add
- op: replace
path: /securityContext/runAsNonRoot
value: true
value: true
+16 -107
View File
@@ -17,21 +17,14 @@ package cmd
import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"os/exec"
"strings"
workloads "github.com/fairwindsops/insights-plugins/plugins/workloads"
workloadsPkg "github.com/fairwindsops/insights-plugins/plugins/workloads/pkg"
"github.com/fairwindsops/polaris/pkg/auth"
cfg "github.com/fairwindsops/polaris/pkg/config"
"github.com/fairwindsops/polaris/pkg/insights"
"github.com/fairwindsops/polaris/pkg/kube"
"github.com/fairwindsops/polaris/pkg/validator"
"github.com/sirupsen/logrus"
@@ -49,15 +42,9 @@ var (
resourceToAudit string
useColor bool
helmChart string
helmValues []string
helmSkipTests bool
helmValues string
checks []string
auditNamespace string
severityLevel string
skipSslValidation bool
uploadInsights bool
clusterName string
quiet bool
)
func init() {
@@ -73,15 +60,9 @@ func init() {
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().StringSliceVar(&helmValues, "helm-values", []string{}, "Optional flag to add helm values")
auditCmd.PersistentFlags().BoolVar(&helmSkipTests, "helm-skip-tests", false, "Corresponds to --skip-tests of helm template")
auditCmd.PersistentFlags().StringVar(&helmValues, "helm-values", "", "Optional flag to add helm values")
auditCmd.PersistentFlags().StringSliceVar(&checks, "checks", []string{}, "Optional flag to specify specific checks to check")
auditCmd.PersistentFlags().StringVar(&auditNamespace, "namespace", "", "Namespace to audit. Only applies to in-cluster audits")
auditCmd.PersistentFlags().StringVar(&severityLevel, "severity", "", "Severity level used to filter results. Behaves like log levels. 'danger' is the least verbose (warning, danger)")
auditCmd.PersistentFlags().BoolVar(&skipSslValidation, "skip-ssl-validation", false, "Skip https certificate verification")
auditCmd.PersistentFlags().BoolVar(&uploadInsights, "upload-insights", false, "Upload scan results to Fairwinds Insights")
auditCmd.PersistentFlags().StringVar(&clusterName, "cluster-name", "", "Set --cluster-name to a descriptive name for the cluster you're auditing")
auditCmd.PersistentFlags().BoolVar(&quiet, "quiet", false, "Suppress the 'upload to Insights' prompt.")
}
var auditCmd = &cobra.Command{
@@ -114,32 +95,14 @@ var auditCmd = &cobra.Command{
}
if helmChart != "" {
var err error
auditPath, err = ProcessHelmTemplates(helmChart, helmValues, helmSkipTests)
auditPath, err = ProcessHelmTemplates(helmChart, helmValues)
if err != nil {
logrus.Errorf("Couldn't process helm chart: %v", err)
logrus.Infof("Couldn't process helm chart: %v", err)
os.Exit(1)
}
}
if uploadInsights && len(clusterName) == 0 {
logrus.Error("cluster-name is required when using --upload-insights")
os.Exit(1)
}
if uploadInsights {
if auditPath != "" {
logrus.Errorf("upload-insights and audit-path are not supported when used simultaneously")
os.Exit(1)
}
if !auth.IsLoggedIn() {
err := auth.HandleLogin(insightsHost)
if err != nil {
logrus.Errorf("error handling logging: %v", err)
os.Exit(1)
}
}
}
ctx := context.TODO()
k, err := kube.CreateResourceProvider(ctx, auditPath, resourceToAudit, config)
k, err := kube.CreateResourceProvider(context.TODO(), auditPath, resourceToAudit, config)
if err != nil {
logrus.Errorf("Error fetching Kubernetes resources %v", err)
os.Exit(1)
@@ -151,43 +114,7 @@ var auditCmd = &cobra.Command{
os.Exit(1)
}
if uploadInsights {
auth, err := auth.GetAuth(insightsHost)
if err != nil {
logrus.Errorf("getting auth: %v", err)
os.Exit(1)
}
// fetch workloads using workload plugin... or should we adapt the workloads from above?
dynamicClient, restMapper, clientSet, host, err := kube.GetKubeClient(ctx, "")
if err != nil {
logrus.Errorf("getting the kubernetes client: %v", err)
os.Exit(1)
}
k8sResources, err := workloadsPkg.CreateResourceProviderFromAPI(ctx, dynamicClient, restMapper, clientSet, host)
if err != nil {
logrus.Errorf("creating resource provider: %v", err)
os.Exit(1)
}
insightsClient := insights.NewHTTPClient(insightsHost, auth.Organization, auth.Token)
insightsReporter := insights.NewInsightsReporter(insightsClient)
wr := insights.WorkloadsReport{Version: workloads.Version, Payload: *k8sResources}
pr := insights.PolarisReport{Version: version, Payload: auditData}
logrus.Infof("Uploading to Fairwinds Insights organization '%s/%s'...", auth.Organization, clusterName)
err = insightsReporter.ReportAuditToFairwindsInsights(clusterName, wr, pr)
if err != nil {
logrus.Errorf("reporting audit file to insights: %v", err)
os.Exit(1)
}
os.Stderr.WriteString("\n\nSuccess! You can see your results at:")
os.Stderr.WriteString(fmt.Sprintf("\n\n%s/orgs/%s/clusters/%s/action-items\n\n", insightsHost, auth.Organization, clusterName))
} else {
outputAudit(auditData, auditOutputFile, auditOutputURL, auditOutputFormat, useColor, onlyShowFailedTests, severityLevel)
if !quiet {
os.Stderr.WriteString("\n\n🚀 Upload your Polaris findings to Fairwinds Insights to see remediation advice, add teammates, integrate with Slack or Jira, and more:")
os.Stderr.WriteString("\n\n polaris " + strings.Join(os.Args[1:], " ") + " --upload-insights --cluster-name=my-cluster\n\n")
}
}
outputAudit(auditData, auditOutputFile, auditOutputURL, auditOutputFormat, useColor, onlyShowFailedTests)
summary := auditData.GetSummary()
score := summary.GetScore()
@@ -202,7 +129,7 @@ var auditCmd = &cobra.Command{
}
// ProcessHelmTemplates turns helm into yaml to be processed by Polaris or the other tools.
func ProcessHelmTemplates(helmChart string, helmValues []string, helmSkipTests bool) (string, error) {
func ProcessHelmTemplates(helmChart, helmValues string) (string, error) {
cmd := exec.Command("helm", "dependency", "update", helmChart)
output, err := cmd.CombinedOutput()
if err != nil {
@@ -210,22 +137,18 @@ func ProcessHelmTemplates(helmChart string, helmValues []string, helmSkipTests b
return "", err
}
dir, err := os.MkdirTemp("", "*")
dir, err := ioutil.TempDir("", "*")
if err != nil {
return "", err
}
params := []string{
"template", helmChart,
"--generate-name",
helmChart,
"--output-dir",
dir,
}
for _, v := range helmValues {
params = append(params, "--values", v)
}
if helmSkipTests {
params = append(params, "--skip-tests")
if helmValues != "" {
params = append(params, "--values", helmValues)
}
cmd = exec.Command("helm", params...)
@@ -238,20 +161,10 @@ func ProcessHelmTemplates(helmChart string, helmValues []string, helmSkipTests b
return dir, nil
}
func outputAudit(auditData validator.AuditData, outputFile, outputURL, outputFormat string, useColor bool, onlyShowFailedTests bool, severityLevel string) {
func outputAudit(auditData validator.AuditData, outputFile, outputURL, outputFormat string, useColor bool, onlyShowFailedTests bool) {
if onlyShowFailedTests {
auditData = auditData.RemoveSuccessfulResults()
}
if severityLevel != "" {
switch severityLevel {
case "danger":
auditData = auditData.FilterResultsBySeverityLevel(cfg.SeverityDanger)
case "warning":
auditData = auditData.FilterResultsBySeverityLevel(cfg.SeverityWarning)
}
}
var outputBytes []byte
var err error
if outputFormat == "score" {
@@ -289,13 +202,9 @@ func outputAudit(auditData validator.AuditData, outputFile, outputURL, outputFor
} else {
req.Header.Set("Content-Type", "text/plain")
}
client := &http.Client{}
if skipSslValidation {
transport := &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}
client = &http.Client{Transport: transport}
}
resp, err := client.Do(req)
if err != nil {
logrus.Errorf("Error making request for output: %v", err)
os.Exit(1)
@@ -303,7 +212,7 @@ func outputAudit(auditData validator.AuditData, outputFile, outputURL, outputFor
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
logrus.Errorf("Error reading response: %v", err)
@@ -314,7 +223,7 @@ func outputAudit(auditData validator.AuditData, outputFile, outputURL, outputFor
}
if outputFile != "" {
err := os.WriteFile(outputFile, outputBytes, 0644)
err := ioutil.WriteFile(outputFile, []byte(outputBytes), 0644)
if err != nil {
logrus.Errorf("Error writing output to file: %v", err)
os.Exit(1)
-81
View File
@@ -1,81 +0,0 @@
package cmd
import (
"github.com/fairwindsops/polaris/pkg/auth"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
func init() {
rootCmd.AddCommand(authCmd)
authCmd.AddCommand(loginCmd)
authCmd.AddCommand(logoutCmd)
authCmd.AddCommand(statusCmd)
authCmd.AddCommand(tokenCmd)
}
var authCmd = &cobra.Command{
Use: "auth",
Short: "Authenticate polaris with Fairwinds Insights",
Long: `Authenticate polaris with Fairwinds Insights so better experience`,
}
var loginCmd = &cobra.Command{
Use: "login",
Short: "Authenticate polaris with Fairwinds Insights.",
Long: `Authenticate polaris with Fairwinds Insights.`,
Run: func(cmd *cobra.Command, args []string) {
err := auth.HandleLogin(insightsHost)
if err != nil {
logrus.Fatal(err)
}
},
PersistentPostRunE: func(cmd *cobra.Command, args []string) error {
return nil
},
}
var logoutCmd = &cobra.Command{
Use: "logout",
Short: "Log out of a Fairwinds Insights.",
Long: `Log out of a Fairwinds Insights.`,
Run: func(cmd *cobra.Command, args []string) {
err := auth.HandleLogout()
if err != nil {
logrus.Fatal(err)
}
},
PersistentPostRunE: func(cmd *cobra.Command, args []string) error {
return nil
},
}
var statusCmd = &cobra.Command{
Use: "status",
Short: "View authentication status.",
Long: `View authentication status.`,
Run: func(cmd *cobra.Command, args []string) {
err := auth.PrintStatus(insightsHost)
if err != nil {
logrus.Fatalf("printing status: %v", err)
}
},
PersistentPostRunE: func(cmd *cobra.Command, args []string) error {
return nil
},
}
var tokenCmd = &cobra.Command{
Use: "token",
Short: "Print the auth token gh is configured to use.",
Long: `Print the auth token gh is configured to use.`,
Run: func(cmd *cobra.Command, args []string) {
err := auth.PrintToken()
if err != nil {
logrus.Fatalf("printing token: %v", err)
}
},
PersistentPostRunE: func(cmd *cobra.Command, args []string) error {
return nil
},
}
+1 -5
View File
@@ -54,11 +54,7 @@ var dashboardCmd = &cobra.Command{
auditData := validator.ReadAuditFromFile(loadAuditFile)
auditDataPtr = &auditData
}
router, err := dashboard.GetRouter(config, auditPath, serverPort, basePath, auditDataPtr)
if err != nil {
logrus.Fatalf("error creating router: %v", err)
}
router := dashboard.GetRouter(config, auditPath, serverPort, basePath, auditDataPtr)
router.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("OK"))
})
+123 -13
View File
@@ -15,25 +15,29 @@
package cmd
import (
"bytes"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/fairwindsops/polaris/pkg/fix"
"github.com/fairwindsops/polaris/pkg/kube"
"github.com/fairwindsops/polaris/pkg/mutation"
"github.com/fairwindsops/polaris/pkg/validator"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
yamlV3 "gopkg.in/yaml.v3"
"sigs.k8s.io/yaml"
)
var (
filesPath string
checksToFix []string
isTemplate bool
)
var filesPath string
func init() {
rootCmd.AddCommand(fixCommand)
fixCommand.PersistentFlags().StringVar(&filesPath, "files-path", "", "mutate and fix one or more YAML files in a specified folder")
fixCommand.PersistentFlags().BoolVar(&isTemplate, "template", false, "set to true when modifyng a YAML template, like a Helm chart (experimental)")
fixCommand.PersistentFlags().StringSliceVar(&checksToFix, "checks", []string{}, "Optional flag to specify specific checks to fix eg. checks=hostIPCSet,hostPIDSet and checks=all applies fix to all defined checks mutations")
}
var fixCommand = &cobra.Command{
@@ -43,14 +47,120 @@ var fixCommand = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) {
logrus.Debug("Setting up controller manager")
err := fix.Execute(config, filesPath, isTemplate, checksToFix...)
if filesPath == "" {
logrus.Error("Please specify a file-path flag")
cmd.Help()
os.Exit(1)
}
var yamlFiles []string
fileInfo, err := os.Stat(filesPath)
if err != nil {
if errors.Is(err, fix.ErrFilesPathRequired) {
logrus.Error("Please specify a files-path flag")
cmd.Help()
logrus.Error(err)
os.Exit(1)
}
if fileInfo.IsDir() {
baseDir := filesPath
if !strings.HasSuffix(filesPath, "/") {
baseDir = baseDir + "/"
}
yamlFiles, err = getYamlFiles(baseDir)
if err != nil {
logrus.Error(err)
os.Exit(1)
}
logrus.Fatal(err)
} else {
yamlFiles = append(yamlFiles, filesPath)
}
var contentStr string
isFirstResource := true
for _, fullFilePath := range yamlFiles {
yamlFile, err := ioutil.ReadFile(fullFilePath)
if err != nil {
logrus.Errorf("Error reading file with file path %s: %v", fullFilePath, err)
os.Exit(1)
}
dec := yamlV3.NewDecoder(bytes.NewReader(yamlFile))
for {
data := map[string]interface{}{}
err := dec.Decode(&data)
// check it was parsed
if data == nil {
continue
}
// break the loop in case of EOF
if errors.Is(err, io.EOF) {
break
}
if err != nil {
logrus.Errorf("Error decoding data for file with file path %s: %v", fullFilePath, err)
os.Exit(1)
}
yamlContent, err := yamlV3.Marshal(data)
if err != nil {
logrus.Errorf("Error marshalling %s: %v", fullFilePath, err)
os.Exit(1)
}
kubeResources := kube.CreateResourceProviderFromYaml(string(yamlContent))
results, err := validator.ApplyAllSchemaChecksToResourceProvider(&config, kubeResources)
if err != nil {
logrus.Errorf("Error applying schema check to the resources %s: %v", fullFilePath, err)
os.Exit(1)
}
comments, allMutations := mutation.GetMutationsAndCommentsFromResults(results)
updatedYamlContent := string(yamlContent)
if len(allMutations) > 0 {
for _, resources := range kubeResources.Resources {
key := fmt.Sprintf("%s/%s/%s", resources[0].Kind, resources[0].Resource.GetName(), resources[0].Resource.GetNamespace())
mutations := allMutations[key]
mutated, err := mutation.ApplyAllSchemaMutations(&config, kubeResources, resources[0], mutations)
if err != nil {
logrus.Errorf("Error applying schema mutations to the resources: %v", err)
os.Exit(1)
}
mutatedYamlContent, err := yaml.JSONToYAML(mutated.OriginalObjectJSON)
if err != nil {
logrus.Errorf("Error converting JSON to Yaml : %v", err)
os.Exit(1)
}
updatedYamlContent = mutation.UpdateMutatedContentWithComments(string(mutatedYamlContent), comments)
}
}
if isFirstResource {
contentStr = updatedYamlContent
isFirstResource = false
} else {
contentStr += "\n"
contentStr += "---"
contentStr += "\n"
contentStr += updatedYamlContent
}
}
if contentStr != "" {
err = ioutil.WriteFile(fullFilePath, []byte(contentStr), 0644)
if err != nil {
logrus.Errorf("Error writing output to file: %v", err)
os.Exit(1)
}
}
}
},
}
func getYamlFiles(rootpath string) ([]string, error) {
var list []string
err := filepath.Walk(rootpath, func(path string, info os.FileInfo, err error) error {
if info.IsDir() {
return nil
}
if filepath.Ext(path) == ".yaml" || filepath.Ext(path) == ".yml" {
list = append(list, path)
}
return nil
})
return list, err
}
+11 -20
View File
@@ -15,26 +15,21 @@
package cmd
import (
"flag"
"os"
"strings"
conf "github.com/fairwindsops/polaris/pkg/config"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
var (
configPath string
disallowExemptions bool
disallowConfigExemptions bool
disallowAnnotationExemptions bool
fixChecks bool
logLevel string
auditPath string
displayName string
kubeContext string
insightsHost string
)
var configPath string
var disallowExemptions, disallowConfigExemptions, disallowAnnotationExemptions, fixChecks bool
var logLevel string
var auditPath string
var displayName string
var kubeContext string
var (
version string
@@ -47,8 +42,9 @@ func init() {
rootCmd.PersistentFlags().BoolVarP(&disallowExemptions, "disallow-exemptions", "", false, "Disallow any configured exemption.")
rootCmd.PersistentFlags().BoolVarP(&disallowConfigExemptions, "disallow-config-exemptions", "", false, "Disallow exemptions set within the configuration file.")
rootCmd.PersistentFlags().BoolVarP(&disallowAnnotationExemptions, "disallow-annotation-exemptions", "", false, "Disallow any exemption defined as a controller annotation.")
rootCmd.PersistentFlags().StringVarP(&logLevel, "log-level", "", logrus.InfoLevel.String(), "Logrus log level to be output (trace, debug, info, warning, error, fatal, panic).")
rootCmd.PersistentFlags().StringVar(&insightsHost, "insights-host", "https://insights.fairwinds.com", "Fairwinds Insights host URL")
rootCmd.PersistentFlags().StringVarP(&logLevel, "log-level", "", logrus.InfoLevel.String(), "Logrus log level.")
flag.Parse()
pflag.CommandLine.AddGoFlagSet(flag.CommandLine)
}
var config conf.Configuration
@@ -84,11 +80,6 @@ var rootCmd = &cobra.Command{
}
os.Exit(1)
},
PersistentPostRun: func(cmd *cobra.Command, args []string) {
if !strings.HasPrefix(cmd.Use, "audit") {
os.Stderr.WriteString("\n\nWant more? Automate Polaris for free with Fairwinds Insights!\n🚀 https://fairwinds.com/insights-signup/polaris 🚀 \n")
}
},
}
// Execute the stuff
-3
View File
@@ -31,7 +31,4 @@ var versionCmd = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Polaris version:" + version)
},
PersistentPostRunE: func(cmd *cobra.Command, args []string) error {
return nil
},
}
+12 -26
View File
@@ -16,6 +16,7 @@ package cmd
import (
"os"
"time"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
@@ -24,22 +25,15 @@ import (
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/webhook"
)
var webhookPort int
var disableWebhookConfigInstaller bool
var enableMutations bool
var enableValidations bool
var certDir string
func init() {
rootCmd.AddCommand(webhookCmd)
webhookCmd.PersistentFlags().IntVarP(&webhookPort, "port", "p", 9876, "Port for the dashboard webserver.")
webhookCmd.PersistentFlags().BoolVar(&disableWebhookConfigInstaller, "disable-webhook-config-installer", false, "Disable the installer in the webhook server, so it won't install webhook configuration resources during bootstrapping.")
webhookCmd.PersistentFlags().BoolVar(&enableValidations, "validate", true, "Enable the validating webhook to reject workloads with issues")
webhookCmd.PersistentFlags().BoolVar(&enableMutations, "mutate", false, "Enable the mutating webhook to modify workloads with issues")
webhookCmd.PersistentFlags().StringVar(&certDir, "cert-dir", "/opt/cert", "Directory in which tls certificate is located")
webhookCmd.PersistentFlags().BoolVar(&disableWebhookConfigInstaller, "disable-webhook-config-installer", false, "disable the installer in the webhook server, so it won't install webhook configuration resources during bootstrapping.")
}
var webhookCmd = &cobra.Command{
@@ -50,36 +44,28 @@ var webhookCmd = &cobra.Command{
logrus.Debug("Setting up controller manager")
mgr, err := manager.New(k8sConfig.GetConfigOrDie(), manager.Options{
CertDir: certDir,
CertDir: "/opt/cert",
Port: webhookPort,
WebhookServer: webhook.NewServer(webhook.Options{
CertDir: certDir,
Port: webhookPort,
CertName: "tls.crt",
KeyName: "tls.key",
}),
})
if err != nil {
logrus.Errorf("Unable to set up overall controller manager: %v", err)
os.Exit(1)
}
_, err = os.Stat(certDir + "/tls.crt")
_, err = os.Stat("/opt/cert/tls.crt")
if os.IsNotExist(err) {
time.Sleep(time.Second * 10)
panic("Cert does not exist")
}
server := mgr.GetWebhookServer()
server.CertName = "tls.crt"
server.KeyName = "tls.key"
if !enableMutations && !enableValidations {
logrus.Errorf("One of --mutate or --validate must be set to true")
os.Exit(1)
}
// Iterate all the configurations supported controllers to scan and register them for webhooks
// Should only register controllers that are configured to be scanned
fwebhook.NewValidateWebhook(mgr, fwebhook.Validator{Config: config, Client: mgr.GetClient()})
fwebhook.NewMutateWebhook(mgr, fwebhook.Mutator{Config: config, Client: mgr.GetClient()})
if enableValidations {
fwebhook.NewValidateWebhook(mgr, config)
}
if enableMutations {
fwebhook.NewMutateWebhook(mgr, config)
}
logrus.Infof("Polaris webhook server listening on port %d", webhookPort)
if err := mgr.Start(signals.SetupSignalHandler()); err != nil {
logrus.Errorf("Error starting manager: %v", err)
+6 -10
View File
@@ -11,16 +11,6 @@ var sf14gv = 32793;
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(sf14g, s);
})();
(function() {
var gtag = document.createElement('script');
gtag.src = "https://www.googletagmanager.com/gtag/js?id=G-ZR5M5SRYKY";
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(gtag, s);
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-ZR5M5SRYKY');
})();
!function(f,b,e,v,n,t,s)
{if(f.fbq)return;n=f.fbq=function(){n.callMethod?
n.callMethod.apply(n,arguments):n.queue.push(arguments)};
@@ -31,3 +21,9 @@ s.parentNode.insertBefore(t,s)}(window,document,'script',
'https://connect.facebook.net/en_US/fbevents.js');
fbq('init', '521127644762074');
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');
-35
View File
@@ -45,38 +45,3 @@ output unless we are rejecting a workload altogether.
This means that any checks with a severity of `warning` will still pass webhook validation,
and the only evidence of that warning will either be in the Polaris dashboard or the
Polaris webhook logs. This will change in a future version of Kubernetes.
## Mutating Webhook
By default, the Admission Controller is just pass/fail, but
Polaris can also operate as a mutating webhook for many of the issues it checks for.
This means Polaris will remediate the issue it finds, rather than rejecting
the deployment.
To enable the mutating webhook, add `--set webhook.mutate=true` to your
Helm installation command.
The following default checks currently have mutation support enabled:
* `hostPIDSet`
* `hostNetworkSet`
* `hostIPCSet`
* `priorityClassNotSet`
* `hostPortSet`
* `pullPolicyNotAlways`
* `deploymentMissingReplicas`
* `dangerousCapabilities`
* `cpuLimitsMissing`
* `memoryLimitsMissing`
* `livenessProbeMissing`
* `memoryRequestsMissing`
* `cpuRequestsMissing`
* `runAsPrivileged`
* `readinessProbeMissing`
* `privilegeEscalationAllowed`
* `notReadOnlyRootFilesystem`
* `insecureCapabilities`
* `runAsRootAllowed`
If you'd like to
enable other mutations, you can set the `webhook.mutations` flag.
+1 -112
View File
@@ -5,117 +5,6 @@ meta:
content: "Fairwinds Polaris | Changelog"
---
## 9.1.1
* Fix `hpaMinAvailability` failure message
* Fix `missingPodDisruptionBudget` typo
* Rewrite `hpaMaxAvailability` check to use go-template
## 9.1.0
* Add HPA `minAvailable` and HPA `maxAvailable` checks
* Fix typo for PDB `minAvailable`
## 9.0.1
* Fix comments handling in `addOrReplaceValue` function
## 9.0.0
* Expose issue fixer and mutations in the library
* Remove `packr` in favor of `go:embed`
## 8.5.6
* Fix trying to list cluster-level resources
## 8.5.5
* Fix missing PDB check
## 8.5.4
* Fix conditional expressions should be at very top of `additionalSchemaStrings`
* Update alpine to 3.19
## 8.5.3
* Add quiet flag to polaris audit CLI command to suppress 'upload to Insights' prompt
## 8.5.2
* Switch to `controller-utils` package to get workloads
## 8.5.1
* Update `topologySpreadConstraint` check
## 8.5.0
* Add helm-skip-tests flag
* Update CLI docs
* Handle multiple helm-values files
## 8.4.0
* Change kubernetes.io/ label from name to instance
## 8.3.0
* Add option to filter audit results by severity level
* Add insights prompt
## 8.2.4
* Fix nil pointer issue with webhook
## 8.2.3
* Add category for `metadataAndNameMismatched`.
* Fix category for `priorityClassNotSet`.
## 8.2.2
* Fix webhook server cert dir argument
## 8.2.1
* Fix on Insights integration
## 8.2.0
* Minor fixes for NSA checks
## 8.1.0
* Add `insights-host` global flag to configure Fairwinds Insights host (defaults to `https://insights.fairwinds.com`).
* Add new `auth` sub-commands be able to authenticate on Polaris using Fairwinds Insights credentials
- `login` - login using Fairwinds Insights credentials via the web interface or provide a token
- `logout` - logout from Fairwinds Insights
- `status` - show relevant information regarding login state
- `token` - prints the token from local storage
* Add new `audit` flags to be able to upload Workloads and Polaris results to Fairwinds Insights
- `upload-insights` - indicates that the results should be uploaded to Fairwinds Insights. (defaults to `false`)
- `cluster-name` - cluster name that the results belongs to. Creates the cluster if it does not exist. (required if `upload-insights` is used)
## 8.0.0
* Change default severity from `ignore` to `warning` for `priorityClassNotSet`, `metadataAndNameMismatched`, `missingPodDisruptionBudget`, `automountServiceAccountToken`, `missingNetworkPolicy` checks.
* Change default severity from `warning` to `danger` for `sensitiveContainerEnvVar`, `sensitiveConfigmapContent`, `clusterrolePodExecAttach`, `rolePodExecAttach`, `clusterrolebindingPodExecAttach`, `rolebindingClusterRolePodExecAttach`, `rolebindingRolePodExecAttach`,`clusterrolebindingClusterAdmin`,`rolebindingClusterAdminClusterRole`,`rolebindingClusterAdminRole` checks.
## 7.4.0
* Skip https certificate verification (#920)
## 7.3.0
* Add a check for `topologySpreadConstraint` (#879)
## 7.2.0
* Enable new RBAC / sensitive content / Pod exec checks, add `hasPrefix` and `hasSuffix` functions to the GO template, exempt `system:` name prefixes for RBAC checks, sensitive content checks ignore `valueFrom`, (#832)
## 7.1.0
* Let Polaris modify YAML without losing comments/formatting (#821)
* Add checks for RBAC allowing exec or attaching to a Pod (#820)
* Add `clusterrolebindingClusterAdmin`, `rolebindingClusterAdminRole`, and `rolebindingClusterAdminClusterRole` checks + schema tests (#823)
## 7.0.2
* Fixes for pretty CLI output
* Some new checks (disabled by default)
* Some additional features in templating engine
## 7.0.1
* Documentation updates
## 7.0.0
* Better support for `polaris fix`
* `target: Pod` is now `target: PodSpec` (to differentiate naked Pods from Controllers)
## 6.0.0
* Preliminary support for `polaris fix` command
* Changes to how Pod owners are determined
* Removed YAML manifests from the `deploy/` directory - Helm is now the default install mechanism
## 5.1.0
* Support `--context` flag for kubecontext
* Treat core Kubernetes controllers (`Deployments`, `StatefulSets`, `DaemonSets`, `CronJobs`) as top-level objects, instead of following owner refs
@@ -251,7 +140,7 @@ JSON schema (see changes to `./checks/multipleReplicasForDeployment.yaml`)
* Docker image now includes the default config
### Breaking Changes
* Breaking changes in both input and output formats. See [Examples](https://github.com/FairwindsOps/polaris/tree/master/pkg/config/examples) for examples of the new formats.
* Breaking changes in both input and output formats. See [Examples](https://github.com/FairwindsOps/polaris/tree/master/examples) for examples of the new formats.
* removed config-level configuration for checks like max/min memory settings
* changed severity `error` to `danger`
* Breaking changes to the CLI
+2 -39
View File
@@ -14,58 +14,22 @@ key | default | description
`livenessProbeMissing` | `warning` | Fails when a liveness probe is not configured for a pod.
`tagNotSpecified` | `danger` | Fails when an image tag is either not specified or `latest`.
`pullPolicyNotAlways` | `warning` | Fails when an image pull policy is not `always`.
`priorityClassNotSet` | `warning` | Fails when a priorityClassName is not set for a pod.
`priorityClassNotSet` | `ignore` | Fails when a priorityClassName is not set for a pod.
`deploymentMissingReplicas` | `warning` | Fails when there is only one replica for a deployment.
`missingPodDisruptionBudget` | `warning` | Fails when PDB is missing.
`metadataAndInstanceMismatched` | `warning` | Fails when label `app.kubernetes.io/instance` and `metadata.name` mismatch
`topologySpreadConstraint` | `warning` | Fails when there is no topology spread constraint on the pod
`hpaMaxAvailability` | `warning` | Fails when `maxAvailable` lesser or equal than `minAvailable` (if defined) for a HorizontalPodAutoscaler
`hpaMinAvailability` | `warning` | Fails when `minAvailable` (if defined) lesser or equal to one for a HorizontalPodAutoscaler
`pdbMinAvailableGreaterThanHPAMinReplicas` | `warning` | Fails when PDB `minAvailable` is greater than HPA `minReplicas`
`missingPodDisruptionBudget` | `ignore`
## Background
### Liveness and Readiness Probes
Readiness and liveness probes can help maintain the health of applications running inside Kubernetes. By default, Kubernetes only knows whether or not a process is running, not if it's healthy. Properly configured readiness and liveness probes will also be able to ensure the health of an application.
Readiness probes are designed to ensure that an application has reached a "ready" state. In many cases there is a period of time between when a webserver process starts and when it is ready to receive traffic. A readiness probe can ensure the traffic is not sent to a pod until it is actually ready to receive traffic.
Liveness probes are designed to ensure that an application stays in a healthy state. When a liveness probe fails, the pod will be restarted.
### Image Pull Policy
Docker's `latest` tag is applied by default to images where a tag hasn't been specified. Not specifying a specific version of an image can lead to a wide variety of problems. The underlying image could include unexpected breaking changes that break your application whenever the latest image is pulled. Reusing the same tag for multiple versions of an image can lead to different nodes in the same cluster having different versions of an image, even if the tag is identical.
Related to that, relying on cached versions of a Docker image can become a security vulnerability. By default, an image will be pulled if it isn't already cached on the node attempting to run it. This can result in variations in images that are running per node, or potentially provide a way to gain access to an image without having direct access to the ImagePullSecret. With that in mind, it's often better to ensure the a pod has `pullPolicy: Always` specified, so images are always pulled directly from their source.
### Topology Spread Constraints
By default, the Kubernetes scheduler uses a bin-packing algorithm to fit as many pods as possible into a cluster. The scheduler prefers a more evenly distributed general node load to app replicas precisely spread across nodes. Therefore, by default, multi-replica is not guaranteed to be spread across multiple availability zones. Kubernetes provides topologySpreadConstraint configuration in order to better ensure pod spread across multiple AZs and/or Hosts.
Example of a topologySpreadConstraint spreading across zones:
```
apiVersion: apps/v1
kind: Deployment
metadata:
name: demo-basic-demo
spec:
selector:
matchLabels:
app.kubernetes.io/name: basic-demo
app.kubernetes.io/instance: demo
template:
metadata:
labels:
app.kubernetes.io/name: basic-demo
app.kubernetes.io/instance: demo
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: "topology.kubernetes.io/zone"
whenUnsatisfiable: ScheduleAnyway
```
## Further Reading
- [What's Wrong With The Docker :latest Tag?](https://vsupalov.com/docker-latest-tag/)
@@ -73,4 +37,3 @@ spec:
- [Kubernetes Docs: Configure Liveness and Readiness Probes](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/)
- [Utilizing Kubernetes Liveness and Readiness Probes to Automatically Recover From Failure](https://medium.com/spire-labs/utilizing-kubernetes-liveness-and-readiness-probes-to-automatically-recover-from-failure-2fe0314f2b2e)
- [Kubernetes Liveness and Readiness Probes: How to Avoid Shooting Yourself in the Foot](https://blog.colinbreck.com/kubernetes-liveness-and-readiness-probes-how-to-avoid-shooting-yourself-in-the-foot/)
- [Topology Spread Constraints](https://kubernetes.io/docs/concepts/scheduling-eviction/topology-spread-constraints/)
+2 -15
View File
@@ -11,30 +11,17 @@ for privilege escalation.
key | default | description
----|---------|------------
`automountServiceAccountToken` | `warning` | Fails when `automountServiceAccountToken` is automounted.
`hostIPCSet` | `danger` | Fails when `hostIPC` attribute is configured.
`hostPIDSet` | `danger` | Fails when `hostPID` attribute is configured.
`linuxHardening` | `danger` | Fails when neither `AppArmor`, `Seccomp`, `SELinux`, or dropping Linux Capabilities is in use.
`notReadOnlyRootFilesystem` | `warning` | Fails when `securityContext.readOnlyRootFilesystem` is not true.
`privilegeEscalationAllowed` | `danger` | Fails when `securityContext.allowPrivilegeEscalation` is true.
`runAsRootAllowed` | `warning` | Fails when `securityContext.runAsNonRoot` is not true.
`runAsPrivileged` | `danger` | Fails when `securityContext.privileged` is true.
`insecureCapabilities` | `warning` | Fails when `securityContext.capabilities` includes one of the capabilities [listed here](https://github.com/FairwindsOps/polaris/tree/master/pkg/config/checks/insecureCapabilities.yaml)
`dangerousCapabilities` | `danger` | Fails when `securityContext.capabilities` includes one of the capabilities [listed here](https://github.com/FairwindsOps/polaris/tree/master/pkg/config/checks/dangerousCapabilities.yaml)
`insecureCapabilities` | `warning` | Fails when `securityContext.capabilities` includes one of the capabilities [listed here](https://github.com/FairwindsOps/polaris/tree/master/checks/insecureCapabilities.yaml)
`dangerousCapabilities` | `danger` | Fails when `securityContext.capabilities` includes one of the capabilities [listed here](https://github.com/FairwindsOps/polaris/tree/master/checks/dangerousCapabilities.yaml)
`hostNetworkSet` | `warning` | Fails when `hostNetwork` attribute is configured.
`hostPortSet` | `warning` | Fails when `hostPort` attribute is configured.
`tlsSettingsMissing` | `warning` | Fails when an Ingress lacks TLS settings.
`sensitiveContainerEnvVar` | `danger` | Fails when the container sets potentially sensitive environment variables.
`sensitiveConfigmapContent` | `danger` | Fails when potentially sensitive content is detected in the ConfigMap keys or values.
`missingNetworkPolicy` | `warning`
`clusterrolePodExecAttach` | `danger` | Fails when the ClusterRole allows Pods/exec or pods/attach.
`rolePodExecAttach` | `danger` | Fails when the Role allows Pods/exec or pods/attach.
`clusterrolebindingPodExecAttach` | `danger` | Fails when the ClusterRoleBinding references a ClusterRole that allows Pods/exec, allows pods/attach, or that does not exist.
`rolebindingRolePodExecAttach` | `danger` | Fails when the RoleBinding references a Role that allows Pods/exec, allows pods/attach, or that does not exist.
`rolebindingClusterRolePodExecAttach` | `danger` | Fails when the RoleBinding references a ClusterRole that allows Pods/exec, allows pods/attach, or that does not exist.
`clusterrolebindingClusterAdmin` | `danger` | Fails when the ClusterRoleBinding references the default cluster-admin ClusterRole or one with wildcard permissions.
`rolebindingClusterAdminClusterRole` | `danger` | Fails when the RoleBinding references the default cluster-admin ClusterRole or one with wildcard permissions.
`rolebindingClusterAdminRole` | `danger` | Fails when the RoleBinding references a Role with wildcard permissions.
## Background
+2 -31
View File
@@ -9,18 +9,14 @@ meta:
# top-level commands
audit
Runs a one-time audit.
auth
Authenticate polaris with Fairwinds Insights
dashboard
Runs the webserver for Polaris dashboard.
fix
Fix Infrastructure as code files.
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.
Runs the webhook webserver
# global flags
-c, --config string Location of Polaris configuration file.
@@ -29,7 +25,6 @@ webhook
--disallow-config-exemptions Disallow exemptions set within the configuration file.
--disallow-annotation-exemptions Disallow any exemption defined as a controller annotation.
--kubeconfig string Paths to a kubeconfig. Only required if out-of-cluster.
--insights-host string Fairwinds Insights host URL. (default "https://insights.fairwinds.com")
--log-level string Logrus log level. (default "info")
# dashboard flags
@@ -43,48 +38,24 @@ webhook
# audit flags
--audit-path string If specified, audits one or more YAML files instead of a cluster.
--checks strings Optional flag to specify specific checks to check
--cluster-name string Set --cluster-name to a descriptive name for the cluster you're auditing
--checks stringArray Optional flag to specify specific checks to check
--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
--helm-skip-tests bool Corresponds to --skip-tests of helm template
-h, --help help for audit
--namespace string Namespace to audit. Only applies to in-cluster audits
--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.
--quiet Suppress the 'upload to Insights' prompt.
--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.
--severity string Severity level used to filter results. Behaves like log levels. 'danger' is the least verbose (warning, danger)
--skip-ssl-validation Skip https certificate verification
--upload-insights Upload scan results to Fairwinds Insights
# fix flags
--checks strings Optional flag to specify specific checks to fix eg. checks=hostIPCSet,hostPIDSet and checks=all applies fix to all defined checks mutations
--files-path string mutate and fix one or more YAML files in a specified folder
-h, --help help for fix
--template set to true when modifyng a YAML template, like a Helm chart (experimental)
# 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)
# auth sub-commands
login Authenticate polaris with Fairwinds Insights.
logout Log out of a Fairwinds Insights.
status View authentication status.
token Print the auth token gh is configured to use.
```
#### Suppressing 'upload to Insights' output
When running the `polaris audit` subcommand, you can suppress the following output using the `--quiet flag`:
> 🚀 Upload your Polaris findings to Fairwinds Insights to see remediation advice, add teammates, integrate with Slack or Jira, and more:
+2 -1
View File
@@ -49,7 +49,8 @@ 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
helm install cert-manager jetstack/cert-manager --namespace cert-manager --version v1.12.1 --set "installCRDs=true" --wait
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
+1 -1
View File
@@ -5,7 +5,7 @@ meta:
---
# Configuration
The default Polaris configuration can be [seen here](https://github.com/FairwindsOps/polaris/blob/master/pkg/config/default.yaml).
The default Polaris configuration can be [seen here](https://github.com/FairwindsOps/polaris/blob/master/examples/config.yaml).
You can customize the configuration to do things like:
* Turn checks [on and off](checks.md)
+2 -20
View File
@@ -7,7 +7,7 @@ meta:
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/pkg/config/checks) for examples.
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!
@@ -44,10 +44,9 @@ check ID. Note that you'll also have to set its severity in the `checks` section
* `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 creates Pods (e.g. Deployments, CronJobs, StatefulSets), as well as naked Pods
* `PodTemplate`, same as `Controller`, but the schema applies to the Pod template rather than the top-level controller
* `PodSpec`, 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`, `PodSpec` or `Container`, you can use this to change which types of controllers are checked
* `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
@@ -135,11 +134,6 @@ schema:
const: "{{ .metadata.name }}"
```
* The object available via the go template is the full object, and not limited by `target`.
* A check of `target: PodSpec` can directly access the pod specification via the go template variable `.Polaris.PodSpec`.
* A check of `target: PodTemplate` can directly access the pod template via the go template variable `.Polaris.PodTemplate`.
* A check of `target: Container` can directly access the container being checked via the go template variable `.Polaris.container`. The pod template and pod specification can also be accessed via the respective variables `.Polaris.PodTemplate` and `.Polaris.PodSpec`. Access to pod-level fields allows a container check to consult related fields from the pod, such as `securityContext`.
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`:
@@ -167,18 +161,6 @@ schemaString: |
{{ end }}
```
### Additional Go Template Functions
These functions are also available in the GO template.
* [hasPrefix](https://pkg.go.dev/strings#HasPrefix) - for example, `hasPrefix "string" "prefix"`
* [hasSuffix](https://pkg.go.dev/strings#HasSuffix) - for example, `hasSuffix "string" "suffix"`
For example, the `hasPrefix` function can be used in a template to determine whether a resource name starts with `system:`
```
{{ if hasPrefix .metadata.name "system:" }}
```
## 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.
-26
View File
@@ -25,32 +25,6 @@ brew install FairwindsOps/tap/polaris
polaris version
```
## Checking Infrastructure as Code files
You can audit Kubernetes YAML files by running:
```bash
polaris audit --audit-path ./deploy/ --format=pretty
```
This will print out any issues Polaris finds in your manifests.
Polaris can only check raw YAML manifests. If you'd like to check a Helm template,
you can run `helm template` to generate a manifest that Polaris can check.
## Fixing Issues
Polaris can automatically fix many of the issues it finds. For example, you can run
```bash
polaris fix --files-path ./deploy/ --checks=all
```
to fix any issues inside the `deploy` directory. Polaris may leave
comments next to some changes (e.g. liveness and readiness probes) prompting
the user to set them to something more appropriate given the context of their
application.
Note that not all issues can be automatically fixed.
Currently only raw YAML manifests can be mutated. Helm charts etc.
still need to be changed manually.
## Running in a CI pipeline
### Set minimum score for an exit code
+14125 -6594
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -8,9 +8,9 @@
},
"description": "A repository with a Vuepress template for Fairwinds projects",
"devDependencies": {
"vuepress": "^1.9.7",
"vuepress": "^1.4.0",
"vuepress-plugin-clean-urls": "^1.1.1",
"vuepress-plugin-redirect": "^1.2.5"
"vuepress-plugin-redirect": "^1.2.3"
},
"directories": {
"doc": "docs"
@@ -6,26 +6,14 @@ checks:
pullPolicyNotAlways: warning
readinessProbeMissing: warning
livenessProbeMissing: warning
topologySpreadConstraint: warning
pdbDisruptionsIsZero: warning
missingPodDisruptionBudget: warning
metadataAndInstanceMismatched: warning
hpaMaxAvailability: warning
hpaMinAvailability: warning
pdbMinAvailableGreaterThanHPAMinReplicas: warning
# efficiency
cpuRequestsMissing: warning
cpuLimitsMissing: warning
memoryRequestsMissing: warning
memoryLimitsMissing: warning
# security
automountServiceAccountToken: warning
hostIPCSet: danger
hostPIDSet: danger
linuxHardening: danger
missingNetworkPolicy: warning
notReadOnlyRootFilesystem: warning
privilegeEscalationAllowed: danger
runAsRootAllowed: danger
@@ -34,21 +22,11 @@ checks:
insecureCapabilities: warning
hostNetworkSet: danger
hostPortSet: warning
tlsSettingsMissing: warning
sensitiveContainerEnvVar: danger
sensitiveConfigmapContent: danger
clusterrolePodExecAttach: danger
rolePodExecAttach: danger
clusterrolebindingPodExecAttach: danger
rolebindingClusterRolePodExecAttach: danger
rolebindingRolePodExecAttach: danger
clusterrolebindingClusterAdmin: danger
rolebindingClusterAdminClusterRole: danger
rolebindingClusterAdminRole: danger
# custom
resourceLimits: warning
imageRegistry: danger
exemptions:
- controllerNames:
- my-network-controller
@@ -1,31 +1,23 @@
checks:
# reliability
deploymentMissingReplicas: warning
priorityClassNotSet: warning
priorityClassNotSet: ignore
tagNotSpecified: danger
pullPolicyNotAlways: warning
readinessProbeMissing: warning
livenessProbeMissing: warning
metadataAndInstanceMismatched: warning
metadataAndNameMismatched: ignore
pdbDisruptionsIsZero: warning
missingPodDisruptionBudget: warning
topologySpreadConstraint: warning
hpaMaxAvailability: warning
hpaMinAvailability: warning
pdbMinAvailableGreaterThanHPAMinReplicas: warning
missingPodDisruptionBudget: ignore
# efficiency
cpuRequestsMissing: warning
cpuLimitsMissing: warning
memoryRequestsMissing: warning
memoryLimitsMissing: warning
# security
automountServiceAccountToken: warning
hostIPCSet: danger
hostPIDSet: danger
linuxHardening: warning
missingNetworkPolicy: warning
notReadOnlyRootFilesystem: warning
privilegeEscalationAllowed: danger
runAsRootAllowed: danger
@@ -35,61 +27,8 @@ checks:
hostNetworkSet: danger
hostPortSet: warning
tlsSettingsMissing: warning
sensitiveContainerEnvVar: danger
sensitiveConfigmapContent: danger
clusterrolePodExecAttach: danger
rolePodExecAttach: danger
clusterrolebindingPodExecAttach: danger
rolebindingClusterRolePodExecAttach: danger
rolebindingRolePodExecAttach: danger
clusterrolebindingClusterAdmin: danger
rolebindingClusterAdminClusterRole: danger
rolebindingClusterAdminRole: danger
mutations:
- pullPolicyNotAlways
exemptions:
- namespace: kube-system
controllerNames:
- dns-controller
- ebs-csi-controller
- ebs-csi-node
- kindnet
- kops-controller
- kube-dns
- kube-flannel-ds
- kube-proxy
- kube-scheduler
- vpa-recommender
rules:
- automountServiceAccountToken
- linuxHardening
- missingNetworkPolicy
- namespace: kube-system
controllerNames:
- coredns
rules:
- automountServiceAccountToken
- missingNetworkPolicy
- namespace: kube-system
controllerNames:
- ebs-csi-controller
rules:
- sensitiveContainerEnvVar
- namespace: kube-system
controllerNames:
- coredns-autoscaler
rules:
- linuxHardening
- namespace: local-path-storage
controllerNames:
- local-path-provisioner
rules:
- automountServiceAccountToken
- linuxHardening
- missingNetworkPolicy
- namespace: kube-system
controllerNames:
- kube-apiserver
@@ -112,48 +51,7 @@ exemptions:
- runAsPrivileged
- notReadOnlyRootFilesystem
- hostPIDSet
- namespace: datadog
controllerNames:
- datadogtoken
rules:
- sensitiveConfigmapContent
- namespace: datadog
controllerNames:
- datadog-cluster-agent-apiserver
rules:
- rolebindingClusterAdminRole
- rolebindingRolePodExecAttach
- controllerNames:
- ingress-nginx-controller
rules:
- sensitiveConfigmapContent
- controllerNames:
- ingress-nginx-controller
- ingress-nginx-default-backend
- polaris
- rbac-manager
rules:
- automountServiceAccountToken
- missingNetworkPolicy
- controllerNames:
- aws-iam-authenticator
- aws-load-balancer-controller
- docker-registry
- external-dns
- kube2iam
- metrics-server
rules:
- automountServiceAccountToken
- linuxHardening
- missingNetworkPolicy
- controllerNames:
- oauth2-proxy
rules:
- automountServiceAccountToken
- linuxHardening
- missingNetworkPolicy
- sensitiveContainerEnvVar
- controllerNames:
- kube-flannel-ds
rules:
@@ -171,9 +69,6 @@ exemptions:
- runAsRootAllowed
- readinessProbeMissing
- livenessProbeMissing
- automountServiceAccountToken
- linuxHardening
- missingNetworkPolicy
- controllerNames:
- cluster-autoscaler
@@ -181,9 +76,6 @@ exemptions:
- notReadOnlyRootFilesystem
- runAsRootAllowed
- readinessProbeMissing
- automountServiceAccountToken
- linuxHardening
- missingNetworkPolicy
- controllerNames:
- vpa
@@ -200,10 +92,6 @@ exemptions:
- readinessProbeMissing
- livenessProbeMissing
- notReadOnlyRootFilesystem
- automountServiceAccountToken
- linuxHardening
- missingNetworkPolicy
- sensitiveContainerEnvVar
- controllerNames:
- nginx-ingress-controller
+1 -1
View File
@@ -1,5 +1,5 @@
options:
organization: fairwinds-production
organization: fairwinds-opensource
baseBranch: master
images:
+65 -55
View File
@@ -1,83 +1,93 @@
module github.com/fairwindsops/polaris
go 1.21
go 1.17
require (
github.com/AlecAivazis/survey/v2 v2.3.7
github.com/fairwindsops/controller-utils v0.3.4
github.com/fairwindsops/insights-plugins/plugins/workloads v0.0.0-20230601204422-5c789e15990c
github.com/fatih/color v1.15.0
github.com/evanphx/json-patch/v5 v5.6.0
github.com/fatih/color v1.13.0
github.com/gobuffalo/packr/v2 v2.8.3
github.com/gorilla/mux v1.8.0
github.com/pkg/errors v0.9.1
github.com/qri-io/jsonschema v0.1.2
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.8.0
github.com/stretchr/testify v1.8.4
github.com/thoas/go-funk v0.9.3
gomodules.xyz/jsonpatch/v2 v2.3.0
github.com/qri-io/jsonschema v0.1.1
github.com/sirupsen/logrus v1.8.1
github.com/spf13/cobra v1.4.0
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.7.1
github.com/thoas/go-funk v0.9.2
golang.org/x/text v0.3.7 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1
k8s.io/api v0.29.0
k8s.io/apimachinery v0.29.0
k8s.io/client-go v0.29.0
sigs.k8s.io/controller-runtime v0.15.0
k8s.io/api v0.24.1
k8s.io/apimachinery v0.24.1
k8s.io/client-go v0.24.1
sigs.k8s.io/controller-runtime v0.12.1
sigs.k8s.io/yaml v1.3.0
)
require gomodules.xyz/jsonpatch/v2 v2.2.0
require (
cloud.google.com/go/compute v1.6.1 // indirect
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
github.com/Azure/go-autorest/autorest v0.11.27 // indirect
github.com/Azure/go-autorest/autorest/adal v0.9.20 // indirect
github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
github.com/Azure/go-autorest/logger v0.2.1 // indirect
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/emicklei/go-restful/v3 v3.8.0 // indirect
github.com/evanphx/json-patch v5.6.0+incompatible // indirect
github.com/evanphx/json-patch/v5 v5.6.0 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/go-logr/logr v1.3.0 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.22.3 // indirect
github.com/fsnotify/fsnotify v1.5.4 // indirect
github.com/go-logr/logr v1.2.3 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.20.0 // indirect
github.com/go-openapi/swag v0.21.1 // indirect
github.com/gobuffalo/logger v1.0.6 // indirect
github.com/gobuffalo/packd v1.0.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v4 v4.4.1 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/gnostic-models v0.6.8 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/gnostic v0.6.9 // indirect
github.com/google/go-cmp v0.5.8 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/imdario/mergo v0.3.13 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/karrick/godirwalk v1.17.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
github.com/markbates/errx v1.1.0 // indirect
github.com/markbates/oncer v1.0.0 // indirect
github.com/markbates/safe v1.0.1 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.15.1 // indirect
github.com/prometheus/client_model v0.4.0 // indirect
github.com/prometheus/common v0.42.0 // indirect
github.com/prometheus/procfs v0.9.0 // indirect
github.com/prometheus/client_golang v1.12.2 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.34.0 // indirect
github.com/prometheus/procfs v0.7.3 // indirect
github.com/qri-io/jsonpointer v0.1.1 // indirect
github.com/samber/lo v1.46.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/oauth2 v0.13.0 // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/term v0.13.0 // indirect
golang.org/x/text v0.16.0 // indirect
golang.org/x/time v0.3.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/protobuf v1.31.0 // indirect
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e // indirect
golang.org/x/net v0.0.0-20220607020251-c690dde0001d // indirect
golang.org/x/oauth2 v0.0.0-20220524215830-622c5d57e401 // indirect
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 // indirect
golang.org/x/time v0.0.0-20220411224347-583f2d630306 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.28.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
k8s.io/component-base v0.27.2 // indirect
k8s.io/klog/v2 v2.110.1 // indirect
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect
k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
k8s.io/component-base v0.24.1 // indirect
k8s.io/klog/v2 v2.60.1 // indirect
k8s.io/kube-openapi v0.0.0-20220603121420-31174f50af60 // indirect
k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 // indirect
sigs.k8s.io/json v0.0.0-20220525155127-227cbc7cc124 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect
)
+1063 -152
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -18,7 +18,7 @@ import (
cmd "github.com/fairwindsops/polaris/cmd/polaris"
)
var (
const (
// Version is set during build
Version = "development"
// Commit is set during build
-53
View File
@@ -1,53 +0,0 @@
package auth
import (
"errors"
"io"
"os"
"github.com/sirupsen/logrus"
"gopkg.in/yaml.v3"
)
var userHomeDir string
var polarisHostsFilepath string
var ErrNotLoggedIn = errors.New("not logged in")
func init() {
var err error
userHomeDir, err = os.UserHomeDir()
if err != nil {
logrus.Fatalf("reading user home dir: %v", err)
}
polarisHostsFilepath = userHomeDir + "/.config/polaris/hosts.yaml"
}
func readPolarisHostsFile() (map[string]Host, error) {
f, err := os.Open(polarisHostsFilepath)
if err != nil {
return nil, err
}
b, err := io.ReadAll(f)
if err != nil {
return nil, err
}
content := map[string]Host{}
err = yaml.Unmarshal(b, &content)
return content, err
}
func GetAuth(insightsHost string) (*Host, error) {
hosts, err := readPolarisHostsFile()
if err != nil {
return nil, err
}
if len(hosts) == 0 {
return nil, ErrNotLoggedIn
}
if h, ok := hosts[insightsHost]; ok {
return &h, nil
}
return nil, ErrNotLoggedIn
}
-45
View File
@@ -1,45 +0,0 @@
package auth
import (
"errors"
"fmt"
"os"
"os/exec"
"runtime"
"strings"
"github.com/sirupsen/logrus"
)
// openBrowser opens up the provided URL in a browser
func openBrowser(url string) error {
var cmd *exec.Cmd
switch runtime.GOOS {
case "openbsd":
fallthrough
case "linux":
cmd = exec.Command("xdg-open", url)
case "darwin":
cmd = exec.Command("open", url)
case "windows":
r := strings.NewReplacer("&", "^&")
cmd = exec.Command("cmd", "/c", "start", r.Replace(url))
}
if cmd != nil {
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Start()
if err != nil {
logrus.Printf("Failed to open browser due to error %v", err)
return fmt.Errorf("Failed to open browser: " + err.Error())
}
err = cmd.Wait()
if err != nil {
logrus.Printf("Failed to wait for open browser command to finish due to error %v", err)
return fmt.Errorf("Failed to wait for open browser command to finish: " + err.Error())
}
return nil
} else {
return errors.New("unsupported platform")
}
}
-291
View File
@@ -1,291 +0,0 @@
package auth
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"net"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/AlecAivazis/survey/v2"
"github.com/fairwindsops/polaris/pkg/insights"
"github.com/gorilla/mux"
"github.com/sirupsen/logrus"
"gopkg.in/yaml.v3"
)
const loginPath = "/auth/login"
const registerPath = "/auth/register"
const (
loginUsingBrowser = "Login with a web browser"
pasteAnAuthenticationToken = "Paste an authentication token"
)
type paramsOrError struct {
token string
user string
organization string
err error
}
type Host struct {
Token string `yaml:"token"`
User string `yaml:"user"`
Organization string `yaml:"organization"`
}
var paramsOrErrorChan = make(chan paramsOrError)
func HandleLogin(insightsHost string) error {
if _, err := os.Stat(polarisHostsFilepath); err == nil {
content, err := readPolarisHostsFile()
if err != nil {
return fmt.Errorf("reading polaris hosts file: %w", err)
}
if len(content) > 0 {
if h, ok := content[insightsHost]; ok {
c := insights.NewHTTPClient(insightsHost, h.Organization, h.Token)
isValid, err := c.IsTokenValid()
if err != nil {
return err
}
if isValid {
var reAuthenticate bool
err = survey.AskOne(&survey.Confirm{Message: fmt.Sprintf("You're already logged into %s. Do you want to re-authenticate?", insightsHost)}, &reAuthenticate)
if err != nil {
return fmt.Errorf("prompting re-authenticate: %w", err)
}
if !reAuthenticate {
// bail-out
return nil
}
}
}
}
}
selection := &survey.Select{
Message: "How would you like to authenticate Polaris?",
Options: []string{loginUsingBrowser, pasteAnAuthenticationToken},
Default: loginUsingBrowser,
}
var answer string
err := survey.AskOne(selection, &answer)
if err != nil {
return fmt.Errorf("asking how to authenticate: %w", err)
}
var user, token, organization string
if answer == loginUsingBrowser {
listener, err := net.Listen("tcp", "localhost:0")
if err != nil {
panic(err)
}
localServerPort := listener.Addr().(*net.TCPAddr).Port
url := fmt.Sprintf(insightsHost + registerPath + "?source=polaris&callbackUrl=" + fmt.Sprintf("http://localhost:%d/auth/login/callback", localServerPort))
err = openBrowser(url)
if err != nil {
logrus.Warnf("could not open browser: %v", err)
logrus.Infoln("paste the link below into your browser:")
os.Stdout.Write([]byte(url + "\n"))
}
var router *mux.Router
go func() {
router = mux.NewRouter()
router.HandleFunc("/auth/login/callback", callbackHandler(insightsHost, localServerPort))
if err := http.Serve(listener, router); err != nil {
paramsOrErrorChan <- paramsOrError{err: fmt.Errorf("starting the local http server: %w", err)}
}
}()
// wait the browser to callback the local server
paramOrError := <-paramsOrErrorChan
if paramOrError.err != nil {
return paramOrError.err
}
user = paramOrError.user
organization = paramOrError.organization
token = paramOrError.token
} else {
var answer string
var bot bot
err := survey.AskOne(&survey.Password{Message: "Paste your authentication token:"}, &answer, survey.WithValidator(validateToken(insightsHost, &bot)))
if err != nil {
return fmt.Errorf("asking how to authenticate: %w", err)
}
token = answer
user = bot.Name
organization = bot.Organization
}
polarisCfgDir := filepath.Join(userHomeDir, ".config", "polaris")
err = os.MkdirAll(polarisCfgDir, os.ModePerm)
if err != nil {
return fmt.Errorf("creating polaris config dir: %w", err)
}
content := map[string]Host{insightsHost: {Token: token, User: user, Organization: organization}}
b, err := yaml.Marshal(content)
if err != nil {
return fmt.Errorf("marshalling yaml data: %w", err)
}
err = os.WriteFile(polarisHostsFilepath, b, os.ModePerm)
if err != nil {
return fmt.Errorf("writing data to file: %w", err)
}
logrus.Debugf("hosts file has been saved")
fmt.Println("✓ Authentication complete.")
fmt.Printf("✓ Logged in organization %s as %s.\n", organization, user)
return nil
}
func fetchAuthToken(insightsHost, organization, code string) (string, error) {
authTokenURL := fmt.Sprintf("%s/v0/organizations/%s/auth/token", insightsHost, organization)
body := map[string]any{"grantType": "authorization_code", "code": code}
b, err := json.Marshal(body)
if err != nil {
return "", err
}
r, err := http.NewRequest("POST", authTokenURL, bytes.NewBuffer(b))
if err != nil {
return "", err
}
r.Header.Add("Content-Type", "application/json")
res, err := http.DefaultClient.Do(r)
if err != nil {
return "", err
}
defer res.Body.Close()
if res.StatusCode < 200 || res.StatusCode >= 400 {
return "", fmt.Errorf("expected 200 OK - received %s", res.Status)
}
var rBody map[string]any
err = json.NewDecoder(res.Body).Decode(&rBody)
if err != nil {
return "", err
}
token, ok := rBody["accessToken"].(string)
if !ok {
return "", fmt.Errorf("unable to parse accessToken from response body: %v", rBody)
}
return token, nil
}
func callbackHandler(insightsHost string, localServerPort int) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
// checks for error in the params
errMsg := r.URL.Query().Get("error")
if len(errMsg) > 0 {
errDescriptionMsg := r.URL.Query().Get("error_description")
fmt.Fprintf(w, "unable to perform integration: %s - %s", errMsg, errDescriptionMsg)
paramsOrErrorChan <- paramsOrError{err: fmt.Errorf("%s - %s", errMsg, errDescriptionMsg)}
return
}
var err error
code := r.URL.Query().Get("code")
if len(code) == 0 {
err = errors.New("code query param is required in callback")
}
user := r.URL.Query().Get("user")
if len(user) == 0 {
err = errors.New("user query param is required in callback")
}
organization := r.URL.Query().Get("organization")
if len(organization) == 0 {
err = errors.New("organization query param is required in callback")
}
token, err := fetchAuthToken(insightsHost, organization, code)
if err != nil {
err = fmt.Errorf("fetching auth token: %w", err)
}
if err != nil {
fmt.Fprintf(w, "unable to perform integration: %v", err)
paramsOrErrorChan <- paramsOrError{err: err}
return
}
fmt.Fprint(w, "Polaris and Fairwinds Insights integration has finished successfully, your credentials are set! You can safely close this tab now.")
paramsOrErrorChan <- paramsOrError{token: token, user: user, organization: organization}
return
}
}
func validateToken(insightsHost string, bot *bot) func(args any) error {
return func(args any) error {
token, ok := args.(string)
if !ok {
return errors.New("casting token to string")
}
if len(strings.TrimSpace(token)) <= 0 {
return errors.New("token is required")
}
return fetchOrganizationBot(insightsHost, token, bot)
}
}
type bot struct {
ID int
Organization string
Name string
Role string
AuthToken string
CreatedAt time.Time
}
func fetchOrganizationBot(insightsHost, authToken string, bot *bot) error {
authTokenURL := fmt.Sprintf("%s/v0/bots/from-request", insightsHost)
r, err := http.NewRequest("GET", authTokenURL, nil)
if err != nil {
return err
}
r.Header.Add("Content-Type", "application/json")
r.Header.Add("Authorization", "Bearer "+authToken)
res, err := http.DefaultClient.Do(r)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode < 200 || res.StatusCode >= 400 {
return fmt.Errorf("expected 200 (OK) - received %d", res.StatusCode)
}
err = json.NewDecoder(res.Body).Decode(bot)
if err != nil {
return err
}
return nil
}
func IsLoggedIn() bool {
if _, err := os.Stat(polarisHostsFilepath); err == nil {
content, err := readPolarisHostsFile()
if err != nil {
return false
}
return len(content) > 0
}
return false
}
-35
View File
@@ -1,35 +0,0 @@
package auth
import (
"fmt"
"os"
)
func HandleLogout() error {
if !IsLoggedIn() {
fmt.Println("not logged in to Fairwinds Insights")
return nil
}
err := performLogout()
if err != nil {
return fmt.Errorf("performing logout: %v", err)
}
fmt.Println("✓ Logged out of Fairwinds Insights")
return nil
}
func performLogout() error {
content, err := readPolarisHostsFile()
if err != nil {
return nil
}
if len(content) > 0 {
err = os.WriteFile(polarisHostsFilepath, []byte("{}"), os.ModePerm)
if err != nil {
return fmt.Errorf("writing data to file: %w", err)
}
return nil
}
return nil
}
-46
View File
@@ -1,46 +0,0 @@
package auth
import (
"fmt"
"strings"
"github.com/fairwindsops/polaris/pkg/insights"
)
func PrintStatus(insightsHost string) error {
if content, err := readPolarisHostsFile(); err == nil {
if len(content) > 0 {
if h, ok := content[insightsHost]; ok {
c := insights.NewHTTPClient(insightsHost, h.Organization, h.Token)
isValid, err := c.IsTokenValid()
if err != nil {
return err
}
if !isValid {
fmt.Println("✕ Your token is no longer valid. Run polaris auth login to authenticate.")
return nil
}
fmt.Printf("✓ Logged in to %s as %s\n", insightsHost, h.User)
fmt.Printf("✓ Token: %s\n", hideToken(h.Token, 3))
return nil
}
}
fmt.Printf("✕ No authentication found for host %s. Run polaris auth login to authenticate.\n", insightsHost)
return nil
}
fmt.Println("You are not logged into Fairwinds Insights. Run polaris auth login to authenticate.")
return nil
}
func hideToken(token string, hideAfter int) string {
var i int
return strings.Map(func(r rune) rune {
defer func() {
i++
}()
if i > hideAfter {
return []rune("*")[0]
}
return r
}, token)
}
-20
View File
@@ -1,20 +0,0 @@
package auth
import "fmt"
func PrintToken() error {
if content, err := readPolarisHostsFile(); err == nil {
if len(content) > 0 {
for k, h := range content {
if len(content) == 1 {
fmt.Println(h.Token)
} else {
fmt.Printf("%s: %s\n", k, h.Token)
}
}
return nil
}
}
fmt.Println("no oauth token")
return nil
}
+7 -28
View File
@@ -15,13 +15,14 @@
package config
import (
"embed"
"fmt"
"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{
@@ -31,8 +32,6 @@ var (
"hostIPCSet",
"hostPIDSet",
"hostNetworkSet",
"automountServiceAccountToken",
"topologySpreadConstraint",
// Container checks
"memoryLimitsMissing",
"memoryRequestsMissing",
@@ -50,38 +49,18 @@ var (
"dangerousCapabilities",
"insecureCapabilities",
"priorityClassNotSet",
"linuxHardening",
"sensitiveContainerEnvVar",
// Other checks
"tlsSettingsMissing",
"pdbDisruptionsIsZero",
"metadataAndInstanceMismatched",
"metadataAndNameMismatched",
"missingPodDisruptionBudget",
"missingNetworkPolicy",
"sensitiveConfigmapContent",
"clusterrolePodExecAttach",
"rolePodExecAttach",
"clusterrolebindingPodExecAttach",
"rolebindingClusterRolePodExecAttach",
"rolebindingRolePodExecAttach",
"clusterrolebindingClusterAdmin",
"rolebindingClusterAdminClusterRole",
"rolebindingClusterAdminRole",
"hpaMaxAvailability",
"hpaMinAvailability",
"pdbMinAvailableGreaterThanHPAMinReplicas",
}
// BuiltInChecks contains the checks that come pre-installed w/ Polaris
BuiltInChecks = map[string]SchemaCheck{}
//go:embed all:checks
checksFS embed.FS
)
func init() {
schemaBox = packr.New("Schemas", "../../checks")
for _, checkID := range checkOrder {
contents, err := checksFS.ReadFile(fmt.Sprintf("checks/%s.yaml", checkID))
contents, err := schemaBox.Find(checkID + ".yaml")
if err != nil {
panic(err)
}
@@ -1,35 +0,0 @@
successMessage: The ServiceAccount will not be automounted
failureMessage: The ServiceAccount will be automounted
category: Security
target: PodSpec
schema:
'$schema': http://json-schema.org/draft-07/schema
type: object
properties:
serviceAccountName:
type: string
automountServiceAccountToken:
type: boolean
not:
const: true
additionalSchemaStrings:
ServiceAccount: |
{{ if not (eq .Polaris.PodSpec.automountServiceAccountToken false) }}
type: object
required:
- metadata
- automountServiceAccountToken
properties:
metadata:
type: object
required: ["name"]
properties:
name:
type: string
const: "{{ .Polaris.PodSpec.serviceAccountName }}"
{{ if not (eq .Polaris.PodSpec.automountServiceAccountToken false) }}
automountServiceAccountToken:
type: boolean
const: false
{{ end }}
{{ end }}
@@ -1,54 +0,0 @@
successMessage: The ClusterRole does not allow pods/exec or pods/attach
failureMessage: The ClusterRole allows Pods/exec or pods/attach
category: Security
target: rbac.authorization.k8s.io/ClusterRole
schemaString: |
'$schema': http://json-schema.org/draft-07/schema
type: object
required: ["metadata", "rules"]
anyOf:
# Do not alert on default ClusterRoles.
- properties:
metadata:
required: ["name"]
properties:
name:
type: string
anyOf:
- const: 'admin'
- const: "cluster-admin"
- const: "edit"
- pattern: '^system:'
- const: "gce:podsecuritypolicy:calico-sa"
- properties:
rules:
type: array
items:
type: object
not:
required: ["apiGroups", "resources", "verbs"]
properties:
apiGroups:
type: array
contains:
type: string
anyOf:
- const: ""
- const: '*'
resources:
type: array
contains:
type: string
anyOf:
- const: '*'
- const: "pods/exec"
- const: "pods/attach"
verbs:
type: array
contains:
type: string
anyOf:
- const: '*'
# An exec is also possible by `get`ing a web socket.
- const: 'get'
- const: 'create'
@@ -1,90 +0,0 @@
successMessage: The ClusterRoleBinding does not reference the default cluster-admin ClusterRole or one with wildcard permissions
failureMessage: The ClusterRoleBinding references the default cluster-admin ClusterRole or one with wildcard permissions
category: Security
target: rbac.authorization.k8s.io/ClusterRoleBinding
schemaString: |
'$schema': http://json-schema.org/draft-07/schema
type: object
anyOf:
# Do not alert on default ClusterRoleBindings.
- required: ["metadata"]
properties:
metadata:
type: object
required: ["name"]
properties:
name:
type: string
anyOf:
- const: "cluster-admin"
- pattern: '^system:'
- const: "gce:podsecuritypolicy:calico-sa"
- required: ["roleRef"]
properties:
roleRef:
required: ["apiGroup", "kind", "name"]
properties:
apiGroup:
type: string
const: "rbac.authorization.k8s.io"
kind:
type: string
const: "ClusterRole"
name:
type: string
minLength: 1
not:
const: "cluster-admin"
additionalSchemaStrings:
rbac.authorization.k8s.io/ClusterRole: |
{{ if (ne .roleRef.name "view") }}
{{ if and (ne .metadata.name "cluster-admin") (not (hasPrefix .metadata.name "system:")) (ne .metadata.name "gce:podsecuritypolicy:calico-sa") }}
# Do not alert on default ClusterRoleBindings.
type: object
required: ["metadata", "rules"]
allOf:
- properties:
metadata:
required: ["name"]
properties:
name:
type: string
const: "{{ .roleRef.name }}"
- properties:
rules:
type: array
items:
type: object
not:
required: ["apiGroups", "resources", "verbs"]
properties:
apiGroups:
type: array
contains:
type: string
const: "*"
resources:
type: array
contains:
type: string
const: '*'
verbs:
type: array
uniqueItems: true
oneOf:
- contains:
type: string
const: '*'
- minItems: 7
items:
type: string
enum:
- "get"
- "list"
- "watch"
- "create"
- "update"
- "patch"
- "delete"
{{ end }}
{{ end }}
@@ -1,84 +0,0 @@
successMessage: The ClusterRoleBinding does not reference a ClusterRole allowing pods/exec or pods/attach
failureMessage: The ClusterRoleBinding references a ClusterRole that allows Pods/exec, allows pods/attach, or that does not exist
category: Security
target: rbac.authorization.k8s.io/ClusterRoleBinding
schemaString: |
'$schema': http://json-schema.org/draft-07/schema
type: object
anyOf:
# Do not alert on default ClusterRoleBindings.
- required: ["metadata"]
properties:
metadata:
type: object
required: ["name"]
properties:
name:
type: string
anyOf:
- const: "cluster-admin"
- pattern: '^system:'
- const: "gce:podsecuritypolicy:calico-sa"
- required: ["roleRef"]
properties:
roleRef:
required: ["apiGroup", "kind", "name"]
properties:
apiGroup:
type: string
const: "rbac.authorization.k8s.io"
kind:
type: string
const: "ClusterRole"
name:
type: string
minLength: 1
additionalSchemaStrings:
rbac.authorization.k8s.io/ClusterRole: |
type: object
# Do not alert on default ClusterRoleBindings.
{{ if (ne .roleRef.name "view") }}
{{ if and (ne .metadata.name "cluster-admin") (not (hasPrefix .metadata.name "system:")) (ne .metadata.name "gce:podsecuritypolicy:calico-sa") }}
required: ["metadata", "rules"]
allOf:
- properties:
metadata:
required: ["name"]
properties:
name:
type: string
const: "{{ .roleRef.name }}"
- properties:
rules:
type: array
items:
type: object
not:
required: ["apiGroups", "resources", "verbs"]
properties:
apiGroups:
type: array
contains:
type: string
anyOf:
- const: ""
- const: '*'
resources:
type: array
contains:
type: string
anyOf:
- const: '*'
- const: "pods/exec"
- const: "pods/attach"
verbs:
type: array
contains:
type: string
anyOf:
- const: '*'
# An exec is also possible by `get`ing a web socket.
- const: 'get'
- const: 'create'
{{ end }}
{{ end }}
-35
View File
@@ -1,35 +0,0 @@
successMessage: HPA has a valid max and min replica configuration
failureMessage: HPA maxReplicas and minReplicas should be different
category: Reliability
target: autoscaling/HorizontalPodAutoscaler
schemaString: |
"$schema": http://json-schema.org/draft-07/schema#
type: object
properties:
spec:
type: object
properties:
minReplicas:
type: integer
minimum: 1
maxReplicas:
type: integer
minimum: 1
required:
- maxReplicas
{{- if .spec.minReplicas }}
if:
properties:
minReplicas:
type: integer
maxReplicas:
type: integer
then:
properties:
maxReplicas:
exclusiveMinimum: {{ .spec.minReplicas }}
else:
properties:
maxReplicas:
minimum: 1
{{- end }}
-14
View File
@@ -1,14 +0,0 @@
successMessage: HPA has a valid min replica configuration
failureMessage: HPA minReplicas should be 2 or more
category: Reliability
target: autoscaling/HorizontalPodAutoscaler
schema:
"$schema": http://json-schema.org/draft-07/schema#
type: object
properties:
spec:
type: object
properties:
minReplicas:
type: integer
minimum: 2
-89
View File
@@ -1,89 +0,0 @@
successMessage: One of AppArmor, Seccomp, SELinux, or dropping Linux Capabilities are used to restrict containers using unwanted privileges
FailureMessage: Use one of AppArmor, Seccomp, SELinux, or dropping Linux Capabilities to restrict containers using unwanted privileges
category: Security
target: Container
schemaString: |
'$schema': http://json-schema.org/draft-07/schema
definitions:
podOrContainerSeccompProfile:
type: object
{{ $podSeccompProfileType := .Polaris.PodSpec.securityContext.seccompProfile.type }}
required:
{{ if or (not $podSeccompProfileType) (eq $podSeccompProfileType "Unconfined") }}
- securityContext
{{ end }}
properties:
securityContext:
type: object
required:
{{ if or (not $podSeccompProfileType) (eq $podSeccompProfileType "Unconfined") }}
- seccompProfile
{{ end }}
properties:
seccompProfile:
type: object
required:
{{ if or (not $podSeccompProfileType) (eq $podSeccompProfileType "Unconfined") }}
- type
{{ end }}
properties:
type:
type: string
allOf:
- not:
const: "Unconfined"
{{ if or (not $podSeccompProfileType) (eq $podSeccompProfileType "Unconfined") }}
- minLength: 1
{{ end }}
podOrContainerSELinuxOptions:
type: object
{{ $podSELinuxOptions := .Polaris.PodSpec.securityContext.seLinuxOptions }}
{{ if not $podSELinuxOptions }}
required: ["securityContext"]
properties:
securityContext:
type: object
required: ["seLinuxOptions"]
properties:
seLinuxOptions:
type: object
minProperties: 1
{{ end }}
containerDropCapabilities:
type: object
required: ["securityContext"]
properties:
securityContext:
type: object
required: ["capabilities"]
properties:
capabilities:
type: object
required: ["drop"]
properties:
drop:
type: array
minItems: 1
add:
type: array
items:
type: string
not:
pattern: '^(?i)ALL$'
# End of definitions
{{/* Check for AppArmor which uses pod annotations. IF pod fields are missing,
require one of the other hardening measures. */}}
{{ $annotationName := (print "container.apparmor.security.beta.kubernetes.io/" .Polaris.Container.name) }}
{{/* Checking annotations before using index() avoids a nil panic when there are no annotations */}}
{{ $annotationExists := false }}
{{ if .Polaris.PodTemplate.metadata.annotations }}
{{ $annotationExists = index .Polaris "PodTemplate" "metadata" "annotations" $annotationName }}
{{ end }}
{{ if $annotationExists }}
type: object
{{ else }}
anyOf:
- $ref: "#/definitions/podOrContainerSeccompProfile"
- $ref: "#/definitions/podOrContainerSELinuxOptions"
- $ref: "#/definitions/containerDropCapabilities"
{{ end}}
@@ -1,49 +0,0 @@
successMessage: A NetworkPolicy matches pod labels and contains egress and ingress rules
failureMessage: A NetworkPolicy should match pod labels and contain applied egress and ingress rules
category: Security
target: PodTemplate
schema:
'$schema': http://json-schema.org/draft-07/schema
type: object
properties:
metadata:
type: object
properties:
labels:
type: object
minProperties: 1
additionalSchemaStrings:
networking.k8s.io/NetworkPolicy: |
type: object
properties:
spec:
type: object
required: ["podSelector", "egress", "ingress"]
properties:
podSelector:
type: object
required: ["matchLabels"]
properties:
matchLabels:
type: object
oneOf:
{{ range $key, $value := .Polaris.PodTemplate.metadata.labels }}
- properties:
"{{ $key }}":
type: string
const: {{ $value }}
required: ["{{ $key }}"]
{{ end }}
egress:
type: array
minItems: 1
ingress:
minItems: 1
type: array
policyTypes:
type: array
allOf:
- contains:
pattern: '^(?i)Egress$'
- contains:
pattern: '^(?i)Ingress$'
@@ -1,7 +0,0 @@
successMessage: PDB and HPA are correctly configured
failureMessage: PDB minAvailable is greater than HPA minReplicas
category: Reliability
target: Controller
controllers:
include:
- Deployment
-56
View File
@@ -1,56 +0,0 @@
successMessage: The Role does not allow pods/exec or pods/attach
failureMessage: The Role allows Pods/exec or pods/attach
category: Security
target: rbac.authorization.k8s.io/Role
schemaString: |
'$schema': http://json-schema.org/draft-07/schema
type: object
required: ["metadata", "rules"]
anyOf:
# Do not alert on default Roles.
- properties:
metadata:
required: ["name"]
properties:
name:
type: string
anyOf:
- pattern: '^system:'
- const: "gce:podsecuritypolicy:calico-sa"
- properties:
metadata:
required: ["name"]
properties:
name:
type: string
rules:
type: array
items:
type: object
not:
required: ["apiGroups", "resources", "verbs"]
properties:
apiGroups:
type: array
contains:
type: string
anyOf:
- const: ""
- const: '*'
resources:
type: array
contains:
type: string
anyOf:
- const: '*'
- const: "pods/exec"
- const: "pods/attach"
verbs:
type: array
contains:
type: string
anyOf:
- const: '*'
# An exec is also possible by `get`ing a web socket.
- const: 'get'
- const: 'create'
@@ -1,98 +0,0 @@
successMessage: The RoleBinding does not reference the default cluster-admin ClusterRole or one with wildcard permissions
failureMessage: The RoleBinding references the default cluster-admin ClusterRole or one with wildcard permissions
category: Security
target: rbac.authorization.k8s.io/RoleBinding
schemaString: |
'$schema': http://json-schema.org/draft-07/schema
type: object
anyOf:
# Pass RoleBindings that point to a Role.
- required: ["roleRef"]
properties:
roleRef:
required: ["kind"]
properties:
kind:
type: string
const: "Role"
# Do not alert on default ClusterRoleBindings.
- required: ["metadata"]
properties:
metadata:
type: object
required: ["name"]
properties:
name:
type: string
anyOf:
- pattern: '^system:'
- const: "gce:podsecuritypolicy:calico-sa"
- required: ["roleRef"]
properties:
roleRef:
required: ["apiGroup", "kind", "name"]
properties:
apiGroup:
type: string
const: "rbac.authorization.k8s.io"
kind:
type: string
const: "ClusterRole"
name:
type: string
minLength: 1
not:
const: "cluster-admin"
additionalSchemaStrings:
rbac.authorization.k8s.io/ClusterRole: |
{{ if eq .roleRef.kind "ClusterRole" }}
{{ if and (not (hasPrefix .metadata.name "system:")) (ne .metadata.name "gce:podsecuritypolicy:calico-sa") }}
# This schema is validated for all roleBindings, regardless of their roleRef.
type: object
required: ["metadata", "rules"]
allOf:
- properties:
metadata:
required: ["name"]
properties:
name:
type: string
const: "{{ .roleRef.name }}"
- properties:
rules:
type: array
items:
type: object
not:
required: ["apiGroups", "resources", "verbs"]
properties:
apiGroups:
type: array
contains:
type: string
const: "*"
resources:
type: array
contains:
type: string
const: '*'
verbs:
type: array
uniqueItems: true
oneOf:
- contains:
type: string
const: '*'
- minItems: 7
items:
type: string
enum:
- "get"
- "list"
- "watch"
- "create"
- "update"
- "patch"
- "delete"
{{ end }}
{{ end }}
@@ -1,96 +0,0 @@
successMessage: The RoleBinding does not reference a Role with wildcard permissions
failureMessage: The RoleBinding references a Role with wildcard permissions
category: Security
target: rbac.authorization.k8s.io/RoleBinding
schemaString: |
'$schema': http://json-schema.org/draft-07/schema
type: object
anyOf:
# Pass RoleBindings that point to a ClusterRole.
- required: ["roleRef"]
properties:
roleRef:
required: ["kind"]
properties:
kind:
type: string
const: "ClusterRole"
# Do not alert on default RoleBindings.
- required: ["metadata"]
properties:
metadata:
type: object
required: ["name"]
properties:
name:
type: string
anyOf:
- pattern: '^system:'
- const: "gce:podsecuritypolicy:calico-sa"
- required: ["roleRef"]
properties:
roleRef:
required: ["apiGroup", "kind", "name"]
properties:
apiGroup:
type: string
const: "rbac.authorization.k8s.io"
kind:
type: string
const: "Role"
name:
type: string
minLength: 1
additionalSchemaStrings:
rbac.authorization.k8s.io/Role: |
type: object
# This schema is validated for all roleBindings, regardless of their roleRef.
{{ if eq .roleRef.kind "Role" }}
{{ if and (not (hasPrefix .metadata.name "system:")) (ne .metadata.name "gce:podsecuritypolicy:calico-sa") }}
required: ["metadata", "rules"]
allOf:
- properties:
metadata:
required: ["name"]
properties:
name:
type: string
const: "{{ .roleRef.name }}"
- properties:
rules:
type: array
items:
type: object
not:
required: ["apiGroups", "resources", "verbs"]
properties:
apiGroups:
type: array
contains:
type: string
const: "*"
resources:
type: array
contains:
type: string
const: '*'
verbs:
type: array
uniqueItems: true
oneOf:
- contains:
type: string
const: '*'
- minItems: 7
items:
type: string
enum:
- "get"
- "list"
- "watch"
- "create"
- "update"
- "patch"
- "delete"
{{ end }}
{{ end }}
@@ -1,92 +0,0 @@
successMessage: The RoleBinding does not reference a ClusterRole allowing pods/exec or pods/attach
failureMessage: The RoleBinding references a ClusterRole that allows Pods/exec, allows pods/attach, or that does not exist
category: Security
target: rbac.authorization.k8s.io/RoleBinding
schemaString: |
'$schema': http://json-schema.org/draft-07/schema
type: object
anyOf:
# Pass RoleBindings that point to a Role.
- required: ["roleRef"]
properties:
roleRef:
required: ["kind"]
properties:
kind:
type: string
const: "Role"
# Do not alert on default RoleBindings.
- required: ["metadata"]
properties:
metadata:
type: object
required: ["name"]
properties:
name:
type: string
anyOf:
- pattern: '^system:'
- const: "gce:podsecuritypolicy:calico-sa"
- required: ["roleRef"]
properties:
roleRef:
required: ["apiGroup", "kind", "name"]
properties:
apiGroup:
type: string
const: "rbac.authorization.k8s.io"
kind:
type: string
const: "ClusterRole"
name:
type: string
minLength: 1
additionalSchemaStrings:
rbac.authorization.k8s.io/ClusterRole: |
{{ if eq .roleRef.kind "ClusterRole" }}
{{ if and (not (hasPrefix .metadata.name "system:")) (ne .metadata.name "gce:podsecuritypolicy:calico-sa") }}
# This schema is validated for all roleBindings, regardless of their roleRef.
type: object
required: ["metadata", "rules"]
allOf:
- properties:
metadata:
required: ["name"]
properties:
name:
type: string
const: "{{ .roleRef.name }}"
- properties:
rules:
type: array
items:
type: object
not:
required: ["apiGroups", "resources", "verbs"]
properties:
apiGroups:
type: array
contains:
type: string
anyOf:
- const: ""
- const: '*'
resources:
type: array
contains:
type: string
anyOf:
- const: '*'
- const: "pods/exec"
- const: "pods/attach"
verbs:
type: array
contains:
type: string
anyOf:
- const: '*'
# An exec is also possible by `get`ing a web socket.
- const: 'get'
- const: 'create'
{{ end }}
{{ end }}
@@ -1,95 +0,0 @@
successMessage: The RoleBinding does not reference a Role allowing Pod exec or attach
failureMessage: The RoleBinding references a Role that allows Pods/exec, allows pods/attach, or that does not exist
category: Security
target: rbac.authorization.k8s.io/RoleBinding
schemaString: |
'$schema': http://json-schema.org/draft-07/schema
type: object
anyOf:
# Pass RoleBindings that point to a ClusterRole.
- required: ["roleRef"]
properties:
roleRef:
required: ["apiGroup", "kind", "name"]
properties:
apiGroup:
type: string
const: "rbac.authorization.k8s.io"
kind:
type: string
const: "Role"
# Do not alert on default RoleBindings.
- required: ["metadata"]
properties:
metadata:
type: object
required: ["name"]
properties:
name:
type: string
anyOf:
- pattern: '^system:'
- const: "gce:podsecuritypolicy:calico-sa"
- required: ["roleRef"]
properties:
roleRef:
required: ["apiGroup", "kind", "name"]
properties:
apiGroup:
type: string
const: "rbac.authorization.k8s.io"
kind:
type: string
const: "Role"
name:
type: string
minLength: 1
additionalSchemaStrings:
rbac.authorization.k8s.io/Role: |
{{ if eq .roleRef.kind "Role" }}
{{ if and (not (hasPrefix .metadata.name "system:")) (ne .metadata.name "gce:podsecuritypolicy:calico-sa") }}
# This schema is validated for all roleBindings, regardless of their roleRef.
type: object
required: ["metadata", "rules"]
allOf:
- properties:
metadata:
required: ["name"]
properties:
name:
type: string
const: "{{ .roleRef.name }}"
- properties:
rules:
type: array
items:
type: object
not:
required: ["apiGroups", "resources", "verbs"]
properties:
apiGroups:
type: array
contains:
type: string
anyOf:
- const: ""
- const: '*'
resources:
type: array
contains:
type: string
anyOf:
- const: '*'
- const: "pods/exec"
- const: "pods/attach"
verbs:
type: array
contains:
type: string
anyOf:
- const: '*'
# An exec is also possible by `get`ing a web socket.
- const: 'get'
- const: 'create'
{{ end }}
{{ end }}
@@ -1,43 +0,0 @@
successMessage: The ConfigMap does not contain potentially sensitive content in its keys and values
failureMessage: Potentially sensitive content is detected in the ConfigMap keys or values
category: Security
target: /ConfigMap
schemaString: |
'$schema': http://json-schema.org/draft-07/schema
type: object
required: ["metadata"]
properties:
metadata:
required: ["name"]
properties:
name:
type: string
data:
type: object
propertyNames:
'$comment': These ConfigMap keys will be disallowed.
allOf:
- not:
pattern: '(?i)^AWS_SECRET_ACCESS_KEY$'
- not:
pattern: '(?i)^GOOGLE_APPLICATION_CREDENTIALS$'
- not:
pattern: '(?i)^AZURE_.+KEY$'
- not:
pattern: '(?i)^OCI_CLI_KEY_CONTENT$'
- not:
pattern: '(?i)password'
- not:
pattern: '(?i)token'
- not:
pattern: '(?i)bearer'
- not:
pattern: '(?i)secret'
'$comment': This allows ConfigMap keys not excluded above.
- pattern: '(?i).*'
additionalProperties:
'$comment': These ConfigMap values will be disallowed.
allOf:
- not:
'$comment': THis matches variations like begin private key, begin rsa private key ...
pattern: '(?i)\s*-BEGIN\s+.*PRIVATE KEY-\s*'
@@ -1,52 +0,0 @@
successMessage: The container does not set potentially sensitive environment variables
failureMessage: The container sets potentially sensitive environment variables
category: Security
target: Container
schemaString: |
'$schema': http://json-schema.org/draft-07/schema
type: object
properties:
env:
type: array
items:
type: object
anyOf:
- not:
required: ["value"]
- required: ["name", "value"]
properties:
name:
type: string
'$comment': These environment variable names will be disallowed.
allOf:
- not:
pattern: '(?i)^AWS_SECRET_ACCESS_KEY$'
- not:
pattern: '(?i)^GOOGLE_APPLICATION_CREDENTIALS$'
- not:
pattern: '(?i)^AZURE_.+KEY$'
- not:
pattern: '(?i)^OCI_CLI_KEY_CONTENT$'
- not:
pattern: '(?i)password'
- not:
pattern: '(?i)token'
- not:
pattern: '(?i)bearer'
- not:
pattern: '(?i)secret'
'$comment': This allows variable names not excluded above.
- pattern: '(?i).*'
value:
type: string
'$comment': These environment variable values will be disallowed.
allOf:
- not:
'$comment': THis matches variations like begin private key, begin rsa private key ...
pattern: '(?i)\s*-BEGIN\s+.*PRIVATE KEY-\s*'
- required: ["name", "valueFrom"]
properties:
name:
type: string
valueFrom:
type: object
@@ -1,17 +0,0 @@
successMessage: Pod has a valid topology spread constraint
failureMessage: Pod should be configured with a valid topology spread constraint
category: Reliability
target: PodSpec
controllers:
exclude:
- Job
- CronJob
schema:
'$schema': http://json-schema.org/draft-07/schema
type: object
required:
- topologySpreadConstraints
properties:
topologySpreadConstraints:
type: array
minItems: 1
-16
View File
@@ -1,16 +0,0 @@
package config
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestRequiredFieldsOnBuiltInChecks(t *testing.T) {
for _, v := range BuiltInChecks {
assert.NotEmpty(t, v.SuccessMessage)
assert.NotEmpty(t, v.FailureMessage)
assert.NotEmpty(t, v.Category)
assert.NotEmpty(t, v.Target)
}
}
+13 -7
View File
@@ -16,14 +16,14 @@ package config
import (
"bytes"
_ "embed"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"strings"
"github.com/gobuffalo/packr/v2"
"k8s.io/apimachinery/pkg/util/yaml"
)
@@ -49,25 +49,31 @@ type Exemption struct {
Namespace string `json:"namespace"`
}
//go:embed default.yaml
var defaultConfig []byte
var configBox = (*packr.Box)(nil)
func getConfigBox() *packr.Box {
if configBox == (*packr.Box)(nil) {
configBox = packr.New("Config", "../../examples")
}
return configBox
}
// ParseFile parses config from a file.
func ParseFile(path string) (Configuration, error) {
var rawBytes []byte
var err error
if path == "" {
rawBytes = defaultConfig
rawBytes, err = getConfigBox().Find("config.yaml")
} else if strings.HasPrefix(path, "https://") || strings.HasPrefix(path, "http://") {
// path is a url
response, err2 := http.Get(path)
if err2 != nil {
return Configuration{}, err2
}
rawBytes, err = io.ReadAll(response.Body)
rawBytes, err = ioutil.ReadAll(response.Body)
} else {
// path is local
rawBytes, err = os.ReadFile(path)
rawBytes, err = ioutil.ReadFile(path)
}
if err != nil {
return Configuration{}, err
+14 -38
View File
@@ -25,6 +25,7 @@ import (
"github.com/qri-io/jsonschema"
"github.com/thoas/go-funk"
"gomodules.xyz/jsonpatch/v2"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
k8sYaml "k8s.io/apimachinery/pkg/util/yaml"
@@ -40,8 +41,6 @@ const (
TargetContainer TargetKind = "Container"
// TargetPodSpec points to the pod spec
TargetPodSpec TargetKind = "PodSpec"
// TargetPodTemplate points to the pod template
TargetPodTemplate TargetKind = "PodTemplate"
)
// HandledTargets is a list of target names that are explicitly handled
@@ -49,15 +48,12 @@ var HandledTargets = []TargetKind{
TargetController,
TargetContainer,
TargetPodSpec,
TargetPodTemplate,
}
// Mutation defines how to change a YAML file, in the style of JSON Patch
type Mutation struct {
Path string
Op string
Value interface{}
Comment string
// MutationComment is the comments added to a mutated file
type MutationComment struct {
Find string `yaml:"find" json:"find"`
Comment string `yaml:"comment" json:"comment"`
}
// SchemaCheck is a Polaris check that runs using JSON Schema
@@ -76,14 +72,14 @@ type SchemaCheck struct {
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:"-"`
Mutations []Mutation `yaml:"mutations" json:"mutations"`
Mutations []jsonpatch.Operation `yaml:"mutations" json:"mutations"`
Comments []MutationComment `yaml:"comments" json:"comments"`
}
type resourceMinimum string
type resourceMaximum string
// UnmarshalYAMLOrJSON is a helper function to unmarshal data in an arbitrary format
func UnmarshalYAMLOrJSON(raw []byte, dest interface{}) error {
func unmarshalYAMLOrJSON(raw []byte, dest interface{}) error {
reader := bytes.NewReader(raw)
d := k8sYaml.NewYAMLOrJSONDecoder(reader, 4096)
for {
@@ -100,7 +96,7 @@ func UnmarshalYAMLOrJSON(raw []byte, dest interface{}) error {
// ParseCheck parses a check from a byte array
func ParseCheck(id string, rawBytes []byte) (SchemaCheck, error) {
check := SchemaCheck{}
err := UnmarshalYAMLOrJSON(rawBytes, &check)
err := unmarshalYAMLOrJSON(rawBytes, &check)
if err != nil {
return check, err
}
@@ -143,9 +139,6 @@ func (max resourceMaximum) Validate(path string, data interface{}, errs *[]jsons
}
func parseQuantity(i interface{}) (resource.Quantity, *[]jsonschema.ValError) {
if resNum, ok := i.(float64); ok {
i = fmt.Sprintf("%f", resNum)
}
resStr, ok := i.(string)
if !ok {
return resource.Quantity{}, &[]jsonschema.ValError{
@@ -226,10 +219,7 @@ func (check SchemaCheck) TemplateForResource(res interface{}) (*SchemaCheck, err
newCheck.AdditionalSchemaStrings = map[string]string{}
for kind, tmplString := range templateStrings {
tmpl := template.New(newCheck.ID).Funcs(template.FuncMap{
"hasPrefix": strings.HasPrefix,
"hasSuffix": strings.HasSuffix,
})
tmpl := template.New(newCheck.ID)
tmpl, err := tmpl.Parse(tmplString)
if err != nil {
return nil, err
@@ -239,28 +229,24 @@ func (check SchemaCheck) TemplateForResource(res interface{}) (*SchemaCheck, err
if err != nil {
return nil, err
}
templated := w.String()
if strings.TrimSpace(templated) == "" {
continue
}
if kind == "" {
newCheck.SchemaString = templated
newCheck.SchemaString = w.String()
} else {
newCheck.AdditionalSchemaStrings[kind] = templated
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)
err := unmarshalYAMLOrJSON([]byte(schemaStr), &val)
if err != nil {
return nil, err
}
newCheck.AdditionalValidators[kind] = val
}
err := UnmarshalYAMLOrJSON([]byte(newCheck.SchemaString), &newCheck.Validator)
err := unmarshalYAMLOrJSON([]byte(newCheck.SchemaString), &newCheck.Validator)
if err != nil {
return nil, err
}
@@ -272,11 +258,6 @@ func (check SchemaCheck) CheckPodSpec(pod *corev1.PodSpec) (bool, []jsonschema.V
return check.CheckObject(pod)
}
// CheckPodTemplate checks a pod template against the schema
func (check SchemaCheck) CheckPodTemplate(podTemplate interface{}) (bool, []jsonschema.ValError, error) {
return check.CheckObject(podTemplate)
}
// 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)
@@ -323,11 +304,6 @@ func (check SchemaCheck) CheckAdditionalObjects(groupkind string, objects []inte
// 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 == TargetPodTemplate && target == TargetPodSpec {
// A target=PodSpec and check.Target=PodTemplate is expected
// because applyPodSchemaChecks() explicitly sets check.Target
return true
}
if check.Target != target {
return false
}
+26 -22
View File
@@ -16,10 +16,8 @@ package dashboard
import (
"bytes"
"embed"
"encoding/json"
"html/template"
"io/fs"
"net/http"
"net/url"
"path"
@@ -28,6 +26,7 @@ import (
"github.com/fairwindsops/polaris/pkg/config"
"github.com/fairwindsops/polaris/pkg/kube"
"github.com/fairwindsops/polaris/pkg/validator"
packr "github.com/gobuffalo/packr/v2"
"github.com/gorilla/mux"
"github.com/sirupsen/logrus"
)
@@ -48,12 +47,27 @@ const (
)
var (
//go:embed all:templates
templatesFS embed.FS
//go:embed all:assets
assetsFS embed.FS
templateBox = (*packr.Box)(nil)
assetBox = (*packr.Box)(nil)
markdownBox = (*packr.Box)(nil)
)
// GetAssetBox returns a binary-friendly set of assets packaged from disk
func GetAssetBox() *packr.Box {
if assetBox == (*packr.Box)(nil) {
assetBox = packr.New("Assets", "assets")
}
return assetBox
}
// GetTemplateBox returns a binary-friendly set of templates for rendering the dash
func GetTemplateBox() *packr.Box {
if templateBox == (*packr.Box)(nil) {
templateBox = packr.New("Templates", "templates")
}
return templateBox
}
// templateData is passed to the dashboard HTML template
type templateData struct {
BasePath string
@@ -89,8 +103,9 @@ func GetBaseTemplate(name string) (*template.Template, error) {
}
func parseTemplateFiles(tmpl *template.Template, templateFileNames []string) (*template.Template, error) {
templateBox := GetTemplateBox()
for _, fname := range templateFileNames {
templateFile, err := templatesFS.ReadFile("templates/" + fname)
templateFile, err := templateBox.Find(fname)
if err != nil {
return nil, err
}
@@ -140,15 +155,9 @@ func stripUnselectedNamespaces(data *validator.AuditData, selectedNamespaces []s
}
// GetRouter returns a mux router serving all routes necessary for the dashboard
func GetRouter(c config.Configuration, auditPath string, port int, basePath string, auditData *validator.AuditData) (*mux.Router, error) {
func GetRouter(c config.Configuration, auditPath string, port int, basePath string, auditData *validator.AuditData) *mux.Router {
router := mux.NewRouter().PathPrefix(basePath).Subrouter()
assetsSubFS, err := fs.Sub(assetsFS, "assets")
if err != nil {
return nil, err
}
fileServer := http.FileServer(http.FS(assetsSubFS))
fileServer := http.FileServer(GetAssetBox())
router.PathPrefix("/static/").Handler(http.StripPrefix(path.Join(basePath, "/static/"), fileServer))
router.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
@@ -156,7 +165,7 @@ func GetRouter(c config.Configuration, auditPath string, port int, basePath stri
})
router.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
favicon, err := assetsFS.ReadFile("assets/favicon-32x32.png")
favicon, err := GetAssetBox().Find("favicon-32x32.png")
if err != nil {
logrus.Errorf("Error getting favicon: %v", err)
http.Error(w, "Error getting favicon", http.StatusInternalServerError)
@@ -195,14 +204,12 @@ func GetRouter(c config.Configuration, auditPath string, port int, basePath stri
router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" && r.URL.Path != basePath {
logrus.Warningf("Path not found: %s", r.URL.Path)
http.NotFound(w, r)
return
}
adjustedConf := getConfigForQuery(c, r.URL.Query())
if auditData == nil {
logrus.Infof("Creating resource provider")
k, err := kube.CreateResourceProvider(r.Context(), auditPath, "", c)
if err != nil {
logrus.Errorf("Error fetching Kubernetes resources %v", err)
@@ -210,7 +217,6 @@ func GetRouter(c config.Configuration, auditPath string, port int, basePath stri
return
}
logrus.Infof("Running audit")
var auditData validator.AuditData
auditData, err = validator.RunAudit(adjustedConf, k)
if err != nil {
@@ -218,15 +224,13 @@ func GetRouter(c config.Configuration, auditPath string, port int, basePath stri
http.Error(w, "Error running audit", 500)
return
}
logrus.Infof("Sending results")
MainHandler(w, r, adjustedConf, auditData, basePath)
} else {
logrus.Infof("Sending results")
MainHandler(w, r, adjustedConf, *auditData, basePath)
}
})
return router, nil
return router
}
// MainHandler gets template data and renders the dashboard with it.
-1
View File
@@ -5,7 +5,6 @@
<a href="https://www.fairwinds.com/polaris-user-insights-demo?utm_source=polaris&utm_medium=polaris&utm_campaign=polaris" target="_blank">
<img class="fw-logo" src="static/images/white_logo_fairwinds.svg" alt="Fairwinds" />
</a>
<div style="color: white;"> Want more? Automate Polaris with <a href="https://www.fairwinds.com/insights-signup/polaris"><strong>Fairwinds Insights</strong></a></div>
<div class="right-section p-0 d-flex justify-content-between">
<a href="https://github.com/FairwindsOps" target="_blank">
<img class="gh-logo" src="static/images/white_icon_github.svg" alt="Github" />
-159
View File
@@ -1,159 +0,0 @@
package fix
import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/fairwindsops/polaris/pkg/config"
"github.com/fairwindsops/polaris/pkg/kube"
"github.com/fairwindsops/polaris/pkg/mutation"
"github.com/fairwindsops/polaris/pkg/validator"
)
const templateLineMarker = "# POLARIS_FIX_TMPL"
const templateOpenMarker = "POLARIS_OPEN_TMPL"
const templateCloseMarker = "POLARIS_CLOSE_TMPL"
var ErrFilesPathRequired = errors.New("files-path flag is required")
func Execute(config config.Configuration, filesPath string, isTemplate bool, checksToFix ...string) error {
if filesPath == "" {
return ErrFilesPathRequired
}
var yamlFiles []string
fileInfo, err := os.Stat(filesPath)
if err != nil {
return fmt.Errorf("error getting file info: %v", err)
}
if fileInfo.IsDir() {
baseDir := filesPath
if !strings.HasSuffix(filesPath, "/") {
baseDir = baseDir + "/"
}
yamlFiles, err = getYamlFiles(baseDir)
if err != nil {
return fmt.Errorf("error getting yaml files from directory: %v", err)
}
} else {
yamlFiles = append(yamlFiles, filesPath)
}
if len(checksToFix) > 0 {
if len(checksToFix) == 1 && checksToFix[0] == "all" {
allchecks := []string{}
for key := range config.Checks {
allchecks = append(allchecks, key)
}
config.Mutations = allchecks
} else if len(checksToFix) == 0 && checksToFix[0] == "none" {
config.Mutations = nil
} else {
config.Mutations = checksToFix
}
}
for _, fullFilePath := range yamlFiles {
yamlContent, err := os.ReadFile(fullFilePath)
if err != nil {
return fmt.Errorf("error reading file with file path %s: %v", fullFilePath, err)
}
if isTemplate {
yamlContent = []byte(detemplate(string(yamlContent)))
}
kubeResources, err := kube.CreateResourceProviderFromYaml(string(yamlContent))
if err != nil {
return fmt.Errorf("error creating resource provider from yaml: %v", err)
}
results, err := validator.ApplyAllSchemaChecksToResourceProvider(&config, kubeResources)
if err != nil {
return fmt.Errorf("error applying schema check to the resources %s: %v", fullFilePath, err)
}
allMutations := mutation.GetMutationsFromResults(results)
updatedYamlContent := ""
if len(allMutations) > 0 {
for _, resources := range kubeResources.Resources {
for _, resource := range resources {
key := fmt.Sprintf("%s/%s/%s", resource.Kind, resource.Resource.GetName(), resource.Resource.GetNamespace())
mutations := allMutations[key]
mutatedYamlContent, err := mutation.ApplyAllMutations(string(resource.OriginalObjectYAML), mutations)
if err != nil {
return fmt.Errorf("error applying schema mutations to the resource %s: %v", key, err)
}
if updatedYamlContent != "" {
updatedYamlContent += "\n---\n"
}
updatedYamlContent += mutatedYamlContent
}
}
}
if isTemplate {
updatedYamlContent = retemplate(updatedYamlContent)
}
if updatedYamlContent != "" {
err = os.WriteFile(fullFilePath, []byte(updatedYamlContent), 0644)
if err != nil {
return fmt.Errorf("error writing output to file: %v", err)
}
}
}
return nil
}
func detemplate(content string) string {
lines := strings.Split(content, "\n")
for idx, line := range lines {
lines[idx] = detemplateLine(line)
}
return strings.Join(lines, "\n")
}
func retemplate(content string) string {
lines := strings.Split(content, "\n")
for idx, line := range lines {
lines[idx] = retemplateLine(line)
}
return strings.Join(lines, "\n")
}
func detemplateLine(line string) string {
if !strings.HasPrefix(strings.TrimSpace(line), "{{") {
line = strings.ReplaceAll(line, "{", templateOpenMarker)
line = strings.ReplaceAll(line, "}", templateCloseMarker)
return line
}
tmplStart := strings.Index(line, "{{")
newLine := line[:tmplStart] + templateLineMarker + line[tmplStart:]
return newLine
}
func retemplateLine(line string) string {
if !strings.Contains(line, templateLineMarker) {
line = strings.ReplaceAll(line, templateOpenMarker, "{")
line = strings.ReplaceAll(line, templateCloseMarker, "}")
return line
}
return strings.Replace(line, templateLineMarker, "", 1)
}
func getYamlFiles(rootpath string) ([]string, error) {
var list []string
err := filepath.Walk(rootpath, func(path string, info os.FileInfo, err error) error {
if info.IsDir() {
return nil
}
if filepath.Ext(path) == ".yaml" || filepath.Ext(path) == ".yml" {
list = append(list, path)
}
return nil
})
return list, err
}
-200
View File
@@ -1,200 +0,0 @@
package insights
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/sirupsen/logrus"
)
type cluster struct {
Name string `json:"Name"`
AuthToken string `json:"AuthToken"`
Organization string `json:"Organization"`
Status string `json:"Status"`
}
type reportJob struct {
ID int `json:"id"`
Status string `json:"status"`
TimeTakenInMs int `json:"timeTaken"`
}
type Client interface {
UpsertCluster(clusterName string) (*cluster, error)
SendReport(cluster cluster, reportType, reportVersion string, payload []byte) (*reportJob, error)
GetReportJob(clusterName string, reportJobID int) (*reportJob, error)
IsTokenValid() (bool, error)
}
type HTTPClient struct {
insightsHost string
organization string
token string
}
func NewHTTPClient(host, organization, token string) Client {
return HTTPClient{host, organization, token}
}
func (ic HTTPClient) UpsertCluster(clusterName string) (*cluster, error) {
clusterURL := fmt.Sprintf("%s/v0/organizations/%s/clusters/%s?showToken=true", ic.insightsHost, ic.organization, clusterName)
req, err := http.NewRequest("GET", clusterURL, nil)
if err != nil {
return nil, fmt.Errorf("building request for fetching cluster: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+ic.token)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, fmt.Errorf("making request fetching cluster: %w", err)
}
defer resp.Body.Close()
if isSuccessful2XX(resp.StatusCode) {
// cluster already created
logrus.Infof("cluster %q found...", clusterName)
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("reading response body: %w", err)
}
var c cluster
err = json.Unmarshal(body, &c)
if err != nil {
return nil, fmt.Errorf("unmarshaling response body: %w", err)
}
return &c, nil
} else {
logrus.Warnf("not able to fetch cluster, expected 2xx - received %d, will try to create", resp.StatusCode)
}
logrus.Infof("cluster %q not found... creating..", clusterName)
req, err = http.NewRequest("POST", clusterURL, nil)
if err != nil {
return nil, fmt.Errorf("building request for creating cluster: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+ic.token)
resp, err = http.DefaultClient.Do(req)
if err != nil {
return nil, fmt.Errorf("making request for creating cluster: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("reading response body: %w", err)
}
if !isSuccessful2XX(resp.StatusCode) {
return nil, fmt.Errorf("creating cluster, expected 200 OK received %s: %v", resp.Status, string(body))
}
var c cluster
err = json.Unmarshal(body, &c)
if err != nil {
return nil, fmt.Errorf("unmarshaling response body: %w", err)
}
logrus.Infof("cluster %q created...", clusterName)
return &c, nil
}
func (ic HTTPClient) SendReport(cluster cluster, reportType, reportVersion string, payload []byte) (*reportJob, error) {
uploadReportURL := fmt.Sprintf("%s/v0/organizations/%s/clusters/%s/data/%s", ic.insightsHost, ic.organization, cluster.Name, reportType)
req, err := http.NewRequest("POST", uploadReportURL, bytes.NewBuffer(payload))
if err != nil {
return nil, fmt.Errorf("building request for output: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+cluster.AuthToken)
req.Header.Set("X-Fairwinds-Report-Version", reportVersion)
req.Header.Set("X-Fairwinds-Report-Priority", "4") // should have higher priority than the default 5
req.Header.Set("X-Fairwinds-Agent-Version", "")
req.Header.Set("X-Fairwinds-Agent-Chart-Version", "")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, fmt.Errorf("making request for output: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("reading response body: %w", err)
}
if !isSuccessful2XX(resp.StatusCode) {
return nil, fmt.Errorf("sending %s report, expected 200 OK received %s: %v", reportType, resp.Status, string(body))
}
var rj reportJob
err = json.Unmarshal(body, &rj)
if err != nil {
return nil, fmt.Errorf("unmarshaling response body: %w", err)
}
logrus.Debugf("%s report sent to fairwinds insights", reportType)
return &rj, nil
}
func isSuccessful2XX(statusCode int) bool {
return statusCode >= 200 && statusCode < 300
}
func (ic HTTPClient) GetReportJob(clusterName string, reportJobID int) (*reportJob, error) {
reportJobsURL := fmt.Sprintf("%s/v0/organizations/%s/clusters/%s/report-jobs/%d", ic.insightsHost, ic.organization, clusterName, reportJobID)
req, err := http.NewRequest("GET", reportJobsURL, nil)
if err != nil {
return nil, fmt.Errorf("building request for fetching report-job: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+ic.token)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, fmt.Errorf("making request fetching report-job: %w", err)
}
defer resp.Body.Close()
if !isSuccessful2XX(resp.StatusCode) {
return nil, fmt.Errorf("fetching report-job, expected 200 OK received %s", resp.Status)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("reading response body: %w", err)
}
var rj reportJob
err = json.Unmarshal(body, &rj)
if err != nil {
return nil, fmt.Errorf("unmarshaling response body: %w", err)
}
return &rj, nil
}
// IsTokenValid checks if the token is valid by fetching the organization
func (ic HTTPClient) IsTokenValid() (bool, error) {
organizationURL := fmt.Sprintf("%s/v0/organizations/%s", ic.insightsHost, ic.organization)
req, err := http.NewRequest("GET", organizationURL, nil)
if err != nil {
return false, fmt.Errorf("building request for fetching organization: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+ic.token)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return false, fmt.Errorf("making request fetching organization: %w", err)
}
defer resp.Body.Close()
if isSuccessful2XX(resp.StatusCode) {
return true, nil // token is valid
}
if resp.StatusCode == http.StatusNotFound || resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden {
return false, nil // token is invalid
}
// unexpected error
return false, fmt.Errorf("fetching organization, expected 200 OK - received %s", resp.Status)
}
-89
View File
@@ -1,89 +0,0 @@
package insights
import (
"encoding/json"
"fmt"
"time"
workloads "github.com/fairwindsops/insights-plugins/plugins/workloads/pkg"
"github.com/fairwindsops/polaris/pkg/validator"
"github.com/sirupsen/logrus"
)
type insightsReporter struct {
client Client
}
func NewInsightsReporter(client Client) *insightsReporter {
return &insightsReporter{
client: client,
}
}
type WorkloadsReport struct {
Version string
Payload workloads.ClusterWorkloadReport
}
type PolarisReport struct {
Version string
Payload validator.AuditData
}
// ReportAuditToFairwindsInsights report audit to insights
// 1 - check if cluster exists, otherwise create it
// 2 - send workload report
// 3 - send polaris report
// 4 - checks if report job is completed for 3 minutes
// 5 - display link to Fairwinds Insights
func (ir insightsReporter) ReportAuditToFairwindsInsights(clusterName string, wr WorkloadsReport, pr PolarisReport) error {
cluster, err := ir.client.UpsertCluster(clusterName)
if err != nil {
return err
}
workloadsPayload, err := json.MarshalIndent(wr.Payload, "", " ")
if err != nil {
return fmt.Errorf("marshaling data: %w", err)
}
_, err = ir.client.SendReport(*cluster, "workloads", wr.Version, workloadsPayload)
if err != nil {
return err
}
polarisPayload, err := json.MarshalIndent(pr.Payload, "", " ")
if err != nil {
return fmt.Errorf("marshaling data: %w", err)
}
reportJob, err := ir.client.SendReport(*cluster, "polaris", pr.Version, polarisPayload)
if err != nil {
return err
}
success, err := verifyReportJobCompletion(&ir, cluster.Name, reportJob.ID)
if err != nil {
return err
}
if !success {
return fmt.Errorf("timed out waiting for report job to complete")
}
return nil
}
// verifyReportJobCompletion checks Insights for reportJob completion (timeout after 3 minutes)
func verifyReportJobCompletion(ir *insightsReporter, clusterName string, reportJobID int) (bool, error) {
defer func() { fmt.Println() }()
logrus.Println("Processing (this usually takes 1-3 minutes)...")
for i := 0; i < 60; i++ {
reportJob, err := ir.client.GetReportJob(clusterName, reportJobID)
if err != nil {
return false, err
}
if reportJob.Status == "completed" {
return true, nil
}
fmt.Print(".")
time.Sleep(3 * time.Second)
}
return false, nil
}
+18 -86
View File
@@ -20,7 +20,6 @@ import (
"fmt"
"github.com/sirupsen/logrus"
"github.com/thoas/go-funk"
"gopkg.in/yaml.v3"
kubeAPICoreV1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/meta"
@@ -30,18 +29,13 @@ import (
"k8s.io/client-go/dynamic"
)
// There are often many ReplicaSets that are orphaned (default 10 per deployment) so we don't cache all of them
var skipListCacheKinds = []string{"ReplicaSet", "Job"}
// GenericResource is a base implementation with some free methods for inherited structs
type GenericResource struct {
Kind string
ObjectMeta kubeAPIMetaV1.Object
Resource unstructured.Unstructured
PodSpec *kubeAPICoreV1.PodSpec
PodTemplate interface{}
OriginalObjectJSON []byte
OriginalObjectYAML []byte
}
// NewGenericResourceFromUnstructured creates a workload from an unstructured.Unstructured
@@ -59,10 +53,6 @@ func NewGenericResourceFromUnstructured(unst unstructured.Unstructured, podSpecM
return workload, err
}
workload.ObjectMeta = objMeta
workload.PodTemplate, err = GetPodTemplate(unst.UnstructuredContent())
if err != nil {
return workload, err
}
b, err := json.Marshal(&unst)
if err != nil {
@@ -94,15 +84,10 @@ func NewGenericResourceFromUnstructured(unst unstructured.Unstructured, podSpecM
// NewGenericResourceFromPod builds a new workload for a given Pod without looking at parents
func NewGenericResourceFromPod(podResource kubeAPICoreV1.Pod, originalObject interface{}) (GenericResource, error) {
podMap, err := SerializePod(&podResource)
if err != nil {
return GenericResource{}, err
}
workload := GenericResource{
Kind: "Pod",
PodSpec: &podResource.Spec,
PodTemplate: podMap,
ObjectMeta: podResource.ObjectMeta.GetObjectMeta(),
Kind: "Pod",
PodSpec: &podResource.Spec,
ObjectMeta: podResource.ObjectMeta.GetObjectMeta(),
}
if originalObject != nil {
bytes, err := json.Marshal(originalObject)
@@ -133,13 +118,11 @@ func NewGenericResourceFromBytes(contentBytes []byte) (GenericResource, error) {
if err != nil {
return GenericResource{}, err
}
res, err := NewGenericResourceFromUnstructured(unst, nil)
res.OriginalObjectYAML = contentBytes
return res, err
return NewGenericResourceFromUnstructured(unst, nil)
}
// 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) {
func ResolveControllerFromPod(ctx context.Context, podResource kubeAPICoreV1.Pod, dynamicClient *dynamic.Interface, restMapper *meta.RESTMapper, objectCache map[string]unstructured.Unstructured) (GenericResource, error) {
workload, err := resolveControllerFromPod(ctx, podResource, dynamicClient, restMapper, objectCache)
if err != nil {
return workload, err
@@ -150,7 +133,7 @@ func ResolveControllerFromPod(ctx context.Context, podResource kubeAPICoreV1.Pod
return workload, err
}
func resolveControllerFromPod(ctx context.Context, podResource kubeAPICoreV1.Pod, dynamicClient dynamic.Interface, restMapper meta.RESTMapper, objectCache map[string]unstructured.Unstructured) (GenericResource, error) {
func resolveControllerFromPod(ctx context.Context, podResource kubeAPICoreV1.Pod, dynamicClient *dynamic.Interface, restMapper *meta.RESTMapper, objectCache map[string]unstructured.Unstructured) (GenericResource, error) {
podWorkload, err := NewGenericResourceFromPod(podResource, nil)
if err != nil {
return podWorkload, err
@@ -174,14 +157,9 @@ func resolveControllerFromPod(ctx context.Context, podResource kubeAPICoreV1.Pod
lastKey = key
abstractObject, ok := objectCache[key]
if !ok {
var err error
if funk.Contains(skipListCacheKinds, firstOwner.Kind) {
err = cacheSingleObject(ctx, firstOwner.APIVersion, firstOwner.Kind, topMeta.GetNamespace(), firstOwner.Name, dynamicClient, restMapper, objectCache)
} else {
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)
logrus.Warnf("Error caching objects of Kind %s %v", firstOwner.Kind, err)
break
}
abstractObject, ok = objectCache[key]
@@ -193,7 +171,7 @@ 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)
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
}
podSpec := GetPodSpec(abstractObject.Object)
@@ -217,48 +195,33 @@ func resolveControllerFromPod(ctx context.Context, podResource kubeAPICoreV1.Pod
return workload, nil
}
func cacheSingleObject(ctx context.Context, apiVersion, kind, namespace, name string, dynamicClient dynamic.Interface, restMapper meta.RESTMapper, objectCache map[string]unstructured.Unstructured) error {
logrus.Debugf("Caching a single %s", kind)
object, err := GetObject(ctx, namespace, kind, apiVersion, name, dynamicClient, restMapper)
if err != nil {
logrus.Warnf("error retrieving object %s/%s/%s/%s because of error: %v", kind, apiVersion, namespace, name, err)
return err
}
key := fmt.Sprintf("%s/%s/%s", object.GetKind(), object.GetNamespace(), object.GetName())
logrus.Debugf("Caching key %s", key)
objectCache[key] = *object
return nil
}
func cacheAllObjectsOfKind(ctx context.Context, apiVersion, kind string, dynamicClient dynamic.Interface, restMapper meta.RESTMapper, objectCache map[string]unstructured.Unstructured) error {
logrus.Debugf("Caching all %s", kind)
func cacheAllObjectsOfKind(ctx context.Context, apiVersion, kind string, dynamicClient *dynamic.Interface, restMapper *meta.RESTMapper, objectCache map[string]unstructured.Unstructured) error {
fqKind := schema.FromAPIVersionAndKind(apiVersion, kind)
mapping, err := restMapper.RESTMapping(fqKind.GroupKind(), fqKind.Version)
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{})
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 {
key := fmt.Sprintf("%s/%s/%s", object.GetKind(), object.GetNamespace(), object.GetName())
logrus.Debugf(" caching key %s", key)
objectCache[key] = objects.Items[idx]
}
return nil
}
func GetObject(ctx context.Context, namespace, kind, version, name string, dynamicClient dynamic.Interface, restMapper meta.RESTMapper) (*unstructured.Unstructured, error) {
fqKind := schema.FromAPIVersionAndKind(version, kind)
mapping, err := restMapper.RESTMapping(fqKind.GroupKind(), fqKind.Version)
func getObject(ctx context.Context, namespace, kind, version, name string, dynamicClient *dynamic.Interface, restMapper *meta.RESTMapper) (*unstructured.Unstructured, error) {
fqKind := schema.ParseGroupKind(kind)
mapping, err := (*restMapper).RESTMapping(fqKind, version)
if err != nil {
return nil, err
}
object, err := dynamicClient.Resource(mapping.Resource).Namespace(namespace).Get(ctx, name, kubeAPIMetaV1.GetOptions{})
object, err := (*dynamicClient).Resource(mapping.Resource).Namespace(namespace).Get(ctx, name, kubeAPIMetaV1.GetOptions{})
return object, err
}
@@ -274,34 +237,3 @@ func GetPodSpec(yaml map[string]interface{}) interface{} {
}
return nil
}
// GetPodTemplate looks inside arbitrary YAML for a Pod template, containing
// fields `spec.containers`.
// For example, it returns the `spec.template` level of a Kubernetes Deployment yaml.
func GetPodTemplate(yaml map[string]interface{}) (podTemplate interface{}, err error) {
if yamlSpec, ok := yaml["spec"]; ok {
if yamlSpecMap, ok := yamlSpec.(map[string]interface{}); ok {
if _, ok := yamlSpecMap["containers"]; ok {
// This is a hack around unstructured.SetNestedField using DeepCopy which does
// not support the type int, and panics.
// Related: https://github.com/kubernetes/kubernetes/issues/62769
podTemplateJSON, err := json.Marshal(yaml)
if err != nil {
return nil, err
}
podTemplateMap := make(map[string]interface{})
err = json.Unmarshal(podTemplateJSON, &podTemplateMap)
if err != nil {
return nil, err
}
return podTemplateMap, nil
}
}
}
for _, podSpecField := range podSpecFields {
if childYaml, ok := yaml[podSpecField]; ok {
return GetPodTemplate(childYaml.(map[string]interface{}))
}
}
return nil, nil
}
+112 -131
View File
@@ -17,16 +17,15 @@ package kube
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"strings"
"time"
"github.com/fairwindsops/controller-utils/pkg/controller"
conf "github.com/fairwindsops/polaris/pkg/config"
"github.com/sirupsen/logrus"
@@ -34,12 +33,12 @@ import (
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"
_ "k8s.io/client-go/plugin/pkg/client/auth" // Required for other auth providers like GKE.
"k8s.io/client-go/rest"
"k8s.io/client-go/restmapper"
"sigs.k8s.io/controller-runtime/pkg/client/config"
)
@@ -59,12 +58,7 @@ type resourceKindMap map[string][]GenericResource
func (rkm resourceKindMap) addResource(r GenericResource) {
gvk := r.Resource.GroupVersionKind()
var key string
if gvk.Group != "" {
key = gvk.Group + "/" + gvk.Kind
} else {
key = gvk.Kind
}
key := gvk.Group + "/" + gvk.Kind
rkm[key] = append(rkm[key], r)
}
@@ -94,16 +88,12 @@ func (rkm resourceKindMap) GetNumberOfControllers() int {
return total
}
var kindRewrites = map[string]string{
"Ingress": "networking.k8s.io/Ingress",
"PodDisruptionBudget": "policy/PodDisruptionBudget",
"HorizontalPodAutoscaler": "autoscaling/HorizontalPodAutoscaler",
}
// This is here for backward compatibility reasons
func maybeTransformKindIntoGroupKind(k string) string {
if val, ok := kindRewrites[k]; ok {
return val
if k == "Ingress" {
return "networking.k8s.io/Ingress"
} else if k == "PodDisruptionBudget" {
return "policy/PodDisruptionBudget"
}
return k
}
@@ -150,13 +140,20 @@ func CreateResourceProvider(ctx context.Context, directory, workload string, c c
// CreateResourceProviderFromResource creates a new ResourceProvider that just contains one workload
func CreateResourceProviderFromResource(ctx context.Context, workload string) (*ResourceProvider, error) {
dynamicClient, restMapper, clientSet, _, err := GetKubeClient(ctx, "")
kubeConf, configError := config.GetConfig()
if configError != nil {
logrus.Errorf("Error fetching KubeConfig: %v", configError)
return nil, configError
}
kube, err := kubernetes.NewForConfig(kubeConf)
if err != nil {
logrus.Errorf("Error creating Kubernetes client: %v", err)
return nil, err
}
serverVersion, err := clientSet.Discovery().ServerVersion()
serverVersion, err := kube.Discovery().ServerVersion()
if err != nil {
return nil, fmt.Errorf("Error fetching Cluster API version: %w", err)
logrus.Errorf("Error fetching Cluster API version: %v", err)
return nil, err
}
resources := newResourceProvider(serverVersion.Major+"."+serverVersion.Minor, "Resource", workload)
@@ -169,14 +166,28 @@ func CreateResourceProviderFromResource(ctx context.Context, workload string) (*
version := parts[2]
name := parts[3]
obj, err := GetObject(ctx, namespace, kind, version, name, dynamicClient, restMapper)
dynamicInterface, err := dynamic.NewForConfig(kubeConf)
if err != nil {
return nil, fmt.Errorf("Could not find workload %s: %w", workload, err)
logrus.Errorf("Error connecting to dynamic interface: %v", err)
return nil, err
}
groupResources, err := restmapper.GetAPIGroupResources(kube.Discovery())
if err != nil {
logrus.Errorf("Error getting API Group resources: %v", err)
return nil, err
}
restMapper := restmapper.NewDiscoveryRESTMapper(groupResources)
obj, err := getObject(ctx, namespace, kind, version, name, &dynamicInterface, &restMapper)
if err != nil {
logrus.Errorf("Could not find workload %s: %v", workload, err)
return nil, err
}
workloadObj, err := NewGenericResourceFromUnstructured(*obj, nil)
if err != nil {
return nil, fmt.Errorf("Could not parse workload %s: %w", workload, err)
logrus.Errorf("Could not parse workload %s: %v", workload, err)
return nil, err
}
resources.Resources.addResource(workloadObj)
return &resources, nil
}
@@ -199,14 +210,14 @@ func CreateResourceProviderFromPath(directory string) (*ResourceProvider, error)
if !strings.HasSuffix(path, ".yml") && !strings.HasSuffix(path, ".yaml") {
return nil
}
contents, err := os.ReadFile(path)
contents, err := ioutil.ReadFile(path)
if err != nil {
logrus.Errorf("Error reading file: %v", path)
return err
}
err = resources.addResourcesFromYaml(string(contents))
if err != nil {
logrus.Warnf("skipping %s: cannot add resource from YAML: %v", path, err)
logrus.Warnf("Skipping %s: cannot add resource from YAML: %v", path, err)
}
return nil
}
@@ -219,52 +230,34 @@ func CreateResourceProviderFromPath(directory string) (*ResourceProvider, error)
}
// CreateResourceProviderFromYaml returns a new ResourceProvider using the yaml
func CreateResourceProviderFromYaml(yamlContent string) (*ResourceProvider, error) {
func CreateResourceProviderFromYaml(yamlContent string) *ResourceProvider {
resources := newResourceProvider("unknown", "Content", "unknown")
err := resources.addResourcesFromYaml(string(yamlContent))
if err != nil {
return nil, err
}
return &resources, nil
resources.addResourcesFromYaml(string(yamlContent))
return &resources
}
// CreateResourceProviderFromCluster creates a new ResourceProvider using live data from a cluster
func CreateResourceProviderFromCluster(ctx context.Context, c conf.Configuration) (*ResourceProvider, error) {
dynamicClient, _, clientSet, clusterHost, err := GetKubeClient(ctx, c.KubeContext)
kubeConf, configError := config.GetConfigWithContext(c.KubeContext)
if configError != nil {
logrus.Errorf("Error fetching KubeConfig: %v", configError)
return nil, configError
}
api, err := kubernetes.NewForConfig(kubeConf)
if err != nil {
logrus.Errorf("Error creating Kubernetes client: %v", err)
return nil, err
}
return CreateResourceProviderFromAPI(ctx, clientSet, clusterHost, dynamicClient, c)
}
func GetKubeClient(ctx context.Context, kubeContext string) (dynamic.Interface, meta.RESTMapper, kubernetes.Interface, string, error) {
var kubeConf *rest.Config
var err error
if len(kubeContext) > 0 {
kubeConf, err = config.GetConfigWithContext(kubeContext)
} else {
kubeConf, err = config.GetConfig()
}
dynamicInterface, err := dynamic.NewForConfig(kubeConf)
if err != nil {
return nil, nil, nil, "", fmt.Errorf("Error fetching KubeConfig: %v", err)
logrus.Errorf("Error connecting to dynamic interface: %v", err)
return nil, err
}
clientSet, err := kubernetes.NewForConfig(kubeConf)
if err != nil {
return nil, nil, nil, "", fmt.Errorf("Error creating Kubernetes client: %v", err)
}
dynamicClient, err := dynamic.NewForConfig(kubeConf)
if err != nil {
return nil, nil, nil, "", fmt.Errorf("Error connecting to dynamic interface: %v", err)
}
resources, err := restmapper.GetAPIGroupResources(clientSet.Discovery())
if err != nil {
return nil, nil, nil, "", fmt.Errorf("Error getting API Group resources: %v", err)
}
return dynamicClient, restmapper.NewDiscoveryRESTMapper(resources), clientSet, kubeConf.Host, nil
return CreateResourceProviderFromAPI(ctx, api, kubeConf.Host, &dynamicInterface, c)
}
// CreateResourceProviderFromAPI creates a new ResourceProvider from an existing k8s interface
func CreateResourceProviderFromAPI(ctx context.Context, kube kubernetes.Interface, clusterName string, dynamic dynamic.Interface, c conf.Configuration) (*ResourceProvider, error) {
func CreateResourceProviderFromAPI(ctx context.Context, kube kubernetes.Interface, clusterName string, dynamic *dynamic.Interface, c conf.Configuration) (*ResourceProvider, error) {
listOpts := metav1.ListOptions{}
serverVersion, err := kube.Discovery().ServerVersion()
if err != nil {
@@ -279,14 +272,12 @@ func CreateResourceProviderFromAPI(ctx context.Context, kube kubernetes.Interfac
}
provider := newResourceProvider(serverVersion.Major+"."+serverVersion.Minor, sourceType, clusterName)
logrus.Info("Loading nodes")
nodes, err := kube.CoreV1().Nodes().List(ctx, listOpts)
if err != nil {
logrus.Errorf("Error fetching Nodes: %v", err)
return nil, err
}
logrus.Info("Loading namespaces")
var namespaces *corev1.NamespaceList
if c.Namespace != "" {
ns, err := kube.CoreV1().Namespaces().Get(ctx, c.Namespace, metav1.GetOptions{})
@@ -304,7 +295,12 @@ func CreateResourceProviderFromAPI(ctx context.Context, kube kubernetes.Interfac
}
namespaces = nsList
}
logrus.Info("Setting up restmapper")
pods, err := kube.CoreV1().Pods(c.Namespace).List(ctx, listOpts)
if err != nil {
logrus.Errorf("Error fetching Pods: %v", err)
return nil, err
}
resources, err := restmapper.GetAPIGroupResources(kube.Discovery())
if err != nil {
logrus.Errorf("Error getting API Group resources: %v", err)
@@ -338,20 +334,15 @@ func CreateResourceProviderFromAPI(ctx context.Context, kube kubernetes.Interfac
var kubernetesResources []GenericResource
for _, kind := range additionalKinds {
groupKind := parseGroupKind(maybeTransformKindIntoGroupKind(string(kind)))
mapping, err := restMapper.RESTMapping(groupKind)
mapping, err := (restMapper).RESTMapping(groupKind)
if err != nil {
logrus.Warnf("error retrieving mapping of Kind %s because of error: %v", kind, err)
logrus.Warnf("Error retrieving mapping of Kind %s because of error: %v", kind, err)
return nil, err
}
if c.Namespace != "" && mapping.Scope.Name() != meta.RESTScopeNameNamespace {
logrus.Infof("Skipping %s because of auditing specific namespace", mapping.GroupVersionKind)
continue
}
logrus.Info("Loading " + kind)
objects, err := dynamic.Resource(mapping.Resource).Namespace(c.Namespace).List(ctx, metav1.ListOptions{})
objects, err := (*dynamic).Resource(mapping.Resource).Namespace(c.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)
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 {
@@ -362,34 +353,67 @@ func CreateResourceProviderFromAPI(ctx context.Context, kube kubernetes.Interfac
kubernetesResources = append(kubernetesResources, res)
}
}
logrus.Info("Loading controllers")
client := controller.Client{
Context: ctx,
Dynamic: dynamic,
RESTMapper: restMapper,
}
topControllers, err := client.GetAllTopControllersSummary("")
objectCache := map[string]unstructured.Unstructured{}
controllers, err := LoadControllers(ctx, pods.Items, dynamic, &restMapper, objectCache)
if err != nil {
return nil, fmt.Errorf("error while getting all TopControllers: %v", err)
}
for _, workload := range topControllers {
topController := workload.TopController
workloadObj, err := NewGenericResourceFromUnstructured(topController, nil)
if err != nil {
return nil, fmt.Errorf("could not parse workload %v: %w", workload, err)
}
kubernetesResources = append(kubernetesResources, workloadObj)
logrus.Errorf("Error loading controllers from pods: %v", err)
return nil, err
}
// resources loaded from custom checks can also contain controllers and thus would be added twice to the provider
kubernetesResources = deduplicateControllers(append(kubernetesResources, controllers...))
provider.Nodes = nodes.Items
provider.Namespaces = namespaces.Items
provider.Resources.addResources(kubernetesResources)
logrus.Info("Done loading Kubernetes resources")
return &provider, nil
}
// LoadControllers loads a list of controllers from the kubeResources Pods
func LoadControllers(ctx context.Context, pods []corev1.Pod, dynamicClientPointer *dynamic.Interface, restMapperPointer *meta.RESTMapper, objectCache map[string]unstructured.Unstructured) ([]GenericResource, error) {
interfaces := []GenericResource{}
deduped := map[string]corev1.Pod{}
for _, pod := range pods {
owners := pod.ObjectMeta.OwnerReferences
if len(owners) == 0 {
deduped[pod.ObjectMeta.Namespace+"/Pod/"+pod.ObjectMeta.Name] = pod
continue
}
deduped[pod.ObjectMeta.Namespace+"/"+owners[0].Kind+"/"+owners[0].Name] = pod
}
for _, pod := range deduped {
workload, err := ResolveControllerFromPod(ctx, pod, dynamicClientPointer, restMapperPointer, objectCache)
if err != nil {
return nil, err
}
interfaces = append(interfaces, workload)
}
return interfaces, nil
}
// Because the controllers with an Owner take on the name of the Owner, this eliminates any duplicates.
// In cases like CronJobs older children can hang around, so this takes the most recent.
func deduplicateControllers(inputResources []GenericResource) []GenericResource {
controllerMap := make(map[string]GenericResource)
for _, controller := range inputResources {
key := controller.ObjectMeta.GetNamespace() + "/" + controller.Kind + "/" + controller.ObjectMeta.GetName()
oldController, ok := controllerMap[key]
if !ok || controller.ObjectMeta.GetCreationTimestamp().Time.After(oldController.ObjectMeta.GetCreationTimestamp().Time) {
controllerMap[key] = controller
}
}
results := make([]GenericResource, len(controllerMap))
idx := 0
for _, controller := range controllerMap {
results[idx] = controller
idx++
}
return results
}
func (resources *ResourceProvider) addResourcesFromReader(reader io.Reader) error {
contents, err := io.ReadAll(reader)
contents, err := ioutil.ReadAll(reader)
if err != nil {
logrus.Errorf("Error reading from %v: %v", reader, err)
return err
@@ -441,7 +465,6 @@ func (resources *ResourceProvider) addResourceFromString(contents string) error
if err != nil {
return err
}
workload.OriginalObjectYAML = contentBytes
resources.Resources.addResource(workload)
} else {
newResource, err := NewGenericResourceFromBytes(contentBytes)
@@ -452,45 +475,3 @@ func (resources *ResourceProvider) addResourceFromString(contents string) error
}
return err
}
// SerializePodSpec converts a typed PodSpec into a map[string]interface{}
func SerializePodSpec(pod *corev1.PodSpec) (map[string]interface{}, error) {
podJSON, err := json.Marshal(pod)
if err != nil {
return nil, err
}
podMap := make(map[string]interface{})
err = json.Unmarshal(podJSON, &podMap)
if err != nil {
return nil, err
}
return podMap, nil
}
// SerializePod converts a typed Pod into a map[string]interface{}
func SerializePod(pod *corev1.Pod) (map[string]interface{}, error) {
podJSON, err := json.Marshal(pod)
if err != nil {
return nil, err
}
podMap := make(map[string]interface{})
err = json.Unmarshal(podJSON, &podMap)
if err != nil {
return nil, err
}
return podMap, nil
}
// SerializeContainer converts a typed Container into a map[string]interface{}
func SerializeContainer(container *corev1.Container) (map[string]interface{}, error) {
containerJSON, err := json.Marshal(container)
if err != nil {
return nil, err
}
containerMap := make(map[string]interface{})
err = json.Unmarshal(containerJSON, &containerMap)
if err != nil {
return nil, err
}
return containerMap, nil
}
+21 -26
View File
@@ -17,8 +17,7 @@ package kube
import (
"bytes"
"context"
"fmt"
"os"
"io/ioutil"
"testing"
"time"
@@ -48,8 +47,8 @@ func TestGetResourcesFromPath(t *testing.T) {
namespaceCount[controller.ObjectMeta.GetNamespace()]++
}
}
assert.Equal(t, 10, provider.Resources.GetLength())
assert.Equal(t, 9, namespaceCount[""])
assert.Equal(t, 11, provider.Resources.GetLength())
assert.Equal(t, 10, namespaceCount[""])
assert.Equal(t, 1, namespaceCount["two"])
}
@@ -65,8 +64,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["apps/Deployment"]), "Should have one controller")
assert.Equal(t, "dashboard", resources.Resources["apps/Deployment"][0].PodSpec.Containers[0].Name)
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, 2, len(resources.Namespaces), "Should have a namespace")
assert.Equal(t, "polaris", resources.Namespaces[0].ObjectMeta.Name)
@@ -79,7 +78,7 @@ func TestGetMultipleResourceFromBadFile(t *testing.T) {
}
func TestAddResourcesFromReader(t *testing.T) {
contents, err := os.ReadFile("./test_files/test_2/multi.yaml")
contents, err := ioutil.ReadFile("./test_files/test_2/multi.yaml")
assert.NoError(t, err)
reader := bytes.NewBuffer(contents)
resources := newResourceProvider("unknown", "Path", "-")
@@ -88,8 +87,8 @@ func TestAddResourcesFromReader(t *testing.T) {
assert.Equal(t, 0, len(resources.Nodes), "Should not have any nodes")
assert.Equal(t, 1, len(resources.Resources["apps/Deployment"]), "Should have one controller")
assert.Equal(t, "dashboard", resources.Resources["apps/Deployment"][0].PodSpec.Containers[0].Name)
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, 2, len(resources.Namespaces), "Should have a namespace")
assert.Equal(t, "polaris", resources.Namespaces[0].ObjectMeta.Name)
@@ -147,30 +146,26 @@ func TestGetResourceFromAPI(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
resources, err := CreateResourceProviderFromAPI(context.Background(), k8s, tt.clusterName, dynamicInterface, tt.config)
resources, err := CreateResourceProviderFromAPI(context.Background(), k8s, tt.clusterName, &dynamicInterface, tt.config)
if tt.wantErr {
assert.Error(t, err)
} else {
if assert.NoError(t, err) {
assert.Equal(t, tt.want.SourceType, resources.SourceType)
assert.Equal(t, tt.want.SourceName, resources.SourceName)
assert.IsType(t, tt.want.CreationTime, resources.CreationTime)
assert.Equal(t, 0, len(resources.Nodes), "Should not have any nodes")
for k, v := range resources.Resources {
fmt.Println("cont", k, v)
}
assert.Equal(t, 5, len(resources.Resources), "Should have 5 controllers")
assert.NoError(t, err)
assert.Equal(t, tt.want.SourceType, resources.SourceType)
assert.Equal(t, tt.want.SourceName, resources.SourceName)
assert.IsType(t, tt.want.CreationTime, resources.CreationTime)
assert.Equal(t, 0, len(resources.Nodes), "Should not have any nodes")
assert.Equal(t, 5, len(resources.Resources), "Should have 5 controllers")
for _, controllers := range resources.Resources {
for _, ctrl := range controllers {
expectedNames[ctrl.ObjectMeta.GetName()] = true
}
}
for name, val := range expectedNames {
assert.Equal(t, true, val, name)
for _, controllers := range resources.Resources {
for _, ctrl := range controllers {
expectedNames[ctrl.ObjectMeta.GetName()] = true
}
}
for name, val := range expectedNames {
assert.Equal(t, true, val, name)
}
}
})
}
+1 -1
View File
@@ -1,4 +1,4 @@
apiVersion: batch/v1
apiVersion: batch/v1beta1
kind: CronJob
metadata:
name: test
+1 -1
View File
@@ -1,4 +1,4 @@
apiVersion: apps/v1
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: test-deployment

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