Compare commits

..
Author SHA1 Message Date
Robert Brennan aa0fa876d1 add debug 2022-12-13 08:53:47 -05:00
Robert Brennan feb0ed681a Merge branch 'master' into rb/fix-webhook 2022-12-13 08:50:46 -05:00
Robert Brennan 870ad9114b fix mutate test 2022-11-30 16:17:57 -05:00
Robert Brennan f872aee25c fix test 2022-11-29 15:37:05 -05:00
Robert Brennan f080ba55a8 fix mutations 2022-11-29 12:38:13 -05:00
Robert Brennan 1866a9649b fix test 2022-11-29 12:24:44 -05:00
Robert Brennan a809875682 add webhook test 2022-11-29 12:08:06 -05:00
185 changed files with 16555 additions and 8893 deletions
+37 -14
View File
@@ -1,7 +1,7 @@
version: 2.1
orbs:
rok8s: fairwinds/rok8s-scripts@14
rok8s: fairwinds/rok8s-scripts@11
oss-docs: fairwinds/oss-docs@0
executors:
@@ -14,6 +14,8 @@ commands:
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
@@ -42,20 +44,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
@@ -66,17 +68,18 @@ references:
./get_helm.sh
echo "Installing cert-manager"
kubectl create namespace cert-manager
helm repo add jetstack https://charts.jetstack.io
helm repo update
echo "Helm install"
helm install cert-manager jetstack/cert-manager --namespace cert-manager --version v1.12.1 --set "installCRDs=true" --wait --create-namespace
helm install cert-manager jetstack/cert-manager --namespace cert-manager --version 0.16.1 --set "installCRDs=true" --wait
echo "Install cert-manager successful"
test_binary_dashboard: &test_binary_dashboard
run:
name: Test Dashboard
command: |
go run main.go dashboard --port 3000 --audit-path ./pkg/config/examples &
go run main.go dashboard --port 3000 --audit-path ./examples &
sleep 30
curl -f http://localhost:3000 > /dev/null
curl -f http://localhost:3000/health > /dev/null
@@ -101,10 +104,10 @@ references:
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:
test_k8s:
working_directory: ~/polaris
@@ -112,13 +115,12 @@ jobs:
executor: vm
steps:
- checkout
- *set_environment_variables
- *install_k8s
- *test_k8s
test:
docker:
- image: cimg/go:1.26.2
- image: cimg/go:1.19
steps:
- checkout
- *set_environment_variables
@@ -126,6 +128,16 @@ jobs:
- run: go test ./... -coverprofile=coverage.txt -covermode=count
- *test_binary_dashboard
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
build_and_push:
working_directory: /go/src/github.com/fairwindsops/polaris/
resource_class: large
@@ -134,17 +146,18 @@ jobs:
# 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:v2.15.4
- image: goreleaser/goreleaser:v1.11.4
steps:
- checkout
- setup_remote_docker
- setup_remote_docker:
version: 20.10.11
- *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}"
- run: docker login quay.io -u="${fairwinds_quay_user}" -p="${fairwinds_quay_token}"
- install_goreleaser_dependencies
- run: scripts/goreleaser.sh
@@ -159,6 +172,16 @@ workflows:
only: /.*/
- build_and_push:
context: org-global
requires:
- test
filters:
branches:
ignore: /pull\/[0-9]+/
tags:
ignore: /^testing-.*/
- insights:
requires:
- build_and_push
filters:
branches:
ignore: /pull\/[0-9]+/
+1 -1
View File
@@ -1,6 +1,6 @@
# The action uses an own Dockerfile on purpose because the root Dockerfile takes way too long to build for an action
FROM alpine:3.23
FROM alpine:3.10
RUN apk add --no-cache \
bash \
+1 -1
View File
@@ -17,4 +17,4 @@ mkdir polaris
tar -xzf $TARGET_FILE -C polaris
rm $TARGET_FILE
echo "polaris" >> $GITHUB_PATH
echo "version=$INPUT_VERSION" >> $GITHUB_OUTPUT
echo "::set-output name=version::$INPUT_VERSION"
+2 -2
View File
@@ -7,7 +7,7 @@ jobs:
build-int:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6.0.2
- 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@v6.0.2
- uses: actions/checkout@v2
- name: Setup polaris
uses: fairwindsops/polaris/.github/actions/setup-polaris@master
with:
+3 -1
View File
@@ -1,3 +1,5 @@
# goreleaser is run via a wrapper that creates .goreleaser.yml from .goreleaser.yml.envsubst
.goreleaser.yml
# dist
# # Binaries for programs and plugins
.go-version
@@ -21,6 +23,7 @@ Tiltfile
main
.DS_Store
*-packr.go
dist
.vscode
@@ -28,4 +31,3 @@ dist
node_modules
/dist
docs/README.md
+21 -21
View File
@@ -1,4 +1,3 @@
version: 2
checksum:
name_template: 'checksums.txt'
changelog:
@@ -12,6 +11,7 @@ env:
before:
hooks:
- go mod download
- ./scripts/install-and-run-packr2.sh
builds:
- id: polaris
ldflags:
@@ -28,12 +28,8 @@ builds:
- arm
- arm64
goarm:
- "6"
- "7"
# Go does not support windows/arm (32-bit); only windows/arm64 is supported.
ignore:
- goos: windows
goarch: arm
- 6
- 7
archives:
- id: polaris
builds: ["polaris"]
@@ -43,8 +39,8 @@ signs:
args: ["sign-blob", "--key=hashivault://cosign", "-output-signature=${signature}", "${artifact}"]
artifacts: checksum
release:
# Branch/PR builds set GORELEASER_SKIP_RELEASE=true; tag builds set false.
disable: '{{ eq .Env.GORELEASER_SKIP_RELEASE "true" }}'
# 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).
@@ -54,13 +50,13 @@ release:
```
brews:
- name: polaris
skip_upload: '{{ eq .Env.GORELEASER_SKIP_RELEASE "true" }}'
repository:
# This is replaced using `envsubst`, depending on the git branch.
skip_upload: ${skip_release}
tap:
owner: FairwindsOps
name: homebrew-tap
directory: Formula
folder: Formula
description: Open Source Best Practices for Kubernetes
url_template: "https://github.com/FairwindsOps/polaris/releases/download/{{ .Tag }}/{{ .ArtifactName }}"
test: |
system "#{bin}/polaris version"
dockers:
@@ -79,35 +75,39 @@ dockers:
build_flag_templates:
- "--platform=linux/arm64"
docker_manifests:
# Create Docker manifests that make multiple architectures available within a tag,
# 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:{{ envOrDefault "FEATURE_DOCKER_TAG" "none" }}'
# Tag builds set GORELEASER_SKIP_FEATURE_DOCKER_TAGS=true; feature branches set false.
skip_push: '{{ eq .Env.GORELEASER_SKIP_FEATURE_DOCKER_TAGS "true" }}'
- 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
skip_push: '{{ eq .Env.GORELEASER_SKIP_RELEASE "true" }}'
# 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 }}
skip_push: '{{ eq .Env.GORELEASER_SKIP_RELEASE "true" }}'
# 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 }}
skip_push: '{{ eq .Env.GORELEASER_SKIP_RELEASE "true" }}'
# 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 }}
skip_push: '{{ eq .Env.GORELEASER_SKIP_RELEASE "true" }}'
# This is replaced using `envsubst`, depending on the git branch.
skip_push: ${skip_release}
image_templates:
- "quay.io/fairwinds/polaris:{{ .FullCommit }}-amd64"
- "quay.io/fairwinds/polaris:{{ .FullCommit }}-arm64"
+1 -1
View File
@@ -1,2 +1,2 @@
## DO NOT EDIT - Managed by Terraform
* @sudermanjr @jdesouza @vitorvezani
* @rbren @mggude
+2 -12
View File
@@ -1,16 +1,6 @@
FROM alpine:3.23.4
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"
FROM alpine:3.17
WORKDIR /usr/local/bin
# Install ca-certs
RUN apk -U upgrade
RUN apk --no-cache add ca-certificates
RUN addgroup -S polaris && adduser -u 1200 -S polaris -G polaris
+12 -2
View File
@@ -34,7 +34,13 @@ Check out the [documentation at docs.fairwinds.com](https://polaris.docs.fairwin
The goal of the Fairwinds Community is to exchange ideas, influence the open source roadmap,
and network with fellow Kubernetes users.
[Chat with us on Slack](https://join.slack.com/t/fairwindscommunity/shared_invite/zt-2na8gtwb4-DGQ4qgmQbczQyB2NlFlYQQ)
[Chat with us on Slack](https://join.slack.com/t/fairwindscommunity/shared_invite/zt-e3c6vj4l-3lIH6dvKqzWII5fSSFDi1g)
or
[join the user group](https://www.fairwinds.com/open-source-software-user-group) to get involved!
<a href="https://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
@@ -49,5 +55,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/insights),
[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://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>
@@ -3,8 +3,9 @@ failureMessage: The ServiceAccount will be automounted
category: Security
target: PodSpec
schema:
'$schema': https://json-schema.org/draft/2019-09/schema
'$schema': http://json-schema.org/draft-07/schema
type: object
required: ["serviceAccountName"]
properties:
serviceAccountName:
type: string
@@ -14,11 +15,12 @@ schema:
const: true
additionalSchemaStrings:
ServiceAccount: |
{{ if not (eq .Polaris.PodSpec.automountServiceAccountToken false) }}
type: object
required:
- metadata
{{ if not (eq .Polaris.PodSpec.automountServiceAccountToken false) }}
- automountServiceAccountToken
{{ end }}
properties:
metadata:
type: object
@@ -32,4 +34,3 @@ additionalSchemaStrings:
type: boolean
const: false
{{ end }}
{{ end }}
@@ -3,7 +3,7 @@ failureMessage: The ClusterRole allows Pods/exec or pods/attach
category: Security
target: rbac.authorization.k8s.io/ClusterRole
schemaString: |
'$schema': https://json-schema.org/draft/2019-09/schema
'$schema': http://json-schema.org/draft-07/schema
type: object
required: ["metadata", "rules"]
anyOf:
@@ -3,7 +3,7 @@ failureMessage: The ClusterRoleBinding references the default cluster-admin Clus
category: Security
target: rbac.authorization.k8s.io/ClusterRoleBinding
schemaString: |
'$schema': https://json-schema.org/draft/2019-09/schema
'$schema': http://json-schema.org/draft-07/schema
type: object
anyOf:
# Do not alert on default ClusterRoleBindings.
@@ -37,10 +37,9 @@ schemaString: |
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
# Do not alert on default ClusterRoleBindings.
{{ if and (ne .metadata.name "cluster-admin") (not (hasPrefix .metadata.name "system:")) (ne .metadata.name "gce:podsecuritypolicy:calico-sa") }}
required: ["metadata", "rules"]
allOf:
- properties:
@@ -87,4 +86,3 @@ additionalSchemaStrings:
- "patch"
- "delete"
{{ end }}
{{ end }}
@@ -3,7 +3,7 @@ failureMessage: The ClusterRoleBinding references a ClusterRole that allows Pods
category: Security
target: rbac.authorization.k8s.io/ClusterRoleBinding
schemaString: |
'$schema': https://json-schema.org/draft/2019-09/schema
'$schema': http://json-schema.org/draft-07/schema
type: object
anyOf:
# Do not alert on default ClusterRoleBindings.
@@ -37,7 +37,6 @@ 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:
@@ -81,4 +80,3 @@ additionalSchemaStrings:
- const: 'get'
- const: 'create'
{{ end }}
{{ end }}
@@ -6,7 +6,7 @@ containers:
exclude:
- initContainer
schema:
'$schema': https://json-schema.org/draft/2019-09/schema
'$schema': http://json-schema.org/draft-07/schema
type: object
required:
- resources
@@ -6,7 +6,7 @@ containers:
exclude:
- initContainer
schema:
'$schema': https://json-schema.org/draft/2019-09/schema
'$schema': http://json-schema.org/draft-07/schema
type: object
required:
- resources
@@ -3,7 +3,7 @@ failureMessage: Container should not have dangerous capabilities
category: Security
target: Container
schema:
'$schema': https://json-schema.org/draft/2019-09/schema
'$schema': http://json-schema.org/draft-07/schema
type: object
properties:
securityContext:
@@ -28,4 +28,4 @@ schema:
mutations:
- op: remove
path: /securityContext/capabilities/add
path: /securityContext/capabilities
@@ -6,7 +6,7 @@ controllers:
include:
- Deployment
schema:
'$schema': https://json-schema.org/draft/2019-09/schema
'$schema': http://json-schema.org/draft-07/schema
type: object
required:
- spec
@@ -3,7 +3,7 @@ failureMessage: Host IPC should not be configured
category: Security
target: PodSpec
schema:
'$schema': https://json-schema.org/draft/2019-09/schema
'$schema': http://json-schema.org/draft-07/schema
type: object
properties:
hostIPC:
@@ -3,7 +3,7 @@ failureMessage: Host network should not be configured
category: Security
target: PodSpec
schema:
'$schema': https://json-schema.org/draft/2019-09/schema
'$schema': http://json-schema.org/draft-07/schema
type: object
properties:
hostNetwork:
@@ -3,7 +3,7 @@ failureMessage: Host PID should not be configured
category: Security
target: PodSpec
schema:
'$schema': https://json-schema.org/draft/2019-09/schema
'$schema': http://json-schema.org/draft-07/schema
type: object
properties:
hostPID:
@@ -3,7 +3,7 @@ failureMessage: Host port should not be configured
category: Security
target: Container
schema:
'$schema': https://json-schema.org/draft/2019-09/schema
'$schema': http://json-schema.org/draft-07/schema
type: object
required:
properties:
@@ -3,7 +3,7 @@ failureMessage: Container should not have insecure capabilities
category: Security
target: Container
schema:
'$schema': https://json-schema.org/draft/2019-09/schema
'$schema': http://json-schema.org/draft-07/schema
type: object
required:
- securityContext
@@ -3,8 +3,8 @@ FailureMessage: Use one of AppArmor, Seccomp, SELinux, or dropping Linux Capabil
category: Security
target: Container
schemaString: |
'$schema': https://json-schema.org/draft/2019-09/schema
$defs:
'$schema': http://json-schema.org/draft-07/schema
definitions:
podOrContainerSeccompProfile:
type: object
{{ $podSeccompProfileType := .Polaris.PodSpec.securityContext.seccompProfile.type }}
@@ -83,7 +83,7 @@ schemaString: |
type: object
{{ else }}
anyOf:
- $ref: "#/$defs/podOrContainerSeccompProfile"
- $ref: "#/$defs/podOrContainerSELinuxOptions"
- $ref: "#/$defs/containerDropCapabilities"
- $ref: "#/definitions/podOrContainerSeccompProfile"
- $ref: "#/definitions/podOrContainerSELinuxOptions"
- $ref: "#/definitions/containerDropCapabilities"
{{ end}}
@@ -10,7 +10,7 @@ containers:
- initContainer
target: Container
schema:
'$schema': https://json-schema.org/draft/2019-09/schema
'$schema': http://json-schema.org/draft-07/schema
type: object
required:
- livenessProbe
@@ -6,7 +6,7 @@ containers:
exclude:
- initContainer
schema:
'$schema': https://json-schema.org/draft/2019-09/schema
'$schema': http://json-schema.org/draft-07/schema
type: object
required:
- resources
@@ -6,7 +6,7 @@ containers:
exclude:
- initContainer
schema:
'$schema': https://json-schema.org/draft/2019-09/schema
'$schema': http://json-schema.org/draft-07/schema
type: object
required:
- resources
+17
View File
@@ -0,0 +1,17 @@
successMessage: Label app.kubernetes.io/name matches metadata.name
failureMessage: Label app.kubernetes.io/name must match metadata.name
target: Controller
schema:
'$schema': http://json-schema.org/draft-07/schema
type: object
properties:
metadata:
type: object
required: ["labels"]
properties:
labels:
type: object
required: ["app.kubernetes.io/name"]
properties:
app.kubernetes.io/name:
const: "{{ .metadata.name }}"
@@ -3,7 +3,7 @@ failureMessage: A NetworkPolicy should match pod labels and contain applied egre
category: Security
target: PodTemplate
schema:
'$schema': https://json-schema.org/draft/2019-09/schema
'$schema': http://json-schema.org/draft-07/schema
type: object
properties:
metadata:
@@ -4,27 +4,17 @@ category: Reliability
target: Controller
controllers:
include:
- Deployment
- Deployment
schema:
"$schema": https://json-schema.org/draft/2019-09/schema#
'$schema': http://json-schema.org/draft-07/schema
type: object
required: [spec]
properties:
spec:
metadata:
type: object
required: [template]
properties:
template:
labels:
type: object
required: [metadata]
properties:
metadata:
type: object
required: [labels]
properties:
labels:
type: object
minProperties: 1
minProperties: 1
additionalSchemaStrings:
policy/PodDisruptionBudget: |
type: object
@@ -40,7 +30,7 @@ additionalSchemaStrings:
matchLabels:
type: object
anyOf:
{{ range $key, $value := .spec.template.metadata.labels }}
{{ range $key, $value := .metadata.labels }}
- properties:
"{{ $key }}":
type: string
@@ -4,8 +4,8 @@ category: Security
target: Container
schemaTarget: PodSpec
schema:
'$schema': https://json-schema.org/draft/2019-09/schema
$defs:
'$schema': http://json-schema.org/draft-07/schema
definitions:
goodSecurityContext:
type: object
anyOf:
@@ -25,13 +25,13 @@ schema:
- securityContext
properties:
securityContext:
$ref: "#/$defs/goodSecurityContext"
$ref: "#/definitions/goodSecurityContext"
containers:
type: array
items:
properties:
securityContext:
$ref: "#/$defs/notBadSecurityContext"
$ref: "#/definitions/notBadSecurityContext"
- properties:
containers:
type: array
@@ -40,7 +40,7 @@ schema:
- securityContext
properties:
securityContext:
$ref: "#/$defs/goodSecurityContext"
$ref: "#/definitions/goodSecurityContext"
mutations:
- op: add
path: /securityContext/readOnlyRootFilesystem
@@ -3,7 +3,7 @@ failureMessage: Voluntary evictions are not possible
category: Reliability
target: policy/PodDisruptionBudget
schema:
'$schema': https://json-schema.org/draft/2019-09/schema
'$schema': http://json-schema.org/draft-07/schema
type: object
required:
- spec
@@ -1,9 +1,9 @@
successMessage: Priority class has been set
failureMessage: Priority class should be set
category: Reliability
category: Security
target: PodSpec
schema:
'$schema': https://json-schema.org/draft/2019-09/schema
'$schema': http://json-schema.org/draft-07/schema
type: object
required:
- priorityClassName
@@ -4,8 +4,8 @@ category: Security
target: Container
schemaTarget: PodSpec
schema:
'$schema': https://json-schema.org/draft/2019-09/schema
$defs:
'$schema': http://json-schema.org/draft-07/schema
definitions:
goodSecurityContext:
type: object
anyOf:
@@ -25,13 +25,13 @@ schema:
- securityContext
properties:
securityContext:
$ref: "#/$defs/goodSecurityContext"
$ref: "#/definitions/goodSecurityContext"
containers:
type: array
items:
properties:
securityContext:
$ref: "#/$defs/notBadSecurityContext"
$ref: "#/definitions/notBadSecurityContext"
- properties:
containers:
type: array
@@ -40,7 +40,7 @@ schema:
- securityContext
properties:
securityContext:
$ref: "#/$defs/goodSecurityContext"
$ref: "#/definitions/goodSecurityContext"
mutations:
- op: add
@@ -3,7 +3,7 @@ failureMessage: Image pull policy should be "Always"
category: Reliability
target: Container
schema:
'$schema': https://json-schema.org/draft/2019-09/schema
'$schema': http://json-schema.org/draft-07/schema
required:
- imagePullPolicy
properties:
@@ -10,7 +10,7 @@ containers:
- initContainer
target: Container
schema:
'$schema': https://json-schema.org/draft/2019-09/schema
'$schema': http://json-schema.org/draft-07/schema
type: object
required:
- readinessProbe
@@ -3,7 +3,7 @@ failureMessage: The Role allows Pods/exec or pods/attach
category: Security
target: rbac.authorization.k8s.io/Role
schemaString: |
'$schema': https://json-schema.org/draft/2019-09/schema
'$schema': http://json-schema.org/draft-07/schema
type: object
required: ["metadata", "rules"]
anyOf:
@@ -3,7 +3,7 @@ failureMessage: The RoleBinding references the default cluster-admin ClusterRole
category: Security
target: rbac.authorization.k8s.io/RoleBinding
schemaString: |
'$schema': https://json-schema.org/draft/2019-09/schema
'$schema': http://json-schema.org/draft-07/schema
type: object
anyOf:
# Pass RoleBindings that point to a Role.
@@ -45,10 +45,10 @@ schemaString: |
const: "cluster-admin"
additionalSchemaStrings:
rbac.authorization.k8s.io/ClusterRole: |
type: object
# This schema is validated for all roleBindings, regardless of their roleRef.
{{ 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:
@@ -3,7 +3,7 @@ failureMessage: The RoleBinding references a Role with wildcard permissions
category: Security
target: rbac.authorization.k8s.io/RoleBinding
schemaString: |
'$schema': https://json-schema.org/draft/2019-09/schema
'$schema': http://json-schema.org/draft-07/schema
type: object
anyOf:
# Pass RoleBindings that point to a ClusterRole.
@@ -3,7 +3,7 @@ failureMessage: The RoleBinding references a ClusterRole that allows Pods/exec,
category: Security
target: rbac.authorization.k8s.io/RoleBinding
schemaString: |
'$schema': https://json-schema.org/draft/2019-09/schema
'$schema': http://json-schema.org/draft-07/schema
type: object
anyOf:
# Pass RoleBindings that point to a Role.
@@ -43,10 +43,10 @@ schemaString: |
minLength: 1
additionalSchemaStrings:
rbac.authorization.k8s.io/ClusterRole: |
type: object
# This schema is validated for all roleBindings, regardless of their roleRef.
{{ 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:
@@ -3,7 +3,7 @@ failureMessage: The RoleBinding references a Role that allows Pods/exec, allows
category: Security
target: rbac.authorization.k8s.io/RoleBinding
schemaString: |
'$schema': https://json-schema.org/draft/2019-09/schema
'$schema': http://json-schema.org/draft-07/schema
type: object
anyOf:
# Pass RoleBindings that point to a ClusterRole.
@@ -17,7 +17,7 @@ schemaString: |
const: "rbac.authorization.k8s.io"
kind:
type: string
const: "ClusterRole"
const: "Role"
# Do not alert on default RoleBindings.
- required: ["metadata"]
properties:
@@ -46,10 +46,10 @@ schemaString: |
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") }}
# This schema is validated for all roleBindings, regardless of their roleRef.
type: object
required: ["metadata", "rules"]
allOf:
- properties:
@@ -4,8 +4,8 @@ category: Security
target: Container
schemaTarget: PodSpec
schema:
'$schema': https://json-schema.org/draft/2019-09/schema
$defs:
'$schema': http://json-schema.org/draft-07/schema
definitions:
notBadSecurityContext:
type: object
properties:
@@ -15,13 +15,13 @@ schema:
type: object
properties:
securityContext:
$ref: "#/$defs/notBadSecurityContext"
$ref: "#/definitions/notBadSecurityContext"
containers:
type: array
items:
properties:
securityContext:
$ref: "#/$defs/notBadSecurityContext"
$ref: "#/definitions/notBadSecurityContext"
mutations:
- op: add
path: /securityContext/privileged
@@ -4,8 +4,8 @@ category: Security
target: Container
schemaTarget: PodSpec
schema:
'$schema': https://json-schema.org/draft/2019-09/schema
$defs:
'$schema': http://json-schema.org/draft-07/schema
definitions:
goodSecurityContext:
type: object
anyOf:
@@ -33,13 +33,13 @@ schema:
- securityContext
properties:
securityContext:
$ref: "#/$defs/goodSecurityContext"
$ref: "#/definitions/goodSecurityContext"
containers:
type: array
items:
properties:
securityContext:
$ref: "#/$defs/notBadSecurityContext"
$ref: "#/definitions/notBadSecurityContext"
# non-root specified at container level
- properties:
containers:
@@ -49,7 +49,7 @@ schema:
- securityContext
properties:
securityContext:
$ref: "#/$defs/goodSecurityContext"
$ref: "#/definitions/goodSecurityContext"
mutations:
- op: add
path: /securityContext/runAsNonRoot
@@ -3,7 +3,7 @@ failureMessage: Potentially sensitive content is detected in the ConfigMap keys
category: Security
target: /ConfigMap
schemaString: |
'$schema': https://json-schema.org/draft/2019-09/schema
'$schema': http://json-schema.org/draft-07/schema
type: object
required: ["metadata"]
properties:
@@ -3,16 +3,14 @@ failureMessage: The container sets potentially sensitive environment variables
category: Security
target: Container
schemaString: |
'$schema': https://json-schema.org/draft/2019-09/schema
'$schema': http://json-schema.org/draft-07/schema
type: object
properties:
env:
type: array
items:
type: object
anyOf:
- not:
required: ["value"]
oneOf:
- required: ["name", "value"]
properties:
name:
@@ -3,7 +3,7 @@ failureMessage: Image tag should be specified
category: Reliability
target: Container
schema:
'$schema': https://json-schema.org/draft/2019-09/schema
'$schema': http://json-schema.org/draft-07/schema
required:
- image
allOf:
@@ -3,7 +3,7 @@ failureMessage: Ingress does not have TLS configured
category: Security
target: networking.k8s.io/Ingress
schema:
'$schema': https://json-schema.org/draft/2019-09/schema
'$schema': http://json-schema.org/draft-07/schema
type: object
required:
- spec
+15 -41
View File
@@ -17,7 +17,6 @@ package cmd
import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"fmt"
"io"
@@ -43,12 +42,9 @@ var (
resourceToAudit string
useColor bool
helmChart string
helmValues []string
helmSkipTests bool
helmValues string
checks []string
auditNamespace string
severityLevel string
skipSslValidation bool
)
func init() {
@@ -64,12 +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")
}
var auditCmd = &cobra.Command{
@@ -102,27 +95,26 @@ 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)
}
}
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)
}
auditData, err := validator.RunAudit(context.Background(), config, k)
auditData, err := validator.RunAudit(config, k)
if err != nil {
logrus.Errorf("Error while running audit on resources: %v", err)
os.Exit(1)
}
outputAudit(auditData, auditOutputFile, auditOutputURL, auditOutputFormat, useColor, onlyShowFailedTests, severityLevel)
outputAudit(auditData, auditOutputFile, auditOutputURL, auditOutputFormat, useColor, onlyShowFailedTests)
summary := auditData.GetSummary()
score := summary.GetScore()
@@ -137,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 {
@@ -151,16 +143,12 @@ func ProcessHelmTemplates(helmChart string, helmValues []string, helmSkipTests b
}
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...)
@@ -173,24 +161,14 @@ 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" {
outputBytes = fmt.Appendf(nil, "%d\n", auditData.GetSummary().GetScore())
outputBytes = []byte(fmt.Sprintf("%d\n", auditData.GetSummary().GetScore()))
} else if outputFormat == "yaml" {
var jsonBytes []byte
jsonBytes, err = json.Marshal(auditData)
@@ -224,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)
@@ -249,7 +223,7 @@ func outputAudit(auditData validator.AuditData, outputFile, outputURL, outputFor
}
if outputFile != "" {
err := os.WriteFile(outputFile, outputBytes, 0644)
err := os.WriteFile(outputFile, []byte(outputBytes), 0644)
if err != nil {
logrus.Errorf("Error writing output to file: %v", err)
os.Exit(1)
+1 -6
View File
@@ -15,7 +15,6 @@
package cmd
import (
"context"
"fmt"
"net/http"
@@ -55,11 +54,7 @@ var dashboardCmd = &cobra.Command{
auditData := validator.ReadAuditFromFile(loadAuditFile)
auditDataPtr = &auditData
}
router, err := dashboard.GetRouter(context.Background(), config, auditPath, serverPort, basePath, auditDataPtr)
if err != nil {
logrus.Fatalf("error creating router: %v", err)
}
router := dashboard.GetRouter(config, auditPath, serverPort, basePath, auditDataPtr)
router.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("OK"))
})
+147 -8
View File
@@ -15,18 +15,26 @@
package cmd
import (
"context"
"errors"
"fmt"
"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"
)
const templateLineMarker = "# POLARIS_FIX_TMPL"
const templateOpenMarker = "POLARIS_OPEN_TMPL"
const templateCloseMarker = "POLARIS_CLOSE_TMPL"
var (
filesPath string
checksToFix []string
fixAll bool
isTemplate bool
)
@@ -44,14 +52,145 @@ var fixCommand = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) {
logrus.Debug("Setting up controller manager")
err := fix.Execute(context.Background(), config, filesPath, isTemplate, checksToFix...)
if filesPath == "" {
logrus.Error("Please specify a files-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)
}
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 {
logrus.Fatalf("Error reading file with file path %s: %v", fullFilePath, err)
}
if err != nil {
logrus.Fatalf("Error marshalling %s: %v", fullFilePath, err)
}
if isTemplate {
yamlContent = []byte(detemplate(string(yamlContent)))
}
kubeResources := kube.CreateResourceProviderFromYaml(string(yamlContent))
results, err := validator.ApplyAllSchemaChecksToResourceProvider(&config, kubeResources)
if err != nil {
logrus.Fatalf("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 {
logrus.Errorf("Error applying schema mutations to the resource %s: %v", key, err)
os.Exit(1)
}
if updatedYamlContent != "" {
updatedYamlContent += "\n---\n"
}
updatedYamlContent += mutatedYamlContent
}
}
}
if isTemplate {
updatedYamlContent = retemplate(updatedYamlContent)
}
if updatedYamlContent != "" {
err = os.WriteFile(fullFilePath, []byte(updatedYamlContent), 0644)
if err != nil {
logrus.Fatalf("Error writing output to file: %v", err)
}
}
}
},
}
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
}
+7 -22
View File
@@ -16,26 +16,18 @@ package cmd
import (
"os"
"strings"
conf "github.com/fairwindsops/polaris/pkg/config"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
var (
mergeConfig bool
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
@@ -43,14 +35,12 @@ var (
func init() {
// Flags
rootCmd.PersistentFlags().BoolVarP(&mergeConfig, "merge-config", "m", false, "If true, custom configuration will be merged with default configuration instead of replacing it.")
rootCmd.PersistentFlags().StringVarP(&configPath, "config", "c", "", "Location of Polaris configuration file.")
rootCmd.PersistentFlags().StringVarP(&kubeContext, "context", "x", "", "Set the kube context.")
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")
}
var config conf.Configuration
@@ -67,7 +57,7 @@ var rootCmd = &cobra.Command{
logrus.SetLevel(parsedLevel)
}
config, err = conf.MergeConfigAndParseFile(configPath, mergeConfig)
config, err = conf.ParseFile(configPath)
if err != nil {
logrus.Errorf("Error parsing config at %s: %v", configPath, err)
os.Exit(1)
@@ -86,11 +76,6 @@ var rootCmd = &cobra.Command{
}
os.Exit(1)
},
PersistentPostRun: func(cmd *cobra.Command, args []string) {
if !strings.HasPrefix(cmd.Use, "audit") {
os.Stderr.WriteString("\n\nWant more? Automate Polaris for free with Fairwinds Insights!\n🚀 https://fairwinds.com/insights-signup/polaris 🚀 \n")
}
},
}
// Execute the stuff
-3
View File
@@ -31,7 +31,4 @@ var versionCmd = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Polaris version:" + version)
},
PersistentPostRunE: func(cmd *cobra.Command, args []string) error {
return nil
},
}
+7 -10
View File
@@ -15,7 +15,6 @@
package cmd
import (
"context"
"os"
"github.com/sirupsen/logrus"
@@ -25,7 +24,6 @@ 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
@@ -51,12 +49,8 @@ var webhookCmd = &cobra.Command{
logrus.Debug("Setting up controller manager")
mgr, err := manager.New(k8sConfig.GetConfigOrDie(), manager.Options{
WebhookServer: webhook.NewServer(webhook.Options{
CertDir: certDir,
Port: webhookPort,
CertName: "tls.crt",
KeyName: "tls.key",
}),
CertDir: certDir,
Port: webhookPort,
})
if err != nil {
logrus.Errorf("Unable to set up overall controller manager: %v", err)
@@ -67,6 +61,9 @@ var webhookCmd = &cobra.Command{
if os.IsNotExist(err) {
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")
@@ -74,10 +71,10 @@ var webhookCmd = &cobra.Command{
}
if enableValidations {
fwebhook.NewValidateWebhook(mgr, config)
fwebhook.NewValidateWebhook(mgr, fwebhook.Validator{Config: config, Client: mgr.GetClient()})
}
if enableMutations {
fwebhook.NewMutateWebhook(context.Background(), mgr, config)
fwebhook.NewMutateWebhook(mgr, fwebhook.Mutator{Config: config, Client: mgr.GetClient()})
}
logrus.Infof("Polaris webhook server listening on port %d", webhookPort)
if err := mgr.Start(signals.SetupSignalHandler()); err != nil {
+29 -10
View File
@@ -11,16 +11,6 @@ var sf14gv = 32793;
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(sf14g, s);
})();
(function() {
var gtag = document.createElement('script');
gtag.src = "https://www.googletagmanager.com/gtag/js?id=G-ZR5M5SRYKY";
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(gtag, s);
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-ZR5M5SRYKY');
})();
!function(f,b,e,v,n,t,s)
{if(f.fbq)return;n=f.fbq=function(){n.callMethod?
n.callMethod.apply(n,arguments):n.queue.push(arguments)};
@@ -31,3 +21,32 @@ s.parentNode.insertBefore(t,s)}(window,document,'script',
'https://connect.facebook.net/en_US/fbevents.js');
fbq('init', '521127644762074');
fbq('track', 'PageView');
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-TM95WXQ');
!function() {
var t = window.driftt = window.drift = window.driftt || [];
if (!t.init) {
if (t.invoked) return void (window.console && console.error && console.error("Drift snippet included twice."));
t.invoked = !0, t.methods = [ "identify", "config", "track", "reset", "debug", "show", "ping", "page", "hide", "off", "on" ],
t.factory = function(e) {
return function() {
var n = Array.prototype.slice.call(arguments);
return n.unshift(e), t.push(n), t;
};
}, t.methods.forEach(function(e) {
t[e] = t.factory(e);
}), t.load = function(t) {
var e = 3e5, n = Math.ceil(new Date() / e) * e, o = document.createElement("script");
o.type = "text/javascript", o.async = !0, o.crossorigin = "anonymous", o.src = "https://js.driftt.com/include/" + n + "/" + t + ".js";
var i = document.getElementsByTagName("script")[0];
i.parentNode.insertBefore(o, i);
};
}
}();
drift.SNIPPET_VERSION = '0.3.1';
drift.load('dp7v3zbc7xhm');
+2 -23
View File
@@ -53,30 +53,9 @@ 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.
Helm instlallation 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
By default, the only mutation enabled is `pullPolicyNotAlways`. If you'd like to
enable other mutations, you can set the `webhook.mutations` flag.
+1 -95
View File
@@ -5,100 +5,6 @@ meta:
content: "Fairwinds Polaris | Changelog"
---
## 9.1.1
* Fix `hpaMinAvailability` failure message
* Fix `missingPodDisruptionBudget` typo
* Rewrite `hpaMaxAvailability` check to use go-template
## 9.1.0
* Add HPA `minAvailable` and HPA `maxAvailable` checks
* Fix typo for PDB `minAvailable`
## 9.0.1
* Fix comments handling in `addOrReplaceValue` function
## 9.0.0
* Expose issue fixer and mutations in the library
* Remove `packr` in favor of `go:embed`
## 8.5.6
* Fix trying to list cluster-level resources
## 8.5.5
* Fix missing PDB check
## 8.5.4
* Fix conditional expressions should be at very top of `additionalSchemaStrings`
* Update alpine to 3.19
## 8.5.3
* Add quiet flag to polaris audit CLI command to suppress 'upload to Insights' prompt
## 8.5.2
* Switch to `controller-utils` package to get workloads
## 8.5.1
* Update `topologySpreadConstraint` check
## 8.5.0
* Add helm-skip-tests flag
* Update CLI docs
* Handle multiple helm-values files
## 8.4.0
* Change kubernetes.io/ label from name to instance
## 8.3.0
* Add option to filter audit results by severity level
* Add insights prompt
## 8.2.4
* Fix nil pointer issue with webhook
## 8.2.3
* Add category for `metadataAndNameMismatched`.
* Fix category for `priorityClassNotSet`.
## 8.2.2
* Fix webhook server cert dir argument
## 8.2.1
* Fix on Insights integration
## 8.2.0
* Minor fixes for NSA checks
## 8.1.0
* Add `insights-host` global flag to configure Fairwinds Insights host (defaults to `https://insights.fairwinds.com`).
* Add new `auth` sub-commands be able to authenticate on Polaris using Fairwinds Insights credentials
- `login` - login using Fairwinds Insights credentials via the web interface or provide a token
- `logout` - logout from Fairwinds Insights
- `status` - show relevant information regarding login state
- `token` - prints the token from local storage
* Add new `audit` flags to be able to upload Workloads and Polaris results to Fairwinds Insights
- `upload-insights` - indicates that the results should be uploaded to Fairwinds Insights. (defaults to `false`)
- `cluster-name` - cluster name that the results belongs to. Creates the cluster if it does not exist. (required if `upload-insights` is used)
## 8.0.0
* Change default severity from `ignore` to `warning` for `priorityClassNotSet`, `metadataAndNameMismatched`, `missingPodDisruptionBudget`, `automountServiceAccountToken`, `missingNetworkPolicy` checks.
* Change default severity from `warning` to `danger` for `sensitiveContainerEnvVar`, `sensitiveConfigmapContent`, `clusterrolePodExecAttach`, `rolePodExecAttach`, `clusterrolebindingPodExecAttach`, `rolebindingClusterRolePodExecAttach`, `rolebindingRolePodExecAttach`,`clusterrolebindingClusterAdmin`,`rolebindingClusterAdminClusterRole`,`rolebindingClusterAdminRole` checks.
## 7.4.0
* Skip https certificate verification (#920)
## 7.3.0
* Add a check for `topologySpreadConstraint` (#879)
## 7.2.0
* Enable new RBAC / sensitive content / Pod exec checks, add `hasPrefix` and `hasSuffix` functions to the GO template, exempt `system:` name prefixes for RBAC checks, sensitive content checks ignore `valueFrom`, (#832)
## 7.1.0
* Let Polaris modify YAML without losing comments/formatting (#821)
* Add checks for RBAC allowing exec or attaching to a Pod (#820)
* Add `clusterrolebindingClusterAdmin`, `rolebindingClusterAdminRole`, and `rolebindingClusterAdminClusterRole` checks + schema tests (#823)
## 7.0.2
* Fixes for pretty CLI output
* Some new checks (disabled by default)
@@ -251,7 +157,7 @@ JSON schema (see changes to `./checks/multipleReplicasForDeployment.yaml`)
* Docker image now includes the default config
### Breaking Changes
* Breaking changes in both input and output formats. See [Examples](https://github.com/FairwindsOps/polaris/tree/master/pkg/config/examples) for examples of the new formats.
* Breaking changes in both input and output formats. See [Examples](https://github.com/FairwindsOps/polaris/tree/master/examples) for examples of the new formats.
* removed config-level configuration for checks like max/min memory settings
* changed severity `error` to `danger`
* Breaking changes to the CLI
+2 -39
View File
@@ -14,58 +14,22 @@ key | default | description
`livenessProbeMissing` | `warning` | Fails when a liveness probe is not configured for a pod.
`tagNotSpecified` | `danger` | Fails when an image tag is either not specified or `latest`.
`pullPolicyNotAlways` | `warning` | Fails when an image pull policy is not `always`.
`priorityClassNotSet` | `warning` | Fails when a priorityClassName is not set for a pod.
`priorityClassNotSet` | `ignore` | Fails when a priorityClassName is not set for a pod.
`deploymentMissingReplicas` | `warning` | Fails when there is only one replica for a deployment.
`missingPodDisruptionBudget` | `warning` | Fails when PDB is missing.
`metadataAndInstanceMismatched` | `warning` | Fails when label `app.kubernetes.io/instance` and `metadata.name` mismatch
`topologySpreadConstraint` | `warning` | Fails when there is no topology spread constraint on the pod
`hpaMaxAvailability` | `warning` | Fails when `maxAvailable` lesser or equal than `minAvailable` (if defined) for a HorizontalPodAutoscaler
`hpaMinAvailability` | `warning` | Fails when `minAvailable` (if defined) lesser or equal to one for a HorizontalPodAutoscaler
`pdbMinAvailableGreaterThanHPAMinReplicas` | `warning` | Fails when PDB `minAvailable` is greater than HPA `minReplicas`
`missingPodDisruptionBudget` | `ignore`
## Background
### Liveness and Readiness Probes
Readiness and liveness probes can help maintain the health of applications running inside Kubernetes. By default, Kubernetes only knows whether or not a process is running, not if it's healthy. Properly configured readiness and liveness probes will also be able to ensure the health of an application.
Readiness probes are designed to ensure that an application has reached a "ready" state. In many cases there is a period of time between when a webserver process starts and when it is ready to receive traffic. A readiness probe can ensure the traffic is not sent to a pod until it is actually ready to receive traffic.
Liveness probes are designed to ensure that an application stays in a healthy state. When a liveness probe fails, the pod will be restarted.
### Image Pull Policy
Docker's `latest` tag is applied by default to images where a tag hasn't been specified. Not specifying a specific version of an image can lead to a wide variety of problems. The underlying image could include unexpected breaking changes that break your application whenever the latest image is pulled. Reusing the same tag for multiple versions of an image can lead to different nodes in the same cluster having different versions of an image, even if the tag is identical.
Related to that, relying on cached versions of a Docker image can become a security vulnerability. By default, an image will be pulled if it isn't already cached on the node attempting to run it. This can result in variations in images that are running per node, or potentially provide a way to gain access to an image without having direct access to the ImagePullSecret. With that in mind, it's often better to ensure the a pod has `pullPolicy: Always` specified, so images are always pulled directly from their source.
### Topology Spread Constraints
By default, the Kubernetes scheduler uses a bin-packing algorithm to fit as many pods as possible into a cluster. The scheduler prefers a more evenly distributed general node load to app replicas precisely spread across nodes. Therefore, by default, multi-replica is not guaranteed to be spread across multiple availability zones. Kubernetes provides topologySpreadConstraint configuration in order to better ensure pod spread across multiple AZs and/or Hosts.
Example of a topologySpreadConstraint spreading across zones:
```
apiVersion: apps/v1
kind: Deployment
metadata:
name: demo-basic-demo
spec:
selector:
matchLabels:
app.kubernetes.io/name: basic-demo
app.kubernetes.io/instance: demo
template:
metadata:
labels:
app.kubernetes.io/name: basic-demo
app.kubernetes.io/instance: demo
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: "topology.kubernetes.io/zone"
whenUnsatisfiable: ScheduleAnyway
```
## Further Reading
- [What's Wrong With The Docker :latest Tag?](https://vsupalov.com/docker-latest-tag/)
@@ -73,4 +37,3 @@ spec:
- [Kubernetes Docs: Configure Liveness and Readiness Probes](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/)
- [Utilizing Kubernetes Liveness and Readiness Probes to Automatically Recover From Failure](https://medium.com/spire-labs/utilizing-kubernetes-liveness-and-readiness-probes-to-automatically-recover-from-failure-2fe0314f2b2e)
- [Kubernetes Liveness and Readiness Probes: How to Avoid Shooting Yourself in the Foot](https://blog.colinbreck.com/kubernetes-liveness-and-readiness-probes-how-to-avoid-shooting-yourself-in-the-foot/)
- [Topology Spread Constraints](https://kubernetes.io/docs/concepts/scheduling-eviction/topology-spread-constraints/)
+2 -15
View File
@@ -11,30 +11,17 @@ for privilege escalation.
key | default | description
----|---------|------------
`automountServiceAccountToken` | `warning` | Fails when `automountServiceAccountToken` is automounted.
`hostIPCSet` | `danger` | Fails when `hostIPC` attribute is configured.
`hostPIDSet` | `danger` | Fails when `hostPID` attribute is configured.
`linuxHardening` | `danger` | Fails when neither `AppArmor`, `Seccomp`, `SELinux`, or dropping Linux Capabilities is in use.
`notReadOnlyRootFilesystem` | `warning` | Fails when `securityContext.readOnlyRootFilesystem` is not true.
`privilegeEscalationAllowed` | `danger` | Fails when `securityContext.allowPrivilegeEscalation` is true.
`runAsRootAllowed` | `warning` | Fails when `securityContext.runAsNonRoot` is not true.
`runAsPrivileged` | `danger` | Fails when `securityContext.privileged` is true.
`insecureCapabilities` | `warning` | Fails when `securityContext.capabilities` includes one of the capabilities [listed here](https://github.com/FairwindsOps/polaris/tree/master/pkg/config/checks/insecureCapabilities.yaml)
`dangerousCapabilities` | `danger` | Fails when `securityContext.capabilities` includes one of the capabilities [listed here](https://github.com/FairwindsOps/polaris/tree/master/pkg/config/checks/dangerousCapabilities.yaml)
`insecureCapabilities` | `warning` | Fails when `securityContext.capabilities` includes one of the capabilities [listed here](https://github.com/FairwindsOps/polaris/tree/master/checks/insecureCapabilities.yaml)
`dangerousCapabilities` | `danger` | Fails when `securityContext.capabilities` includes one of the capabilities [listed here](https://github.com/FairwindsOps/polaris/tree/master/checks/dangerousCapabilities.yaml)
`hostNetworkSet` | `warning` | Fails when `hostNetwork` attribute is configured.
`hostPortSet` | `warning` | Fails when `hostPort` attribute is configured.
`tlsSettingsMissing` | `warning` | Fails when an Ingress lacks TLS settings.
`sensitiveContainerEnvVar` | `danger` | Fails when the container sets potentially sensitive environment variables.
`sensitiveConfigmapContent` | `danger` | Fails when potentially sensitive content is detected in the ConfigMap keys or values.
`missingNetworkPolicy` | `warning`
`clusterrolePodExecAttach` | `danger` | Fails when the ClusterRole allows Pods/exec or pods/attach.
`rolePodExecAttach` | `danger` | Fails when the Role allows Pods/exec or pods/attach.
`clusterrolebindingPodExecAttach` | `danger` | Fails when the ClusterRoleBinding references a ClusterRole that allows Pods/exec, allows pods/attach, or that does not exist.
`rolebindingRolePodExecAttach` | `danger` | Fails when the RoleBinding references a Role that allows Pods/exec, allows pods/attach, or that does not exist.
`rolebindingClusterRolePodExecAttach` | `danger` | Fails when the RoleBinding references a ClusterRole that allows Pods/exec, allows pods/attach, or that does not exist.
`clusterrolebindingClusterAdmin` | `danger` | Fails when the ClusterRoleBinding references the default cluster-admin ClusterRole or one with wildcard permissions.
`rolebindingClusterAdminClusterRole` | `danger` | Fails when the RoleBinding references the default cluster-admin ClusterRole or one with wildcard permissions.
`rolebindingClusterAdminRole` | `danger` | Fails when the RoleBinding references a Role with wildcard permissions.
## Background
+3 -16
View File
@@ -11,14 +11,12 @@ audit
Runs a one-time audit.
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.
@@ -27,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
@@ -41,13 +38,12 @@ 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
--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.
@@ -56,19 +52,10 @@ webhook
--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
# 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)
```
+2 -1
View File
@@ -49,7 +49,8 @@ kind create cluster --wait=90s --image kindest/node:v1.15.11 --name polaris-test
docker build -t quay.io/fairwinds/polaris:debug . # or use your own registry
docker push quay.io/fairwinds/polaris:debug
helm repo add jetstack https://charts.jetstack.io
helm install cert-manager jetstack/cert-manager --namespace cert-manager --version v1.12.1 --set "installCRDs=true" --wait
kubectl create ns cert-manager
helm install cert-manager jetstack/cert-manager --namespace cert-manager --version 0.16.1 --set "installCRDs=true" --wait
POLARIS_IMAGE=quay.io/fairwinds/polaris:debug ./test/webhook_test.sh
```
to avoid the final cleanup for debugging purposes, you can run
+1 -1
View File
@@ -5,7 +5,7 @@ meta:
---
# Configuration
The default Polaris configuration can be [seen here](https://github.com/FairwindsOps/polaris/blob/master/pkg/config/default.yaml).
The default Polaris configuration can be [seen here](https://github.com/FairwindsOps/polaris/blob/master/examples/config.yaml).
You can customize the configuration to do things like:
* Turn checks [on and off](checks.md)
+6 -6
View File
@@ -7,7 +7,7 @@ meta:
If you'd like to create your own checks, you can use [JSON Schema](https://json-schema.org/).
This is how built-in Polaris checks are defined as well - you can see all the built-in checks
in the [checks folder](https://github.com/FairwindsOps/polaris/tree/master/pkg/config/checks) for examples.
in the [checks folder](https://github.com/FairwindsOps/polaris/tree/master/checks) for examples.
If you write a check that could be useful for others, feel free to open a PR to add it in!
@@ -25,7 +25,7 @@ customChecks:
category: Security
target: Container
schema:
'$schema': https://json-schema.org/draft/2019-09/schema
'$schema': http://json-schema.org/draft-07/schema
type: object
properties:
image:
@@ -73,7 +73,7 @@ customChecks:
category: Resources
target: Container
schema:
'$schema': https://json-schema.org/draft/2019-09/schema
'$schema': http://json-schema.org/draft-07/schema
type: object
required:
- resources
@@ -120,7 +120,7 @@ successMessage: Label app.kubernetes.io/name matches metadata.name
failureMessage: Label app.kubernetes.io/name must match metadata.name
target: Controller
schema:
'$schema': https://json-schema.org/draft/2019-09/schema
'$schema': http://json-schema.org/draft-07/schema
type: object
properties:
metadata:
@@ -193,7 +193,7 @@ controllers:
include:
- Deployment
schema:
'$schema': https://json-schema.org/draft/2019-09/schema
'$schema': http://json-schema.org/draft-07/schema
type: object
properties:
metadata:
@@ -233,7 +233,7 @@ customChecks:
foo:
jsonSchema: |
{
"$schema": "https://json-schema.org/draft/2019-09/schema",
"$schema": "http://json-schema.org/draft-07/schema",
"type": "object"
}
```
+14464 -5296
View File
File diff suppressed because it is too large Load Diff
@@ -6,38 +6,25 @@ checks:
pullPolicyNotAlways: warning
readinessProbeMissing: warning
livenessProbeMissing: warning
topologySpreadConstraint: warning
pdbDisruptionsIsZero: warning
missingPodDisruptionBudget: warning
metadataAndInstanceMismatched: warning
hpaMaxAvailability: warning
hpaMinAvailability: warning
pdbMinAvailableGreaterThanHPAMinReplicas: warning
# efficiency
cpuRequestsMissing: warning
cpuLimitsMissing: warning
memoryRequestsMissing: warning
memoryLimitsMissing: warning
# security
automountServiceAccountToken: warning
hostIPCSet: danger
hostPathSet: warning
hostProcess: warning
hostPIDSet: danger
linuxHardening: danger
missingNetworkPolicy: warning
notReadOnlyRootFilesystem: warning
privilegeEscalationAllowed: danger
procMount: warning
runAsRootAllowed: danger
runAsPrivileged: danger
dangerousCapabilities: danger
insecureCapabilities: warning
hostNetworkSet: danger
hostPortSet: warning
tlsSettingsMissing: warning
sensitiveContainerEnvVar: danger
sensitiveConfigmapContent: danger
clusterrolePodExecAttach: danger
@@ -52,6 +39,7 @@ checks:
resourceLimits: warning
imageRegistry: danger
exemptions:
- controllerNames:
- my-network-controller
@@ -75,7 +63,7 @@ customChecks:
category: Resources
target: Container
schema:
'$schema': https://json-schema.org/draft/2019-09/schema
'$schema': http://json-schema.org/draft-07/schema
type: object
required:
- resources
@@ -105,7 +93,7 @@ customChecks:
category: Images
target: Container
schema:
'$schema': https://json-schema.org/draft/2019-09/schema
'$schema': http://json-schema.org/draft-07/schema
type: object
properties:
image:
@@ -1,36 +1,28 @@
checks:
# reliability
deploymentMissingReplicas: warning
priorityClassNotSet: warning
priorityClassNotSet: ignore
tagNotSpecified: danger
pullPolicyNotAlways: warning
readinessProbeMissing: warning
livenessProbeMissing: warning
metadataAndInstanceMismatched: warning
metadataAndNameMismatched: ignore
pdbDisruptionsIsZero: warning
missingPodDisruptionBudget: warning
topologySpreadConstraint: warning
hpaMaxAvailability: warning
hpaMinAvailability: warning
pdbMinAvailableGreaterThanHPAMinReplicas: warning
missingPodDisruptionBudget: ignore
# efficiency
cpuRequestsMissing: warning
cpuLimitsMissing: warning
memoryRequestsMissing: warning
memoryLimitsMissing: warning
# security
automountServiceAccountToken: warning
automountServiceAccountToken: ignore
hostIPCSet: danger
hostPathSet: warning
hostProcess: warning
hostPIDSet: danger
linuxHardening: warning
missingNetworkPolicy: warning
missingNetworkPolicy: ignore
notReadOnlyRootFilesystem: warning
privilegeEscalationAllowed: danger
procMount: warning
runAsRootAllowed: danger
runAsPrivileged: danger
dangerousCapabilities: danger
@@ -38,16 +30,17 @@ 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
# These are initially warning and will later be promoted to danger.
sensitiveContainerEnvVar: warning
sensitiveConfigmapContent: warning
clusterrolePodExecAttach: warning
rolePodExecAttach: warning
clusterrolebindingPodExecAttach: warning
rolebindingClusterRolePodExecAttach: warning
rolebindingRolePodExecAttach: warning
clusterrolebindingClusterAdmin: warning
rolebindingClusterAdminClusterRole: warning
rolebindingClusterAdminRole: warning
mutations:
+81 -73
View File
@@ -1,87 +1,95 @@
module github.com/fairwindsops/polaris
go 1.26.2
go 1.19
require (
github.com/fairwindsops/controller-utils v0.3.4
github.com/fatih/color v1.19.0
github.com/gorilla/mux v1.8.1
github.com/pkg/errors v0.9.1
github.com/qri-io/jsonpointer v0.1.1
github.com/qri-io/jsonschema v0.2.1
github.com/sirupsen/logrus v1.9.4
github.com/spf13/cobra v1.10.2
github.com/stretchr/testify v1.11.1
github.com/thoas/go-funk v0.9.3
gomodules.xyz/jsonpatch/v2 v2.5.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/qri-io/jsonschema v0.1.1
github.com/sirupsen/logrus v1.9.0
github.com/spf13/cobra v1.6.1
github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/testify v1.8.1
github.com/thoas/go-funk v0.9.2
golang.org/x/text v0.4.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1
k8s.io/api v0.35.4
k8s.io/apimachinery v0.35.4
k8s.io/client-go v0.35.4
sigs.k8s.io/controller-runtime v0.23.3
sigs.k8s.io/yaml v1.6.0
k8s.io/api v0.25.3
k8s.io/apimachinery v0.25.3
k8s.io/client-go v0.25.3
sigs.k8s.io/controller-runtime v0.13.0
sigs.k8s.io/yaml v1.3.0
)
require (
github.com/pkg/errors v0.9.1
gomodules.xyz/jsonpatch/v2 v2.2.0
)
require (
cloud.google.com/go/compute v1.10.0 // indirect
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
github.com/Azure/go-autorest/autorest v0.11.28 // indirect
github.com/Azure/go-autorest/autorest/adal v0.9.21 // 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.3.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
github.com/evanphx/json-patch v5.9.0+incompatible // indirect
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.22.5 // indirect
github.com/go-openapi/jsonreference v0.21.5 // indirect
github.com/go-openapi/swag v0.25.5 // indirect
github.com/go-openapi/swag/cmdutils v0.25.5 // indirect
github.com/go-openapi/swag/conv v0.25.5 // indirect
github.com/go-openapi/swag/fileutils v0.25.5 // indirect
github.com/go-openapi/swag/jsonname v0.25.5 // indirect
github.com/go-openapi/swag/jsonutils v0.25.5 // indirect
github.com/go-openapi/swag/loading v0.25.5 // indirect
github.com/go-openapi/swag/mangling v0.25.5 // indirect
github.com/go-openapi/swag/netutils v0.25.5 // indirect
github.com/go-openapi/swag/stringutils v0.25.5 // indirect
github.com/go-openapi/swag/typeutils v0.25.5 // indirect
github.com/go-openapi/swag/yamlutils v0.25.5 // indirect
github.com/google/gnostic-models v0.7.1 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/inconshreveable/mousetrap v1.1.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.9.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.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.22.3 // indirect
github.com/gobuffalo/logger v1.0.7 // indirect
github.com/gobuffalo/packd v1.0.2 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v4 v4.4.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/gnostic v0.6.9 // indirect
github.com/google/go-cmp v0.5.9 // 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.0.1 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // 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.16 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_golang v1.23.2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/procfs v0.20.1 // indirect
github.com/samber/lo v1.53.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/x448/float16 v0.8.4 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/term v0.40.0 // indirect
golang.org/x/text v0.34.0 // indirect
golang.org/x/time v0.15.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.13.0 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.37.0 // indirect
github.com/prometheus/procfs v0.8.0 // indirect
github.com/qri-io/jsonpointer v0.1.1 // indirect
golang.org/x/crypto v0.0.0-20221012134737-56aed061732a // indirect
golang.org/x/net v0.0.0-20221017152216-f25eb7ecb193 // indirect
golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783 // indirect
golang.org/x/sys v0.1.0 // indirect
golang.org/x/term v0.0.0-20221017184919-83659145692c // indirect
golang.org/x/time v0.1.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
k8s.io/apiextensions-apiserver v0.35.4 // indirect
k8s.io/klog/v2 v2.140.0 // indirect
k8s.io/kube-openapi v0.0.0-20260304202019-5b3e3fdb0acf // indirect
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // indirect
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect
k8s.io/component-base v0.25.3 // indirect
k8s.io/klog/v2 v2.80.1 // indirect
k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 // indirect
k8s.io/utils v0.0.0-20221012122500-cfd413dd9e85 // indirect
sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
)
+847 -187
View File
File diff suppressed because it is too large Load Diff
+7 -18
View File
@@ -15,13 +15,14 @@
package config
import (
"embed"
"fmt"
"github.com/gobuffalo/packr/v2"
"github.com/sirupsen/logrus"
)
var (
// BuiltInChecks contains the checks that come pre-installed w/ Polaris
BuiltInChecks = map[string]SchemaCheck{}
schemaBox = (*packr.Box)(nil)
// We explicitly set the order to avoid thrash in the
// tests as we migrate toward JSON schema
checkOrder = []string{
@@ -29,14 +30,10 @@ var (
"deploymentMissingReplicas",
// Pod checks
"hostIPCSet",
"hostPathSet",
"hostProcess",
"hostPIDSet",
"hostNetworkSet",
"automountServiceAccountToken",
"topologySpreadConstraint",
// Container checks
"procMount",
"memoryLimitsMissing",
"memoryRequestsMissing",
"cpuLimitsMissing",
@@ -58,7 +55,7 @@ var (
// Other checks
"tlsSettingsMissing",
"pdbDisruptionsIsZero",
"metadataAndInstanceMismatched",
"metadataAndNameMismatched",
"missingPodDisruptionBudget",
"missingNetworkPolicy",
"sensitiveConfigmapContent",
@@ -70,21 +67,13 @@ var (
"clusterrolebindingClusterAdmin",
"rolebindingClusterAdminClusterRole",
"rolebindingClusterAdminRole",
"hpaMaxAvailability",
"hpaMinAvailability",
"pdbMinAvailableGreaterThanHPAMinReplicas",
}
// BuiltInChecks contains the checks that come pre-installed w/ Polaris
BuiltInChecks = map[string]SchemaCheck{}
//go:embed all:checks
checksFS embed.FS
)
func init() {
schemaBox = packr.New("Schemas", "../../checks")
for _, checkID := range checkOrder {
contents, err := checksFS.ReadFile(fmt.Sprintf("checks/%s.yaml", checkID))
contents, err := schemaBox.Find(checkID + ".yaml")
if err != nil {
panic(err)
}
-16
View File
@@ -1,16 +0,0 @@
successMessage: HostPath volumes are not configured
failureMessage: HostPath volumes must be forbidden
category: Security
target: PodSpec
schema:
'$schema': https://json-schema.org/draft/2019-09/schema
type: object
properties:
volumes:
type: array
items:
type: object
properties:
hostPath:
type: string
const: ''
-31
View File
@@ -1,31 +0,0 @@
successMessage: Privileged access to the host check is valid
failureMessage: Privileged access to the host is disallowed
category: Security
target: PodSpec
schema:
'$schema': https://json-schema.org/draft/2019-09/schema
type: object
properties:
containers:
type: array
items:
type: object
properties:
securityContext:
type: object
properties:
windowsOptions:
type: object
properties:
hostProcess:
type: boolean
const: false
securityContext:
type: object
properties:
windowsOptions:
type: object
properties:
hostProcess:
type: boolean
const: false
-35
View File
@@ -1,35 +0,0 @@
successMessage: HPA has a valid max and min replica configuration
failureMessage: HPA maxReplicas and minReplicas should be different
category: Reliability
target: autoscaling/HorizontalPodAutoscaler
schemaString: |
"$schema": https://json-schema.org/draft/2019-09/schema#
type: object
properties:
spec:
type: object
properties:
minReplicas:
type: integer
minimum: 1
maxReplicas:
type: integer
minimum: 1
required:
- maxReplicas
{{- if .spec.minReplicas }}
if:
properties:
minReplicas:
type: integer
maxReplicas:
type: integer
then:
properties:
maxReplicas:
exclusiveMinimum: {{ .spec.minReplicas }}
else:
properties:
maxReplicas:
minimum: 1
{{- end }}
-14
View File
@@ -1,14 +0,0 @@
successMessage: HPA has a valid min replica configuration
failureMessage: HPA minReplicas should be 2 or more
category: Reliability
target: autoscaling/HorizontalPodAutoscaler
schema:
"$schema": https://json-schema.org/draft/2019-09/schema#
type: object
properties:
spec:
type: object
properties:
minReplicas:
type: integer
minimum: 2
@@ -1,18 +0,0 @@
successMessage: Label app.kubernetes.io/instance matches metadata.name
failureMessage: Label app.kubernetes.io/instance must match metadata.name
category: Reliability
target: Controller
schema:
'$schema': https://json-schema.org/draft/2019-09/schema
type: object
properties:
metadata:
type: object
required: ["labels"]
properties:
labels:
type: object
required: ["app.kubernetes.io/instance"]
properties:
app.kubernetes.io/instance:
const: "{{ .metadata.name }}"
@@ -1,7 +0,0 @@
successMessage: PDB and HPA are correctly configured
failureMessage: PDB minAvailable is greater than HPA minReplicas
category: Reliability
target: Controller
controllers:
include:
- Deployment
-19
View File
@@ -1,19 +0,0 @@
successMessage: The default /proc masks are set up to reduce attack surface, and should be required
failureMessage: Proc mount must not be changed from the default
category: Security
target: PodSpec
schema:
'$schema': https://json-schema.org/draft/2019-09/schema
type: object
properties:
containers:
type: array
items:
type: object
properties:
securityContext:
type: object
properties:
procMount:
type: string
const: Default
@@ -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': https://json-schema.org/draft/2019-09/schema
type: object
required:
- topologySpreadConstraints
properties:
topologySpreadConstraints:
type: array
minItems: 1
-16
View File
@@ -1,16 +0,0 @@
package config
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestRequiredFieldsOnBuiltInChecks(t *testing.T) {
for _, v := range BuiltInChecks {
assert.NotEmpty(t, v.SuccessMessage)
assert.NotEmpty(t, v.FailureMessage)
assert.NotEmpty(t, v.Category)
assert.NotEmpty(t, v.Target)
}
}
+26 -43
View File
@@ -16,7 +16,6 @@ package config
import (
"bytes"
_ "embed"
"errors"
"fmt"
"io"
@@ -24,6 +23,7 @@ import (
"os"
"strings"
"github.com/gobuffalo/packr/v2"
"k8s.io/apimachinery/pkg/util/yaml"
)
@@ -49,55 +49,38 @@ type Exemption struct {
Namespace string `json:"namespace"`
}
//go:embed default.yaml
var defaultConfig []byte
var configBox = (*packr.Box)(nil)
// MergeConfigAndParseFile parses config from a file.
func MergeConfigAndParseFile(customConfigPath string, mergeConfig bool) (Configuration, error) {
rawBytes, err := mergeConfigFile(customConfigPath, mergeConfig)
func getConfigBox() *packr.Box {
if configBox == (*packr.Box)(nil) {
configBox = packr.New("Config", "../../examples")
}
return configBox
}
// ParseFile parses config from a file.
func ParseFile(path string) (Configuration, error) {
var rawBytes []byte
var err error
if path == "" {
rawBytes, err = getConfigBox().Find("config.yaml")
} else if strings.HasPrefix(path, "https://") || strings.HasPrefix(path, "http://") {
// path is a url
response, err2 := http.Get(path)
if err2 != nil {
return Configuration{}, err2
}
rawBytes, err = io.ReadAll(response.Body)
} else {
// path is local
rawBytes, err = os.ReadFile(path)
}
if err != nil {
return Configuration{}, err
}
return Parse(rawBytes)
}
func mergeConfigFile(customConfigPath string, mergeConfig bool) ([]byte, error) {
if customConfigPath == "" {
return defaultConfig, nil
}
var customConfigContent []byte
var err error
if strings.HasPrefix(customConfigPath, "https://") || strings.HasPrefix(customConfigPath, "http://") {
// path is a url
response, err := http.Get(customConfigPath)
if err != nil {
return nil, err
}
customConfigContent, err = io.ReadAll(response.Body)
if err != nil {
return nil, err
}
} else {
// path is local
customConfigContent, err = os.ReadFile(customConfigPath)
if err != nil {
return nil, err
}
}
if mergeConfig {
mergedConfig, err := mergeYaml(defaultConfig, customConfigContent)
if err != nil {
return nil, err
}
return mergedConfig, nil
}
return customConfigContent, nil
}
// Parse parses config from a byte array.
func Parse(rawBytes []byte) (Configuration, error) {
reader := bytes.NewReader(rawBytes)
+14 -14
View File
@@ -52,7 +52,7 @@ customChecks:
category: Security
target: Container
schema:
'$schema': https://json-schema.org/draft/2019-09/schema
'$schema': http://json-schema.org/draft-07/schema
type: object
required:
- securityContext
@@ -69,7 +69,7 @@ customChecks:
target: Container
jsonSchema: >
{
"$schema": "https://json-schema.org/draft/2019-09/schema",
"$schema": "http://json-schema.org/draft-07/schema",
"type": "object",
"required": ["securityContext"]
}
@@ -83,7 +83,7 @@ customChecks:
category: Security
target: Container
schema:
'$schema': https://json-schema.org/draft/2019-09/schema
'$schema': http://json-schema.org/draft-07/schema
type: object
required:
- securityContext
@@ -125,7 +125,7 @@ func TestConfigFromURL(t *testing.T) {
}()
time.Sleep(time.Second)
parsedConf, err = MergeConfigAndParseFile("http://localhost:8081/exampleURL", false)
parsedConf, err = ParseFile("http://localhost:8081/exampleURL")
assert.NoError(t, err, "Expected no error when parsing YAML from URL")
if err := srv.Shutdown(context.TODO()); err != nil {
panic(err)
@@ -136,41 +136,41 @@ func TestConfigFromURL(t *testing.T) {
func TestConfigNoServerError(t *testing.T) {
var err error
_, err = MergeConfigAndParseFile("http://localhost:8081/exampleURL", false)
_, err = ParseFile("http://localhost:8081/exampleURL")
assert.Error(t, err)
assert.Regexp(t, regexp.MustCompile("connection refused"), err.Error())
}
func TestConfigWithCustomChecks(t *testing.T) {
valid := map[string]any{
"securityContext": map[string]any{
valid := map[string]interface{}{
"securityContext": map[string]interface{}{
"foo": "bar",
},
}
invalid := map[string]any{
"notSecurityContext": map[string]any{},
invalid := map[string]interface{}{
"notSecurityContext": map[string]interface{}{},
}
parsedConf, err := Parse([]byte(confCustomChecks))
assert.NoError(t, err, "Expected no error when parsing YAML config")
assert.Equal(t, 1, len(parsedConf.CustomChecks))
check, err := parsedConf.CustomChecks["foo"].TemplateForResource(map[string]any{})
isValid, _, err := check.CheckObject(context.TODO(), valid)
check, err := parsedConf.CustomChecks["foo"].TemplateForResource(map[string]interface{}{})
isValid, _, err := check.CheckObject(valid)
assert.NoError(t, err)
assert.Equal(t, true, isValid)
isValid, _, err = check.CheckObject(context.TODO(), invalid)
isValid, _, err = check.CheckObject(invalid)
assert.NoError(t, err)
assert.Equal(t, false, isValid)
parsedConf, err = Parse([]byte(confCustomChecksWithJSONSchema))
assert.NoError(t, err, "Expected no error when parsing YAML config")
assert.Equal(t, 1, len(parsedConf.CustomChecks))
isValid, problems, err := parsedConf.CustomChecks["foo"].CheckObject(context.TODO(), valid)
isValid, problems, err := parsedConf.CustomChecks["foo"].CheckObject(valid)
assert.NoError(t, err)
if !assert.Equal(t, true, isValid) {
fmt.Println(problems[0].PropertyPath, problems[0].InvalidValue, problems[0].Message)
}
isValid, _, err = check.CheckObject(context.TODO(), invalid)
isValid, _, err = check.CheckObject(invalid)
assert.NoError(t, err)
assert.Equal(t, false, isValid)
}
-45
View File
@@ -1,45 +0,0 @@
package config
import (
"gopkg.in/yaml.v3" // do not change the yaml import
)
func mergeYaml(defaultConfig, overridesConfig []byte) ([]byte, error) {
var defaultData, overrideConfig map[string]any
err := yaml.Unmarshal([]byte(defaultConfig), &defaultData)
if err != nil {
return nil, err
}
err = yaml.Unmarshal([]byte(overridesConfig), &overrideConfig)
if err != nil {
return nil, err
}
mergedData := mergeYAMLMaps(defaultData, overrideConfig)
mergedConfig, err := yaml.Marshal(mergedData)
if err != nil {
return nil, err
}
return mergedConfig, nil
}
func mergeYAMLMaps(defaults, overrides map[string]any) map[string]any {
for k, v := range overrides {
if vMap, ok := v.(map[string]any); ok {
// if the key exists in defaults and is a map, recursively merge
if mv1, ok := defaults[k].(map[string]any); ok {
defaults[k] = mergeYAMLMaps(mv1, vMap)
} else {
defaults[k] = vMap
}
} else {
// add or overwrite the value in defaults
defaults[k] = v
}
}
return defaults
}
-50
View File
@@ -1,50 +0,0 @@
package config
import (
"testing"
"github.com/stretchr/testify/assert"
)
var defaults = `
checks:
deploymentMissingReplicas: warning
priorityClassNotSet: warning
tagNotSpecified: danger
existing:
sub:
key: value
`
var overrides = `
checks:
pullPolicyNotAlways: ignore
tagNotSpecified: overrides
existing:
sub:
key1: value1
new: value
new:
key: value
`
func TestMergeYaml(t *testing.T) {
mergedContent, err := mergeYaml([]byte(defaults), []byte(overrides))
assert.NoError(t, err)
expectedYAML := `checks:
deploymentMissingReplicas: warning
priorityClassNotSet: warning
pullPolicyNotAlways: ignore
tagNotSpecified: overrides
existing:
new: value
sub:
key: value
key1: value1
new:
key: value
`
assert.Equal(t, expectedYAML, string(mergedContent))
}
+67 -90
View File
@@ -16,17 +16,13 @@ package config
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"maps"
"slices"
"strings"
"text/template"
"github.com/qri-io/jsonpointer"
"github.com/qri-io/jsonschema"
"github.com/thoas/go-funk"
corev1 "k8s.io/api/core/v1"
@@ -60,34 +56,34 @@ var HandledTargets = []TargetKind{
type Mutation struct {
Path string
Op string
Value any
Value interface{}
Comment string
}
// SchemaCheck is a Polaris check that runs using JSON Schema
type SchemaCheck struct {
ID string `yaml:"id" json:"id"`
Category string `yaml:"category" json:"category"`
SuccessMessage string `yaml:"successMessage" json:"successMessage"`
FailureMessage string `yaml:"failureMessage" json:"failureMessage"`
Controllers includeExcludeList `yaml:"controllers" json:"controllers"`
Containers includeExcludeList `yaml:"containers" json:"containers"`
Target TargetKind `yaml:"target" json:"target"`
SchemaTarget TargetKind `yaml:"schemaTarget" json:"schemaTarget"`
Schema map[string]any `yaml:"schema" json:"schema"`
SchemaString string `yaml:"schemaString" json:"schemaString"`
Validator jsonschema.Schema `yaml:"-" json:"-"`
AdditionalSchemas map[string]map[string]any `yaml:"additionalSchemas" json:"additionalSchemas"`
AdditionalSchemaStrings map[string]string `yaml:"additionalSchemaStrings" json:"additionalSchemaStrings"`
AdditionalValidators map[string]jsonschema.Schema `yaml:"-" json:"-"`
Mutations []Mutation `yaml:"mutations" json:"mutations"`
ID string `yaml:"id" json:"id"`
Category string `yaml:"category" json:"category"`
SuccessMessage string `yaml:"successMessage" json:"successMessage"`
FailureMessage string `yaml:"failureMessage" json:"failureMessage"`
Controllers includeExcludeList `yaml:"controllers" json:"controllers"`
Containers includeExcludeList `yaml:"containers" json:"containers"`
Target TargetKind `yaml:"target" json:"target"`
SchemaTarget TargetKind `yaml:"schemaTarget" json:"schemaTarget"`
Schema map[string]interface{} `yaml:"schema" json:"schema"`
SchemaString string `yaml:"schemaString" json:"schemaString"`
Validator jsonschema.RootSchema `yaml:"-" json:"-"`
AdditionalSchemas map[string]map[string]interface{} `yaml:"additionalSchemas" json:"additionalSchemas"`
AdditionalSchemaStrings map[string]string `yaml:"additionalSchemaStrings" json:"additionalSchemaStrings"`
AdditionalValidators map[string]jsonschema.RootSchema `yaml:"-" json:"-"`
Mutations []Mutation `yaml:"mutations" json:"mutations"`
}
type resourceMinimum string
type resourceMaximum string
// UnmarshalYAMLOrJSON is a helper function to unmarshal data in an arbitrary format
func UnmarshalYAMLOrJSON(raw []byte, dest any) error {
func UnmarshalYAMLOrJSON(raw []byte, dest interface{}) error {
reader := bytes.NewReader(raw)
d := k8sYaml.NewYAMLOrJSONDecoder(reader, 4096)
for {
@@ -113,9 +109,8 @@ func ParseCheck(id string, rawBytes []byte) (SchemaCheck, error) {
}
func init() {
jsonschema.RegisterKeyword("resourceMinimum", newResourceMinimum)
jsonschema.RegisterKeyword("resourceMaximum", newResourceMaximum)
jsonschema.LoadDraft2019_09()
jsonschema.RegisterValidator("resourceMinimum", newResourceMinimum)
jsonschema.RegisterValidator("resourceMaximum", newResourceMaximum)
}
type includeExcludeList struct {
@@ -123,68 +118,47 @@ type includeExcludeList struct {
Exclude []string `yaml:"exclude"`
}
func newResourceMinimum() jsonschema.Keyword {
func newResourceMinimum() jsonschema.Validator {
return new(resourceMinimum)
}
func newResourceMaximum() jsonschema.Keyword {
func newResourceMaximum() jsonschema.Validator {
return new(resourceMaximum)
}
func (min resourceMinimum) ValidateKeyword(ctx context.Context, currentState *jsonschema.ValidationState, data any) {
err := validateRange(string(min), data, true)
// Validate checks that a specified quanitity is not less than the minimum
func (min resourceMinimum) Validate(path string, data interface{}, errs *[]jsonschema.ValError) {
err := validateRange(path, string(min), data, true)
if err != nil {
errs := currentState.Errs
*errs = append(*errs, *err...)
currentState.Errs = errs
}
}
func (max resourceMaximum) ValidateKeyword(ctx context.Context, currentState *jsonschema.ValidationState, data any) {
err := validateRange(string(max), data, false)
// Validate checks that a specified quanitity is not greater than the maximum
func (max resourceMaximum) Validate(path string, data interface{}, errs *[]jsonschema.ValError) {
err := validateRange(path, string(max), data, false)
if err != nil {
errs := currentState.Errs
*errs = append(*errs, *err...)
currentState.Errs = errs
}
}
func (min resourceMinimum) Resolve(pointer jsonpointer.Pointer, uri string) *jsonschema.Schema {
// Not implemented
return nil
}
func (min resourceMinimum) Register(uri string, registry *jsonschema.SchemaRegistry) {
// Not implemented
}
func (max resourceMaximum) Resolve(pointer jsonpointer.Pointer, uri string) *jsonschema.Schema {
// Not implemented
return nil
}
func (max resourceMaximum) Register(uri string, registry *jsonschema.SchemaRegistry) {
// Not implemented
}
func parseQuantity(i any) (resource.Quantity, *[]jsonschema.KeyError) {
if resNum, ok := i.(float64); ok {
i = fmt.Sprintf("%f", resNum)
}
func parseQuantity(i interface{}) (resource.Quantity, *[]jsonschema.ValError) {
resStr, ok := i.(string)
if !ok {
return resource.Quantity{}, &[]jsonschema.KeyError{
return resource.Quantity{}, &[]jsonschema.ValError{
{Message: fmt.Sprintf("Resource quantity %v is not a string", i)},
}
}
q, err := resource.ParseQuantity(resStr)
if err != nil {
return resource.Quantity{}, &[]jsonschema.KeyError{
return resource.Quantity{}, &[]jsonschema.ValError{
{Message: fmt.Sprintf("Could not parse resource quantity: %s", resStr)},
}
}
return q, nil
}
func validateRange(limit any, data any, isMinimum bool) *[]jsonschema.KeyError {
func validateRange(path string, limit interface{}, data interface{}, isMinimum bool) *[]jsonschema.ValError {
limitQuantity, err := parseQuantity(limit)
if err != nil {
return err
@@ -196,14 +170,14 @@ func validateRange(limit any, data any, isMinimum bool) *[]jsonschema.KeyError {
cmp := limitQuantity.Cmp(actualQuantity)
if isMinimum {
if cmp == 1 {
return &[]jsonschema.KeyError{
{Message: fmt.Sprintf("quantity %v is > %v", actualQuantity, limitQuantity)},
return &[]jsonschema.ValError{
{Message: fmt.Sprintf("%s quantity %v is > %v", path, actualQuantity, limitQuantity)},
}
}
} else {
if cmp == -1 {
return &[]jsonschema.KeyError{
{Message: fmt.Sprintf("quantity %v is < %v", actualQuantity, limitQuantity)},
return &[]jsonschema.ValError{
{Message: fmt.Sprintf("%s quantity %v is < %v", path, actualQuantity, limitQuantity)},
}
}
}
@@ -230,19 +204,21 @@ func (check *SchemaCheck) Initialize(id string) error {
}
check.AdditionalSchemaStrings[kind] = string(jsonBytes)
}
check.Schema = map[string]any{}
check.AdditionalSchemas = map[string]map[string]any{}
check.Schema = map[string]interface{}{}
check.AdditionalSchemas = map[string]map[string]interface{}{}
return nil
}
// TemplateForResource fills out a check's templated fields given a particular resource
func (check SchemaCheck) TemplateForResource(res any) (*SchemaCheck, error) {
func (check SchemaCheck) TemplateForResource(res interface{}) (*SchemaCheck, error) {
newCheck := check // Make a copy of the check, since we're going to modify the schema
templateStrings := map[string]string{
"": newCheck.SchemaString,
}
maps.Copy(templateStrings, newCheck.AdditionalSchemaStrings)
for kind, schema := range newCheck.AdditionalSchemaStrings {
templateStrings[kind] = schema
}
newCheck.SchemaString = ""
newCheck.AdditionalSchemaStrings = map[string]string{}
@@ -260,21 +236,17 @@ func (check SchemaCheck) TemplateForResource(res any) (*SchemaCheck, error) {
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.Schema{}
newCheck.AdditionalValidators = map[string]jsonschema.RootSchema{}
for kind, schemaStr := range newCheck.AdditionalSchemaStrings {
val := jsonschema.Schema{}
val := jsonschema.RootSchema{}
err := UnmarshalYAMLOrJSON([]byte(schemaStr), &val)
if err != nil {
return nil, err
@@ -289,38 +261,38 @@ func (check SchemaCheck) TemplateForResource(res any) (*SchemaCheck, error) {
}
// CheckPodSpec checks a pod spec against the schema
func (check SchemaCheck) CheckPodSpec(ctx context.Context, pod *corev1.PodSpec) (bool, []jsonschema.KeyError, error) {
return check.CheckObject(ctx, pod)
func (check SchemaCheck) CheckPodSpec(pod *corev1.PodSpec) (bool, []jsonschema.ValError, error) {
return check.CheckObject(pod)
}
// CheckPodTemplate checks a pod template against the schema
func (check SchemaCheck) CheckPodTemplate(ctx context.Context, podTemplate any) (bool, []jsonschema.KeyError, error) {
return check.CheckObject(ctx, podTemplate)
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(ctx context.Context, bytes []byte) (bool, []jsonschema.KeyError, error) {
errs, err := check.Validator.ValidateBytes(ctx, bytes)
func (check SchemaCheck) CheckController(bytes []byte) (bool, []jsonschema.ValError, error) {
errs, err := check.Validator.ValidateBytes(bytes)
return len(errs) == 0, errs, err
}
// CheckContainer checks a container spec against the schema
func (check SchemaCheck) CheckContainer(ctx context.Context, container *corev1.Container) (bool, []jsonschema.KeyError, error) {
return check.CheckObject(ctx, container)
func (check SchemaCheck) CheckContainer(container *corev1.Container) (bool, []jsonschema.ValError, error) {
return check.CheckObject(container)
}
// CheckObject checks arbitrary data against the schema
func (check SchemaCheck) CheckObject(ctx context.Context, obj any) (bool, []jsonschema.KeyError, error) {
func (check SchemaCheck) CheckObject(obj interface{}) (bool, []jsonschema.ValError, error) {
bytes, err := json.Marshal(obj)
if err != nil {
return false, nil, err
}
errs, err := check.Validator.ValidateBytes(ctx, bytes)
errs, err := check.Validator.ValidateBytes(bytes)
return len(errs) == 0, errs, err
}
// CheckAdditionalObjects looks for an object that passes the specified additional schema
func (check SchemaCheck) CheckAdditionalObjects(ctx context.Context, groupkind string, objects []any) (bool, error) {
func (check SchemaCheck) CheckAdditionalObjects(groupkind string, objects []interface{}) (bool, error) {
val, ok := check.AdditionalValidators[groupkind]
if !ok {
return false, errors.New("No validator found for " + groupkind)
@@ -330,7 +302,7 @@ func (check SchemaCheck) CheckAdditionalObjects(ctx context.Context, groupkind s
if err != nil {
return false, err
}
errs, err := val.ValidateBytes(ctx, bytes)
errs, err := val.ValidateBytes(bytes)
if err != nil {
return false, err
}
@@ -356,14 +328,19 @@ func (check SchemaCheck) IsActionable(target TargetKind, kind string, isInit boo
return false
}
isIncluded := len(check.Controllers.Include) == 0
if slices.Contains(check.Controllers.Include, kind) {
isIncluded = true
for _, inclusion := range check.Controllers.Include {
if inclusion == kind {
isIncluded = true
break
}
}
if !isIncluded {
return false
}
if slices.Contains(check.Controllers.Exclude, kind) {
return false
for _, exclusion := range check.Controllers.Exclude {
if exclusion == kind {
return false
}
}
if check.Target == TargetContainer {
isIncluded := len(check.Containers.Include) == 0
+28 -20
View File
@@ -16,11 +16,8 @@ package dashboard
import (
"bytes"
"context"
"embed"
"encoding/json"
"html/template"
"io/fs"
"net/http"
"net/url"
"path"
@@ -29,6 +26,7 @@ import (
"github.com/fairwindsops/polaris/pkg/config"
"github.com/fairwindsops/polaris/pkg/kube"
"github.com/fairwindsops/polaris/pkg/validator"
packr "github.com/gobuffalo/packr/v2"
"github.com/gorilla/mux"
"github.com/sirupsen/logrus"
)
@@ -49,12 +47,27 @@ const (
)
var (
//go:embed all:templates
templatesFS embed.FS
//go:embed all:assets
assetsFS embed.FS
templateBox = (*packr.Box)(nil)
assetBox = (*packr.Box)(nil)
markdownBox = (*packr.Box)(nil)
)
// GetAssetBox returns a binary-friendly set of assets packaged from disk
func GetAssetBox() *packr.Box {
if assetBox == (*packr.Box)(nil) {
assetBox = packr.New("Assets", "assets")
}
return assetBox
}
// GetTemplateBox returns a binary-friendly set of templates for rendering the dash
func GetTemplateBox() *packr.Box {
if templateBox == (*packr.Box)(nil) {
templateBox = packr.New("Templates", "templates")
}
return templateBox
}
// templateData is passed to the dashboard HTML template
type templateData struct {
BasePath string
@@ -90,8 +103,9 @@ func GetBaseTemplate(name string) (*template.Template, error) {
}
func parseTemplateFiles(tmpl *template.Template, templateFileNames []string) (*template.Template, error) {
templateBox := GetTemplateBox()
for _, fname := range templateFileNames {
templateFile, err := templatesFS.ReadFile("templates/" + fname)
templateFile, err := templateBox.Find(fname)
if err != nil {
return nil, err
}
@@ -141,15 +155,9 @@ func stripUnselectedNamespaces(data *validator.AuditData, selectedNamespaces []s
}
// GetRouter returns a mux router serving all routes necessary for the dashboard
func GetRouter(ctx context.Context, c config.Configuration, auditPath string, port int, basePath string, auditData *validator.AuditData) (*mux.Router, error) {
func GetRouter(c config.Configuration, auditPath string, port int, basePath string, auditData *validator.AuditData) *mux.Router {
router := mux.NewRouter().PathPrefix(basePath).Subrouter()
assetsSubFS, err := fs.Sub(assetsFS, "assets")
if err != nil {
return nil, err
}
fileServer := http.FileServer(http.FS(assetsSubFS))
fileServer := http.FileServer(GetAssetBox())
router.PathPrefix("/static/").Handler(http.StripPrefix(path.Join(basePath, "/static/"), fileServer))
router.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
@@ -157,7 +165,7 @@ func GetRouter(ctx context.Context, c config.Configuration, auditPath string, po
})
router.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
favicon, err := assetsFS.ReadFile("assets/favicon-32x32.png")
favicon, err := GetAssetBox().Find("favicon-32x32.png")
if err != nil {
logrus.Errorf("Error getting favicon: %v", err)
http.Error(w, "Error getting favicon", http.StatusInternalServerError)
@@ -177,7 +185,7 @@ func GetRouter(ctx context.Context, c config.Configuration, auditPath string, po
}
var auditDataObj validator.AuditData
auditDataObj, err = validator.RunAudit(ctx, adjustedConf, k)
auditDataObj, err = validator.RunAudit(adjustedConf, k)
if err != nil {
http.Error(w, "Error Fetching Deployments", http.StatusInternalServerError)
return
@@ -213,7 +221,7 @@ func GetRouter(ctx context.Context, c config.Configuration, auditPath string, po
logrus.Infof("Running audit")
var auditData validator.AuditData
auditData, err = validator.RunAudit(ctx, adjustedConf, k)
auditData, err = validator.RunAudit(adjustedConf, k)
if err != nil {
logrus.Errorf("Error getting audit data: %v", err)
http.Error(w, "Error running audit", 500)
@@ -227,7 +235,7 @@ func GetRouter(ctx context.Context, c config.Configuration, auditPath string, po
}
})
return router, nil
return router
}
// MainHandler gets template data and renders the dashboard with it.
+6 -2
View File
@@ -16,7 +16,6 @@ package dashboard
import (
"fmt"
"slices"
"strings"
"github.com/fairwindsops/polaris/pkg/config"
@@ -158,5 +157,10 @@ func getCategoryInfo(category string) string {
}
func stringInSlice(a string, list []string) bool {
return slices.Contains(list, a)
for _, b := range list {
if b == a {
return true
}
}
return false
}
-1
View File
@@ -5,7 +5,6 @@
<a href="https://www.fairwinds.com/polaris-user-insights-demo?utm_source=polaris&utm_medium=polaris&utm_campaign=polaris" target="_blank">
<img class="fw-logo" src="static/images/white_logo_fairwinds.svg" alt="Fairwinds" />
</a>
<div style="color: white;"> Want more? Automate Polaris with <a href="https://www.fairwinds.com/insights-signup/polaris"><strong>Fairwinds Insights</strong></a></div>
<div class="right-section p-0 d-flex justify-content-between">
<a href="https://github.com/FairwindsOps" target="_blank">
<img class="gh-logo" src="static/images/white_icon_github.svg" alt="Github" />
-160
View File
@@ -1,160 +0,0 @@
package fix
import (
"context"
"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(ctx context.Context, 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(ctx, &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
}
+26 -26
View File
@@ -39,13 +39,13 @@ type GenericResource struct {
ObjectMeta kubeAPIMetaV1.Object
Resource unstructured.Unstructured
PodSpec *kubeAPICoreV1.PodSpec
PodTemplate any
PodTemplate interface{}
OriginalObjectJSON []byte
OriginalObjectYAML []byte
}
// NewGenericResourceFromUnstructured creates a workload from an unstructured.Unstructured
func NewGenericResourceFromUnstructured(unst unstructured.Unstructured, podSpecMap any) (GenericResource, error) {
func NewGenericResourceFromUnstructured(unst unstructured.Unstructured, podSpecMap interface{}) (GenericResource, error) {
if unst.GetCreationTimestamp().Time.IsZero() {
unstructured.RemoveNestedField(unst.Object, "metadata", "creationTimestamp")
unstructured.RemoveNestedField(unst.Object, "status")
@@ -69,7 +69,7 @@ func NewGenericResourceFromUnstructured(unst unstructured.Unstructured, podSpecM
return workload, err
}
workload.OriginalObjectJSON = b
m := make(map[string]any)
m := make(map[string]interface{})
err = json.Unmarshal(b, &m)
if err != nil {
return workload, err
@@ -93,7 +93,7 @@ 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 any) (GenericResource, error) {
func NewGenericResourceFromPod(podResource kubeAPICoreV1.Pod, originalObject interface{}) (GenericResource, error) {
podMap, err := SerializePod(&podResource)
if err != nil {
return GenericResource{}, err
@@ -139,7 +139,7 @@ func NewGenericResourceFromBytes(contentBytes []byte) (GenericResource, error) {
}
// 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,14 +150,14 @@ 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
}
topKind := "Pod"
topMeta := podWorkload.ObjectMeta
var topPodSpec any
var topPodSpec interface{}
topPodSpec = podWorkload.Resource.Object
owners := podResource.ObjectMeta.GetOwnerReferences()
lastKey := ""
@@ -181,7 +181,7 @@ func resolveControllerFromPod(ctx context.Context, podResource kubeAPICoreV1.Pod
err = cacheAllObjectsOfKind(ctx, firstOwner.APIVersion, firstOwner.Kind, dynamicClient, restMapper, objectCache)
}
if err != nil {
logrus.Warnf("error caching objects of Kind %s %v", firstOwner.Kind, err)
logrus.Warnf("Error caching objects of Kind %s %v", firstOwner.Kind, err)
break
}
abstractObject, ok = objectCache[key]
@@ -193,7 +193,7 @@ func resolveControllerFromPod(ctx context.Context, podResource kubeAPICoreV1.Pod
objMeta, err := meta.Accessor(&abstractObject)
if err != nil {
logrus.Warnf("error retrieving parent metadata %s of API %s and Kind %s because of error: %v ", firstOwner.Name, firstOwner.APIVersion, firstOwner.Kind, err)
logrus.Warnf("Error retrieving parent metadata %s of API %s and Kind %s because of error: %v ", firstOwner.Name, firstOwner.APIVersion, firstOwner.Kind, err)
return GenericResource{}, err
}
podSpec := GetPodSpec(abstractObject.Object)
@@ -217,11 +217,11 @@ 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 {
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)
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)
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())
@@ -230,18 +230,18 @@ func cacheSingleObject(ctx context.Context, apiVersion, kind, namespace, name st
return nil
}
func cacheAllObjectsOfKind(ctx context.Context, apiVersion, kind string, dynamicClient dynamic.Interface, restMapper meta.RESTMapper, objectCache map[string]unstructured.Unstructured) error {
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)
fqKind := schema.FromAPIVersionAndKind(apiVersion, kind)
mapping, err := restMapper.RESTMapping(fqKind.GroupKind(), fqKind.Version)
mapping, err := (*restMapper).RESTMapping(fqKind.GroupKind(), fqKind.Version)
if err != nil {
logrus.Warnf("error retrieving mapping of API %s and Kind %s because of error: %v", apiVersion, kind, err)
logrus.Warnf("Error retrieving mapping of API %s and Kind %s because of error: %v", apiVersion, kind, err)
return err
}
objects, err := dynamicClient.Resource(mapping.Resource).Namespace("").List(ctx, kubeAPIMetaV1.ListOptions{})
objects, err := (*dynamicClient).Resource(mapping.Resource).Namespace("").List(ctx, kubeAPIMetaV1.ListOptions{})
if err != nil {
logrus.Warnf("error retrieving parent object API %s and Kind %s because of error: %v", mapping.Resource.Version, mapping.Resource.Resource, err)
logrus.Warnf("Error retrieving parent object API %s and Kind %s because of error: %v", mapping.Resource.Version, mapping.Resource.Resource, err)
return err
}
for idx, object := range objects.Items {
@@ -252,21 +252,21 @@ func cacheAllObjectsOfKind(ctx context.Context, apiVersion, kind string, dynamic
return nil
}
func GetObject(ctx context.Context, namespace, kind, version, name string, dynamicClient dynamic.Interface, restMapper meta.RESTMapper) (*unstructured.Unstructured, error) {
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)
mapping, err := (*restMapper).RESTMapping(fqKind.GroupKind(), 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
}
// GetPodSpec looks inside arbitrary YAML for a PodSpec
func GetPodSpec(yaml map[string]any) any {
func GetPodSpec(yaml map[string]interface{}) interface{} {
for _, child := range podSpecFields {
if childYaml, ok := yaml[child]; ok {
return GetPodSpec(childYaml.(map[string]any))
return GetPodSpec(childYaml.(map[string]interface{}))
}
}
if _, ok := yaml["containers"]; ok {
@@ -278,9 +278,9 @@ func GetPodSpec(yaml map[string]any) any {
// 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]any) (podTemplate any, err error) {
func GetPodTemplate(yaml map[string]interface{}) (podTemplate interface{}, err error) {
if yamlSpec, ok := yaml["spec"]; ok {
if yamlSpecMap, ok := yamlSpec.(map[string]any); 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.
@@ -289,7 +289,7 @@ func GetPodTemplate(yaml map[string]any) (podTemplate any, err error) {
if err != nil {
return nil, err
}
podTemplateMap := make(map[string]any)
podTemplateMap := make(map[string]interface{})
err = json.Unmarshal(podTemplateJSON, &podTemplateMap)
if err != nil {
return nil, err
@@ -300,7 +300,7 @@ func GetPodTemplate(yaml map[string]any) (podTemplate any, err error) {
}
for _, podSpecField := range podSpecFields {
if childYaml, ok := yaml[podSpecField]; ok {
return GetPodTemplate(childYaml.(map[string]any))
return GetPodTemplate(childYaml.(map[string]interface{}))
}
}
return nil, nil
+120 -83
View File
@@ -26,7 +26,6 @@ import (
"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"
)
@@ -94,28 +93,24 @@ func (rkm resourceKindMap) GetNumberOfControllers() int {
return total
}
var kindRewrites = map[string]string{
"Ingress": "networking.k8s.io/Ingress",
"PodDisruptionBudget": "policy/PodDisruptionBudget",
"HorizontalPodAutoscaler": "autoscaling/HorizontalPodAutoscaler",
}
// This is here for backward compatibility reasons
func maybeTransformKindIntoGroupKind(k string) string {
if val, ok := kindRewrites[k]; ok {
return val
if k == "Ingress" {
return "networking.k8s.io/Ingress"
} else if k == "PodDisruptionBudget" {
return "policy/PodDisruptionBudget"
}
return k
}
func parseGroupKind(gk string) schema.GroupKind {
before, after, ok := strings.Cut(gk, "/")
if !ok {
i := strings.Index(gk, "/")
if i == -1 {
return schema.GroupKind{Kind: gk}
}
group := before
kind := after
group := gk[:i]
kind := gk[i+1:]
return schema.GroupKind{Group: group, Kind: kind}
}
@@ -150,13 +145,20 @@ func CreateResourceProvider(ctx context.Context, directory, workload string, c c
// CreateResourceProviderFromResource creates a new ResourceProvider that just contains one workload
func CreateResourceProviderFromResource(ctx context.Context, workload string) (*ResourceProvider, error) {
dynamicClient, restMapper, clientSet, _, err := GetKubeClient(ctx, "")
kubeConf, configError := config.GetConfig()
if configError != nil {
logrus.Errorf("Error fetching KubeConfig: %v", configError)
return nil, configError
}
kube, err := kubernetes.NewForConfig(kubeConf)
if err != nil {
logrus.Errorf("Error creating Kubernetes client: %v", err)
return nil, err
}
serverVersion, err := clientSet.Discovery().ServerVersion()
serverVersion, err := kube.Discovery().ServerVersion()
if err != nil {
return nil, fmt.Errorf("Error fetching Cluster API version: %w", err)
logrus.Errorf("Error fetching Cluster API version: %v", err)
return nil, err
}
resources := newResourceProvider(serverVersion.Major+"."+serverVersion.Minor, "Resource", workload)
@@ -169,14 +171,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
}
@@ -206,7 +222,7 @@ func CreateResourceProviderFromPath(directory string) (*ResourceProvider, error)
}
err = resources.addResourcesFromYaml(string(contents))
if err != nil {
logrus.Warnf("skipping %s: cannot add resource from YAML: %v", path, err)
logrus.Warnf("Skipping %s: cannot add resource from YAML: %v", path, err)
}
return nil
}
@@ -219,52 +235,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 {
@@ -304,6 +302,13 @@ func CreateResourceProviderFromAPI(ctx context.Context, kube kubernetes.Interfac
}
namespaces = nsList
}
logrus.Info("Loading pods")
pods, err := kube.CoreV1().Pods(c.Namespace).List(ctx, listOpts)
if err != nil {
logrus.Errorf("Error fetching Pods: %v", err)
return nil, err
}
logrus.Info("Setting up restmapper")
resources, err := restmapper.GetAPIGroupResources(kube.Discovery())
if err != nil {
@@ -338,20 +343,16 @@ func CreateResourceProviderFromAPI(ctx context.Context, kube kubernetes.Interfac
var kubernetesResources []GenericResource
for _, kind := range additionalKinds {
groupKind := parseGroupKind(maybeTransformKindIntoGroupKind(string(kind)))
mapping, err := restMapper.RESTMapping(groupKind)
mapping, err := (restMapper).RESTMapping(groupKind)
if err != nil {
logrus.Warnf("error retrieving mapping of Kind %s because of error: %v", kind, err)
logrus.Warnf("Error retrieving mapping of Kind %s because of error: %v", kind, err)
return nil, err
}
if c.Namespace != "" && mapping.Scope.Name() != meta.RESTScopeNameNamespace {
logrus.Infof("Skipping %s because of auditing specific namespace", mapping.GroupVersionKind)
continue
}
logrus.Info("Loading " + kind)
objects, err := dynamic.Resource(mapping.Resource).Namespace(c.Namespace).List(ctx, metav1.ListOptions{})
objects, err := (*dynamic).Resource(mapping.Resource).Namespace(c.Namespace).List(ctx, metav1.ListOptions{})
if err != nil {
logrus.Warnf("error retrieving parent object API %s and Kind %s because of error: %v", mapping.Resource.Version, mapping.Resource.Resource, err)
logrus.Warnf("Error retrieving parent object API %s and Kind %s because of error: %v", mapping.Resource.Version, mapping.Resource.Resource, err)
return nil, err
}
for _, obj := range objects.Items {
@@ -362,24 +363,17 @@ func CreateResourceProviderFromAPI(ctx context.Context, kube kubernetes.Interfac
kubernetesResources = append(kubernetesResources, res)
}
}
objectCache := map[string]unstructured.Unstructured{}
logrus.Info("Loading controllers")
client := controller.Client{
Context: ctx,
Dynamic: dynamic,
RESTMapper: restMapper,
}
topControllers, err := client.GetAllTopControllersSummary("")
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
@@ -388,6 +382,49 @@ func CreateResourceProviderFromAPI(ctx context.Context, kube kubernetes.Interfac
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 idx, pod := range pods {
owners := pod.ObjectMeta.OwnerReferences
if len(owners) == 0 {
deduped[pod.ObjectMeta.Namespace+"/Pod/"+pod.ObjectMeta.Name] = &pods[idx]
continue
}
deduped[pod.ObjectMeta.Namespace+"/"+owners[0].Kind+"/"+owners[0].Name] = &pods[idx]
}
for key, pod := range deduped {
logrus.Debugf("Resolving controller from pod %s", key)
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)
if err != nil {
@@ -454,12 +491,12 @@ func (resources *ResourceProvider) addResourceFromString(contents string) error
}
// SerializePodSpec converts a typed PodSpec into a map[string]interface{}
func SerializePodSpec(pod *corev1.PodSpec) (map[string]any, error) {
func SerializePodSpec(pod *corev1.PodSpec) (map[string]interface{}, error) {
podJSON, err := json.Marshal(pod)
if err != nil {
return nil, err
}
podMap := make(map[string]any)
podMap := make(map[string]interface{})
err = json.Unmarshal(podJSON, &podMap)
if err != nil {
return nil, err
@@ -468,12 +505,12 @@ func SerializePodSpec(pod *corev1.PodSpec) (map[string]any, error) {
}
// SerializePod converts a typed Pod into a map[string]interface{}
func SerializePod(pod *corev1.Pod) (map[string]any, error) {
func SerializePod(pod *corev1.Pod) (map[string]interface{}, error) {
podJSON, err := json.Marshal(pod)
if err != nil {
return nil, err
}
podMap := make(map[string]any)
podMap := make(map[string]interface{})
err = json.Unmarshal(podJSON, &podMap)
if err != nil {
return nil, err
@@ -482,12 +519,12 @@ func SerializePod(pod *corev1.Pod) (map[string]any, error) {
}
// SerializeContainer converts a typed Container into a map[string]interface{}
func SerializeContainer(container *corev1.Container) (map[string]any, error) {
func SerializeContainer(container *corev1.Container) (map[string]interface{}, error) {
containerJSON, err := json.Marshal(container)
if err != nil {
return nil, err
}
containerMap := make(map[string]any)
containerMap := make(map[string]interface{})
err = json.Unmarshal(containerJSON, &containerMap)
if err != nil {
return nil, err
+19 -24
View File
@@ -17,7 +17,6 @@ package kube
import (
"bytes"
"context"
"fmt"
"os"
"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)
@@ -88,8 +87,8 @@ func TestAddResourcesFromReader(t *testing.T) {
assert.Equal(t, 0, len(resources.Nodes), "Should not have any nodes")
assert.Equal(t, 1, len(resources.Resources["apps/Deployment"]), "Should have one controller")
assert.Equal(t, "dashboard", resources.Resources["apps/Deployment"][0].PodSpec.Containers[0].Name)
assert.Equal(t, 1, len(resources.Resources["extensions/Deployment"]), "Should have one controller")
assert.Equal(t, "dashboard", resources.Resources["extensions/Deployment"][0].PodSpec.Containers[0].Name)
assert.Equal(t, 2, len(resources.Namespaces), "Should have a namespace")
assert.Equal(t, "polaris", resources.Namespaces[0].ObjectMeta.Name)
@@ -147,30 +146,26 @@ func TestGetResourceFromAPI(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
resources, err := CreateResourceProviderFromAPI(context.Background(), k8s, tt.clusterName, dynamicInterface, tt.config)
resources, err := CreateResourceProviderFromAPI(context.Background(), k8s, tt.clusterName, &dynamicInterface, tt.config)
if tt.wantErr {
assert.Error(t, err)
} else {
if assert.NoError(t, err) {
assert.Equal(t, tt.want.SourceType, resources.SourceType)
assert.Equal(t, tt.want.SourceName, resources.SourceName)
assert.IsType(t, tt.want.CreationTime, resources.CreationTime)
assert.Equal(t, 0, len(resources.Nodes), "Should not have any nodes")
for k, v := range resources.Resources {
fmt.Println("cont", k, v)
}
assert.Equal(t, 5, len(resources.Resources), "Should have 5 controllers")
assert.NoError(t, err)
assert.Equal(t, tt.want.SourceType, resources.SourceType)
assert.Equal(t, tt.want.SourceName, resources.SourceName)
assert.IsType(t, tt.want.CreationTime, resources.CreationTime)
assert.Equal(t, 0, len(resources.Nodes), "Should not have any nodes")
assert.Equal(t, 5, len(resources.Resources), "Should have 5 controllers")
for _, controllers := range resources.Resources {
for _, ctrl := range controllers {
expectedNames[ctrl.ObjectMeta.GetName()] = true
}
}
for name, val := range expectedNames {
assert.Equal(t, true, val, name)
for _, controllers := range resources.Resources {
for _, ctrl := range controllers {
expectedNames[ctrl.ObjectMeta.GetName()] = true
}
}
for name, val := range expectedNames {
assert.Equal(t, true, val, name)
}
}
})
}
+1 -1
View File
@@ -1,4 +1,4 @@
apiVersion: batch/v1
apiVersion: batch/v1beta1
kind: CronJob
metadata:
name: test
+1 -1
View File
@@ -1,4 +1,4 @@
apiVersion: apps/v1
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: test-deployment
+1 -1
View File
@@ -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"]
+1 -1
View File
@@ -6,7 +6,7 @@ metadata:
name: polaris
---
# Source: polaris/templates/dashboard.deployment.yaml
apiVersion: apps/v1
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
annotations:
+1 -1
View File
@@ -1,5 +1,5 @@
--
# Source: polaris/templates/dashboard.deployment.yaml
apiVersion: apps/v1
apiVersion: extensions/v1beta1
kind: Deployment
::::
+2 -4
View File
@@ -161,6 +161,7 @@ func createPathAndFindNodes(node *yaml.Node, selectors []string, create bool) ([
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)
}
@@ -181,9 +182,6 @@ func addOrReplaceValue(node *yaml.Node, splits []string, value *yaml.Node) error
}
for _, node := range nodes {
if node.Kind == yaml.ScalarNode {
if value.LineComment == "" {
value.LineComment = node.LineComment // keep the original comment if override is not provided
}
// Overwrite an existing scalar value with a new value (whatever kind).
*node = *value
} else if node.Kind == yaml.MappingNode && value.Kind == yaml.MappingNode {
@@ -224,7 +222,7 @@ func addOrReplaceValue(node *yaml.Node, splits []string, value *yaml.Node) error
return nil
}
func getNodeFromValue(value any, comment string) (*yaml.Node, error) {
func getNodeFromValue(value interface{}, comment string) (*yaml.Node, error) {
bytes, err := yaml.Marshal(value)
if err != nil {
return nil, err
+68 -104
View File
@@ -25,15 +25,14 @@ var testCases = []struct {
mutated string
patch config.Mutation
message string
}{
{
original: oldYaml,
patch: config.Mutation{
Op: "add",
Value: "Denver",
Path: "/pets/0/owners/*/location",
},
mutated: `pets:
}{{
original: oldYaml,
patch: config.Mutation{
Op: "add",
Value: "Denver",
Path: "/pets/0/owners/*/location",
},
mutated: `pets:
- name: fido
owners:
- name: Alice
@@ -44,27 +43,27 @@ var testCases = []struct {
location: Denver
- name: scooby
`,
}, {
original: oldYaml,
patch: config.Mutation{
Op: "remove",
Path: "/pets/0/owners/*/aliases",
},
mutated: `pets:
}, {
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:
}, {
original: oldYaml,
patch: config.Mutation{
Op: "add",
Value: "rob",
Path: "/pets/0/owners/*/aliases/-",
},
mutated: `pets:
- name: fido
owners:
- name: Alice
@@ -76,23 +75,23 @@ var testCases = []struct {
- rob
- name: scooby
`,
}, {
original: `
}, {
original: `
pets:
- name: fido
`,
patch: config.Mutation{
Op: "add",
Value: "Alice",
Path: "/pets/0/owners/0/name",
},
mutated: `pets:
patch: config.Mutation{
Op: "add",
Value: "Alice",
Path: "/pets/0/owners/0/name",
},
mutated: `pets:
- name: fido
owners:
- name: Alice
`,
}, {
original: `
}, {
original: `
obj:
foo:
bar:
@@ -100,89 +99,54 @@ obj:
- b
baz: quux
`,
patch: config.Mutation{
Op: "replace",
Value: map[string]any{
"bar": []string{"c", "d"},
},
Path: "/obj/foo",
patch: config.Mutation{
Op: "replace",
Value: map[string]interface{}{
"bar": []string{"c", "d"},
},
mutated: `
obj:
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",
}, {
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",
},
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]any{
"baz": "quux",
},
Path: "/extra",
Comment: "# These are extra things",
},
mutated: `
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]any{
"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 # we should keep this comment`,
patch: config.Mutation{
Op: "replace",
Value: "baz",
Path: "/foo",
},
mutated: `foo: baz # we should keep this comment`,
message: "Expected a comment to be kept",
}, {
original: `foo: bar # we should override this comment`,
patch: config.Mutation{
Op: "replace",
Value: "baz",
Path: "/foo",
Comment: "override",
},
mutated: `foo: baz # override`,
message: "Expected a comment to overridden",
},
}
message: "Expected a comment to appear next to an object",
}}
func TestApplyAllMutations(t *testing.T) {
for _, tc := range testCases {
+2 -3
View File
@@ -15,7 +15,6 @@
package validator
import (
"context"
"encoding/json"
"testing"
@@ -37,7 +36,7 @@ func TestValidatePDB(t *testing.T) {
res, err := kube.NewGenericResourceFromUnstructured(pdb, nil)
res.Kind = "PodDisruptionBudget"
actualResult, err := applyNonControllerSchemaChecks(context.Background(), &c, nil, res)
actualResult, err := applyNonControllerSchemaChecks(&c, nil, res)
if err != nil {
panic(err)
}
@@ -77,7 +76,7 @@ func TestValidateIngress(t *testing.T) {
}
res.Kind = "Ingress"
actualResult, err := applyNonControllerSchemaChecks(context.Background(), &c, nil, res)
actualResult, err := applyNonControllerSchemaChecks(&c, nil, res)
if err != nil {
panic(err)
}
+7 -8
View File
@@ -15,7 +15,6 @@
package validator
import (
"context"
"fmt"
"testing"
@@ -70,7 +69,7 @@ func testValidateWithWorkload(t *testing.T, container *corev1.Container, resourc
assert.NoError(t, err, "Expected no error when parsing config")
var results ResultSet
results, err = applyContainerSchemaChecks(context.Background(), &parsedConf, nil, workload, container, false)
results, err = applyContainerSchemaChecks(&parsedConf, nil, workload, container, false)
if err != nil {
panic(err)
}
@@ -94,7 +93,7 @@ func TestValidateResourcesEmptyConfig(t *testing.T) {
Name: "Empty",
}
results, err := applyContainerSchemaChecks(context.Background(), &conf.Configuration{}, nil, getEmptyWorkload(t, ""), container, false)
results, err := applyContainerSchemaChecks(&conf.Configuration{}, nil, getEmptyWorkload(t, ""), container, false)
if err != nil {
panic(err)
}
@@ -191,7 +190,7 @@ func TestValidateHealthChecks(t *testing.T) {
for idx, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
controller := getEmptyWorkload(t, "")
results, err := applyContainerSchemaChecks(context.Background(), &conf.Configuration{Checks: tt.probes}, nil, controller, tt.container, tt.isInit)
results, err := applyContainerSchemaChecks(&conf.Configuration{Checks: tt.probes}, nil, controller, tt.container, tt.isInit)
if err != nil {
panic(err)
}
@@ -305,7 +304,7 @@ func TestValidateImage(t *testing.T) {
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
controller := getEmptyWorkload(t, "")
results, err := applyContainerSchemaChecks(context.Background(), &conf.Configuration{Checks: tt.image}, nil, controller, tt.container, false)
results, err := applyContainerSchemaChecks(&conf.Configuration{Checks: tt.image}, nil, controller, tt.container, false)
if err != nil {
panic(err)
}
@@ -422,7 +421,7 @@ func TestValidateNetworking(t *testing.T) {
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
controller := getEmptyWorkload(t, "")
results, err := applyContainerSchemaChecks(context.Background(), &conf.Configuration{Checks: tt.networkConf}, nil, controller, tt.container, false)
results, err := applyContainerSchemaChecks(&conf.Configuration{Checks: tt.networkConf}, nil, controller, tt.container, false)
if err != nil {
panic(err)
}
@@ -927,7 +926,7 @@ func TestValidateSecurity(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
workload, err := kube.NewGenericResourceFromPod(corev1.Pod{Spec: *tt.pod}, nil)
assert.NoError(t, err)
results, err := applyContainerSchemaChecks(context.Background(), &conf.Configuration{Checks: tt.securityConf}, nil, workload, tt.container, false)
results, err := applyContainerSchemaChecks(&conf.Configuration{Checks: tt.securityConf}, nil, workload, tt.container, false)
if err != nil {
panic(err)
}
@@ -1072,7 +1071,7 @@ func TestValidateRunAsRoot(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
workload, err := kube.NewGenericResourceFromPod(corev1.Pod{Spec: *tt.pod}, nil)
assert.NoError(t, err)
results, err := applyContainerSchemaChecks(context.Background(), &config, nil, workload, tt.container, false)
results, err := applyContainerSchemaChecks(&config, nil, workload, tt.container, false)
if err != nil {
panic(err)
}
+12 -12
View File
@@ -49,7 +49,7 @@ func TestValidateController(t *testing.T) {
}
var actualResult Result
actualResult, err = applyControllerSchemaChecks(context.Background(), &c, nil, deployment)
actualResult, err = applyControllerSchemaChecks(&c, nil, deployment)
if err != nil {
panic(err)
}
@@ -73,7 +73,7 @@ func TestControllerLevelChecks(t *testing.T) {
Category: "Reliability",
}
for _, controller := range res.Resources["Deployment"] {
actualResult, err := applyControllerSchemaChecks(context.Background(), &c, nil, controller)
actualResult, err := applyControllerSchemaChecks(&c, nil, controller)
if err != nil {
panic(err)
}
@@ -96,10 +96,10 @@ 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]any{"replicas": 2}
replicaSpec := map[string]interface{}{"replicas": 2}
b, err := json.Marshal(replicaSpec)
assert.NoError(t, err)
err = json.Unmarshal(b, &replicaSpec)
@@ -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)
@@ -137,7 +137,7 @@ func TestSkipHealthChecks(t *testing.T) {
"livenessProbeMissing": {ID: "livenessProbeMissing", Message: "Liveness probe should be configured", Success: false, Severity: "warning", Category: "Reliability"},
}
var actualResult Result
actualResult, err = applyControllerSchemaChecks(context.Background(), &c, nil, deployment)
actualResult, err = applyControllerSchemaChecks(&c, nil, deployment)
if err != nil {
panic(err)
}
@@ -156,7 +156,7 @@ func TestSkipHealthChecks(t *testing.T) {
Dangers: uint(0),
}
expectedResults = ResultSet{}
actualResult, err = applyControllerSchemaChecks(context.Background(), &c, nil, job)
actualResult, err = applyControllerSchemaChecks(&c, nil, job)
if err != nil {
panic(err)
}
@@ -174,7 +174,7 @@ func TestSkipHealthChecks(t *testing.T) {
Dangers: uint(0),
}
expectedResults = ResultSet{}
actualResult, err = applyControllerSchemaChecks(context.Background(), &c, nil, cronjob)
actualResult, err = applyControllerSchemaChecks(&c, nil, cronjob)
if err != nil {
panic(err)
}
@@ -210,7 +210,7 @@ func TestControllerExemptions(t *testing.T) {
resources := []kube.GenericResource{workload}
var actualResults []Result
actualResults, err = ApplyAllSchemaChecksToAllResources(context.Background(), &c, nil, resources)
actualResults, err = ApplyAllSchemaChecksToAllResources(&c, nil, resources)
if err != nil {
panic(err)
}
@@ -221,7 +221,7 @@ func TestControllerExemptions(t *testing.T) {
c.Exemptions = []conf.Exemption{{
Namespace: "foo",
}}
actualResults, err = ApplyAllSchemaChecksToAllResources(context.Background(), &c, nil, resources)
actualResults, err = ApplyAllSchemaChecksToAllResources(&c, nil, resources)
if err != nil {
panic(err)
}
@@ -233,7 +233,7 @@ func TestControllerExemptions(t *testing.T) {
resources[0].ObjectMeta.SetAnnotations(map[string]string{
exemptionAnnotationKey: "true",
})
actualResults, err = ApplyAllSchemaChecksToAllResources(context.Background(), &c, nil, resources)
actualResults, err = ApplyAllSchemaChecksToAllResources(&c, nil, resources)
if err != nil {
panic(err)
}
@@ -242,7 +242,7 @@ func TestControllerExemptions(t *testing.T) {
assert.EqualValues(t, expectedExemptSum, actualResults[0].GetSummary())
c.DisallowExemptions = true
actualResults, err = ApplyAllSchemaChecksToAllResources(context.Background(), &c, nil, resources)
actualResults, err = ApplyAllSchemaChecksToAllResources(&c, nil, resources)
if err != nil {
panic(err)
}

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