mirror of
https://github.com/FairwindsOps/polaris.git
synced 2026-08-23 22:26:34 +00:00
Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b53110f924 | ||
|
|
25030cd9ba | ||
|
|
220d23b7fb | ||
|
|
cef4b657db | ||
|
|
508038c095 | ||
|
|
c96f0c2521 | ||
|
|
5257f43042 | ||
|
|
c177472fd2 |
+162
-46
@@ -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,10 +56,11 @@ 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
|
||||
@@ -97,88 +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.22
|
||||
- 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.25.0-nightly # Go 1.22.0
|
||||
environment:
|
||||
GOLANG_VERSION: "1.22.0"
|
||||
- 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: /.*/
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: [triage, enhancement]
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
name: Other
|
||||
about: For misc. tasks like research or continued conversation
|
||||
title: ''
|
||||
labels: [triage]
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -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,4 +1,3 @@
|
||||
|
||||
This PR fixes #
|
||||
|
||||
## Checklist
|
||||
|
||||
@@ -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:
|
||||
|
||||
+1
-4
@@ -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~
|
||||
|
||||
@@ -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"
|
||||
@@ -1,113 +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
|
||||
- ./scripts/install-and-run-packr2.sh
|
||||
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", "--yes", "--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
@@ -1,2 +1,2 @@
|
||||
## DO NOT EDIT - Managed by Terraform
|
||||
* @rbren @sudermanjr
|
||||
* @rbren @makoscafee
|
||||
|
||||
+17
-11
@@ -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
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -38,9 +41,8 @@ and network with fellow Kubernetes users.
|
||||
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>
|
||||
|
||||
@@ -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 }}
|
||||
@@ -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"]}
|
||||
|
||||
@@ -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}}
|
||||
@@ -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 }}"
|
||||
@@ -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$'
|
||||
@@ -9,18 +9,12 @@ schema:
|
||||
'$schema': http://json-schema.org/draft-07/schema
|
||||
type: object
|
||||
properties:
|
||||
spec:
|
||||
metadata:
|
||||
type: object
|
||||
properties:
|
||||
template:
|
||||
labels:
|
||||
type: object
|
||||
properites:
|
||||
metadata:
|
||||
type: object
|
||||
properties:
|
||||
labels:
|
||||
type: object
|
||||
minProperties: 1
|
||||
minProperties: 1
|
||||
additionalSchemaStrings:
|
||||
policy/PodDisruptionBudget: |
|
||||
type: object
|
||||
@@ -36,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"
|
||||
@@ -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 }}
|
||||
@@ -51,6 +51,6 @@ schema:
|
||||
securityContext:
|
||||
$ref: "#/definitions/goodSecurityContext"
|
||||
mutations:
|
||||
- op: add
|
||||
- op: replace
|
||||
path: /securityContext/runAsNonRoot
|
||||
value: true
|
||||
value: true
|
||||
@@ -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
-107
@@ -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)
|
||||
|
||||
@@ -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
|
||||
},
|
||||
}
|
||||
+123
-13
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
|
||||
@@ -5,61 +5,6 @@ meta:
|
||||
content: "Fairwinds Polaris | Changelog"
|
||||
|
||||
---
|
||||
|
||||
## Unreleased
|
||||
* Change `metadataAndNameMismatched` to `metadataAndInstanceMismatched`
|
||||
|
||||
## 8.1.1
|
||||
* Add category for `metadataAndNameMismatched`.
|
||||
* Fix category for `priorityClassNotSet`.
|
||||
|
||||
## 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
|
||||
|
||||
@@ -14,55 +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
|
||||
`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/)
|
||||
@@ -70,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/)
|
||||
|
||||
@@ -11,10 +11,8 @@ 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.
|
||||
@@ -24,17 +22,6 @@ key | default | description
|
||||
`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
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
Generated
+3618
-5345
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -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,23 +6,14 @@ checks:
|
||||
pullPolicyNotAlways: warning
|
||||
readinessProbeMissing: warning
|
||||
livenessProbeMissing: warning
|
||||
topologySpreadConstraint: warning
|
||||
pdbDisruptionsIsZero: warning
|
||||
missingPodDisruptionBudget: warning
|
||||
metadataAndInstanceMismatched: 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
|
||||
@@ -31,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
|
||||
|
||||
+4
-113
@@ -1,28 +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
|
||||
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
|
||||
@@ -32,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
|
||||
@@ -109,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:
|
||||
@@ -168,9 +69,6 @@ exemptions:
|
||||
- runAsRootAllowed
|
||||
- readinessProbeMissing
|
||||
- livenessProbeMissing
|
||||
- automountServiceAccountToken
|
||||
- linuxHardening
|
||||
- missingNetworkPolicy
|
||||
|
||||
- controllerNames:
|
||||
- cluster-autoscaler
|
||||
@@ -178,9 +76,6 @@ exemptions:
|
||||
- notReadOnlyRootFilesystem
|
||||
- runAsRootAllowed
|
||||
- readinessProbeMissing
|
||||
- automountServiceAccountToken
|
||||
- linuxHardening
|
||||
- missingNetworkPolicy
|
||||
|
||||
- controllerNames:
|
||||
- vpa
|
||||
@@ -197,10 +92,6 @@ exemptions:
|
||||
- readinessProbeMissing
|
||||
- livenessProbeMissing
|
||||
- notReadOnlyRootFilesystem
|
||||
- automountServiceAccountToken
|
||||
- linuxHardening
|
||||
- missingNetworkPolicy
|
||||
- sensitiveContainerEnvVar
|
||||
|
||||
- controllerNames:
|
||||
- nginx-ingress-controller
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
options:
|
||||
organization: fairwinds-production
|
||||
organization: fairwinds-opensource
|
||||
baseBranch: master
|
||||
|
||||
images:
|
||||
|
||||
@@ -1,88 +1,93 @@
|
||||
module github.com/fairwindsops/polaris
|
||||
|
||||
go 1.22
|
||||
go 1.17
|
||||
|
||||
require (
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7
|
||||
github.com/fairwindsops/controller-utils v0.3.0
|
||||
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-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/karrick/godirwalk v1.16.1 // 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/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.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/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/spf13/pflag v1.0.5 // indirect
|
||||
golang.org/x/net v0.17.0 // indirect
|
||||
golang.org/x/oauth2 v0.10.0 // indirect
|
||||
golang.org/x/sys v0.13.0 // indirect
|
||||
golang.org/x/term v0.13.0 // indirect
|
||||
golang.org/x/text v0.13.0 // indirect
|
||||
golang.org/x/time v0.3.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.31.0 // 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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
+3
-16
@@ -22,6 +22,7 @@ import (
|
||||
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,28 +49,16 @@ var (
|
||||
"dangerousCapabilities",
|
||||
"insecureCapabilities",
|
||||
"priorityClassNotSet",
|
||||
"linuxHardening",
|
||||
"sensitiveContainerEnvVar",
|
||||
// Other checks
|
||||
"tlsSettingsMissing",
|
||||
"pdbDisruptionsIsZero",
|
||||
"metadataAndInstanceMismatched",
|
||||
"metadataAndNameMismatched",
|
||||
"missingPodDisruptionBudget",
|
||||
"missingNetworkPolicy",
|
||||
"sensitiveConfigmapContent",
|
||||
"clusterrolePodExecAttach",
|
||||
"rolePodExecAttach",
|
||||
"clusterrolebindingPodExecAttach",
|
||||
"rolebindingClusterRolePodExecAttach",
|
||||
"rolebindingRolePodExecAttach",
|
||||
"clusterrolebindingClusterAdmin",
|
||||
"rolebindingClusterAdminClusterRole",
|
||||
"rolebindingClusterAdminRole",
|
||||
}
|
||||
)
|
||||
|
||||
func init() {
|
||||
schemaBox := packr.New("Schemas", "../../checks")
|
||||
schemaBox = packr.New("Schemas", "../../checks")
|
||||
for _, checkID := range checkOrder {
|
||||
contents, err := schemaBox.Find(checkID + ".yaml")
|
||||
if err != nil {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -19,8 +19,8 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/gobuffalo/packr/v2"
|
||||
@@ -70,10 +70,10 @@ func ParseFile(path string) (Configuration, error) {
|
||||
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
@@ -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
|
||||
}
|
||||
|
||||
@@ -204,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)
|
||||
@@ -219,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 {
|
||||
@@ -227,10 +224,8 @@ 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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
+14
-82
@@ -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,12 +157,7 @@ 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)
|
||||
break
|
||||
@@ -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)
|
||||
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)
|
||||
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
|
||||
}
|
||||
|
||||
+105
-120
@@ -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)
|
||||
}
|
||||
|
||||
@@ -146,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)
|
||||
|
||||
@@ -165,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
|
||||
}
|
||||
@@ -195,7 +210,7 @@ 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
|
||||
@@ -215,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 {
|
||||
@@ -275,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{})
|
||||
@@ -300,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)
|
||||
@@ -334,18 +334,13 @@ 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)
|
||||
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)
|
||||
return nil, err
|
||||
@@ -358,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
|
||||
@@ -437,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)
|
||||
@@ -448,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
@@ -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,4 +1,4 @@
|
||||
apiVersion: batch/v1
|
||||
apiVersion: batch/v1beta1
|
||||
kind: CronJob
|
||||
metadata:
|
||||
name: test
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
apiVersion: apps/v1
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: test-deployment
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
apiVersion: apps/v1
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: test-deployment-2
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
apiVersion: v1
|
||||
kind: ReplicationController
|
||||
metadata:
|
||||
name: test
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
app: test
|
||||
template:
|
||||
metadata:
|
||||
name: test
|
||||
labels:
|
||||
app: test
|
||||
spec:
|
||||
containers:
|
||||
- name: test
|
||||
image: busybox
|
||||
command: ["tail"]
|
||||
args: ["-f", "/dev/null"]
|
||||
@@ -6,7 +6,7 @@ metadata:
|
||||
name: polaris
|
||||
---
|
||||
# Source: polaris/templates/dashboard.deployment.yaml
|
||||
apiVersion: apps/v1
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
--
|
||||
# Source: polaris/templates/dashboard.deployment.yaml
|
||||
apiVersion: apps/v1
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Deployment
|
||||
::::
|
||||
|
||||
+70
-390
@@ -1,435 +1,115 @@
|
||||
package mutation
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
yaml "gopkg.in/yaml.v3"
|
||||
|
||||
jsonpatchV5 "github.com/evanphx/json-patch/v5"
|
||||
"github.com/fairwindsops/polaris/pkg/config"
|
||||
"github.com/fairwindsops/polaris/pkg/kube"
|
||||
"github.com/fairwindsops/polaris/pkg/validator"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/thoas/go-funk"
|
||||
"gomodules.xyz/jsonpatch/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
strTag = "!!str"
|
||||
seqTag = "!!seq"
|
||||
mapTag = "!!map"
|
||||
intTag = "!!int"
|
||||
boolTag = "!!bool"
|
||||
)
|
||||
|
||||
// ApplyAllMutations applies available mutation to a single resource
|
||||
func ApplyAllMutations(manifest string, mutations []config.Mutation) (string, error) {
|
||||
var mutated string
|
||||
var doc yaml.Node
|
||||
err := yaml.Unmarshal([]byte(manifest), &doc)
|
||||
// ApplyAllSchemaMutations applies available mutation to a single resource
|
||||
func ApplyAllSchemaMutations(conf *config.Configuration, resourceProvider *kube.ResourceProvider, resource kube.GenericResource, mutations []jsonpatch.Operation) (kube.GenericResource, error) {
|
||||
resByte := resource.OriginalObjectJSON
|
||||
var jsonByte []byte
|
||||
mutationByte, err := json.Marshal(mutations)
|
||||
if err != nil {
|
||||
return mutated, err
|
||||
return resource, err
|
||||
}
|
||||
|
||||
for _, patch := range mutations {
|
||||
splits := getSplitFromPath(patch.Path)
|
||||
valueNode, err := getNodeFromValue(patch.Value, patch.Comment)
|
||||
if err != nil {
|
||||
return mutated, err
|
||||
}
|
||||
switch patch.Op {
|
||||
case "add", "replace":
|
||||
err = addOrReplaceValue(&doc, splits, valueNode)
|
||||
if err != nil {
|
||||
return mutated, err
|
||||
}
|
||||
case "remove":
|
||||
// ignore error if the value specified does not exists
|
||||
_ = removeNodes(&doc, splits)
|
||||
}
|
||||
}
|
||||
|
||||
buf := bytes.Buffer{}
|
||||
enc := yaml.NewEncoder(&buf)
|
||||
enc.SetIndent(2)
|
||||
err = enc.Encode(&doc)
|
||||
patch, err := jsonpatchV5.DecodePatch(mutationByte)
|
||||
if err != nil {
|
||||
return mutated, err
|
||||
return resource, err
|
||||
}
|
||||
err = enc.Close()
|
||||
jsonByte, err = patch.Apply(resByte)
|
||||
if err != nil {
|
||||
return mutated, err
|
||||
return resource, err
|
||||
}
|
||||
mutated, err := kube.NewGenericResourceFromBytes(jsonByte)
|
||||
if err != nil {
|
||||
return resource, err
|
||||
}
|
||||
|
||||
return buf.String(), nil
|
||||
return mutated, nil
|
||||
}
|
||||
|
||||
// GetMutationsFromResults returns all mutations from results
|
||||
func GetMutationsFromResults(results []validator.Result) map[string][]config.Mutation {
|
||||
allMutationsFromResults := make(map[string][]config.Mutation)
|
||||
// GetMutationsAndCommentsFromResults returns all mutations from results
|
||||
func GetMutationsAndCommentsFromResults(results []validator.Result) ([]config.MutationComment, map[string][]jsonpatch.Operation) {
|
||||
allMutationsFromResults := make(map[string][]jsonpatch.Operation)
|
||||
comments := []config.MutationComment{}
|
||||
for _, result := range results {
|
||||
key := fmt.Sprintf("%s/%s/%s", result.Kind, result.Name, result.Namespace)
|
||||
mutations := GetMutationsFromResult(&result)
|
||||
|
||||
mutations, resultsComments := GetMutationsAndCommentsFromResult(&result)
|
||||
allMutationsFromResults[key] = mutations
|
||||
comments = append(comments, resultsComments...)
|
||||
|
||||
}
|
||||
return allMutationsFromResults
|
||||
return comments, allMutationsFromResults
|
||||
}
|
||||
|
||||
// GetMutationsFromResult returns all mutations from single result
|
||||
func GetMutationsFromResult(result *validator.Result) []config.Mutation {
|
||||
mutations := []config.Mutation{}
|
||||
// GetMutationsAndCommentsFromResult returns all mutations from single result
|
||||
func GetMutationsAndCommentsFromResult(result *validator.Result) ([]jsonpatch.Operation, []config.MutationComment) {
|
||||
mutations := []jsonpatch.Operation{}
|
||||
comments := []config.MutationComment{}
|
||||
for _, resultMessage := range result.Results {
|
||||
if len(resultMessage.Mutations) > 0 {
|
||||
mutations = append(mutations, resultMessage.Mutations...)
|
||||
}
|
||||
if len(resultMessage.Comments) > 0 {
|
||||
comments = append(comments, resultMessage.Comments...)
|
||||
}
|
||||
}
|
||||
|
||||
if result.PodResult != nil {
|
||||
for _, resultMessage := range result.PodResult.Results {
|
||||
for _, resultMessage := range result.PodResult.Results {
|
||||
if len(resultMessage.Mutations) > 0 {
|
||||
mutations = append(mutations, resultMessage.Mutations...)
|
||||
}
|
||||
if len(resultMessage.Comments) > 0 {
|
||||
comments = append(comments, resultMessage.Comments...)
|
||||
}
|
||||
}
|
||||
|
||||
for _, containerResult := range result.PodResult.ContainerResults {
|
||||
for _, resultMessage := range containerResult.Results {
|
||||
if len(resultMessage.Mutations) > 0 {
|
||||
mutations = append(mutations, resultMessage.Mutations...)
|
||||
}
|
||||
}
|
||||
|
||||
for _, containerResult := range result.PodResult.ContainerResults {
|
||||
for _, resultMessage := range containerResult.Results {
|
||||
if len(resultMessage.Mutations) > 0 {
|
||||
mutations = append(mutations, resultMessage.Mutations...)
|
||||
}
|
||||
if len(resultMessage.Comments) > 0 {
|
||||
comments = append(comments, resultMessage.Comments...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mutations
|
||||
return mutations, comments
|
||||
}
|
||||
|
||||
func createPathAndFindNodes(node *yaml.Node, selectors []string, create bool) ([]*yaml.Node, error) {
|
||||
var nodes []*yaml.Node
|
||||
currentSelector := selectors[0]
|
||||
isLastSelector := len(selectors) == 1
|
||||
// array[N] or array[*] selectors.
|
||||
if i := strings.LastIndex(currentSelector, "["); i > 0 && strings.HasSuffix(currentSelector, "]") {
|
||||
arrayIndex := currentSelector[i+1 : len(currentSelector)-1]
|
||||
currentSelector = currentSelector[:i]
|
||||
if checkIfNodeExistedInContent(node.Content, currentSelector) || !create {
|
||||
return findArrayNodes(selectors, currentSelector, node, nodes, arrayIndex, create)
|
||||
}
|
||||
index, err := strconv.Atoi(arrayIndex)
|
||||
if err != nil {
|
||||
if arrayIndex != "-" {
|
||||
return nil, errors.Wrapf(err, "can't parse array index from %v/%v/", currentSelector, arrayIndex)
|
||||
}
|
||||
// if index provided is greater than or less than 0 for an empty array should throw an exception
|
||||
} else if index != 0 {
|
||||
return nil, errors.Errorf("array index (%s) does not exists because array (%s) does not exists", arrayIndex, currentSelector)
|
||||
}
|
||||
// default to zero since no node is present.
|
||||
selectorsToCreateNodes := []string{currentSelector, "0"}
|
||||
if len(selectors) > 1 {
|
||||
selectorsToCreateNodes = append(selectorsToCreateNodes, selectors[1:]...)
|
||||
return createNonExistingPath(selectorsToCreateNodes, node), nil
|
||||
}
|
||||
// UpdateMutatedContentWithComments Updates mutated object with comments
|
||||
func UpdateMutatedContentWithComments(yamlContent string, comments []config.MutationComment) string {
|
||||
var lines []string
|
||||
scanner := bufio.NewScanner(strings.NewReader(yamlContent))
|
||||
scanner.Split(bufio.ScanLines)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
lines = append(lines, line)
|
||||
}
|
||||
if currentSelector == "-" {
|
||||
if isLastSelector {
|
||||
return []*yaml.Node{node}, nil
|
||||
}
|
||||
_, err := createPathAndFindNodes(node, selectors[1:], create)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
commentMap := funk.Map(comments, func(c config.MutationComment) (string, string) {
|
||||
return c.Find, c.Comment
|
||||
}).(map[string]string)
|
||||
fileContent := ""
|
||||
for _, line := range lines {
|
||||
comment, ok := commentMap[strings.TrimSpace(line)]
|
||||
if ok {
|
||||
line += (" #" + comment)
|
||||
}
|
||||
fileContent += line
|
||||
fileContent += "\n"
|
||||
}
|
||||
switch node.Kind {
|
||||
case yaml.MappingNode:
|
||||
for i := 0; i < len(node.Content); i += 2 {
|
||||
// Does the current key match the selector?
|
||||
if node.Content[i].Value == currentSelector {
|
||||
// Found last key, return its value.
|
||||
if !isLastSelector {
|
||||
// Match the rest of the selector path, ie. go deeper
|
||||
// in to the value node.
|
||||
return createPathAndFindNodes(node.Content[i+1], selectors[1:], create)
|
||||
}
|
||||
return []*yaml.Node{node.Content[i+1]}, nil
|
||||
}
|
||||
}
|
||||
case yaml.ScalarNode:
|
||||
// Overwrite any existing nodes.
|
||||
node.Kind = yaml.MappingNode
|
||||
node.Tag = mapTag
|
||||
node.Value = ""
|
||||
case yaml.SequenceNode:
|
||||
return nil, errors.Errorf("parent node is array, use /*/ or /0/../%v/ instead of .%v to access its item(s) first", len(node.Content)-1, currentSelector)
|
||||
|
||||
default:
|
||||
return nil, errors.Errorf("parent node is of unknown kind %v", node.Kind)
|
||||
}
|
||||
if !create {
|
||||
return nil, errors.Errorf("failed to match %s", strings.Join(selectors, "/"))
|
||||
}
|
||||
|
||||
return createNonExistingPath(selectors, node), nil
|
||||
}
|
||||
|
||||
func addOrReplaceValue(node *yaml.Node, splits []string, value *yaml.Node) error {
|
||||
if len(node.Content) == 0 {
|
||||
return errors.New("No content in node")
|
||||
}
|
||||
nodes, err := createPathAndFindNodes(node.Content[0], splits, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, node := range nodes {
|
||||
if node.Kind == yaml.ScalarNode {
|
||||
// Overwrite an existing scalar value with a new value (whatever kind).
|
||||
*node = *value
|
||||
} else if node.Kind == yaml.MappingNode && value.Kind == yaml.MappingNode {
|
||||
// Append new values onto an existing map node.
|
||||
if len(value.Content) < 2 {
|
||||
logrus.Error("Found a mapping node without enough content")
|
||||
continue
|
||||
}
|
||||
mapKey := value.Content[0].Value
|
||||
found := false
|
||||
for idx, subNode := range node.Content {
|
||||
if idx%2 != 0 {
|
||||
continue
|
||||
}
|
||||
if subNode.Value == mapKey {
|
||||
found = true
|
||||
node.Content[idx+1] = value.Content[1]
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
node.Content = append(value.Content, node.Content...)
|
||||
}
|
||||
} else if node.Kind == yaml.MappingNode && node.Content == nil {
|
||||
// Overwrite a new map node we created in createPathAndFindNodes(), as confirmed
|
||||
// by the nil check (the node.Content wouldn't be nil otherwise).
|
||||
*node = *value
|
||||
} else if node.Kind == yaml.SequenceNode && value.Kind == yaml.SequenceNode {
|
||||
// Append new values onto an existing array node.
|
||||
node.Content = append(node.Content, value.Content...)
|
||||
} else if node.Kind == yaml.SequenceNode && value.Kind == yaml.ScalarNode {
|
||||
// Append new value onto an existing array node.
|
||||
node.Content = append(node.Content, value)
|
||||
} else {
|
||||
return errors.Errorf("can't overwrite %v value (line: %v, column: %v) with %v value", node.Tag, node.Line, node.Column, value.Tag)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getNodeFromValue(value interface{}, comment string) (*yaml.Node, error) {
|
||||
bytes, err := yaml.Marshal(value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var doc yaml.Node
|
||||
err = yaml.Unmarshal(bytes, &doc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(doc.Content) == 0 {
|
||||
return nil, errors.New("Generated an empty YAML document")
|
||||
}
|
||||
if doc.Content[0].Kind == yaml.MappingNode {
|
||||
doc.Content[0].Content[0].HeadComment = comment
|
||||
} else {
|
||||
doc.Content[0].LineComment = comment
|
||||
}
|
||||
return doc.Content[0], nil
|
||||
}
|
||||
|
||||
func removeNodes(doc *yaml.Node, selectors []string) error {
|
||||
err := removeMatchingNode(doc.Content[0], selectors)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to match %q", strings.Join(selectors, "/"))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func removeMatchingNode(node *yaml.Node, selectors []string) error {
|
||||
currentSelector := selectors[0]
|
||||
lastSelector := len(selectors) == 1
|
||||
if i := strings.LastIndex(currentSelector, "["); i > 0 && strings.HasSuffix(currentSelector, "]") {
|
||||
arrayIndex := currentSelector[i+1 : len(currentSelector)-1]
|
||||
currentSelector = currentSelector[:i]
|
||||
|
||||
index, err := strconv.Atoi(arrayIndex)
|
||||
if err != nil {
|
||||
if arrayIndex == "*" {
|
||||
index = -1
|
||||
} else {
|
||||
return errors.Wrapf(err, "can't parse array index from %v/%v/", currentSelector, arrayIndex)
|
||||
}
|
||||
} else if index < 0 {
|
||||
return errors.Wrapf(err, "array index can't be negative %v/%v/", currentSelector, arrayIndex)
|
||||
}
|
||||
// Go into array node(s).
|
||||
arrayNodes, err := createPathAndFindNodes(node, []string{currentSelector}, false)
|
||||
if err != nil {
|
||||
return errors.Errorf("can't find %v", currentSelector)
|
||||
}
|
||||
for _, arrayNode := range arrayNodes {
|
||||
if arrayNode.Kind != yaml.SequenceNode {
|
||||
return errors.Errorf("%v is not an array", currentSelector)
|
||||
}
|
||||
if index >= len(arrayNode.Content) {
|
||||
return errors.Errorf("%v array doesn't have index %v", currentSelector, index)
|
||||
}
|
||||
|
||||
var visitArrayNodes []*yaml.Node
|
||||
if index >= 0 { // array[N]
|
||||
visitArrayNodes = []*yaml.Node{arrayNode.Content[index]}
|
||||
} else { // array[*]
|
||||
visitArrayNodes = arrayNode.Content
|
||||
}
|
||||
for _, node := range visitArrayNodes {
|
||||
lastSelector := len(selectors) == 1
|
||||
if !lastSelector {
|
||||
removeMatchingNode(node, selectors[1:])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Iterate over the keys (the slice is key/value pairs).
|
||||
for i := 0; i < len(node.Content); i += 2 {
|
||||
if node.Content[i].Value == currentSelector {
|
||||
// Key matches the selector.
|
||||
if !lastSelector {
|
||||
// Try to match the rest of the selector path in the value.
|
||||
return removeMatchingNode(node.Content[i+1], selectors[1:])
|
||||
}
|
||||
|
||||
node.Content[i] = nil // Delete key.
|
||||
node.Content[i+1] = nil // Delete value.
|
||||
node.Content = append(node.Content[:i], node.Content[i+2:]...)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return errors.Errorf("can't find %q", strings.Join(selectors, "."))
|
||||
}
|
||||
|
||||
func getSplitFromPath(path string) []string {
|
||||
var digitStarCheck = regexp.MustCompile(`^[0-9*]+$`)
|
||||
splits := strings.Split(path, "/")
|
||||
var formatedSplit []string
|
||||
for _, key := range splits {
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
if digitStarCheck.MatchString(key) {
|
||||
lastElementIdx := len(formatedSplit) - 1
|
||||
lastElement := formatedSplit[lastElementIdx]
|
||||
lastElement = fmt.Sprintf("%s[%s]", lastElement, key)
|
||||
formatedSplit[lastElementIdx] = lastElement
|
||||
continue
|
||||
}
|
||||
formatedSplit = append(formatedSplit, key)
|
||||
}
|
||||
return formatedSplit
|
||||
}
|
||||
|
||||
func findArrayNodes(selectors []string, currentSelector string, node *yaml.Node, nodes []*yaml.Node, arrayIndex string, create bool) ([]*yaml.Node, error) {
|
||||
|
||||
index, err := strconv.Atoi(arrayIndex)
|
||||
if err != nil {
|
||||
if arrayIndex == "*" {
|
||||
index = -1
|
||||
} else {
|
||||
return nil, errors.Wrapf(err, "can't parse array index from %v/%v/", currentSelector, arrayIndex)
|
||||
}
|
||||
} else if index < 0 {
|
||||
return nil, errors.Wrapf(err, "array index can't be negative %v/%v/", currentSelector, arrayIndex)
|
||||
}
|
||||
// Go into array node(s).
|
||||
arrayNodes, err := createPathAndFindNodes(node, []string{currentSelector}, create)
|
||||
if err != nil {
|
||||
return nil, errors.Errorf("can't find %v", currentSelector)
|
||||
}
|
||||
for _, arrayNode := range arrayNodes {
|
||||
if arrayNode.Kind != yaml.SequenceNode {
|
||||
return nil, errors.Errorf("%v is not an array", currentSelector)
|
||||
}
|
||||
if index >= len(arrayNode.Content) {
|
||||
return nil, errors.Errorf("%v array doesn't have index %v", currentSelector, index)
|
||||
}
|
||||
|
||||
var visitArrayNodes []*yaml.Node
|
||||
if index >= 0 { // array[N]
|
||||
visitArrayNodes = []*yaml.Node{arrayNode.Content[index]}
|
||||
} else { // array[*]
|
||||
visitArrayNodes = arrayNode.Content
|
||||
}
|
||||
|
||||
for i, node := range visitArrayNodes {
|
||||
if len(selectors) == 1 {
|
||||
// Last selector, use this as final node.
|
||||
nodes = append(nodes, node)
|
||||
} else {
|
||||
// Go deeper into a specific array.
|
||||
deeperNodes, err := createPathAndFindNodes(node, selectors[1:], create)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to go deeper into %v/%v/", currentSelector, i)
|
||||
}
|
||||
nodes = append(nodes, deeperNodes...)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nodes, nil
|
||||
}
|
||||
|
||||
func checkIfNodeExistedInContent(nodes []*yaml.Node, currentSelector string) bool {
|
||||
for i := 0; i < len(nodes); i += 2 {
|
||||
// Does the current key match the selector?
|
||||
if nodes[i].Value == currentSelector {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func createNonExistingPath(selectors []string, node *yaml.Node) []*yaml.Node {
|
||||
var digitDashCheck = regexp.MustCompile(`^[0-9-]+$`)
|
||||
// Create the rest of the selector path.
|
||||
for idx, selector := range selectors {
|
||||
if digitDashCheck.MatchString(selector) {
|
||||
continue
|
||||
}
|
||||
kind, tag := yaml.MappingNode, mapTag
|
||||
// if the next selector is "-" then current selector is sequence/slice/array
|
||||
if idx < len(selectors)-1 && digitDashCheck.MatchString(selectors[idx+1]) {
|
||||
kind, tag = yaml.SequenceNode, seqTag
|
||||
}
|
||||
var newNode = yaml.Node{
|
||||
Content: []*yaml.Node{
|
||||
{
|
||||
Kind: yaml.ScalarNode,
|
||||
Tag: strTag,
|
||||
Value: selector,
|
||||
},
|
||||
{
|
||||
Kind: kind,
|
||||
Tag: tag,
|
||||
},
|
||||
},
|
||||
}
|
||||
// if previous node is array/sequenceNode append a node rather than appending contents
|
||||
if node.Kind == yaml.SequenceNode {
|
||||
newNode.Kind = kind
|
||||
node.Content = append(node.Content, &newNode)
|
||||
} else {
|
||||
node.Content = append(node.Content, newNode.Content...)
|
||||
}
|
||||
node = newNode.Content[len(newNode.Content)-1]
|
||||
}
|
||||
return []*yaml.Node{node}
|
||||
|
||||
return fileContent
|
||||
}
|
||||
|
||||
@@ -1,179 +0,0 @@
|
||||
package mutation
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/fairwindsops/polaris/pkg/config"
|
||||
)
|
||||
|
||||
var oldYaml = `
|
||||
pets:
|
||||
- name: fido
|
||||
owners:
|
||||
- name: Alice
|
||||
- name: Bob
|
||||
aliases:
|
||||
- Robert
|
||||
- name: scooby
|
||||
`
|
||||
|
||||
var testCases = []struct {
|
||||
original string
|
||||
mutated string
|
||||
patch config.Mutation
|
||||
message string
|
||||
}{
|
||||
{
|
||||
original: oldYaml,
|
||||
patch: config.Mutation{
|
||||
Op: "add",
|
||||
Value: "Denver",
|
||||
Path: "/pets/0/owners/*/location",
|
||||
},
|
||||
mutated: `pets:
|
||||
- name: fido
|
||||
owners:
|
||||
- name: Alice
|
||||
location: Denver
|
||||
- name: Bob
|
||||
aliases:
|
||||
- Robert
|
||||
location: Denver
|
||||
- name: scooby
|
||||
`,
|
||||
}, {
|
||||
original: oldYaml,
|
||||
patch: config.Mutation{
|
||||
Op: "remove",
|
||||
Path: "/pets/0/owners/*/aliases",
|
||||
},
|
||||
mutated: `pets:
|
||||
- name: fido
|
||||
owners:
|
||||
- name: Alice
|
||||
- name: Bob
|
||||
- name: scooby
|
||||
`,
|
||||
}, {
|
||||
original: oldYaml,
|
||||
patch: config.Mutation{
|
||||
Op: "add",
|
||||
Value: "rob",
|
||||
Path: "/pets/0/owners/*/aliases/-",
|
||||
},
|
||||
mutated: `pets:
|
||||
- name: fido
|
||||
owners:
|
||||
- name: Alice
|
||||
aliases:
|
||||
- rob
|
||||
- name: Bob
|
||||
aliases:
|
||||
- Robert
|
||||
- rob
|
||||
- name: scooby
|
||||
`,
|
||||
}, {
|
||||
original: `
|
||||
pets:
|
||||
- name: fido
|
||||
`,
|
||||
patch: config.Mutation{
|
||||
Op: "add",
|
||||
Value: "Alice",
|
||||
Path: "/pets/0/owners/0/name",
|
||||
},
|
||||
mutated: `pets:
|
||||
- name: fido
|
||||
owners:
|
||||
- name: Alice
|
||||
`,
|
||||
}, {
|
||||
original: `
|
||||
obj:
|
||||
foo:
|
||||
bar:
|
||||
- a
|
||||
- b
|
||||
baz: quux
|
||||
`,
|
||||
patch: config.Mutation{
|
||||
Op: "replace",
|
||||
Value: map[string]interface{}{
|
||||
"bar": []string{"c", "d"},
|
||||
},
|
||||
Path: "/obj/foo",
|
||||
},
|
||||
mutated: `obj:
|
||||
foo:
|
||||
bar:
|
||||
- c
|
||||
- d
|
||||
baz: quux
|
||||
`,
|
||||
}, {
|
||||
original: `
|
||||
foo: bar
|
||||
`,
|
||||
patch: config.Mutation{
|
||||
Op: "replace",
|
||||
Value: "baz",
|
||||
Path: "/foo",
|
||||
Comment: "# We set this to baz",
|
||||
},
|
||||
mutated: `
|
||||
foo: baz # We set this to baz
|
||||
`,
|
||||
message: "Expected a comment to appear",
|
||||
}, {
|
||||
original: `
|
||||
foo: bar
|
||||
`,
|
||||
patch: config.Mutation{
|
||||
Op: "add",
|
||||
Value: map[string]interface{}{
|
||||
"baz": "quux",
|
||||
},
|
||||
Path: "/extra",
|
||||
Comment: "# These are extra things",
|
||||
},
|
||||
mutated: `
|
||||
foo: bar
|
||||
extra:
|
||||
# These are extra things
|
||||
baz: quux
|
||||
`,
|
||||
message: "Expected a comment to appear next to an object",
|
||||
},
|
||||
{
|
||||
original: `
|
||||
foo: bar
|
||||
`,
|
||||
patch: config.Mutation{
|
||||
Op: "replace",
|
||||
Value: map[string]interface{}{
|
||||
"baz": "quux",
|
||||
},
|
||||
Path: "/extra",
|
||||
Comment: "# These are extra things",
|
||||
},
|
||||
mutated: `
|
||||
foo: bar
|
||||
extra:
|
||||
# These are extra things
|
||||
baz: quux
|
||||
`,
|
||||
message: "Expected a comment to appear next to an object",
|
||||
},
|
||||
}
|
||||
|
||||
func TestApplyAllMutations(t *testing.T) {
|
||||
for _, tc := range testCases {
|
||||
mutated, err := ApplyAllMutations(tc.original, []config.Mutation{tc.patch})
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, strings.TrimSpace(tc.mutated), strings.TrimSpace(mutated), tc.message)
|
||||
}
|
||||
}
|
||||
@@ -96,7 +96,7 @@ func TestControllerLevelChecks(t *testing.T) {
|
||||
|
||||
res, err := kube.CreateResourceProviderFromPath("../kube/test_files/test_1")
|
||||
assert.Equal(t, nil, err, "Error should be nil")
|
||||
assert.Equal(t, 10, res.Resources.GetLength())
|
||||
assert.Equal(t, 11, res.Resources.GetLength())
|
||||
testResources(res)
|
||||
|
||||
replicaSpec := map[string]interface{}{"replicas": 2}
|
||||
@@ -109,7 +109,7 @@ func TestControllerLevelChecks(t *testing.T) {
|
||||
two := int32(2)
|
||||
d2.Spec.Replicas = &two
|
||||
k8s, dynamicClient := test.SetupTestAPI(&d1, &p1, &d2, &p2)
|
||||
res, err = kube.CreateResourceProviderFromAPI(context.Background(), k8s, "test", dynamicClient, conf.Configuration{})
|
||||
res, err = kube.CreateResourceProviderFromAPI(context.Background(), k8s, "test", &dynamicClient, conf.Configuration{})
|
||||
assert.Equal(t, err, nil, "error should be nil")
|
||||
assert.Equal(t, 2, res.Resources.GetLength(), "Should have two controllers")
|
||||
testResources(res)
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
@@ -61,7 +62,7 @@ func RunAudit(config conf.Configuration, kubeResources *kube.ResourceProvider) (
|
||||
// ReadAuditFromFile reads the data from a past audit stored in a JSON or YAML file.
|
||||
func ReadAuditFromFile(fileName string) AuditData {
|
||||
auditData := AuditData{}
|
||||
oldFileBytes, err := os.ReadFile(fileName)
|
||||
oldFileBytes, err := ioutil.ReadFile(fileName)
|
||||
if err != nil {
|
||||
logrus.Errorf("Unable to read contents of loaded file: %v", err)
|
||||
os.Exit(1)
|
||||
|
||||
@@ -33,7 +33,7 @@ func TestGetTemplateData(t *testing.T) {
|
||||
}
|
||||
|
||||
k8s, dynamicClient := test.SetupTestAPI(test.GetMockControllers("test")...)
|
||||
resources, err := kube.CreateResourceProviderFromAPI(context.Background(), k8s, "test", dynamicClient, c)
|
||||
resources, err := kube.CreateResourceProviderFromAPI(context.Background(), k8s, "test", &dynamicClient, c)
|
||||
assert.Equal(t, err, nil, "error should be nil")
|
||||
assert.Equal(t, 5, len(resources.Resources))
|
||||
|
||||
|
||||
+8
-104
@@ -16,11 +16,11 @@ package validator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/thoas/go-funk"
|
||||
"gomodules.xyz/jsonpatch/v2"
|
||||
|
||||
"github.com/fairwindsops/polaris/pkg/config"
|
||||
)
|
||||
@@ -53,41 +53,12 @@ type AuditData struct {
|
||||
Score uint
|
||||
}
|
||||
|
||||
// FilterResultsBySeverityLevel includes results according to the provided severity level:
|
||||
// 'danger' is the least verbose, 'warning' is medium verbosity, default behavior will
|
||||
// include all results, which currently also includes 'ignore'
|
||||
func (res AuditData) FilterResultsBySeverityLevel(severityLevel config.Severity) AuditData {
|
||||
resCopy := res
|
||||
resCopy.Results = []Result{}
|
||||
|
||||
filteredResults := funk.Map(res.Results, func(auditDataResult Result) Result {
|
||||
return auditDataResult.filterResultsBySeverityLevel(severityLevel)
|
||||
}).([]Result)
|
||||
|
||||
for _, result := range filteredResults {
|
||||
if result.isNotEmpty() {
|
||||
resCopy.Results = append(resCopy.Results, result)
|
||||
}
|
||||
}
|
||||
|
||||
return resCopy
|
||||
}
|
||||
|
||||
// RemoveSuccessfulResults removes all tests that have passed
|
||||
// RemoveSuccessfulResults remove all test that have passed.
|
||||
func (res AuditData) RemoveSuccessfulResults() AuditData {
|
||||
resCopy := res
|
||||
resCopy.Results = []Result{}
|
||||
|
||||
filteredResults := funk.Map(res.Results, func(auditDataResult Result) Result {
|
||||
resCopy.Results = funk.Map(res.Results, func(auditDataResult Result) Result {
|
||||
return auditDataResult.removeSuccessfulResults()
|
||||
}).([]Result)
|
||||
|
||||
for _, result := range filteredResults {
|
||||
if result.isNotEmpty() {
|
||||
resCopy.Results = append(resCopy.Results, result)
|
||||
}
|
||||
}
|
||||
|
||||
return resCopy
|
||||
}
|
||||
|
||||
@@ -108,16 +79,13 @@ type ResultMessage struct {
|
||||
Success bool
|
||||
Severity config.Severity
|
||||
Category string
|
||||
Mutations []config.Mutation
|
||||
Mutations []jsonpatch.Operation
|
||||
Comments []config.MutationComment
|
||||
}
|
||||
|
||||
// ResultSet contiains the results for a set of checks
|
||||
type ResultSet map[string]ResultMessage
|
||||
|
||||
func (res ResultSet) isNotEmpty() bool {
|
||||
return len(res) > 0
|
||||
}
|
||||
|
||||
func (res ResultSet) removeSuccessfulResults() ResultSet {
|
||||
newResults := ResultSet{}
|
||||
for k, resultMessage := range res {
|
||||
@@ -128,25 +96,6 @@ func (res ResultSet) removeSuccessfulResults() ResultSet {
|
||||
return newResults
|
||||
}
|
||||
|
||||
func (res ResultSet) filterResultsBySeverityLevel(severityLevel config.Severity) ResultSet {
|
||||
newResults := ResultSet{}
|
||||
for k, resultMessage := range res {
|
||||
switch severityLevel {
|
||||
case config.SeverityDanger:
|
||||
if resultMessage.Severity == config.SeverityDanger {
|
||||
newResults[k] = resultMessage
|
||||
}
|
||||
case config.SeverityWarning:
|
||||
if resultMessage.Severity == config.SeverityDanger || resultMessage.Severity == config.SeverityWarning {
|
||||
newResults[k] = resultMessage
|
||||
}
|
||||
default:
|
||||
return res
|
||||
}
|
||||
}
|
||||
return newResults
|
||||
}
|
||||
|
||||
// Result provides results for a Kubernetes object
|
||||
type Result struct {
|
||||
Name string
|
||||
@@ -167,23 +116,6 @@ func (res Result) removeSuccessfulResults() Result {
|
||||
return resCopy
|
||||
}
|
||||
|
||||
func (res Result) filterResultsBySeverityLevel(severityLevel config.Severity) Result {
|
||||
resCopy := res
|
||||
resCopy.Results = res.Results.filterResultsBySeverityLevel(severityLevel)
|
||||
if res.PodResult != nil {
|
||||
podCopy := res.PodResult.filterResultsBySeverityLevel(severityLevel)
|
||||
resCopy.PodResult = &podCopy
|
||||
}
|
||||
return resCopy
|
||||
}
|
||||
|
||||
func (res Result) isNotEmpty() bool {
|
||||
if res.PodResult != nil {
|
||||
return res.PodResult.isNotEmpty()
|
||||
}
|
||||
return res.Results.isNotEmpty()
|
||||
}
|
||||
|
||||
// PodResult provides a list of validation messages for each pod.
|
||||
type PodResult struct {
|
||||
Name string
|
||||
@@ -200,24 +132,6 @@ func (res PodResult) removeSuccessfulResults() PodResult {
|
||||
return resCopy
|
||||
}
|
||||
|
||||
func (res PodResult) filterResultsBySeverityLevel(severityLevel config.Severity) PodResult {
|
||||
resCopy := PodResult{}
|
||||
resCopy.Results = res.Results.filterResultsBySeverityLevel(severityLevel)
|
||||
resCopy.ContainerResults = funk.Map(res.ContainerResults, func(containerResult ContainerResult) ContainerResult {
|
||||
return containerResult.filterResultsBySeverityLevel(severityLevel)
|
||||
}).([]ContainerResult)
|
||||
return resCopy
|
||||
}
|
||||
|
||||
func (res PodResult) isNotEmpty() bool {
|
||||
for _, cr := range res.ContainerResults {
|
||||
if cr.isNotEmpty() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return res.Results.isNotEmpty()
|
||||
}
|
||||
|
||||
// ContainerResult provides a list of validation messages for each container.
|
||||
type ContainerResult struct {
|
||||
Name string
|
||||
@@ -230,16 +144,6 @@ func (res ContainerResult) removeSuccessfulResults() ContainerResult {
|
||||
return resCopy
|
||||
}
|
||||
|
||||
func (res ContainerResult) filterResultsBySeverityLevel(severityLevel config.Severity) ContainerResult {
|
||||
resCopy := res
|
||||
resCopy.Results = res.Results.filterResultsBySeverityLevel(severityLevel)
|
||||
return resCopy
|
||||
}
|
||||
|
||||
func (res ContainerResult) isNotEmpty() bool {
|
||||
return res.Results.isNotEmpty()
|
||||
}
|
||||
|
||||
func fillString(id string, l int) string {
|
||||
for len(id) < l {
|
||||
id += " "
|
||||
@@ -250,7 +154,7 @@ func fillString(id string, l int) string {
|
||||
// GetPrettyOutput returns a human-readable string
|
||||
func (res AuditData) GetPrettyOutput(useColor bool) string {
|
||||
color.NoColor = !useColor
|
||||
str := titleColor.Sprint(fmt.Sprintf("Polaris audited %s %s at %s\n", res.SourceType, res.SourceName, res.AuditTime))
|
||||
str := titleColor.Sprint(fmt.Sprintf("\n\nPolaris audited %s %s at %s\n", res.SourceType, res.SourceName, res.AuditTime))
|
||||
str += color.CyanString(fmt.Sprintf(" Nodes: %d | Namespaces: %d | Controllers: %d\n", res.ClusterInfo.Nodes, res.ClusterInfo.Namespaces, res.ClusterInfo.Controllers))
|
||||
str += color.GreenString(fmt.Sprintf(" Final score: %d\n", res.Score))
|
||||
str += "\n"
|
||||
@@ -279,7 +183,7 @@ func (res Result) GetPrettyOutput() string {
|
||||
func (res PodResult) GetPrettyOutput() string {
|
||||
str := res.Results.GetPrettyOutput()
|
||||
for _, cont := range res.ContainerResults {
|
||||
str += cont.GetPrettyOutput()
|
||||
str += cont.GetPrettyOutput() + "\n"
|
||||
}
|
||||
return str
|
||||
}
|
||||
@@ -307,7 +211,7 @@ func (res ResultSet) GetPrettyOutput() string {
|
||||
}
|
||||
}
|
||||
if color.NoColor {
|
||||
status = strings.Fields(status)[1] // remove emoji
|
||||
status = status[2:] // remove emoji
|
||||
}
|
||||
str += fmt.Sprintf("%s%s %s\n", indent, checkColor.Sprint(fillString(msg.ID, minIDLength-len(indent))), status)
|
||||
str += fmt.Sprintf("%s %s - %s\n", indent, msg.Category, msg.Message)
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
// Copyright 2019 FairwindsOps Inc
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package validator
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
conf "github.com/fairwindsops/polaris/pkg/config"
|
||||
"github.com/fairwindsops/polaris/pkg/kube"
|
||||
"github.com/fairwindsops/polaris/test"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
)
|
||||
|
||||
func TestGetTemplateInputReturnsPolarisSubKeys(t *testing.T) {
|
||||
pod := test.MockPod() // Includes a container, required by GetPodSpec
|
||||
pod.Spec.NodeName = "testNodeName"
|
||||
pod.ObjectMeta.Name = "testpod"
|
||||
genRes, err := kube.NewGenericResourceFromPod(pod, pod)
|
||||
require.NoError(t, err, "creating new generic resource from a pod")
|
||||
schemaTest := schemaTestCase{
|
||||
Target: conf.TargetPodSpec, // ends up being set in the case of target: PodTemplate
|
||||
Resource: genRes,
|
||||
}
|
||||
|
||||
templateInput, err := getTemplateInput(schemaTest)
|
||||
require.NoError(t, err, "getting template input from a generic resource")
|
||||
require.NotNil(t, templateInput)
|
||||
nodeName, ok, err := unstructured.NestedString(templateInput, "Polaris", "PodSpec", "nodeName")
|
||||
require.NoError(t, err, "getting Polaris.PodSpec.nodeName from template input")
|
||||
require.True(t, ok, "getting Polaris.PodSpec.nodeName from template input")
|
||||
require.Equal(t, "testNodeName", nodeName, "the nodeName from template output")
|
||||
podName, ok, err := unstructured.NestedString(templateInput, "Polaris", "PodTemplate", "metadata", "name")
|
||||
require.NoError(t, err, "getting Polaris.PodTemplate.metadata.name from template input")
|
||||
require.True(t, ok, "getting Polaris.PodTemplate.metadata.name from template input")
|
||||
require.Equal(t, "testpod", podName, "the pod from template input")
|
||||
}
|
||||
+24
-104
@@ -24,9 +24,9 @@ import (
|
||||
"github.com/qri-io/jsonschema"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/thoas/go-funk"
|
||||
"gomodules.xyz/jsonpatch/v2"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
|
||||
"github.com/fairwindsops/polaris/pkg/config"
|
||||
"github.com/fairwindsops/polaris/pkg/kube"
|
||||
@@ -35,30 +35,11 @@ import (
|
||||
type schemaTestCase struct {
|
||||
Target config.TargetKind
|
||||
Resource kube.GenericResource
|
||||
IsInitContainer bool
|
||||
IsInitContianer bool
|
||||
Container *corev1.Container
|
||||
ResourceProvider *kube.ResourceProvider
|
||||
}
|
||||
|
||||
// ShortString supplies some fields of a schemaTestCase suitable for brief
|
||||
// output.
|
||||
func (s schemaTestCase) ShortString() string {
|
||||
var msg strings.Builder
|
||||
targetStr := s.Target
|
||||
if targetStr != "" {
|
||||
msg.WriteString(fmt.Sprintf("target=%s, ", targetStr))
|
||||
}
|
||||
ns := s.Resource.ObjectMeta.GetNamespace()
|
||||
if ns != "" {
|
||||
msg.WriteString(fmt.Sprintf("namespace=%s, ", ns))
|
||||
}
|
||||
msg.WriteString(fmt.Sprintf("resource=%s/%s", s.Resource.Kind, s.Resource.ObjectMeta.GetName()))
|
||||
if s.Target == config.TargetContainer {
|
||||
msg.WriteString(fmt.Sprintf(", container=%s", s.Container.Name))
|
||||
}
|
||||
return msg.String()
|
||||
}
|
||||
|
||||
func resolveCheck(conf *config.Configuration, checkID string, test schemaTestCase) (*config.SchemaCheck, error) {
|
||||
if !conf.DisallowExemptions &&
|
||||
!conf.DisallowAnnotationExemptions &&
|
||||
@@ -80,59 +61,16 @@ func resolveCheck(conf *config.Configuration, checkID string, test schemaTestCas
|
||||
if !conf.IsActionable(check.ID, test.Resource.ObjectMeta, containerName) {
|
||||
return nil, nil
|
||||
}
|
||||
if !check.IsActionable(test.Target, test.Resource.Kind, test.IsInitContainer) {
|
||||
if !check.IsActionable(test.Target, test.Resource.Kind, test.IsInitContianer) {
|
||||
return nil, nil
|
||||
}
|
||||
templateInput, err := getTemplateInput(test)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
checkPtr, err := check.TemplateForResource(templateInput)
|
||||
checkPtr, err := check.TemplateForResource(test.Resource.Resource.Object)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return checkPtr, nil
|
||||
}
|
||||
|
||||
// getTemplateInput augments a schemaTestCase.Resource.Resource.Object with
|
||||
// Polaris built-in variables. The result can be used as input for
|
||||
// CheckSchema.TemplateForResource().
|
||||
func getTemplateInput(test schemaTestCase) (map[string]interface{}, error) {
|
||||
templateInput := test.Resource.Resource.Object
|
||||
if templateInput == nil {
|
||||
return nil, nil
|
||||
}
|
||||
if test.Target == config.TargetPodSpec || test.Target == config.TargetContainer {
|
||||
podSpecMap, err := kube.SerializePodSpec(test.Resource.PodSpec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = unstructured.SetNestedMap(templateInput, podSpecMap, "Polaris", "PodSpec")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
podTemplateMap, ok := test.Resource.PodTemplate.(map[string]interface{})
|
||||
if ok {
|
||||
err := unstructured.SetNestedMap(templateInput, podTemplateMap, "Polaris", "PodTemplate")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if test.Target == config.TargetContainer {
|
||||
containerMap, err := kube.SerializeContainer(test.Container)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = unstructured.SetNestedMap(templateInput, containerMap, "Polaris", "Container")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
logrus.Debugf("the go template input for schema test-case %s is: %v", test.ShortString(), templateInput)
|
||||
return templateInput, nil
|
||||
}
|
||||
|
||||
func makeResult(conf *config.Configuration, check *config.SchemaCheck, passes bool, issues []jsonschema.ValError) ResultMessage {
|
||||
details := []string{}
|
||||
for _, issue := range issues {
|
||||
@@ -174,9 +112,6 @@ func hasExemptionAnnotation(objMeta metaV1.Object, checkID string) bool {
|
||||
// ApplyAllSchemaChecksToResourceProvider applies all available checks to a ResourceProvider
|
||||
func ApplyAllSchemaChecksToResourceProvider(conf *config.Configuration, resourceProvider *kube.ResourceProvider) ([]Result, error) {
|
||||
results := []Result{}
|
||||
if resourceProvider == nil {
|
||||
return nil, errors.New("No resource provider set, cannot apply schema checks")
|
||||
}
|
||||
for _, resources := range resourceProvider.Resources {
|
||||
kindResults, err := ApplyAllSchemaChecksToAllResources(conf, resourceProvider, resources)
|
||||
if err != nil {
|
||||
@@ -195,9 +130,7 @@ func ApplyAllSchemaChecksToAllResources(conf *config.Configuration, resourceProv
|
||||
if err != nil {
|
||||
return results, err
|
||||
}
|
||||
if result.Kind != "" && result.Name != "" {
|
||||
results = append(results, result)
|
||||
}
|
||||
results = append(results, result)
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
@@ -306,7 +239,7 @@ func applyContainerSchemaChecks(conf *config.Configuration, resources *kube.Reso
|
||||
ResourceProvider: resources,
|
||||
Resource: controller,
|
||||
Container: container,
|
||||
IsInitContainer: isInit,
|
||||
IsInitContianer: isInit,
|
||||
}
|
||||
return applySchemaChecks(conf, test)
|
||||
}
|
||||
@@ -355,9 +288,6 @@ func applySchemaCheck(conf *config.Configuration, checkID string, test schemaTes
|
||||
} else if check.Target == config.TargetPodSpec {
|
||||
passes, issues, err = check.CheckPodSpec(test.Resource.PodSpec)
|
||||
prefix = getJSONSchemaPrefix(test.Resource.Kind)
|
||||
} else if check.Target == config.TargetPodTemplate {
|
||||
passes, issues, err = check.CheckPodTemplate(test.Resource.PodTemplate)
|
||||
prefix = getJSONSchemaPrefix(test.Resource.Kind)
|
||||
} else if check.Target == config.TargetContainer {
|
||||
containerIndex := funk.IndexOf(test.Resource.PodSpec.Containers, func(value corev1.Container) bool {
|
||||
return value.Name == test.Container.Name
|
||||
@@ -377,17 +307,13 @@ func applySchemaCheck(conf *config.Configuration, checkID string, test schemaTes
|
||||
if !passes {
|
||||
break
|
||||
}
|
||||
if test.ResourceProvider == nil {
|
||||
logrus.Warnf("No ResourceProvider available, check %s will not work in this context (e.g. admission control)", checkID)
|
||||
break
|
||||
}
|
||||
resources := test.ResourceProvider.Resources[groupkind]
|
||||
namespace := test.Resource.ObjectMeta.GetNamespace()
|
||||
if test.Resource.Kind == "Namespace" {
|
||||
namespace = test.Resource.ObjectMeta.GetName()
|
||||
}
|
||||
resources = funk.Filter(resources, func(res kube.GenericResource) bool {
|
||||
return res.ObjectMeta.GetNamespace() == "" || res.ObjectMeta.GetNamespace() == namespace
|
||||
return res.ObjectMeta.GetNamespace() == namespace
|
||||
}).([]kube.GenericResource)
|
||||
objects := funk.Map(resources, func(res kube.GenericResource) interface{} {
|
||||
return res.Resource.Object
|
||||
@@ -397,24 +323,17 @@ func applySchemaCheck(conf *config.Configuration, checkID string, test schemaTes
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if len(issues) > 0 {
|
||||
issueMessages := make([]string, len(issues))
|
||||
for i, issue := range issues {
|
||||
issueMessages[i] = issue.Message
|
||||
}
|
||||
logrus.Debugf("there were %d issue(s) validating the schema for test-case %s: %v", len(issueMessages), test.ShortString(), issueMessages)
|
||||
} else {
|
||||
logrus.Debugf("there were no issues validating the schema for test-case %s", test.ShortString())
|
||||
|
||||
}
|
||||
result := makeResult(conf, check, passes, issues)
|
||||
if funk.Contains(conf.Mutations, checkID) && len(check.Mutations) > 0 {
|
||||
mutations := funk.Map(check.Mutations, func(mutation config.Mutation) config.Mutation {
|
||||
mutationCopy := deepCopyMutation(mutation)
|
||||
mutationCopy.Path = prefix + mutationCopy.Path
|
||||
return mutationCopy
|
||||
}).([]config.Mutation)
|
||||
result.Mutations = mutations
|
||||
if !passes {
|
||||
if funk.Contains(conf.Mutations, checkID) {
|
||||
mutations := funk.Map(check.Mutations, func(mutation jsonpatch.Operation) jsonpatch.Operation {
|
||||
mutationCopy := deepCopyMutation(mutation)
|
||||
mutationCopy.Path = prefix + mutationCopy.Path
|
||||
return mutationCopy
|
||||
}).([]jsonpatch.Operation)
|
||||
result.Mutations = mutations
|
||||
result.Comments = check.Comments
|
||||
}
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
@@ -428,12 +347,11 @@ func getSortedKeys(m map[string]config.Severity) []string {
|
||||
return keys
|
||||
}
|
||||
|
||||
func deepCopyMutation(source config.Mutation) config.Mutation {
|
||||
destination := config.Mutation{
|
||||
Op: source.Op,
|
||||
Path: source.Path,
|
||||
Value: source.Value,
|
||||
Comment: source.Comment,
|
||||
func deepCopyMutation(source jsonpatch.Operation) jsonpatch.Operation {
|
||||
destination := jsonpatch.Operation{
|
||||
Operation: source.Operation,
|
||||
Path: source.Path,
|
||||
Value: source.Value,
|
||||
}
|
||||
return destination
|
||||
}
|
||||
@@ -446,6 +364,8 @@ func getJSONSchemaPrefix(kind string) (prefix string) {
|
||||
} else if (kind == "Deployment") || (kind == "DaemonSet") ||
|
||||
(kind == "StatefulSet") || (kind == "Job") || (kind == "ReplicationController") {
|
||||
prefix = "/spec/template/spec"
|
||||
} else {
|
||||
logrus.Warningf("Mutation for this this resource (%s) is not supported", kind)
|
||||
}
|
||||
return prefix
|
||||
}
|
||||
|
||||
+7
-42
@@ -21,12 +21,10 @@ import (
|
||||
"github.com/fairwindsops/polaris/pkg/mutation"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gomodules.xyz/jsonpatch/v2"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/manager"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
// Mutator mutate k8s resources.
|
||||
@@ -36,66 +34,33 @@ type Mutator struct {
|
||||
decoder *admission.Decoder
|
||||
}
|
||||
|
||||
var _ admission.Handler = &Mutator{}
|
||||
|
||||
// NewMutateWebhook creates a mutating admission webhook for the apiType.
|
||||
func NewMutateWebhook(mgr manager.Manager, c config.Configuration) {
|
||||
func NewMutateWebhook(mgr manager.Manager, mutator Mutator) {
|
||||
path := "/mutate"
|
||||
|
||||
mutator := Mutator{
|
||||
Client: mgr.GetClient(),
|
||||
decoder: admission.NewDecoder(runtime.NewScheme()),
|
||||
Config: c,
|
||||
}
|
||||
mgr.GetWebhookServer().Register(path, &webhook.Admission{Handler: &mutator})
|
||||
}
|
||||
|
||||
func (m *Mutator) mutate(req admission.Request) ([]jsonpatch.Operation, error) {
|
||||
results, kubeResources, err := GetValidatedResults(req.AdmissionRequest.Kind.Kind, m.decoder, req, m.Config)
|
||||
results, err := getValidateResults(req.AdmissionRequest.Kind.Kind, m.decoder, req, m.Config)
|
||||
if err != nil {
|
||||
logrus.Errorf("Error while validating resource: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
if results == nil {
|
||||
logrus.Infof("Not mutating owned pod")
|
||||
return nil, nil
|
||||
}
|
||||
patches := mutation.GetMutationsFromResult(results)
|
||||
originalYaml, err := yaml.JSONToYAML(kubeResources.OriginalObjectJSON)
|
||||
if err != nil {
|
||||
logrus.Errorf("Failed to convert JSON to YAML: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
mutatedYamlStr, err := mutation.ApplyAllMutations(string(originalYaml), patches)
|
||||
if err != nil {
|
||||
logrus.Errorf("Failed to apply mutations: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mutatedJson, err := yaml.YAMLToJSON([]byte(mutatedYamlStr))
|
||||
if err != nil {
|
||||
logrus.Errorf("Failed to convert YAML to JSON: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ops, err := jsonpatch.CreatePatch(kubeResources.OriginalObjectJSON, mutatedJson)
|
||||
if err != nil {
|
||||
logrus.Errorf("Failed to create patch from mutation: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
return ops, nil
|
||||
patches, _ := mutation.GetMutationsAndCommentsFromResult(results)
|
||||
return patches, nil
|
||||
}
|
||||
|
||||
// Handle for Validator to run validation checks.
|
||||
func (m *Mutator) Handle(ctx context.Context, req admission.Request) admission.Response {
|
||||
logrus.Info("Starting mutation request")
|
||||
logrus.Info("Starting request")
|
||||
patches, err := m.mutate(req)
|
||||
if err != nil {
|
||||
logrus.Errorf("Error while getting mutations: %v", err)
|
||||
return admission.Errored(403, err)
|
||||
}
|
||||
if patches == nil {
|
||||
logrus.Infof("No patches generated")
|
||||
return admission.Allowed("Allowed")
|
||||
}
|
||||
logrus.Infof("Generated %d patches", len(patches))
|
||||
return admission.Patched("", patches...)
|
||||
}
|
||||
|
||||
+26
-27
@@ -25,7 +25,6 @@ import (
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/manager"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook"
|
||||
@@ -39,58 +38,58 @@ type Validator struct {
|
||||
Config config.Configuration
|
||||
}
|
||||
|
||||
// InjectDecoder injects the decoder.
|
||||
func (v *Validator) InjectDecoder(d *admission.Decoder) error {
|
||||
logrus.Info("Injecting decoder")
|
||||
v.decoder = d
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ admission.Handler = &Validator{}
|
||||
|
||||
// NewValidateWebhook creates a validating admission webhook for the apiType.
|
||||
func NewValidateWebhook(mgr manager.Manager, c config.Configuration) {
|
||||
func NewValidateWebhook(mgr manager.Manager, validator Validator) {
|
||||
path := "/validate"
|
||||
validator := Validator{
|
||||
Client: mgr.GetClient(),
|
||||
decoder: admission.NewDecoder(runtime.NewScheme()),
|
||||
Config: c,
|
||||
}
|
||||
|
||||
mgr.GetWebhookServer().Register(path, &webhook.Admission{Handler: &validator})
|
||||
}
|
||||
|
||||
func (v *Validator) handleInternal(req admission.Request) (*validator.Result, kube.GenericResource, error) {
|
||||
return GetValidatedResults(req.AdmissionRequest.Kind.Kind, v.decoder, req, v.Config)
|
||||
func (v *Validator) handleInternal(req admission.Request) (*validator.Result, error) {
|
||||
return getValidateResults(req.AdmissionRequest.Kind.Kind, v.decoder, req, v.Config)
|
||||
}
|
||||
|
||||
// GetValidatedResults returns the validated results.
|
||||
func GetValidatedResults(kind string, decoder *admission.Decoder, req admission.Request, config config.Configuration) (*validator.Result, kube.GenericResource, error) {
|
||||
var resource kube.GenericResource
|
||||
func getValidateResults(kind string, decoder *admission.Decoder, req admission.Request, config config.Configuration) (*validator.Result, error) {
|
||||
var controller kube.GenericResource
|
||||
var err error
|
||||
if kind == "Pod" {
|
||||
if decoder == nil {
|
||||
panic("Decoder is nil!")
|
||||
}
|
||||
pod := corev1.Pod{}
|
||||
err := decoder.Decode(req, &pod)
|
||||
if err != nil {
|
||||
logrus.Errorf("Failed to decode pod: %v", err)
|
||||
return nil, resource, err
|
||||
return nil, err
|
||||
}
|
||||
if len(pod.ObjectMeta.OwnerReferences) > 0 {
|
||||
logrus.Infof("Allowing owned pod %s/%s to pass through webhook", pod.ObjectMeta.Namespace, pod.ObjectMeta.Name)
|
||||
return nil, resource, nil
|
||||
return nil, nil
|
||||
}
|
||||
resource, err = kube.NewGenericResourceFromPod(pod, pod)
|
||||
controller, err = kube.NewGenericResourceFromPod(pod, pod)
|
||||
} else {
|
||||
resource, err = kube.NewGenericResourceFromBytes(req.Object.Raw)
|
||||
controller, err = kube.NewGenericResourceFromBytes(req.Object.Raw)
|
||||
}
|
||||
if err != nil {
|
||||
logrus.Errorf("Failed to create resource: %v", err)
|
||||
return nil, resource, err
|
||||
return nil, err
|
||||
}
|
||||
resourceResult, err := validator.ApplyAllSchemaChecks(&config, nil, resource)
|
||||
// TODO: consider enabling multi-resource checks
|
||||
controllerResult, err := validator.ApplyAllSchemaChecks(&config, nil, controller)
|
||||
if err != nil {
|
||||
return nil, resource, err
|
||||
return nil, err
|
||||
}
|
||||
return &resourceResult, resource, nil
|
||||
return &controllerResult, nil
|
||||
}
|
||||
|
||||
// Handle for Validator to run validation checks.
|
||||
func (v *Validator) Handle(ctx context.Context, req admission.Request) admission.Response {
|
||||
logrus.Info("Starting admission request")
|
||||
result, _, err := v.handleInternal(req)
|
||||
logrus.Info("Starting request")
|
||||
result, err := v.handleInternal(req)
|
||||
if err != nil {
|
||||
logrus.Errorf("Error validating request: %v", err)
|
||||
return admission.Errored(http.StatusBadRequest, err)
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
# Wrap goreleaser by using envsubst on .goreleaser.yml,
|
||||
# and creating a temporary git tag.
|
||||
|
||||
function cleanup {
|
||||
if [ "${CIRCLE_TAG}" == "" ] ; then
|
||||
echo "${this_script} deleting git tag ${temporary_git_tag} for goreleaser"
|
||||
unset GORELEASER_CURRENT_TAG
|
||||
git tag -d ${temporary_git_tag}
|
||||
fi
|
||||
}
|
||||
|
||||
set -eE # errexit and errtrace
|
||||
trap 'cleanup' ERR
|
||||
this_script="$(basename $0)"
|
||||
hash envsubst
|
||||
hash goreleaser
|
||||
if [ "${TMPDIR}" == "" ] ; then
|
||||
export TMPDIR="/tmp"
|
||||
echo "${this_script} temporarily set the TMPDIR environment variable to ${TMPDIR}, used for a temporary GOBIN environment variable"
|
||||
fi
|
||||
|
||||
export skip_feature_docker_tags=false
|
||||
export skip_release=true
|
||||
if [ "${CIRCLE_TAG}" == "" ] ; then
|
||||
# Create a temporary tag for goreleaser, incrementing the last tag.
|
||||
last_git_tag="$(git describe --tags --abbrev=0 2>/dev/null)"
|
||||
if [ "${last_git_tag}" == "" ] ; then
|
||||
echo "${this_script} is unable to determine the last git tag so a temporary tag can be created, using: git describe --tags --abbrev=0"
|
||||
exit 1
|
||||
fi
|
||||
if [ "$(git config user.email)" == "" ] ; then
|
||||
# git will use this env var as its user.email.
|
||||
# git tag -m is used in case tags are manually pushed by accident,
|
||||
# however git tag -m requires an email.
|
||||
export EMAIL='goreleaser_ci@fairwinds.com'
|
||||
echo "${this_script} using ${EMAIL} temporarily as the git user.email"
|
||||
fi
|
||||
temporary_git_tag=$(echo "${last_git_tag}" | awk -F. '{$NF = $NF + 1;} 1' | sed 's/ /./g')-rc
|
||||
echo "${this_script} creating temporary git tag ${temporary_git_tag} for goreleaser, the last real tag is ${last_git_tag}"
|
||||
# The -f is included to overwrite existing tags, perhaps from previous CI jobs.
|
||||
git tag -f -m "temporary local tag for goreleaser" ${temporary_git_tag}
|
||||
export GORELEASER_CURRENT_TAG=${temporary_git_tag}
|
||||
# Use an adjusted git feature branch name as a docker tag.
|
||||
export feature_docker_tag=$(echo "${CIRCLE_BRANCH:0:26}" | sed 's/[^a-zA-Z0-9]/-/g' | sed 's/-\+$//')
|
||||
echo "${this_script} also using docker tag ${feature_docker_tag} since ${CIRCLE_BRANCH} is a feature branch"
|
||||
else
|
||||
export GORELEASER_CURRENT_TAG=${CIRCLE_TAG}
|
||||
echo "${this_script} setting skip_release to false, and skip_feature_docker_tags to true, because CIRCLE_TAG is set"
|
||||
export skip_feature_docker_tags=true
|
||||
export skip_release=false
|
||||
fi
|
||||
|
||||
echo "${this_script} using git tag ${GORELEASER_CURRENT_TAG}"
|
||||
# Only substitute specific variables, as goreleaser uses shell variable syntax
|
||||
# for its `signs` section `signature` and `artifact` variables.
|
||||
cat .goreleaser.yml.envsubst |envsubst '${skip_release} ${skip_feature_docker_tags} ${feature_docker_tag}' >.goreleaser.yml
|
||||
goreleaser $@
|
||||
if [ $? -eq 0 ] ; then
|
||||
echo "${this_script} removing the temporary .goreleaser.yml since goreleaser was successful"
|
||||
rm .goreleaser.yml # Keep git clean for additional goreleaser runs
|
||||
fi
|
||||
cleanup
|
||||
@@ -1,10 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
set -e
|
||||
# Install packr2 and generate packr boxes for Polaris.
|
||||
# IDeally this script is called with $GOBIN already set to a temporary
|
||||
# directory, where packr2 will be installed.
|
||||
if [ "x${GOBIN}" != "x" ] ; then
|
||||
PATH=$GOBIN:$PATH
|
||||
fi
|
||||
go install github.com/gobuffalo/packr/v2/packr2@latest
|
||||
packr2
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user