Compare commits

..
183 changed files with 16542 additions and 8663 deletions
+35 -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:
@@ -44,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
@@ -68,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
@@ -103,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
@@ -114,13 +115,12 @@ jobs:
executor: vm
steps:
- checkout
- *set_environment_variables
- *install_k8s
- *test_k8s
test:
docker:
- image: cimg/go:1.26
- image: cimg/go:1.19
steps:
- checkout
- *set_environment_variables
@@ -128,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
@@ -136,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.14.2
- 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
@@ -161,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@v3
- uses: actions/checkout@v2
- name: Setup polaris
uses: ./.github/actions/setup-polaris
with:
@@ -18,7 +18,7 @@ jobs:
build-ext:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2
- name: Setup polaris
uses: fairwindsops/polaris/.github/actions/setup-polaris@master
with:
+1 -1
View File
@@ -23,6 +23,7 @@ Tiltfile
main
.DS_Store
*-packr.go
dist
.vscode
@@ -30,4 +31,3 @@ dist
node_modules
/dist
docs/README.md
+3 -8
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:
@@ -30,10 +30,6 @@ builds:
goarm:
- 6
- 7
# Go does not support windows/arm (32-bit); only windows/arm64 is supported.
ignore:
- goos: windows
goarch: arm
archives:
- id: polaris
builds: ["polaris"]
@@ -56,12 +52,11 @@ brews:
- name: polaris
# This is replaced using `envsubst`, depending on the git branch.
skip_upload: ${skip_release}
repository:
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:
+1 -1
View File
@@ -1,2 +1,2 @@
## DO NOT EDIT - Managed by Terraform
* @sudermanjr @jdesouza @vitorvezani
* @rbren @mggude
+3 -12
View File
@@ -1,16 +1,7 @@
FROM alpine:3.23
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
RUN apk update && apk -U upgrade && apk --no-cache add ca-certificates
RUN apk -U upgrade
RUN apk --no-cache add ca-certificates
RUN addgroup -S polaris && adduser -u 1200 -S polaris -G polaris
USER 1200
+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://fairwinds.com/pricing),
a platform for auditing and enforcing policy in Kubernetes clusters.
<a href="https://fairwinds.com/pricing">
<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:
@@ -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:
+45
View File
@@ -0,0 +1,45 @@
successMessage: The container does not set potentially sensitive environment variables
failureMessage: The container sets potentially sensitive environment variables
category: Security
target: Container
schemaString: |
'$schema': http://json-schema.org/draft-07/schema
type: object
properties:
env:
type: array
items:
type: object
oneOf:
- not:
required: ["value"]
- properties:
name:
type: string
'$comment': These environment variable names will be disallowed.
allOf:
- not:
pattern: '(?i)^AWS_SECRET_ACCESS_KEY$'
- not:
pattern: '(?i)^GOOGLE_APPLICATION_CREDENTIALS$'
- not:
pattern: '(?i)^AZURE_.+KEY$'
- not:
pattern: '(?i)^OCI_CLI_KEY_CONTENT$'
- not:
pattern: '(?i)password'
- not:
pattern: '(?i)token'
- not:
pattern: '(?i)bearer'
- not:
pattern: '(?i)secret'
'$comment': This allows variable names not excluded above.
- pattern: '(?i).*'
value:
type: string
'$comment': These environment variable values will be disallowed.
allOf:
- not:
'$comment': This matches variations like begin private key, begin rsa private key ...
pattern: '(?i)\s*-BEGIN\s+.*PRIVATE KEY-\s*'
@@ -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
@@ -2,16 +2,20 @@ 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
'$schema': http://json-schema.org/draft-07/schema
type: object
required:
- topologySpreadConstraints
properties:
topologySpreadConstraints:
type: array
minItems: 1
items:
type: object
properties:
topologyKey:
anyOf:
- type: string
const: "kubernetes.io/hostname"
- type: string
const: "topology.kubernetes.io/zone"
+14 -40
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 {
@@ -155,12 +147,8 @@ func ProcessHelmTemplates(helmChart string, helmValues []string, helmSkipTests b
"--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
}
+8 -20
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)
@@ -87,9 +77,7 @@ 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")
}
os.Stderr.WriteString("\n\nWant more? Automate Polaris for free with Fairwinds Insights!\n🚀 https://fairwinds.com/insights-signup/polaris 🚀 \n")
},
}
+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 {
+6 -10
View File
@@ -11,16 +11,6 @@ var sf14gv = 32793;
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(sf14g, s);
})();
(function() {
var gtag = document.createElement('script');
gtag.src = "https://www.googletagmanager.com/gtag/js?id=G-ZR5M5SRYKY";
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(gtag, s);
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-ZR5M5SRYKY');
})();
!function(f,b,e,v,n,t,s)
{if(f.fbq)return;n=f.fbq=function(){n.callMethod?
n.callMethod.apply(n,arguments):n.queue.push(arguments)};
@@ -31,3 +21,9 @@ s.parentNode.insertBefore(t,s)}(window,document,'script',
'https://connect.facebook.net/en_US/fbevents.js');
fbq('init', '521127644762074');
fbq('track', 'PageView');
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-TM95WXQ');
+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
+3 -7
View File
@@ -14,14 +14,10 @@ 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
`missingPodDisruptionBudget` | `ignore`
`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`
## Background
@@ -73,4 +69,4 @@ 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/)
- [Topology Spread Cosntraints](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,29 @@
checks:
# reliability
deploymentMissingReplicas: warning
priorityClassNotSet: warning
priorityClassNotSet: ignore
tagNotSpecified: danger
pullPolicyNotAlways: warning
readinessProbeMissing: warning
livenessProbeMissing: warning
metadataAndInstanceMismatched: warning
metadataAndNameMismatched: ignore
pdbDisruptionsIsZero: warning
missingPodDisruptionBudget: warning
missingPodDisruptionBudget: ignore
topologySpreadConstraint: warning
hpaMaxAvailability: warning
hpaMinAvailability: warning
pdbMinAvailableGreaterThanHPAMinReplicas: warning
# 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 +31,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:
+80 -71
View File
@@ -1,87 +1,96 @@
module github.com/fairwindsops/polaris
go 1.26
go 1.19
require (
github.com/fairwindsops/controller-utils v0.3.4
github.com/fatih/color v1.18.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/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.3
gomodules.xyz/jsonpatch/v2 v2.5.0
golang.org/x/text v0.5.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1
k8s.io/api v0.35.2
k8s.io/apimachinery v0.35.2
k8s.io/client-go v0.35.2
sigs.k8s.io/controller-runtime v0.23.3
sigs.k8s.io/yaml v1.6.0
k8s.io/api v0.26.0
k8s.io/apimachinery v0.26.0
k8s.io/client-go v0.26.0
sigs.k8s.io/controller-runtime v0.14.1
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.14.0 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // 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/cespare/xxhash/v2 v2.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/emicklei/go-restful/v3 v3.10.1 // 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.6 // indirect
github.com/go-openapi/jsonreference v0.20.1 // 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.3 // 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.1.0 // 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.17 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // 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.14.0 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.39.0 // indirect
github.com/prometheus/procfs v0.9.0 // indirect
github.com/qri-io/jsonpointer v0.1.1 // indirect
golang.org/x/crypto v0.4.0 // indirect
golang.org/x/net v0.4.0 // indirect
golang.org/x/oauth2 v0.3.0 // indirect
golang.org/x/sys v0.3.0 // indirect
golang.org/x/term v0.3.0 // indirect
golang.org/x/time v0.3.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.2 // 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.26.0 // indirect
k8s.io/klog/v2 v2.80.1 // indirect
k8s.io/kube-openapi v0.0.0-20221207184640-f3cff1453715 // indirect
k8s.io/utils v0.0.0-20221128185143-99ec85e7a448 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
)
+905 -178
View File
File diff suppressed because it is too large Load Diff
+7 -17
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,11 @@ var (
"deploymentMissingReplicas",
// Pod checks
"hostIPCSet",
"hostPathSet",
"hostProcess",
"hostPIDSet",
"hostNetworkSet",
"automountServiceAccountToken",
"topologySpreadConstraint",
// Container checks
"procMount",
"memoryLimitsMissing",
"memoryRequestsMissing",
"cpuLimitsMissing",
@@ -58,7 +56,7 @@ var (
// Other checks
"tlsSettingsMissing",
"pdbDisruptionsIsZero",
"metadataAndInstanceMismatched",
"metadataAndNameMismatched",
"missingPodDisruptionBudget",
"missingNetworkPolicy",
"sensitiveConfigmapContent",
@@ -70,21 +68,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,52 +0,0 @@
successMessage: The container does not set potentially sensitive environment variables
failureMessage: The container sets potentially sensitive environment variables
category: Security
target: Container
schemaString: |
'$schema': https://json-schema.org/draft/2019-09/schema
type: object
properties:
env:
type: array
items:
type: object
anyOf:
- not:
required: ["value"]
- required: ["name", "value"]
properties:
name:
type: string
'$comment': These environment variable names will be disallowed.
allOf:
- not:
pattern: '(?i)^AWS_SECRET_ACCESS_KEY$'
- not:
pattern: '(?i)^GOOGLE_APPLICATION_CREDENTIALS$'
- not:
pattern: '(?i)^AZURE_.+KEY$'
- not:
pattern: '(?i)^OCI_CLI_KEY_CONTENT$'
- not:
pattern: '(?i)password'
- not:
pattern: '(?i)token'
- not:
pattern: '(?i)bearer'
- not:
pattern: '(?i)secret'
'$comment': This allows variable names not excluded above.
- pattern: '(?i).*'
value:
type: string
'$comment': These environment variable values will be disallowed.
allOf:
- not:
'$comment': THis matches variations like begin private key, begin rsa private key ...
pattern: '(?i)\s*-BEGIN\s+.*PRIVATE KEY-\s*'
- required: ["name", "valueFrom"]
properties:
name:
type: string
valueFrom:
type: object
-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
}
-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
}
+24 -24
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{})
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,9 +252,9 @@ 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
}
@@ -263,10 +263,10 @@ func GetObject(ctx context.Context, namespace, kind, version, name string, dynam
}
// 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
+118 -81
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,48 +235,30 @@ 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
@@ -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{})
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, dynamicClient 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, dynamicClient, 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
+18 -23
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)
@@ -152,25 +151,21 @@ func TestGetResourceFromAPI(t *testing.T) {
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)
}
+11 -11
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)
@@ -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)
}
-19
View File
@@ -1,19 +0,0 @@
package validator
import (
"sync"
"github.com/qri-io/jsonschema"
)
type validatorFunction func(test schemaTestCase) (bool, []jsonschema.KeyError, error)
var validatorMapper = map[string]validatorFunction{}
var lock = &sync.Mutex{}
func registerCustomChecks(name string, check validatorFunction) {
lock.Lock()
defer lock.Unlock()
validatorMapper[name] = check
}

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