1
0
Fork 0
mirror of https://github.com/kyverno/policy-reporter.git synced 2024-12-14 11:57:32 +00:00

Internal Rewrite (#91)

* Internal Rewrite

Signed-off-by: Frank Jogeleit <frank.jogeleit@web.de>
This commit is contained in:
Frank Jogeleit 2021-12-13 16:02:40 +01:00 committed by GitHub
parent ad8fa022fd
commit 0de8e8bead
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
112 changed files with 4894 additions and 3269 deletions

View file

@ -1,4 +1,8 @@
.deploy
config.yaml config.yaml
build build
README.md README.md
docs docs
**/test.db
sqlite-database.db
values.yaml

6
.gitignore vendored
View file

@ -1,4 +1,8 @@
.deploy .deploy
/config.yaml /config.yaml
build build
/test.yaml /test.yaml
**/test.db
sqlite-database.db
values.yaml
coverage.out

View file

@ -1,5 +1,42 @@
# Changelog # Changelog
# 2.0.0
## Chart
* Removed deprecated values `crdVersion`, `cleanupDebounceTime`
* Simplify `policyPriorities`, `policyPriorities.enabled` was removed along with the watch feature
* Priority determined mainly over severity
* Add `sources` filter to target configurations
* Improved `NetworkPolicy` configuration for all components
* Metrics now an optional feature
* Each component expose a single Port `8080`
See [Migration Docs](http://localhost:3000/guide/05-migration) for details
## Policy Reporter
* modular functions for separate activation/deactivation
* REST API
* Metrics API
* Target pushes
* PolicyReports are now stored in an internal SQLite
* extended REST API based on the new SQLite DB for filters and grouping of data
* metrics API is now optional
* metrics and REST API using the same HTTP Server (were separated before)
* improved CRD watch logic with Kubernetes client informer
* `Yandex` changed to a general `S3` target.
## Policy Reporter UI
* Rewrite with NuxtJS
* Simplified Proxy
* Improved SPA file handling
## Policy Reporter Kyverno Plugin
* modular functions for separate activation/deactivation
* REST API
* Metrics API
* metrics and REST API using the same HTTP Server (were separated before)
* improved CRD watch logic with Kubernetes client informer
# 1.12.6 # 1.12.6
* Update Go Base Image for all Components * Update Go Base Image for all Components
* Policy Reporter [[#90](https://github.com/kyverno/policy-reporter-ui/pull/90) by [fjogeleit](https://github.com/fjogeleit)] * Policy Reporter [[#90](https://github.com/kyverno/policy-reporter-ui/pull/90) by [fjogeleit](https://github.com/fjogeleit)]

View file

@ -14,7 +14,7 @@ RUN go env
RUN go get -d -v \ RUN go get -d -v \
&& go install -v && go install -v
RUN CGO_ENABLED=0 go build -ldflags="${LD_FLAGS}" -o /app/build/policyreporter -v RUN CGO_ENABLED=1 go build -ldflags="${LD_FLAGS}" -o /app/build/policyreporter -v
FROM scratch FROM scratch
LABEL MAINTAINER="Frank Jogeleit <frank.jogeleit@gweb.de>" LABEL MAINTAINER="Frank Jogeleit <frank.jogeleit@gweb.de>"

View file

@ -1,8 +1,8 @@
GO ?= go GO ?= go
BUILD ?= build BUILD ?= build
REPO ?= ghcr.io/kyverno/policy-reporter REPO ?= ghcr.io/kyverno/policy-reporter
IMAGE_TAG ?= 1.10.1 IMAGE_TAG ?= 1.11.0
LD_FLAGS="-s -w" LD_FLAGS='-s -w -linkmode external -extldflags "-static"'
all: build all: build
@ -16,15 +16,15 @@ prepare:
.PHONY: test .PHONY: test
test: test:
go test -v ./... -timeout=120s go test -v ./... -timeout=10s
.PHONY: coverage .PHONY: coverage
coverage: coverage:
go test -v ./... -covermode=count -coverprofile=coverage.out -timeout=120s go test -v ./... -covermode=count -coverprofile=coverage.out -timeout=30s
.PHONY: build .PHONY: build
build: prepare build: prepare
CGO_ENABLED=0 $(GO) build -v -ldflags="-s -w" $(GOFLAGS) -o $(BUILD)/policyreporter . CGO_ENABLED=1 $(GO) build -v -ldflags="-s -w" $(GOFLAGS) -o $(BUILD)/policyreporter .
.PHONY: docker-build .PHONY: docker-build
docker-build: docker-build:
@ -37,4 +37,4 @@ docker-push:
.PHONY: docker-push-dev .PHONY: docker-push-dev
docker-push-dev: docker-push-dev:
@docker buildx build --progress plane --platform linux/amd64 --tag $(REPO):dev . --build-arg LD_FLAGS=$(LD_FLAGS) --push @docker buildx build --progress plane --platform linux/arm64,linux/amd64 --tag $(REPO):dev . --build-arg LD_FLAGS=$(LD_FLAGS) --push

View file

@ -5,13 +5,13 @@
Kyverno ships with two types of validation. You can either enforce a rule or audit it. If you don't want to block developers or if you want to try out a new rule, you can use the audit functionality. The audit configuration creates [PolicyReports](https://kyverno.io/docs/policy-reports/) which you can access with `kubectl`. Because I can't find a simple solution to get a general overview of this PolicyReports and PolicyReportResults, I created this tool to send information about PolicyReports to different targets like [Grafana Loki](https://grafana.com/oss/loki/), [Elasticsearch](https://www.elastic.co/de/elasticsearch/) or [Slack](https://slack.com/). Kyverno ships with two types of validation. You can either enforce a rule or audit it. If you don't want to block developers or if you want to try out a new rule, you can use the audit functionality. The audit configuration creates [PolicyReports](https://kyverno.io/docs/policy-reports/) which you can access with `kubectl`. Because I can't find a simple solution to get a general overview of this PolicyReports and PolicyReportResults, I created this tool to send information about PolicyReports to different targets like [Grafana Loki](https://grafana.com/oss/loki/), [Elasticsearch](https://www.elastic.co/de/elasticsearch/) or [Slack](https://slack.com/).
Policy Reporter provides also a Prometheus Metrics API as well as an standalone mode along with the [Policy Reporter UI](https://github.com/kyverno/policy-reporter/wiki/policy-reporter-ui). Policy Reporter provides also a Prometheus Metrics API as well as an standalone mode along with the [Policy Reporter UI](https://kyverno.github.io/policy-reporter/guide/02-getting-started#core--policy-reporter-ui).
This project is in an early stage. Please let me know if anything did not work as expected or if you want to send your audits to unsupported targets. This project is in an early stage. Please let me know if anything did not work as expected or if you want to send your audits to unsupported targets.
## Documentation ## Documentation
You can find detailed Information and Screens about Features and Configurations in the [Documentation](https://github.com/kyverno/policy-reporter/wiki). You can find detailed Information and Screens about Features and Configurations in the [Documentation](https://kyverno.github.io/policy-reporter).
## Getting Started ## Getting Started
@ -27,10 +27,10 @@ helm repo update
### Basic Installation ### Basic Installation
The basic installation provides an Prometheus Metrics Endpoint and different REST APIs, for more details have a look at the [Documentation](https://github.com/kyverno/policy-reporter/wiki/getting-started). The basic installation provides optional Prometheus Metrics and/or optional REST APIs, for more details have a look at the [Documentation](https://kyverno.github.io/policy-reporter/guide/02-getting-started).
```bash ```bash
helm install policy-reporter policy-reporter/policy-reporter -n policy-reporter --create-namespace helm install policy-reporter policy-reporter/policy-reporter -n policy-reporter --set metrics.enabled=true --set rest.enabled=true --create-namespace
``` ```
### Installation without Helm or Kustomize ### Installation without Helm or Kustomize
@ -48,24 +48,25 @@ kubectl port-forward service/policy-reporter-ui 8082:8080 -n policy-reporter
``` ```
Open `http://localhost:8082/` in your browser. Open `http://localhost:8082/` in your browser.
Check the [Documentation](https://github.com/kyverno/policy-reporter/wiki/policy-reporter-ui) for Screens and additional Information Check the [Documentation](https://kyverno.github.io/policy-reporter/guide/02-getting-started#core--policy-reporter-ui) for Screens and additional Information
## Targets ## Targets
Policy Reporter supports the following [Targets](https://github.com/kyverno/policy-reporter/wiki/targets) to send new (Cluster)PolicyReport Results too: Policy Reporter supports the following [Targets](https://kyverno.github.io/policy-reporter/core/06-targets) to send new (Cluster)PolicyReport Results too:
* [Grafana Loki](https://github.com/kyverno/policy-reporter/wiki/grafana-loki) * [Grafana Loki](https://kyverno.github.io/policy-reporter/core/06-targets#grafana-loki)
* [Elasticsearch](https://github.com/kyverno/policy-reporter/wiki/elasticsearch) * [Elasticsearch](https://kyverno.github.io/policy-reporter/core/06-targets#elasticsearch)
* [Slack](https://github.com/kyverno/policy-reporter/wiki/slack) * [Slack](https://kyverno.github.io/policy-reporter/core/06-targets#slack)
* [Discord](https://github.com/kyverno/policy-reporter/wiki/discord) * [Discord](https://kyverno.github.io/policy-reporter/core/06-targets#discord)
* [MS Teams](https://github.com/kyverno/policy-reporter/wiki/ms-teams) * [MS Teams](https://kyverno.github.io/policy-reporter/core/06-targets#microsoft-teams)
* [Policy Reporter UI](https://github.com/kyverno/policy-reporter/wiki/policy-reporter-ui-log) * [Policy Reporter UI](https://kyverno.github.io/policy-reporter/core/06-targets#policy-reporter-ui)
* [S3](https://kyverno.github.io/policy-reporter/core/06-targets#s3)
## Monitoring ## Monitoring
The Helm Chart includes optional SubChart for [Prometheus Operator](https://github.com/prometheus-community/helm-charts/tree/main/charts/kube-prometheus-stack) Integration. The provided Dashboards working without Loki and are only based on the Prometheus Metrics. The Helm Chart includes optional SubChart for [Prometheus Operator](https://github.com/prometheus-community/helm-charts/tree/main/charts/kube-prometheus-stack) Integration. The provided Dashboards working without Loki and are only based on the Prometheus Metrics.
Have a look into the [Documentation](https://github.com/kyverno/policy-reporter/wiki/prometheus-operator-integration) for details. Have a look into the [Documentation](https://kyverno.github.io/policy-reporter/guide/04-helm-chart-core/#configure-the-servicemonitor) for details.
### Grafana Dashboard Import ### Grafana Dashboard Import

View file

@ -1,12 +1,12 @@
dependencies: dependencies:
- name: monitoring - name: monitoring
repository: "" repository: ""
version: 1.5.0 version: 2.0.0
- name: ui - name: ui
repository: "" repository: ""
version: 1.10.3 version: 2.0.0
- name: kyvernoPlugin - name: kyvernoPlugin
repository: "" repository: ""
version: 0.7.1 version: 1.0.0
digest: sha256:ee1646e3f1a6dd7c329a7a4e6acb4d629aa1a4f750c82de737ae55c10e3136c0 digest: sha256:7346779f27b9446f94271cb4b7233bac1b2549cf1205219b055bef926d2ea110
generated: "2021-11-11T09:48:37.183013+01:00" generated: "2021-12-13T15:40:00.73344+01:00"

View file

@ -5,11 +5,11 @@ description: |
It creates Prometheus Metrics and can send rule validation events to different targets like Loki, Elasticsearch, Slack or Discord It creates Prometheus Metrics and can send rule validation events to different targets like Loki, Elasticsearch, Slack or Discord
type: application type: application
version: 1.12.6 version: 2.0.0
appVersion: 1.10.3 appVersion: 2.0.0
icon: https://github.com/kyverno/kyverno/raw/main/img/logo.png icon: https://github.com/kyverno/kyverno/raw/main/img/logo.png
home: https://github.com/kyverno/policy-reporter/wiki home: https://kyverno.github.io/policy-reporter
sources: sources:
- https://github.com/kyverno/policy-reporter - https://github.com/kyverno/policy-reporter
maintainers: maintainers:
@ -18,10 +18,10 @@ maintainers:
dependencies: dependencies:
- name: monitoring - name: monitoring
condition: monitoring.enabled condition: monitoring.enabled
version: "1.5.0" version: "2.0.0"
- name: ui - name: ui
condition: ui.enabled condition: ui.enabled
version: "1.10.3" version: "2.0.0"
- name: kyvernoPlugin - name: kyvernoPlugin
condition: kyvernoPlugin.enabled condition: kyvernoPlugin.enabled
version: "0.7.1" version: "1.0.0"

View file

@ -4,7 +4,7 @@ Kyverno ships with two types of validation. You can either enforce a rule or aud
## Documentation ## Documentation
You can find detailed Information and Screens about Features and Configurations in the [Documentation](https://github.com/kyverno/policy-reporter/wiki). You can find detailed Information and Screens about Features and Configurations in the [Documentation](https://kyverno.github.io/policy-reporter/guide/02-getting-started#core--policy-reporter-ui).
## Getting Started ## Getting Started
@ -20,7 +20,7 @@ helm repo update
### Basic Installation ### Basic Installation
The basic installation provides an Prometheus Metrics Endpoint and different REST APIs, for more details have a look at the [Documentation](https://github.com/kyverno/policy-reporter/wiki/getting-started). The basic installation provides an Prometheus Metrics Endpoint and different REST APIs, for more details have a look at the [Documentation](https://kyverno.github.io/policy-reporter/guide/02-getting-started).
```bash ```bash
helm install policy-reporter policy-reporter/policy-reporter -n policy-reporter --create-namespace helm install policy-reporter policy-reporter/policy-reporter -n policy-reporter --create-namespace
@ -37,7 +37,7 @@ kubectl port-forward service/policy-reporter-ui 8082:8080 -n policy-reporter
``` ```
Open `http://localhost:8082/` in your browser. Open `http://localhost:8082/` in your browser.
Check the [Documentation](https://github.com/kyverno/policy-reporter/wiki/policy-reporter-ui) for Screens and additional Information Check the [Documentation](https://kyverno.github.io/policy-reporter/guide/02-getting-started#core--policy-reporter-ui) for Screens and additional Information
## Resources ## Resources

View file

@ -3,5 +3,5 @@ name: kyvernoPlugin
description: Policy Reporter Kyverno Plugin description: Policy Reporter Kyverno Plugin
type: application type: application
version: 0.7.1 version: 1.0.0
appVersion: 0.3.3 appVersion: 1.0.0

View file

@ -58,3 +58,11 @@ Create the name of the service account to use
{{- default "default" .Values.serviceAccount.name }} {{- default "default" .Values.serviceAccount.name }}
{{- end }} {{- end }}
{{- end }} {{- end }}
{{/*
Selector labels
*/}}
{{- define "ui.selectorLabels" -}}
app.kubernetes.io/name: ui
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}

View file

@ -45,7 +45,9 @@ spec:
{{- toYaml .Values.securityContext | nindent 12 }} {{- toYaml .Values.securityContext | nindent 12 }}
{{- end }} {{- end }}
args: args:
- --apiPort=8080 - --port=8080
- --metrics-enabled={{ .Values.metrics.enabled }}
- --rest-enabled={{ .Values.rest.enabled }}
ports: ports:
- name: http - name: http
containerPort: 2113 containerPort: 2113

View file

@ -12,19 +12,17 @@ spec:
- Egress - Egress
ingress: ingress:
- from: - from:
- podSelector:
matchLabels:
{{- include "ui.selectorLabels" . | nindent 10 }}
ports: ports:
- protocol: TCP - protocol: TCP
port: 8080 port: 8080
- from: {{- with .Values.networkPolicy.ingress }}
ports: {{- toYaml . | nindent 2 }}
- protocol: TCP {{- end }}
port: 2113
egress:
- to:
ports:
- protocol: TCP
port: {{ .Values.networkPolicy.kubernetesApiPort }}
{{- with .Values.networkPolicy.egress }} {{- with .Values.networkPolicy.egress }}
egress:
{{- toYaml . | nindent 2 }} {{- toYaml . | nindent 2 }}
{{- end }} {{- end }}
{{- end }} {{- end }}

View file

@ -2,7 +2,7 @@ image:
registry: ghcr.io registry: ghcr.io
repository: kyverno/policy-reporter-kyverno-plugin repository: kyverno/policy-reporter-kyverno-plugin
pullPolicy: IfNotPresent pullPolicy: IfNotPresent
tag: 0.3.3 tag: 1.0.0
imagePullSecrets: [] imagePullSecrets: []
@ -81,9 +81,22 @@ tolerations: []
# Anti-affinity to disallow deploying client and master nodes on the same worker node # Anti-affinity to disallow deploying client and master nodes on the same worker node
affinity: {} affinity: {}
# REST API
rest:
enabled: true
# Prometheus Metrics API
metrics:
enabled: true
# Enable a NetworkPolicy for this chart. Useful on clusters where Network Policies are # Enable a NetworkPolicy for this chart. Useful on clusters where Network Policies are
# used and configured in a default-deny fashion. # used and configured in a default-deny fashion.
networkPolicy: networkPolicy:
enabled: false enabled: false
kubernetesApiPort: 6443 # Kubernetes API Server
egress: [] egress:
- to:
ports:
- protocol: TCP
port: 6443
ingress: []

View file

@ -3,5 +3,5 @@ name: monitoring
description: Policy Reporter Monitoring with predefined ServiceMonitor and Grafana Dashboards description: Policy Reporter Monitoring with predefined ServiceMonitor and Grafana Dashboards
type: application type: application
version: 1.5.0 version: 2.0.0
appVersion: 0.0.0 appVersion: 0.0.0

View file

@ -44,8 +44,6 @@ app.kubernetes.io/instance: {{ .Release.Name }}
{{- define "monitoring.namespace" -}} {{- define "monitoring.namespace" -}}
{{- if .Values.grafana.namespace -}} {{- if .Values.grafana.namespace -}}
{{- .Values.grafana.namespace -}} {{- .Values.grafana.namespace -}}
{{- else if .Values.namespace -}}
{{- .Values.namespace -}}
{{- else -}} {{- else -}}
{{- .Release.Namespace -}} {{- .Release.Namespace -}}
{{- end }} {{- end }}

View file

@ -1,6 +1,3 @@
# monitoring namespace for Dashboard Configurations
namespace: cattle-dashboards
plugins: plugins:
kyverno: false kyverno: false

View file

@ -3,5 +3,5 @@ name: ui
description: Policy Reporter UI description: Policy Reporter UI
type: application type: application
version: 1.10.3 version: 2.0.0
appVersion: 0.15.1 appVersion: 1.0.0

View file

@ -51,6 +51,22 @@ app.kubernetes.io/name: {{ include "ui.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }} app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }} {{- end }}
{{/*
Policy Reporter Selector labels
*/}}
{{- define "policyreporter.selectorLabels" -}}
app.kubernetes.io/name: policy-reporter
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Kyverno Plugin Selector labels
*/}}
{{- define "kyvernoplugin.selectorLabels" -}}
app.kubernetes.io/name: kyverno-plugin
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/* {{/*
Create the name of the service account to use Create the name of the service account to use
*/}} */}}

View file

@ -0,0 +1,10 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "ui.fullname" . }}-config
labels:
{{- include "ui.labels" . | nindent 4 }}
data:
config.yaml: |-
logSize: {{ .Values.log.size }}
displayMode: {{ .Values.displayMode | quote }}

View file

@ -45,8 +45,8 @@ spec:
{{- toYaml .Values.securityContext | nindent 12 }} {{- toYaml .Values.securityContext | nindent 12 }}
{{- end }} {{- end }}
args: args:
- -backend=http://{{ include "ui.policyReportServiceName" . }}:{{ .Values.global.port }} - -config=/app/config/config.yaml
- -log-size={{ .Values.log.size }} - -policy-reporter=http://{{ include "ui.policyReportServiceName" . }}:{{ .Values.global.port }}
{{- if or .Values.plugins.kyverno .Values.global.plugins.kyverno }} {{- if or .Values.plugins.kyverno .Values.global.plugins.kyverno }}
- -kyverno-plugin=http://{{ include "ui.kyvernoPluginServiceName" . }}:8080 - -kyverno-plugin=http://{{ include "ui.kyvernoPluginServiceName" . }}:8080
{{- end }} {{- end }}
@ -62,8 +62,16 @@ spec:
httpGet: httpGet:
path: / path: /
port: http port: http
volumeMounts:
- name: config-file
mountPath: /app/config
subPath: config.yaml
resources: resources:
{{- toYaml .Values.resources | nindent 12 }} {{- toYaml .Values.resources | nindent 12 }}
volumes:
- name: config-file
configMap:
name: {{ include "ui.fullname" . }}-config
{{- with .Values.nodeSelector }} {{- with .Values.nodeSelector }}
nodeSelector: nodeSelector:
{{- toYaml . | nindent 8 }} {{- toYaml . | nindent 8 }}

View file

@ -2,8 +2,9 @@
apiVersion: networking.k8s.io/v1 apiVersion: networking.k8s.io/v1
kind: NetworkPolicy kind: NetworkPolicy
metadata: metadata:
labels: {{ include "policyreporter.labels" . | nindent 4 }} name: {{ include "ui.fullname" . }}
name: {{ include "policyreporter.fullname" . }} labels:
{{- include "ui.labels" . | nindent 4 }}
spec: spec:
podSelector: podSelector:
matchLabels: {{- include "ui.selectorLabels" . | nindent 6 }} matchLabels: {{- include "ui.selectorLabels" . | nindent 6 }}
@ -17,9 +18,21 @@ spec:
port: {{ .Values.service.port }} port: {{ .Values.service.port }}
egress: egress:
- to: - to:
- podSelector:
matchLabels:
{{- include "policyreporter.selectorLabels" . | nindent 10 }}
ports: ports:
- protocol: TCP - protocol: TCP
port: {{ .Values.global.port }} port: 8080
{{- if or .Values.plugins.kyverno .Values.global.plugins.kyverno }}
- to:
- podSelector:
matchLabels:
{{- include "kyvernoplugin.selectorLabels" . | nindent 10 }}
ports:
- protocol: TCP
port: 8080
{{- end }}
{{- with .Values.networkPolicy.egress }} {{- with .Values.networkPolicy.egress }}
{{- toYaml . | nindent 2 }} {{- toYaml . | nindent 2 }}
{{- end }} {{- end }}

View file

@ -1,5 +1,8 @@
enabled: false enabled: false
# possible default displayModes: light/dark
displayMode: ""
log: log:
# holds the latest 200 validation results in the UI Log # holds the latest 200 validation results in the UI Log
size: 200 size: 200
@ -11,7 +14,7 @@ image:
registry: ghcr.io registry: ghcr.io
repository: kyverno/policy-reporter-ui repository: kyverno/policy-reporter-ui
pullPolicy: IfNotPresent pullPolicy: IfNotPresent
tag: 0.15.1 tag: 1.0.0
imagePullSecrets: [] imagePullSecrets: []

View file

@ -2,6 +2,10 @@ loki:
host: {{ .Values.target.loki.host | quote }} host: {{ .Values.target.loki.host | quote }}
minimumPriority: {{ .Values.target.loki.minimumPriority | quote }} minimumPriority: {{ .Values.target.loki.minimumPriority | quote }}
skipExistingOnStartup: {{ .Values.target.loki.skipExistingOnStartup }} skipExistingOnStartup: {{ .Values.target.loki.skipExistingOnStartup }}
{{- with .Values.target.loki.sources }}
sources:
{{- toYaml . | nindent 4 }}
{{- end }}
elasticsearch: elasticsearch:
host: {{ .Values.target.elasticsearch.host | quote }} host: {{ .Values.target.elasticsearch.host | quote }}
@ -9,33 +13,62 @@ elasticsearch:
rotation: {{ .Values.target.elasticsearch.rotation | default "dayli" | quote }} rotation: {{ .Values.target.elasticsearch.rotation | default "dayli" | quote }}
minimumPriority: {{ .Values.target.elasticsearch.minimumPriority | quote }} minimumPriority: {{ .Values.target.elasticsearch.minimumPriority | quote }}
skipExistingOnStartup: {{ .Values.target.elasticsearch.skipExistingOnStartup }} skipExistingOnStartup: {{ .Values.target.elasticsearch.skipExistingOnStartup }}
{{- with .Values.target.elasticsearch.sources }}
sources:
{{- toYaml . | nindent 4 }}
{{- end }}
slack: slack:
webhook: {{ .Values.target.slack.webhook | quote }} webhook: {{ .Values.target.slack.webhook | quote }}
minimumPriority: {{ .Values.target.slack.minimumPriority | quote }} minimumPriority: {{ .Values.target.slack.minimumPriority | quote }}
skipExistingOnStartup: {{ .Values.target.slack.skipExistingOnStartup }} skipExistingOnStartup: {{ .Values.target.slack.skipExistingOnStartup }}
{{- with .Values.target.slack.sources }}
sources:
{{- toYaml . | nindent 4 }}
{{- end }}
discord: discord:
webhook: {{ .Values.target.discord.webhook | quote }} webhook: {{ .Values.target.discord.webhook | quote }}
minimumPriority: {{ .Values.target.discord.minimumPriority | quote }} minimumPriority: {{ .Values.target.discord.minimumPriority | quote }}
skipExistingOnStartup: {{ .Values.target.discord.skipExistingOnStartup }} skipExistingOnStartup: {{ .Values.target.discord.skipExistingOnStartup }}
{{- with .Values.target.discord.sources }}
sources:
{{- toYaml . | nindent 4 }}
{{- end }}
teams: teams:
webhook: {{ .Values.target.teams.webhook | quote }} webhook: {{ .Values.target.teams.webhook | quote }}
minimumPriority: {{ .Values.target.teams.minimumPriority | quote }} minimumPriority: {{ .Values.target.teams.minimumPriority | quote }}
skipExistingOnStartup: {{ .Values.target.teams.skipExistingOnStartup }} skipExistingOnStartup: {{ .Values.target.teams.skipExistingOnStartup }}
{{- with .Values.target.teams.sources }}
sources:
{{- toYaml . | nindent 4 }}
{{- end }}
ui: ui:
host: {{ include "policyreporter.uihost" . }} host: {{ include "policyreporter.uihost" . }}
minimumPriority: {{ .Values.target.ui.minimumPriority | quote }} minimumPriority: {{ .Values.target.ui.minimumPriority | quote }}
skipExistingOnStartup: {{ .Values.target.ui.skipExistingOnStartup }} skipExistingOnStartup: {{ .Values.target.ui.skipExistingOnStartup }}
{{- with .Values.target.ui.sources }}
sources:
{{- toYaml . | nindent 4 }}
{{- end }}
yandex: s3:
accessKeyID: {{ .Values.target.yandex.accessKeyID }} accessKeyID: {{ .Values.target.s3.accessKeyID }}
secretAccessKey: {{ .Values.target.yandex.secretAccessKey }} secretAccessKey: {{ .Values.target.s3.secretAccessKey }}
region: {{ .Values.target.yandex.region }} region: {{ .Values.target.s3.region }}
endpoint: {{ .Values.target.yandex.endpoint }} endpoint: {{ .Values.target.s3.endpoint }}
bucket: {{ .Values.target.yandex.bucket }} bucket: {{ .Values.target.s3.bucket }}
prefix: {{ .Values.target.yandex.prefix }} prefix: {{ .Values.target.s3.prefix }}
minimumPriority: {{ .Values.target.yandex.minimumPriority | quote }} minimumPriority: {{ .Values.target.s3.minimumPriority | quote }}
skipExistingOnStartup: {{ .Values.target.yandex.skipExistingOnStartup }} skipExistingOnStartup: {{ .Values.target.s3.skipExistingOnStartup }}
{{- with .Values.target.s3.sources }}
sources:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- with .Values.policyPriorities }}
priorityMap:
{{- toYaml . | nindent 2 }}
{{- end }}

View file

@ -2,7 +2,7 @@
apiVersion: v1 apiVersion: v1
kind: Secret kind: Secret
metadata: metadata:
name: {{ include "policyreporter.fullname" . }}-targets name: {{ include "policyreporter.fullname" . }}-config
labels: labels:
{{- include "policyreporter.labels" . | nindent 4 }} {{- include "policyreporter.labels" . | nindent 4 }}
type: Opaque type: Opaque

View file

@ -28,8 +28,7 @@ spec:
{{- toYaml . | nindent 8 }} {{- toYaml . | nindent 8 }}
{{- end }} {{- end }}
annotations: annotations:
checksum/secret: {{ include (print .Template.BasePath "/targetssecret.yaml") . | sha256sum | quote }} checksum/secret: {{ include (print .Template.BasePath "/config-secret.yaml") . | sha256sum | quote }}
policy-priorities/enabled: {{ .Values.policyPriorities.enabled | quote }}
{{- with .Values.podAnnotations }} {{- with .Values.podAnnotations }}
{{- toYaml . | nindent 8 }} {{- toYaml . | nindent 8 }}
{{- end }} {{- end }}
@ -40,6 +39,10 @@ spec:
{{- end }} {{- end }}
serviceAccountName: {{ include "policyreporter.serviceAccountName" . }} serviceAccountName: {{ include "policyreporter.serviceAccountName" . }}
automountServiceAccountToken: true automountServiceAccountToken: true
{{- if .Values.podSecurityContext }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
{{- end }}
containers: containers:
- name: {{ .Chart.Name }} - name: {{ .Chart.Name }}
image: "{{ .Values.image.registry }}/{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" image: "{{ .Values.image.registry }}/{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
@ -50,11 +53,11 @@ spec:
{{- end }} {{- end }}
args: args:
- --config=/app/config.yaml - --config=/app/config.yaml
- --dbfile=/sqlite/database.db
- --metrics-enabled={{ or .Values.metrics.enabled .Values.monitoring.enabled }}
- --rest-enabled={{ or .Values.rest.enabled .Values.ui.enabled }}
ports: ports:
- name: http - name: http
containerPort: 2112
protocol: TCP
- name: rest
containerPort: 8080 containerPort: 8080
protocol: TCP protocol: TCP
livenessProbe: livenessProbe:
@ -64,6 +67,8 @@ spec:
resources: resources:
{{- toYaml .Values.resources | nindent 12 }} {{- toYaml .Values.resources | nindent 12 }}
volumeMounts: volumeMounts:
- name: sqlite
mountPath: /sqlite
- name: config-file - name: config-file
mountPath: /app/config.yaml mountPath: /app/config.yaml
{{- if and .Values.existingTargetConfig.enabled .Values.existingTargetConfig.subPath }} {{- if and .Values.existingTargetConfig.enabled .Values.existingTargetConfig.subPath }}
@ -75,12 +80,14 @@ spec:
- name: NAMESPACE - name: NAMESPACE
value: {{ .Release.Namespace }} value: {{ .Release.Namespace }}
volumes: volumes:
- name: sqlite
emptyDir: {}
- name: config-file - name: config-file
secret: secret:
{{- if and .Values.existingTargetConfig.enabled .Values.existingTargetConfig.name }} {{- if and .Values.existingTargetConfig.enabled .Values.existingTargetConfig.name }}
secretName: {{ .Values.existingTargetConfig.name }} secretName: {{ .Values.existingTargetConfig.name }}
{{- else }} {{- else }}
secretName: {{ include "policyreporter.fullname" . }}-targets secretName: {{ include "policyreporter.fullname" . }}-config
{{- end }} {{- end }}
optional: true optional: true
{{- with .Values.nodeSelector }} {{- with .Values.nodeSelector }}

View file

@ -12,24 +12,23 @@ spec:
- Egress - Egress
ingress: ingress:
- from: - from:
- podSelector:
matchLabels: {{- include "ui.selectorLabels" . | nindent 10 }}
ports: ports:
- protocol: TCP - protocol: TCP
port: {{ .Values.global.port }} port: 8080
- from: {{- with .Values.networkPolicy.ingress }}
ports: {{- toYaml . | nindent 2 }}
- protocol: TCP {{- end }}
port: {{ .Values.service.port }}
egress: egress:
{{- if .Values.ui.enabled }}
- to: - to:
- podSelector: - podSelector:
matchLabels: {{- include "ui.selectorLabels" . | nindent 10 }} matchLabels: {{- include "ui.selectorLabels" . | nindent 10 }}
ports: ports:
- protocol: TCP - protocol: TCP
port: {{ .Values.ui.service.port }} port: {{ .Values.ui.service.port }}
- to: {{- end }}
ports:
- protocol: TCP
port: {{ .Values.networkPolicy.kubernetesApiPort }}
{{- with .Values.networkPolicy.egress }} {{- with .Values.networkPolicy.egress }}
{{- toYaml . | nindent 2 }} {{- toYaml . | nindent 2 }}
{{- end }} {{- end }}

View file

@ -1,12 +0,0 @@
{{- if and .Values.policyPriorities.enabled .Values.policyPriorities.mapping -}}
apiVersion: v1
kind: ConfigMap
metadata:
name: policy-reporter-priorities
labels:
{{- include "policyreporter.labels" . | nindent 4 }}
data:
{{- with .Values.policyPriorities.mapping }}
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}

View file

@ -1,17 +0,0 @@
{{- if .Values.policyPriorities.enabled -}}
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: {{ include "policyreporter.fullname" . }}
labels:
{{- include "policyreporter.labels" . | nindent 4 }}
rules:
- apiGroups:
- ''
resources:
- configmaps
verbs:
- get
- list
- watch
{{- end }}

View file

@ -1,16 +0,0 @@
{{- if .Values.policyPriorities.enabled -}}
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: {{ include "policyreporter.fullname" . }}
labels:
{{- include "policyreporter.labels" . | nindent 4 }}
roleRef:
kind: Role
name: {{ include "policyreporter.fullname" . }}
apiGroup: rbac.authorization.k8s.io
subjects:
- kind: "ServiceAccount"
name: {{ include "policyreporter.serviceAccountName" . }}
namespace: {{ .Release.Namespace }}
{{- end }}

View file

@ -1,5 +1,3 @@
{{- $apiEnabled := .Values.api.enabled -}}
{{- $uiEnabled := .Values.ui.enabled -}}
{{- if .Values.service.enabled -}} {{- if .Values.service.enabled -}}
apiVersion: v1 apiVersion: v1
kind: Service kind: Service
@ -21,12 +19,6 @@ spec:
targetPort: http targetPort: http
protocol: TCP protocol: TCP
name: http name: http
{{- if or $apiEnabled $uiEnabled }}
- port: {{ .Values.global.port }}
targetPort: rest
protocol: TCP
name: rest
{{- end }}
selector: selector:
{{- include "policyreporter.selectorLabels" . | nindent 4 }} {{- include "policyreporter.selectorLabels" . | nindent 4 }}
{{- end }} {{- end }}

View file

@ -2,7 +2,7 @@ image:
registry: ghcr.io registry: ghcr.io
repository: kyverno/policy-reporter repository: kyverno/policy-reporter
pullPolicy: IfNotPresent pullPolicy: IfNotPresent
tag: 1.10.3 tag: 2.0.0
imagePullSecrets: [] imagePullSecrets: []
@ -42,7 +42,10 @@ service:
labels: {} labels: {}
type: ClusterIP type: ClusterIP
# integer number. This is port for service # integer number. This is port for service
port: 2112 port: 8080
podSecurityContext:
fsGroup: 1234
securityContext: securityContext:
runAsUser: 1234 runAsUser: 1234
@ -66,18 +69,31 @@ resources: {}
# resources, such as Minikube. If you do want to specify resources, uncomment the following # resources, such as Minikube. If you do want to specify resources, uncomment the following
# lines, adjust them as necessary, and remove the curly braces after 'resources:'. # lines, adjust them as necessary, and remove the curly braces after 'resources:'.
# limits: # limits:
# memory: 30Mi # memory: 100Mi
# cpu: 10m # cpu: 10m
# requests: # requests:
# memory: 20Mi # memory: 75Mi
# cpu: 5m # cpu: 5m
# Enable a NetworkPolicy for this chart. Useful on clusters where Network Policies are # Enable a NetworkPolicy for this chart. Useful on clusters where Network Policies are
# used and configured in a default-deny fashion. # used and configured in a default-deny fashion.
networkPolicy: networkPolicy:
enabled: false enabled: false
egress: [] # Kubernetes API Server
kubernetesApiPort: 6443 egress:
- to:
ports:
- protocol: TCP
port: 6443
ingress: []
# REST API
rest:
enabled: false
# Prometheus Metrics API
metrics:
enabled: false
# enable policy-report-ui # enable policy-report-ui
ui: ui:
@ -107,26 +123,12 @@ global:
# additional labels added on each resource # additional labels added on each resource
labels: {} labels: {}
# DEPRECTED - Can be removed # configure mappings from policy to priority
# Policy Reporter watches now for both existing versions by default # you can use default to configure a default priority for fail results
crdVersion: v1alpha1 # example mapping
# default: warning
# DEPRECTED - Can be removed # require-ns-labels: error
# Policy Reporter uses a new internal cache instead policyPriorities: {}
cleanupDebounceTime: 20
api:
enabled: false
# Policy Priorities
policyPriorities:
enabled: false
# configure mappings from policy to priority
# you can use default to configure a default priority not passing results
# example mapping
# default: warning
# require-ns-labels: error
mapping: {}
# Reference a configuration which already exists instead of creating one # Reference a configuration which already exists instead of creating one
existingTargetConfig: existingTargetConfig:
@ -143,6 +145,8 @@ target:
host: "" host: ""
# minimum priority "" < info < warning < critical < error # minimum priority "" < info < warning < critical < error
minimumPriority: "" minimumPriority: ""
# list of sources which should send to loki
sources: []
# Skip already existing PolicyReportResults on startup # Skip already existing PolicyReportResults on startup
skipExistingOnStartup: true skipExistingOnStartup: true
@ -156,6 +160,8 @@ target:
rotation: "" rotation: ""
# minimum priority "" < info < warning < critical < error # minimum priority "" < info < warning < critical < error
minimumPriority: "" minimumPriority: ""
# list of sources which should send to elasticsearch
sources: []
# Skip already existing PolicyReportResults on startup # Skip already existing PolicyReportResults on startup
skipExistingOnStartup: true skipExistingOnStartup: true
@ -164,6 +170,8 @@ target:
webhook: "" webhook: ""
# minimum priority "" < info < warning < critical < error # minimum priority "" < info < warning < critical < error
minimumPriority: "" minimumPriority: ""
# list of sources which should send to slack
sources: []
# Skip already existing PolicyReportResults on startup # Skip already existing PolicyReportResults on startup
skipExistingOnStartup: true skipExistingOnStartup: true
@ -172,6 +180,8 @@ target:
webhook: "" webhook: ""
# minimum priority "" < info < warning < critical < error # minimum priority "" < info < warning < critical < error
minimumPriority: "" minimumPriority: ""
# list of sources which should send to discord
sources: []
# Skip already existing PolicyReportResults on startup # Skip already existing PolicyReportResults on startup
skipExistingOnStartup: true skipExistingOnStartup: true
@ -180,6 +190,8 @@ target:
webhook: "" webhook: ""
# minimum priority "" < info < warning < critical < error # minimum priority "" < info < warning < critical < error
minimumPriority: "" minimumPriority: ""
# list of sources which should send to teams
sources: []
# Skip already existing PolicyReportResults on startup # Skip already existing PolicyReportResults on startup
skipExistingOnStartup: true skipExistingOnStartup: true
@ -188,18 +200,30 @@ target:
host: "" host: ""
# minimum priority "" < info < warning < critical < error # minimum priority "" < info < warning < critical < error
minimumPriority: "warning" minimumPriority: "warning"
# list of sources which should send to the UI Log
sources: []
# Skip already existing PolicyReportResults on startup # Skip already existing PolicyReportResults on startup
skipExistingOnStartup: true skipExistingOnStartup: true
yandex: s3:
accessKeyID: "" # yandex access key # S3 access key
secretAccessKey: "" # yandex secret access key accessKeyID: ""
region: "" # yandex storage region (default: ru-central-1) # S3 secret access key
endpoint: "" # yandex storage endpoint (default: https://storage.yandexcloud.net) secretAccessKey: ""
bucket: "" # Yandex storage, bucket name # S3 storage region
prefix: "" # name of prefix, keys will have format: s3://<bucket>/<prefix>/YYYY-MM-DD/YYYY-MM-DDTHH:mm:ss.s+01:00.json region: ""
minimumPriority: "" # minimum priority "" < info < warning < critical < error # S3 storage endpoint
skipExistingOnStartup: true # Skip already existing PolicyReportResults on startup endpoint: ""
# S3 storage, bucket name
bucket: ""
# name of prefix, keys will have format: s3://<bucket>/<prefix>/YYYY-MM-DD/YYYY-MM-DDTHH:mm:ss.s+01:00.json
prefix: ""
# minimum priority "" < info < warning < critical < error
minimumPriority: ""
# list of sources which should send to S3
sources: []
# Skip already existing PolicyReportResults on startup
skipExistingOnStartup: true
# Node labels for pod assignment # Node labels for pod assignment
# ref: https://kubernetes.io/docs/user-guide/node-selection/ # ref: https://kubernetes.io/docs/user-guide/node-selection/
@ -216,10 +240,10 @@ affinity: {}
livenessProbe: livenessProbe:
httpGet: httpGet:
path: /ready path: /ready
port: rest port: http
# readinessProbe for policy-reporter # readinessProbe for policy-reporter
readinessProbe: readinessProbe:
httpGet: httpGet:
path: /healthz path: /healthz
port: rest port: http

View file

@ -25,8 +25,6 @@ func NewCLI() *cobra.Command {
func loadConfig(cmd *cobra.Command) (*config.Config, error) { func loadConfig(cmd *cobra.Command) (*config.Config, error) {
v := viper.New() v := viper.New()
v.SetDefault("namespace", "policy-reporter")
cfgFile := "" cfgFile := ""
configFlag := cmd.Flags().Lookup("config") configFlag := cmd.Flags().Lookup("config")
@ -44,38 +42,36 @@ func loadConfig(cmd *cobra.Command) (*config.Config, error) {
v.AutomaticEnv() v.AutomaticEnv()
if err := v.ReadInConfig(); err != nil { if err := v.ReadInConfig(); err != nil {
log.Println("[INFO] No target configuration file found") log.Println("[INFO] No configuration file found")
}
if flag := cmd.Flags().Lookup("loki"); flag != nil {
v.BindPFlag("loki.host", flag)
}
if flag := cmd.Flags().Lookup("loki-minimum-priority"); flag != nil {
v.BindPFlag("loki.minimumPriority", flag)
}
if flag := cmd.Flags().Lookup("loki-skip-existing-on-startup"); flag != nil {
v.BindPFlag("loki.skipExistingOnStartup", flag)
} }
if flag := cmd.Flags().Lookup("kubeconfig"); flag != nil { if flag := cmd.Flags().Lookup("kubeconfig"); flag != nil {
v.BindPFlag("kubeconfig", flag) v.BindPFlag("kubeconfig", flag)
} }
if flag := cmd.Flags().Lookup("crd-version"); flag != nil { if flag := cmd.Flags().Lookup("port"); flag != nil {
v.BindPFlag("crdVersion", flag)
}
if flag := cmd.Flags().Lookup("cleanup-debounce-time"); flag != nil {
v.BindPFlag("cleanupDebounceTime", flag)
}
if flag := cmd.Flags().Lookup("apiPort"); flag != nil {
v.BindPFlag("api.port", flag) v.BindPFlag("api.port", flag)
} }
if flag := cmd.Flags().Lookup("rest-enabled"); flag != nil {
v.BindPFlag("rest.enabled", flag)
}
if flag := cmd.Flags().Lookup("metrics-enabled"); flag != nil {
v.BindPFlag("metrics.enabled", flag)
}
if flag := cmd.Flags().Lookup("dbfile"); flag != nil {
v.BindPFlag("dbfile", flag)
}
c := &config.Config{} c := &config.Config{}
err := v.Unmarshal(c) err := v.Unmarshal(c)
if c.DBFile == "" {
c.DBFile = "sqlite-database.db"
}
return c, err return c, err
} }

View file

@ -2,15 +2,12 @@ package cmd
import ( import (
"context" "context"
"errors"
"flag" "flag"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
"net/http"
"github.com/kyverno/policy-reporter/pkg/config" "github.com/kyverno/policy-reporter/pkg/config"
"github.com/kyverno/policy-reporter/pkg/metrics"
"github.com/kyverno/policy-reporter/pkg/report"
"github.com/kyverno/policy-reporter/pkg/target"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"k8s.io/client-go/rest" "k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/tools/clientcmd"
@ -40,43 +37,46 @@ func newRunCMD() *cobra.Command {
resolver := config.NewResolver(c, k8sConfig) resolver := config.NewResolver(c, k8sConfig)
client, err := resolver.PolicyReportClient(ctx) client, err := resolver.PolicyReportClient()
if err != nil { if err != nil {
return err return err
} }
client.RegisterCallback(metrics.CreateMetricsCallback()) resolver.RegisterSendResultListener()
targets := resolver.TargetClients()
if len(targets) > 0 {
client.RegisterPolicyResultCallback(func(r report.Result, e bool) {
for _, t := range targets {
go func(target target.Client, result report.Result, preExisted bool) {
if preExisted && target.SkipExistingOnStartup() {
return
}
target.Send(result)
}(t, r, e)
}
})
client.RegisterPolicyResultWatcher(resolver.SkipExistingOnStartup())
}
g := &errgroup.Group{} g := &errgroup.Group{}
g.Go(func() error { server := resolver.APIServer(client.GetFoundResources())
return client.StartWatching(ctx)
})
g.Go(resolver.APIServer().Start) if c.REST.Enabled {
db, err := resolver.Database()
if err != nil {
return err
}
defer db.Close()
store, err := resolver.PolicyReportStore(db)
if err != nil {
return err
}
resolver.RegisterStoreListener(store)
server.RegisterV1Handler(store)
}
if c.Metrics.Enabled {
resolver.RegisterMetricsListener()
server.RegisterMetricsHandler()
}
g.Go(server.Start)
g.Go(func() error { g.Go(func() error {
http.Handle("/metrics", promhttp.Handler()) eventChan := client.WatchPolicyReports(ctx)
return http.ListenAndServe(":2112", nil) resolver.EventPublisher().Publish(eventChan)
return errors.New("event publisher stoped")
}) })
return g.Wait() return g.Wait()
@ -86,7 +86,10 @@ func newRunCMD() *cobra.Command {
// For local usage // For local usage
cmd.PersistentFlags().StringP("kubeconfig", "k", "", "absolute path to the kubeconfig file") cmd.PersistentFlags().StringP("kubeconfig", "k", "", "absolute path to the kubeconfig file")
cmd.PersistentFlags().StringP("config", "c", "", "target configuration file") cmd.PersistentFlags().StringP("config", "c", "", "target configuration file")
cmd.PersistentFlags().IntP("apiPort", "a", 8080, "http port for the optional rest api") cmd.PersistentFlags().IntP("port", "p", 8080, "http port for the optional rest api")
cmd.PersistentFlags().StringP("dbfile", "d", "sqlite-database.db", "path to the SQLite DB File")
cmd.PersistentFlags().BoolP("metrics-enabled", "m", false, "Enable Policy Reporter's Metrics API")
cmd.PersistentFlags().BoolP("rest-enabled", "r", false, "Enable Policy Reporter's REST API")
flag.Parse() flag.Parse()

67
go.mod
View file

@ -1,30 +1,67 @@
module github.com/kyverno/policy-reporter module github.com/kyverno/policy-reporter
go 1.15 go 1.17
require ( require (
github.com/aws/aws-sdk-go v1.41.9 github.com/aws/aws-sdk-go v1.42.8
github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/mattn/go-sqlite3 v1.14.9
github.com/google/gofuzz v1.2.0 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/prometheus/client_golang v1.11.0 github.com/prometheus/client_golang v1.11.0
github.com/prometheus/client_model v0.2.0 github.com/prometheus/client_model v0.2.0
github.com/prometheus/common v0.32.1 // indirect
github.com/prometheus/procfs v0.7.3 // indirect
github.com/spf13/cobra v1.2.1 github.com/spf13/cobra v1.2.1
github.com/spf13/viper v1.9.0 github.com/spf13/viper v1.9.0
golang.org/x/net v0.0.0-20211020060615-d418f374d309 // indirect
golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1 // indirect
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
golang.org/x/sys v0.0.0-20211022215931-8e5104632af7 // indirect k8s.io/api v0.22.4
k8s.io/apimachinery v0.22.4
k8s.io/client-go v0.22.4
)
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/evanphx/json-patch v4.11.0+incompatible // indirect
github.com/fsnotify/fsnotify v1.5.1 // indirect
github.com/go-logr/logr v1.2.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-cmp v0.5.6 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/googleapis/gnostic v0.5.5 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/magiconair/properties v1.8.5 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/mitchellh/mapstructure v1.4.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml v1.9.4 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/common v0.32.1 // indirect
github.com/prometheus/procfs v0.7.3 // indirect
github.com/spf13/afero v1.6.0 // indirect
github.com/spf13/cast v1.4.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.2.0 // indirect
golang.org/x/net v0.0.0-20211118161319-6a13c67c3ce4 // indirect
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect
golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1 // indirect
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
golang.org/x/text v0.3.7 // indirect golang.org/x/text v0.3.7 // indirect
k8s.io/api v0.22.2 golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 // indirect
k8s.io/apimachinery v0.22.2 google.golang.org/appengine v1.6.7 // indirect
k8s.io/client-go v0.22.2 google.golang.org/protobuf v1.27.1 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/ini.v1 v1.64.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
k8s.io/klog/v2 v2.30.0 // indirect k8s.io/klog/v2 v2.30.0 // indirect
k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b // indirect k8s.io/kube-openapi v0.0.0-20211109043538-20434351676c // indirect
k8s.io/utils v0.0.0-20211116205334-6203023598ed // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.0 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect sigs.k8s.io/yaml v1.3.0 // indirect
) )

48
go.sum
View file

@ -67,8 +67,8 @@ github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmV
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
github.com/aws/aws-sdk-go v1.41.9 h1:Xb4gWjA90ju0u6Fr2lMAsMOGuhw1g4sTFOqh9SUHgN0= github.com/aws/aws-sdk-go v1.42.8 h1:Tj2RP4Fas1mYchwbmw0qWLJIEATAseyp5iTa1D+LWYQ=
github.com/aws/aws-sdk-go v1.41.9/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q= github.com/aws/aws-sdk-go v1.42.8/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@ -212,6 +212,7 @@ github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLe
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
@ -299,6 +300,8 @@ github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-sqlite3 v1.14.9 h1:10HX2Td0ocZpYEjhilsuo6WWtUqttj2Kb0KtD86/KYA=
github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
@ -530,8 +533,8 @@ golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211020060615-d418f374d309 h1:A0lJIi+hcTR6aajJH4YqKWwohY4aW9RO7oRMcdv+HKI= golang.org/x/net v0.0.0-20211118161319-6a13c67c3ce4 h1:DZshvxDdVoeKIbudAdFEKi+f70l51luSy/7b76ibTY0=
golang.org/x/net v0.0.0-20211020060615-d418f374d309/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211118161319-6a13c67c3ce4/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -548,8 +551,8 @@ golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ
golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1 h1:B333XXssMuKQeBwiNODx4TupZy7bf4sxFZnN2ZOcvUE= golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 h1:RerP+noqYHUQ8CMRcPlC2nvTa4dcBIjegkuWdcUDuqg=
golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -630,8 +633,8 @@ golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211022215931-8e5104632af7 h1:e2q1CMOFXDvurT2sa2yhJAkuA2n8Rd9tMDd7Tcfvs6M= golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1 h1:kwrAHlwJ0DUBZwQ238v+Uod/3eZ8B2K5rYsUHBQvzmI=
golang.org/x/sys v0.0.0-20211022215931-8e5104632af7/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@ -650,8 +653,9 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs=
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 h1:GZokNIeuVkl3aZHJchRrr13WCsols02MLUcz1U9is6M=
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@ -853,8 +857,9 @@ gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMy
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.63.2 h1:tGK/CyBg7SMzb60vP1M03vNZ3VDu3wGQJwn7Sxi9r3c=
gopkg.in/ini.v1 v1.63.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.63.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.64.0 h1:Mj2zXEXcNb5joEiSA0zc3HZpTst/iyjNiR4CN8tDzOg=
gopkg.in/ini.v1 v1.64.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
@ -877,28 +882,29 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
k8s.io/api v0.22.2 h1:M8ZzAD0V6725Fjg53fKeTJxGsJvRbk4TEm/fexHMtfw= k8s.io/api v0.22.4 h1:UvyHW0ezB2oIgHAxlYoo6UJQObYXU7awuNarwoHEOjw=
k8s.io/api v0.22.2/go.mod h1:y3ydYpLJAaDI+BbSe2xmGcqxiWHmWjkEeIbiwHvnPR8= k8s.io/api v0.22.4/go.mod h1:Rgs+9gIGYC5laXQSZZ9JqT5NevNgoGiOdVWi1BAB3qk=
k8s.io/apimachinery v0.22.2 h1:ejz6y/zNma8clPVfNDLnPbleBo6MpoFy/HBiBqCouVk= k8s.io/apimachinery v0.22.4 h1:9uwcvPpukBw/Ri0EUmWz+49cnFtaoiyEhQTK+xOe7Ck=
k8s.io/apimachinery v0.22.2/go.mod h1:O3oNtNadZdeOMxHFVxOreoznohCpy0z6mocxbZr7oJ0= k8s.io/apimachinery v0.22.4/go.mod h1:yU6oA6Gnax9RrxGzVvPFFJ+mpnW6PBSqp0sx0I0HHW0=
k8s.io/client-go v0.22.2 h1:DaSQgs02aCC1QcwUdkKZWOeaVsQjYvWv8ZazcZ6JcHc= k8s.io/client-go v0.22.4 h1:aAQ1Wk+I3bjCNk35YWUqbaueqrIonkfDPJSPDDe8Kfg=
k8s.io/client-go v0.22.2/go.mod h1:sAlhrkVDf50ZHx6z4K0S40wISNTarf1r800F+RlCF6U= k8s.io/client-go v0.22.4/go.mod h1:Yzw4e5e7h1LNHA4uqnMVrpEpUs1hJOiuBsJKIlRCHDA=
k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE=
k8s.io/klog/v2 v2.9.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec= k8s.io/klog/v2 v2.9.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec=
k8s.io/klog/v2 v2.30.0 h1:bUO6drIvCIsvZ/XFgfxoGFQU/a4Qkh0iAlvUR7vlHJw= k8s.io/klog/v2 v2.30.0 h1:bUO6drIvCIsvZ/XFgfxoGFQU/a4Qkh0iAlvUR7vlHJw=
k8s.io/klog/v2 v2.30.0/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= k8s.io/klog/v2 v2.30.0/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e h1:KLHHjkdQFomZy8+06csTWZ0m1343QqxZhR2LJ1OxCYM= k8s.io/kube-openapi v0.0.0-20211109043538-20434351676c h1:jvamsI1tn9V0S8jicyX82qaFC0H/NKxv2e5mbqsgR80=
k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e/go.mod h1:vHXdDvt9+2spS2Rx9ql3I8tycm3H9FDfdUoIuKCefvw= k8s.io/kube-openapi v0.0.0-20211109043538-20434351676c/go.mod h1:vHXdDvt9+2spS2Rx9ql3I8tycm3H9FDfdUoIuKCefvw=
k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b h1:wxEMGetGMur3J1xuGLQY7GEQYg9bZxKn3tKo5k/eYcs= k8s.io/utils v0.0.0-20211116205334-6203023598ed h1:ck1fRPWPJWsMd8ZRFsWc6mh/zHp5fZ/shhbrgPUxDAE=
k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= k8s.io/utils v0.0.0-20211116205334-6203023598ed/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw=
sigs.k8s.io/structured-merge-diff/v4 v4.1.2 h1:Hr/htKFmJEbtMgS/UD0N+gtgctAqz81t3nu+sPzynno=
sigs.k8s.io/structured-merge-diff/v4 v4.1.2/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= sigs.k8s.io/structured-merge-diff/v4 v4.1.2/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4=
sigs.k8s.io/structured-merge-diff/v4 v4.2.0 h1:kDvPBbnPk+qYmkHmSo8vKGp438IASWofnbbUKDE/bv0=
sigs.k8s.io/structured-merge-diff/v4 v4.2.0/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4=
sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=

View file

@ -6,7 +6,7 @@ The installation requires a `policy-reporter` namespace. Because the installatio
## Policy Reporter ## Policy Reporter
The `policy-reporter` folder is the basic installation for Policy Reporter without the UI. Includes a basic Configuration Secret `policy-reporter-targets`, empty by default and the `http://policy-reporter:2112/metrics` Endpoint. The `policy-reporter` folder is the basic installation for Policy Reporter without the UI. Includes a basic Configuration Secret `policy-reporter-targets`, empty by default and the `http://policy-reporter:8080/metrics` Endpoint.
### Installation ### Installation
@ -58,6 +58,8 @@ kubectl apply -f https://raw.githubusercontent.com/kyverno/policy-reporter/main/
To configure your notification targets for Policy Reporter create a secret called `policy-reporter-targets` in the `policy-reporter` namespace with an key `config.yaml` as key and the following structure as value: To configure your notification targets for Policy Reporter create a secret called `policy-reporter-targets` in the `policy-reporter` namespace with an key `config.yaml` as key and the following structure as value:
```yaml ```yaml
priorityMap: {}
loki: loki:
host: "" host: ""
minimumPriority: "" minimumPriority: ""
@ -90,6 +92,15 @@ ui:
minimumPriority: "" minimumPriority: ""
skipExistingOnStartup: true skipExistingOnStartup: true
s3:
endpoint: ""
region: ""
bucket: ""
secretAccessKey: ""
accessKeyID: ""
minimumPriority: "warning"
skipExistingOnStartup: true
sources: []
``` ```
The `kyverno-policy-reporter-ui` and `default-policy-reporter-ui` installation has an optional preconfigured `target-security.yaml` to apply. This secret configures the Policy Reporter UI as target for Policy Reporter. The `kyverno-policy-reporter-ui` and `default-policy-reporter-ui` installation has an optional preconfigured `target-security.yaml` to apply. This secret configures the Policy Reporter UI as target for Policy Reporter.

View file

@ -2,7 +2,7 @@
apiVersion: v1 apiVersion: v1
kind: Secret kind: Secret
metadata: metadata:
name: policy-reporter-targets name: policy-reporter-config
namespace: policy-reporter namespace: policy-reporter
labels: labels:
app.kubernetes.io/name: policy-reporter app.kubernetes.io/name: policy-reporter

View file

@ -66,14 +66,10 @@ metadata:
spec: spec:
type: ClusterIP type: ClusterIP
ports: ports:
- port: 2112 - port: 8080
targetPort: http targetPort: http
protocol: TCP protocol: TCP
name: http name: http
- port: 8080
targetPort: rest
protocol: TCP
name: rest
selector: selector:
app.kubernetes.io/name: policy-reporter app.kubernetes.io/name: policy-reporter
--- ---
@ -97,7 +93,7 @@ spec:
automountServiceAccountToken: false automountServiceAccountToken: false
containers: containers:
- name: ui - name: ui
image: "ghcr.io/kyverno/policy-reporter-ui:0.15.0" image: "ghcr.io/kyverno/policy-reporter-ui:1.0.0"
imagePullPolicy: IfNotPresent imagePullPolicy: IfNotPresent
securityContext: securityContext:
allowPrivilegeEscalation: false allowPrivilegeEscalation: false
@ -109,8 +105,7 @@ spec:
runAsNonRoot: true runAsNonRoot: true
runAsUser: 1234 runAsUser: 1234
args: args:
- -backend=http://policy-reporter:8080 - -policy-reporter=http://policy-reporter:8080
- -log-size=200
ports: ports:
- name: http - name: http
containerPort: 8080 containerPort: 8080
@ -146,9 +141,11 @@ spec:
spec: spec:
serviceAccountName: policy-reporter serviceAccountName: policy-reporter
automountServiceAccountToken: true automountServiceAccountToken: true
securityContext:
fsGroup: 1234
containers: containers:
- name: policy-reporter - name: policy-reporter
image: "ghcr.io/kyverno/policy-reporter:1.8.7" image: "ghcr.io/kyverno/policy-reporter:2.0.0"
imagePullPolicy: IfNotPresent imagePullPolicy: IfNotPresent
securityContext: securityContext:
allowPrivilegeEscalation: false allowPrivilegeEscalation: false
@ -161,32 +158,32 @@ spec:
runAsUser: 1234 runAsUser: 1234
args: args:
- --config=/app/config.yaml - --config=/app/config.yaml
- --dbfile=/sqlite/database.db
- --rest-enabled
ports: ports:
- name: http - name: http
containerPort: 2112
protocol: TCP
- name: rest
containerPort: 8080 containerPort: 8080
protocol: TCP protocol: TCP
livenessProbe: livenessProbe:
httpGet: httpGet:
path: /healthz path: /healthz
port: rest port: http
readinessProbe: readinessProbe:
httpGet: httpGet:
path: /ready path: /ready
port: rest port: http
resources: resources:
{} {}
volumeMounts: volumeMounts:
- name: sqlite
mountPath: /sqlite
- name: config-file - name: config-file
mountPath: /app/config.yaml mountPath: /app/config.yaml
subPath: config.yaml subPath: config.yaml
env:
- name: NAMESPACE
value: policy-reporter
volumes: volumes:
- name: sqlite
emptyDir: {}
- name: config-file - name: config-file
secret: secret:
secretName: policy-reporter-targets secretName: policy-reporter-config
optional: true optional: true

View file

@ -2,7 +2,7 @@
apiVersion: v1 apiVersion: v1
kind: Secret kind: Secret
metadata: metadata:
name: policy-reporter-targets name: policy-reporter-config
namespace: policy-reporter namespace: policy-reporter
labels: labels:
app.kubernetes.io/name: policy-reporter app.kubernetes.io/name: policy-reporter

View file

@ -91,14 +91,10 @@ metadata:
spec: spec:
type: ClusterIP type: ClusterIP
ports: ports:
- port: 2113 - port: 8080
targetPort: http targetPort: http
protocol: TCP protocol: TCP
name: http name: http
- port: 8080
targetPort: rest
protocol: TCP
name: rest
selector: selector:
app.kubernetes.io/name: kyverno-plugin app.kubernetes.io/name: kyverno-plugin
app.kubernetes.io/instance: policy-reporter app.kubernetes.io/instance: policy-reporter
@ -130,14 +126,10 @@ metadata:
spec: spec:
type: ClusterIP type: ClusterIP
ports: ports:
- port: 2112 - port: 8080
targetPort: http targetPort: http
protocol: TCP protocol: TCP
name: http name: http
- port: 8080
targetPort: rest
protocol: TCP
name: rest
selector: selector:
app.kubernetes.io/name: policy-reporter app.kubernetes.io/name: policy-reporter
--- ---
@ -165,7 +157,7 @@ spec:
automountServiceAccountToken: true automountServiceAccountToken: true
containers: containers:
- name: "kyverno-plugin" - name: "kyverno-plugin"
image: "ghcr.io/kyverno/policy-reporter-kyverno-plugin:0.3.2" image: "ghcr.io/kyverno/policy-reporter-kyverno-plugin:1.0.0"
imagePullPolicy: IfNotPresent imagePullPolicy: IfNotPresent
securityContext: securityContext:
allowPrivilegeEscalation: false allowPrivilegeEscalation: false
@ -177,22 +169,19 @@ spec:
runAsNonRoot: true runAsNonRoot: true
runAsUser: 1234 runAsUser: 1234
args: args:
- --apiPort=8080 - --rest-enabled
ports: ports:
- name: http - name: http
containerPort: 2113
protocol: TCP
- name: rest
containerPort: 8080 containerPort: 8080
protocol: TCP protocol: TCP
livenessProbe: livenessProbe:
httpGet: httpGet:
path: /policies path: /policies
port: rest port: http
readinessProbe: readinessProbe:
httpGet: httpGet:
path: /policies path: /policies
port: rest port: http
resources: resources:
{} {}
--- ---
@ -215,7 +204,7 @@ spec:
spec: spec:
containers: containers:
- name: ui - name: ui
image: "fjogeleit/policy-reporter-ui:0.14.0" image: "fjogeleit/policy-reporter-ui:1.0.0"
imagePullPolicy: IfNotPresent imagePullPolicy: IfNotPresent
securityContext: securityContext:
allowPrivilegeEscalation: false allowPrivilegeEscalation: false
@ -227,8 +216,7 @@ spec:
runAsNonRoot: true runAsNonRoot: true
runAsUser: 1234 runAsUser: 1234
args: args:
- -backend=http://policy-reporter:8080 - -policy-reporter=http://policy-reporter:8080
- -log-size=200
- -kyverno-plugin=http://policy-reporter-kyverno-plugin:8080 - -kyverno-plugin=http://policy-reporter-kyverno-plugin:8080
ports: ports:
- name: http - name: http
@ -264,9 +252,11 @@ spec:
spec: spec:
serviceAccountName: policy-reporter serviceAccountName: policy-reporter
automountServiceAccountToken: true automountServiceAccountToken: true
securityContext:
fsGroup: 1234
containers: containers:
- name: policy-reporter - name: policy-reporter
image: "ghcr.io/kyverno/policy-reporter:1.8.7" image: "ghcr.io/kyverno/policy-reporter:2.0.0"
imagePullPolicy: IfNotPresent imagePullPolicy: IfNotPresent
securityContext: securityContext:
allowPrivilegeEscalation: false allowPrivilegeEscalation: false
@ -279,32 +269,32 @@ spec:
runAsUser: 1234 runAsUser: 1234
args: args:
- --config=/app/config.yaml - --config=/app/config.yaml
- --dbfile=/sqlite/database.db
- --rest-enabled
ports: ports:
- name: http - name: http
containerPort: 2112
protocol: TCP
- name: rest
containerPort: 8080 containerPort: 8080
protocol: TCP protocol: TCP
livenessProbe: livenessProbe:
httpGet: httpGet:
path: /ready path: /ready
port: rest port: http
readinessProbe: readinessProbe:
httpGet: httpGet:
path: /healthz path: /healthz
port: rest port: http
resources: resources:
{} {}
volumeMounts: volumeMounts:
- name: sqlite
mountPath: /sqlite
- name: config-file - name: config-file
mountPath: /app/config.yaml mountPath: /app/config.yaml
subPath: config.yaml subPath: config.yaml
env:
- name: NAMESPACE
value: policy-reporter
volumes: volumes:
- name: sqlite
emptyDir: {}
- name: config-file - name: config-file
secret: secret:
secretName: policy-reporter-targets secretName: policy-reporter-config
optional: true optional: true

View file

@ -56,7 +56,7 @@ metadata:
spec: spec:
type: ClusterIP type: ClusterIP
ports: ports:
- port: 2112 - port: 8080
targetPort: http targetPort: http
protocol: TCP protocol: TCP
name: http name: http
@ -84,7 +84,7 @@ spec:
automountServiceAccountToken: true automountServiceAccountToken: true
containers: containers:
- name: policy-reporter - name: policy-reporter
image: "ghcr.io/kyverno/policy-reporter:1.8.7" image: "ghcr.io/kyverno/policy-reporter:2.0.0"
imagePullPolicy: IfNotPresent imagePullPolicy: IfNotPresent
securityContext: securityContext:
allowPrivilegeEscalation: false allowPrivilegeEscalation: false
@ -97,21 +97,19 @@ spec:
runAsUser: 1234 runAsUser: 1234
args: args:
- --config=/app/config.yaml - --config=/app/config.yaml
- --metrics-enabled=true
ports: ports:
- name: http - name: http
containerPort: 2112
protocol: TCP
- name: rest
containerPort: 8080 containerPort: 8080
protocol: TCP protocol: TCP
livenessProbe: livenessProbe:
httpGet: httpGet:
path: /healthz path: /healthz
port: rest port: http
readinessProbe: readinessProbe:
httpGet: httpGet:
path: /ready path: /ready
port: rest port: http
resources: resources:
{} {}
volumeMounts: volumeMounts:

View file

@ -5,9 +5,12 @@ import (
"compress/gzip" "compress/gzip"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"strings"
"testing" "testing"
"github.com/kyverno/policy-reporter/pkg/api" "github.com/kyverno/policy-reporter/pkg/api"
v1 "github.com/kyverno/policy-reporter/pkg/api/v1"
"github.com/kyverno/policy-reporter/pkg/target"
) )
func Test_GzipCompression(t *testing.T) { func Test_GzipCompression(t *testing.T) {
@ -19,7 +22,7 @@ func Test_GzipCompression(t *testing.T) {
req.Header.Add("Accept-Encoding", "gzip") req.Header.Add("Accept-Encoding", "gzip")
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
handler := api.Gzip(api.TargetsHandler(make([]api.Target, 0))) handler := api.Gzip(v1.TargetsHandler(make([]target.Client, 0)))
handler.ServeHTTP(rr, req) handler.ServeHTTP(rr, req)
@ -33,8 +36,8 @@ func Test_GzipCompression(t *testing.T) {
t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK)
} }
expected := `[]` expected := "[]"
if buf.String() != expected { if !strings.Contains(buf.String(), expected) {
t.Errorf("handler returned unexpected body: got %v want %v", buf.String(), expected) t.Errorf("handler returned unexpected body: got %v want %v", buf.String(), expected)
} }
}) })
@ -45,7 +48,7 @@ func Test_GzipCompression(t *testing.T) {
} }
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
handler := api.Gzip(api.TargetsHandler(make([]api.Target, 0))) handler := api.Gzip(v1.TargetsHandler(make([]target.Client, 0)))
handler.ServeHTTP(rr, req) handler.ServeHTTP(rr, req)
@ -53,9 +56,28 @@ func Test_GzipCompression(t *testing.T) {
t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK)
} }
expected := `[]` expected := "[]"
if rr.Body.String() != expected { if !strings.Contains(rr.Body.String(), expected) {
t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected) t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected)
} }
}) })
t.Run("Uncompressed Respose", func(t *testing.T) {
req, err := http.NewRequest("GET", "/targets", nil)
if err != nil {
t.Fatal(err)
}
req.Header.Add("Accept-Encoding", "gzip")
rr := httptest.NewRecorder()
handler := api.Gzip(func(w http.ResponseWriter, req *http.Request) {
w.WriteHeader(204)
})
handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusNoContent {
t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK)
}
})
} }

View file

@ -1,12 +1,9 @@
package api package api
import ( import (
"encoding/json"
"fmt" "fmt"
"log" "log"
"net/http" "net/http"
"github.com/kyverno/policy-reporter/pkg/report"
) )
// HealthzHandler for the Halthz REST API // HealthzHandler for the Halthz REST API
@ -38,72 +35,3 @@ func ReadyHandler() http.HandlerFunc {
fmt.Fprint(w, "{}") fmt.Fprint(w, "{}")
} }
} }
// PolicyReportHandler for the PolicyReport REST API
func PolicyReportHandler(s *report.PolicyReportStore) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
w.WriteHeader(http.StatusOK)
reports := s.List("PolicyReport")
if len(reports) == 0 {
fmt.Fprint(w, "[]")
return
}
apiReports := make([]PolicyReport, 0, len(reports))
for _, r := range reports {
apiReports = append(apiReports, mapPolicyReport(r))
}
if err := json.NewEncoder(w).Encode(apiReports); err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(w, `{ "message": "%s" }`, err.Error())
}
}
}
// ClusterPolicyReportHandler for the ClusterPolicyReport REST API
func ClusterPolicyReportHandler(s *report.PolicyReportStore) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
w.WriteHeader(http.StatusOK)
reports := s.List(report.ClusterPolicyReportType)
if len(reports) == 0 {
fmt.Fprint(w, "[]")
return
}
apiReports := make([]PolicyReport, 0, len(reports))
for _, r := range reports {
apiReports = append(apiReports, mapPolicyReport(r))
}
if err := json.NewEncoder(w).Encode(apiReports); err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(w, `{ "message": "%s" }`, err.Error())
}
}
}
// TargetsHandler for the Targets REST API
func TargetsHandler(targets []Target) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
w.WriteHeader(http.StatusOK)
if len(targets) == 0 {
fmt.Fprint(w, "[]")
return
}
if err := json.NewEncoder(w).Encode(targets); err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(w, `{ "message": "%s" }`, err.Error())
}
}
}

View file

@ -3,199 +3,11 @@ package api_test
import ( import (
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"strings"
"testing" "testing"
"time"
"github.com/kyverno/policy-reporter/pkg/api" "github.com/kyverno/policy-reporter/pkg/api"
"github.com/kyverno/policy-reporter/pkg/report"
) )
func Test_TargetsAPI(t *testing.T) {
t.Run("Empty Respose", func(t *testing.T) {
req, err := http.NewRequest("GET", "/targets", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
handler := api.TargetsHandler(make([]api.Target, 0))
handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK)
}
expected := `[]`
if rr.Body.String() != expected {
t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected)
}
})
t.Run("Respose", func(t *testing.T) {
req, err := http.NewRequest("GET", "/targets", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
handler := api.TargetsHandler([]api.Target{
{Name: "Loki", MinimumPriority: "debug", SkipExistingOnStartup: true},
})
handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK)
}
expected := `[{"name":"Loki","minimumPriority":"debug","skipExistingOnStartup":true}]`
if !strings.Contains(rr.Body.String(), expected) {
t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected)
}
})
}
func Test_PolicyReportAPI(t *testing.T) {
t.Run("Empty Respose", func(t *testing.T) {
req, err := http.NewRequest("GET", "/policy-reports", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
handler := api.PolicyReportHandler(report.NewPolicyReportStore())
handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK)
}
expected := `[]`
if rr.Body.String() != expected {
t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected)
}
})
t.Run("Respose", func(t *testing.T) {
req, err := http.NewRequest("GET", "/policy-reports", nil)
if err != nil {
t.Fatal(err)
}
result := report.Result{
Message: "validation error: requests and limits required. Rule autogen-check-for-requests-and-limits failed at path /spec/template/spec/containers/0/resources/requests/",
Policy: "require-requests-and-limits-required",
Rule: "autogen-check-for-requests-and-limits",
Priority: report.ErrorPriority,
Status: report.Fail,
Category: "resources",
Scored: true,
Resource: report.Resource{
APIVersion: "v1",
Kind: "Deployment",
Name: "nginx",
Namespace: "test",
UID: "536ab69f-1b3c-4bd9-9ba4-274a56188409",
},
}
preport := report.PolicyReport{
Name: "polr-test",
Namespace: "test",
Results: map[string]report.Result{"": result},
Summary: report.Summary{},
CreationTimestamp: time.Now(),
}
store := report.NewPolicyReportStore()
store.Add(preport)
rr := httptest.NewRecorder()
handler := api.PolicyReportHandler(store)
handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK)
}
expected := `[{"name":"polr-test","namespace":"test","results":[{"message":"validation error: requests and limits required. Rule autogen-check-for-requests-and-limits failed at path /spec/template/spec/containers/0/resources/requests/","policy":"require-requests-and-limits-required","rule":"autogen-check-for-requests-and-limits","priority":"error","status":"fail","category":"resources","scored":true,"resource":{"apiVersion":"v1","kind":"Deployment","name":"nginx","namespace":"test","uid":"536ab69f-1b3c-4bd9-9ba4-274a56188409"}}],"summary":{"pass":0,"skip":0,"warn":0,"error":0,"fail":0}`
if !strings.Contains(rr.Body.String(), expected) {
t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected)
}
})
}
func Test_ClusterPolicyReportAPI(t *testing.T) {
t.Run("Empty Respose", func(t *testing.T) {
req, err := http.NewRequest("GET", "/cluster-policy-reports", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
handler := api.ClusterPolicyReportHandler(report.NewPolicyReportStore())
handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK)
}
expected := `[]`
if rr.Body.String() != expected {
t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected)
}
})
t.Run("Respose", func(t *testing.T) {
req, err := http.NewRequest("GET", "/cluster-policy-reports", nil)
if err != nil {
t.Fatal(err)
}
result := report.Result{
Message: "validation error: Namespace label missing",
Policy: "ns-label-env-required",
Rule: "ns-label-required",
Priority: report.ErrorPriority,
Status: report.Fail,
Category: "resources",
Scored: true,
Resource: report.Resource{
APIVersion: "v1",
Kind: "Namespace",
Name: "dev",
UID: "536ab69f-1b3c-4bd9-9ba4-274a56188409",
},
}
creport := report.PolicyReport{
Name: "cpolr-test",
Summary: report.Summary{},
CreationTimestamp: time.Now(),
Results: map[string]report.Result{"": result},
}
store := report.NewPolicyReportStore()
store.Add(creport)
rr := httptest.NewRecorder()
handler := api.ClusterPolicyReportHandler(store)
handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK)
}
expected := `[{"name":"cpolr-test","results":[{"message":"validation error: Namespace label missing","policy":"ns-label-env-required","rule":"ns-label-required","priority":"error","status":"fail","category":"resources","scored":true,"resource":{"apiVersion":"v1","kind":"Namespace","name":"dev","uid":"536ab69f-1b3c-4bd9-9ba4-274a56188409"}}],"summary":{"pass":0,"skip":0,"warn":0,"error":0,"fail":0}`
if !strings.Contains(rr.Body.String(), expected) {
t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected)
}
})
}
func Test_HealthzAPI(t *testing.T) { func Test_HealthzAPI(t *testing.T) {
t.Run("Respose", func(t *testing.T) { t.Run("Respose", func(t *testing.T) {
req, err := http.NewRequest("GET", "/healthz", nil) req, err := http.NewRequest("GET", "/healthz", nil)

View file

@ -1,115 +0,0 @@
package api
import (
"time"
"github.com/kyverno/policy-reporter/pkg/report"
"github.com/kyverno/policy-reporter/pkg/target"
)
// Resource API Model
type Resource struct {
APIVersion string `json:"apiVersion"`
Kind string `json:"kind"`
Name string `json:"name"`
Namespace string `json:"namespace,omitempty"`
UID string `json:"uid"`
}
// Result API Model
type Result struct {
Message string `json:"message"`
Policy string `json:"policy"`
Rule string `json:"rule"`
Priority string `json:"priority"`
Status string `json:"status"`
Severity string `json:"severity,omitempty"`
Category string `json:"category,omitempty"`
Scored bool `json:"scored"`
Properties map[string]string `json:"properties,omitempty"`
Source string `json:"source,omitempty"`
Resource *Resource `json:"resource,omitempty"`
}
// Summary API Model
type Summary struct {
Pass int `json:"pass"`
Skip int `json:"skip"`
Warn int `json:"warn"`
Error int `json:"error"`
Fail int `json:"fail"`
}
// PolicyReport API Model
type PolicyReport struct {
Name string `json:"name"`
Namespace string `json:"namespace,omitempty"`
Results []Result `json:"results"`
Summary Summary `json:"summary"`
CreationTimestamp time.Time `json:"creationTimestamp"`
}
func mapPolicyReport(p report.PolicyReport) PolicyReport {
results := make([]Result, 0, len(p.Results))
for _, r := range p.Results {
result := Result{
Message: r.Message,
Policy: r.Policy,
Rule: r.Rule,
Priority: r.Priority.String(),
Status: r.Status,
Severity: r.Severity,
Category: r.Category,
Scored: r.Scored,
Properties: r.Properties,
Source: r.Source,
}
if r.HasResource() {
result.Resource = &Resource{
Namespace: r.Resource.Namespace,
APIVersion: r.Resource.APIVersion,
Kind: r.Resource.Kind,
Name: r.Resource.Name,
UID: r.Resource.UID,
}
}
results = append(results, result)
}
return PolicyReport{
Name: p.Name,
Namespace: p.Namespace,
CreationTimestamp: p.CreationTimestamp,
Summary: Summary{
Skip: p.Summary.Skip,
Pass: p.Summary.Pass,
Warn: p.Summary.Warn,
Fail: p.Summary.Fail,
Error: p.Summary.Error,
},
Results: results,
}
}
// Target API Model
type Target struct {
Name string `json:"name"`
MinimumPriority string `json:"minimumPriority"`
SkipExistingOnStartup bool `json:"skipExistingOnStartup"`
}
func mapTarget(t target.Client) Target {
minPrio := t.MinimumPriority()
if minPrio == "" {
minPrio = report.Priority(report.DebugPriority).String()
}
return Target{
Name: t.Name(),
MinimumPriority: minPrio,
SkipExistingOnStartup: t.SkipExistingOnStartup(),
}
}

View file

@ -1,60 +1,85 @@
package api package api
import ( import (
"context"
"fmt" "fmt"
"net/http" "net/http"
"github.com/kyverno/policy-reporter/pkg/report" v1 "github.com/kyverno/policy-reporter/pkg/api/v1"
"github.com/kyverno/policy-reporter/pkg/target" "github.com/kyverno/policy-reporter/pkg/target"
"github.com/prometheus/client_golang/prometheus/promhttp"
) )
// Server for the optional HTTP REST API // Server for the Lifecycle and optional HTTP REST API
type Server interface { type Server interface {
// Start the HTTP REST API // Start the HTTP Server
Start() error Start() error
// Shutdown the HTTP Sever
Shutdown(ctx context.Context) error
// RegisterLifecycleHandler adds healthy and readiness APIs
RegisterLifecycleHandler()
// RegisterMetricsHandler adds the optional metrics endpoint
RegisterMetricsHandler()
// RegisterV1Handler adds the optional v1 REST APIs
RegisterV1Handler(finder v1.PolicyReportFinder)
} }
type httpServer struct { type httpServer struct {
port int http http.Server
mux *http.ServeMux mux *http.ServeMux
store *report.PolicyReportStore targets []target.Client
targets []Target
foundResources map[string]string foundResources map[string]string
} }
func (s *httpServer) registerHandler() { func (s *httpServer) RegisterLifecycleHandler() {
s.mux.HandleFunc("/policy-reports", Gzip(PolicyReportHandler(s.store)))
s.mux.HandleFunc("/cluster-policy-reports", Gzip(ClusterPolicyReportHandler(s.store)))
s.mux.HandleFunc("/targets", Gzip(TargetsHandler(s.targets)))
s.mux.HandleFunc("/healthz", HealthzHandler(s.foundResources)) s.mux.HandleFunc("/healthz", HealthzHandler(s.foundResources))
s.mux.HandleFunc("/ready", ReadyHandler()) s.mux.HandleFunc("/ready", ReadyHandler())
} }
func (s *httpServer) Start() error { func (s *httpServer) RegisterV1Handler(finder v1.PolicyReportFinder) {
server := http.Server{ s.mux.HandleFunc("/v1/targets", Gzip(v1.TargetsHandler(s.targets)))
Addr: fmt.Sprintf(":%d", s.port), s.mux.HandleFunc("/v1/categories", Gzip(v1.CategoryListHandler(finder)))
Handler: s.mux, s.mux.HandleFunc("/v1/namespaces", Gzip(v1.NamespaceListHandler(finder)))
} s.mux.HandleFunc("/v1/rule-status-count", Gzip(v1.RuleStatusCountHandler(finder)))
return server.ListenAndServe() s.mux.HandleFunc("/v1/namespaced-resources/policies", Gzip(v1.NamespacedResourcesPolicyListHandler(finder)))
s.mux.HandleFunc("/v1/namespaced-resources/kinds", Gzip(v1.NamespacedResourcesKindListHandler(finder)))
s.mux.HandleFunc("/v1/namespaced-resources/sources", Gzip(v1.NamespacedSourceListHandler(finder)))
s.mux.HandleFunc("/v1/namespaced-resources/status-counts", Gzip(v1.NamespacedResourcesStatusCountsHandler(finder)))
s.mux.HandleFunc("/v1/namespaced-resources/results", Gzip(v1.NamespacedResourcesResultHandler(finder)))
s.mux.HandleFunc("/v1/cluster-resources/policies", Gzip(v1.ClusterResourcesPolicyListHandler(finder)))
s.mux.HandleFunc("/v1/cluster-resources/kinds", Gzip(v1.ClusterResourcesKindListHandler(finder)))
s.mux.HandleFunc("/v1/cluster-resources/sources", Gzip(v1.ClusterResourcesSourceListHandler(finder)))
s.mux.HandleFunc("/v1/cluster-resources/status-counts", Gzip(v1.ClusterResourcesStatusCountHandler(finder)))
s.mux.HandleFunc("/v1/cluster-resources/results", Gzip(v1.ClusterResourcesResultHandler(finder)))
}
func (s *httpServer) RegisterMetricsHandler() {
s.mux.Handle("/metrics", promhttp.Handler())
}
func (s *httpServer) Start() error {
return s.http.ListenAndServe()
}
func (s *httpServer) Shutdown(ctx context.Context) error {
return s.http.Shutdown(ctx)
} }
// NewServer constructor for a new API Server // NewServer constructor for a new API Server
func NewServer(store *report.PolicyReportStore, targets []target.Client, port int, foundResources map[string]string) Server { func NewServer(targets []target.Client, port int, foundResources map[string]string) Server {
apiTargets := make([]Target, 0, len(targets))
for _, t := range targets {
apiTargets = append(apiTargets, mapTarget(t))
}
s := &httpServer{ s := &httpServer{
port: port, targets: targets,
targets: apiTargets, mux: http.DefaultServeMux,
store: store,
mux: http.NewServeMux(),
foundResources: foundResources, foundResources: foundResources,
http: http.Server{
Addr: fmt.Sprintf(":%d", port),
Handler: http.DefaultServeMux,
},
} }
s.registerHandler() s.RegisterLifecycleHandler()
return s return s
} }

View file

@ -1,26 +1,64 @@
package api_test package api_test
import ( import (
"context"
"fmt"
"math/rand"
"net/http" "net/http"
"testing" "testing"
"time"
"github.com/kyverno/policy-reporter/pkg/api" "github.com/kyverno/policy-reporter/pkg/api"
"github.com/kyverno/policy-reporter/pkg/report"
"github.com/kyverno/policy-reporter/pkg/target" "github.com/kyverno/policy-reporter/pkg/target"
"github.com/kyverno/policy-reporter/pkg/target/discord"
"github.com/kyverno/policy-reporter/pkg/target/loki"
) )
func Test_NewServer(t *testing.T) { func Test_NewServer(t *testing.T) {
server := api.NewServer( rnd := rand.New(rand.NewSource(time.Now().Unix())).Float64()
report.NewPolicyReportStore(), if rnd < 0.3 {
[]target.Client{ rnd += 0.4
loki.NewClient("http://localhost:3100", "debug", true, &http.Client{}), }
discord.NewClient("http://webhook:2000", "", false, &http.Client{}),
},
8080,
make(map[string]string),
)
go server.Start() port := int(rnd * 10000)
server := api.NewServer(make([]target.Client, 0), port, make(map[string]string))
server.RegisterMetricsHandler()
server.RegisterV1Handler(nil)
serviceRunning := make(chan struct{})
serviceDone := make(chan struct{})
go func() {
close(serviceRunning)
err := server.Start()
if err != nil {
fmt.Println(err)
}
defer close(serviceDone)
}()
<-serviceRunning
client := http.Client{}
req, err := http.NewRequest("GET", fmt.Sprintf("http://localhost:%d/ready", port), nil)
if err != nil {
t.Errorf("Unexpected Error: %s", err)
return
}
res, err := client.Do(req)
server.Shutdown(context.Background())
if err != nil {
t.Errorf("Unexpected Error: %s", err)
return
}
if res.StatusCode != http.StatusOK {
t.Errorf("Unexpected Error Code: %d", res.StatusCode)
}
<-serviceDone
} }

40
pkg/api/v1/finder.go Normal file
View file

@ -0,0 +1,40 @@
package v1
type Filter struct {
Kinds []string
Categories []string
Namespaces []string
Sources []string
Policies []string
Severities []string
Status []string
}
type PolicyReportFinder interface {
// FetchClusterPolicies from current PolicyReportResults
FetchClusterPolicies(source string) ([]string, error)
// FetchNamespacedPolicies from current PolicyReportResults with a Namespace
FetchNamespacedPolicies(source string) ([]string, error)
// FetchCategories from current PolicyReportResults
FetchCategories(source string) ([]string, error)
// FetchClusterSources from current PolicyReportResults
FetchClusterSources() ([]string, error)
// FetchNamespacedSources from current PolicyReportResults with a Namespace
FetchNamespacedSources() ([]string, error)
// FetchNamespacedKinds from current PolicyReportResults with a Namespace
FetchNamespacedKinds(source string) ([]string, error)
// FetchClusterKinds from current PolicyReportResults
FetchClusterKinds(source string) ([]string, error)
// FetchNamespaces from current PolicyReports
FetchNamespaces(source string) ([]string, error)
// FetchNamespacedStatusCounts from current PolicyReportResults with a Namespace
FetchNamespacedStatusCounts(Filter) ([]NamespacedStatusCount, error)
// FetchStatusCounts from current PolicyReportResults
FetchStatusCounts(Filter) ([]StatusCount, error)
// FetchNamespacedResults from current PolicyReportResults with a Namespace
FetchNamespacedResults(filter Filter) ([]*ListResult, error)
// FetchClusterResults from current PolicyReportResults
FetchClusterResults(filter Filter) ([]*ListResult, error)
// FetchRuleStatusCounts from current PolicyReportResults
FetchRuleStatusCounts(policy, rule string) ([]StatusCount, error)
}

157
pkg/api/v1/handler.go Normal file
View file

@ -0,0 +1,157 @@
package v1
import (
"net/http"
"github.com/kyverno/policy-reporter/pkg/helper"
"github.com/kyverno/policy-reporter/pkg/target"
)
// TargetsHandler for the Targets REST API
func TargetsHandler(targets []target.Client) http.HandlerFunc {
apiTargets := make([]Target, 0, len(targets))
for _, t := range targets {
apiTargets = append(apiTargets, mapTarget(t))
}
return func(w http.ResponseWriter, req *http.Request) {
helper.SendJSONResponse(w, apiTargets, nil)
}
}
// ClusterResourcesPolicyListHandler REST API
func ClusterResourcesPolicyListHandler(finder PolicyReportFinder) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
list, err := finder.FetchClusterPolicies(req.URL.Query().Get("source"))
helper.SendJSONResponse(w, list, err)
}
}
// NamespacedResourcesPolicyListHandler REST API
func NamespacedResourcesPolicyListHandler(finder PolicyReportFinder) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
list, err := finder.FetchNamespacedPolicies(req.URL.Query().Get("source"))
helper.SendJSONResponse(w, list, err)
}
}
// CategoryListHandler REST API
func CategoryListHandler(finder PolicyReportFinder) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
list, err := finder.FetchCategories(req.URL.Query().Get("source"))
helper.SendJSONResponse(w, list, err)
}
}
// ClusterResourcesKindListHandler REST API
func ClusterResourcesKindListHandler(finder PolicyReportFinder) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
list, err := finder.FetchClusterKinds(req.URL.Query().Get("source"))
helper.SendJSONResponse(w, list, err)
}
}
// NamespacedResourcesKindListHandler REST API
func NamespacedResourcesKindListHandler(finder PolicyReportFinder) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
list, err := finder.FetchNamespacedKinds(req.URL.Query().Get("source"))
helper.SendJSONResponse(w, list, err)
}
}
// ClusterResourcesSourceListHandler REST API
func ClusterResourcesSourceListHandler(finder PolicyReportFinder) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
list, err := finder.FetchClusterSources()
helper.SendJSONResponse(w, list, err)
}
}
// NamespacedSourceListHandler REST API
func NamespacedSourceListHandler(finder PolicyReportFinder) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
list, err := finder.FetchNamespacedSources()
helper.SendJSONResponse(w, list, err)
}
}
// ClusterResourcesStatusCountHandler REST API
func ClusterResourcesStatusCountHandler(finder PolicyReportFinder) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
list, err := finder.FetchStatusCounts(Filter{
Kinds: req.URL.Query()["kinds"],
Sources: req.URL.Query()["sources"],
Categories: req.URL.Query()["categories"],
Severities: req.URL.Query()["severities"],
Policies: req.URL.Query()["policies"],
Status: req.URL.Query()["status"],
})
helper.SendJSONResponse(w, list, err)
}
}
// NamespacedResourcesStatusCountsHandler REST API
func NamespacedResourcesStatusCountsHandler(finder PolicyReportFinder) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
list, err := finder.FetchNamespacedStatusCounts(Filter{
Namespaces: req.URL.Query()["namespaces"],
Kinds: req.URL.Query()["kinds"],
Sources: req.URL.Query()["sources"],
Categories: req.URL.Query()["categories"],
Severities: req.URL.Query()["severities"],
Policies: req.URL.Query()["policies"],
Status: req.URL.Query()["status"],
})
helper.SendJSONResponse(w, list, err)
}
}
// RuleStatusCountHandler REST API
func RuleStatusCountHandler(finder PolicyReportFinder) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
list, err := finder.FetchRuleStatusCounts(
req.URL.Query().Get("policy"),
req.URL.Query().Get("rule"),
)
helper.SendJSONResponse(w, list, err)
}
}
// NamespacedResourcesResultHandler REST API
func NamespacedResourcesResultHandler(finder PolicyReportFinder) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
list, err := finder.FetchNamespacedResults(Filter{
Namespaces: req.URL.Query()["namespaces"],
Kinds: req.URL.Query()["kinds"],
Sources: req.URL.Query()["sources"],
Categories: req.URL.Query()["categories"],
Severities: req.URL.Query()["severities"],
Policies: req.URL.Query()["policies"],
Status: req.URL.Query()["status"],
})
helper.SendJSONResponse(w, list, err)
}
}
// ClusterResourcesResultHandler REST API
func ClusterResourcesResultHandler(finder PolicyReportFinder) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
list, err := finder.FetchClusterResults(Filter{
Kinds: req.URL.Query()["kinds"],
Sources: req.URL.Query()["sources"],
Categories: req.URL.Query()["categories"],
Severities: req.URL.Query()["severities"],
Policies: req.URL.Query()["policies"],
Status: req.URL.Query()["status"],
})
helper.SendJSONResponse(w, list, err)
}
}
// NamespaceListHandler REST API
func NamespaceListHandler(finder PolicyReportFinder) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
list, err := finder.FetchNamespaces(req.URL.Query().Get("source"))
helper.SendJSONResponse(w, list, err)
}
}

450
pkg/api/v1/handler_test.go Normal file
View file

@ -0,0 +1,450 @@
package v1_test
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
v1 "github.com/kyverno/policy-reporter/pkg/api/v1"
"github.com/kyverno/policy-reporter/pkg/report"
"github.com/kyverno/policy-reporter/pkg/sqlite3"
"github.com/kyverno/policy-reporter/pkg/target"
"github.com/kyverno/policy-reporter/pkg/target/loki"
)
var result1 = &report.Result{
ID: "123",
Message: "validation error: requests and limits required. Rule autogen-check-for-requests-and-limits failed at path /spec/template/spec/containers/0/resources/requests/",
Policy: "require-requests-and-limits-required",
Rule: "autogen-check-for-requests-and-limits",
Priority: report.ErrorPriority,
Status: report.Fail,
Category: "Best Practices",
Severity: report.High,
Scored: true,
Source: "Kyverno",
Resource: &report.Resource{
APIVersion: "v1",
Kind: "Deployment",
Name: "nginx",
Namespace: "test",
UID: "536ab69f-1b3c-4bd9-9ba4-274a56188409",
},
}
var result2 = &report.Result{
ID: "124",
Message: "validation error: requests and limits required. Rule autogen-check-for-requests-and-limits failed at path /spec/template/spec/containers/0/resources/requests/",
Policy: "require-requests-and-limits-required",
Rule: "autogen-check-for-requests-and-limits",
Priority: report.WarningPriority,
Status: report.Pass,
Category: "Best Practices",
Scored: true,
Source: "Kyverno",
Resource: &report.Resource{
APIVersion: "v1",
Kind: "Pod",
Name: "nginx",
Namespace: "test",
UID: "536ab69f-1b3c-4bd9-9ba4-274a56188419",
},
}
var cresult1 = &report.Result{
ID: "125",
Message: "validation error: The label `test` is required. Rule check-for-labels-on-namespace",
Policy: "require-ns-labels",
Rule: "check-for-labels-on-namespace",
Priority: report.ErrorPriority,
Status: report.Pass,
Category: "Convention",
Severity: report.Medium,
Scored: true,
Source: "Kyverno",
Resource: &report.Resource{
APIVersion: "v1",
Kind: "Namespace",
Name: "test",
UID: "536ab69f-1b3c-4bd9-9ba4-274a56188411",
},
}
var cresult2 = &report.Result{
ID: "126",
Message: "validation error: The label `test` is required. Rule check-for-labels-on-namespace",
Policy: "require-ns-labels",
Rule: "check-for-labels-on-namespace",
Priority: report.WarningPriority,
Status: report.Fail,
Category: "Convention",
Severity: report.High,
Scored: true,
Source: "Kyverno",
Resource: &report.Resource{
APIVersion: "v1",
Kind: "Namespace",
Name: "dev",
UID: "536ab69f-1b3c-4bd9-9ba4-274a56188412",
},
}
var preport = &report.PolicyReport{
ID: report.GeneratePolicyReportID("polr-test", "test"),
Name: "polr-test",
Namespace: "test",
Results: map[string]*report.Result{
result1.GetIdentifier(): result1,
result2.GetIdentifier(): result2,
},
Summary: &report.Summary{Fail: 1},
CreationTimestamp: time.Now(),
}
var creport = &report.PolicyReport{
ID: report.GeneratePolicyReportID("cpolr", ""),
Name: "cpolr",
Results: map[string]*report.Result{
cresult1.GetIdentifier(): cresult1,
cresult2.GetIdentifier(): cresult2,
},
Summary: &report.Summary{},
CreationTimestamp: time.Now(),
}
func Test_V1_API(t *testing.T) {
db, err := sqlite3.NewDatabase("test.db")
if err != nil {
t.Error(err)
}
defer db.Close()
if err != nil {
t.Fatal(err)
}
store, err := sqlite3.NewPolicyReportStore(db)
if err != nil {
t.Fatal(err)
}
defer store.CleanUp()
store.Add(preport)
store.Add(creport)
t.Run("ClusterPolicyListHandler", func(t *testing.T) {
req, err := http.NewRequest("GET", "/v1/cluster-policies", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
handler := v1.ClusterResourcesPolicyListHandler(store)
handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK)
}
expected := `["require-ns-labels"]`
if !strings.Contains(rr.Body.String(), expected) {
t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected)
}
})
t.Run("NamespacedPolicyListHandler", func(t *testing.T) {
req, err := http.NewRequest("GET", "/v1/namespaced-policies", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
handler := v1.NamespacedResourcesPolicyListHandler(store)
handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK)
}
expected := `["require-requests-and-limits-required"]`
if !strings.Contains(rr.Body.String(), expected) {
t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected)
}
})
t.Run("CategoryListHandler", func(t *testing.T) {
req, err := http.NewRequest("GET", "/v1/categories", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
handler := v1.CategoryListHandler(store)
handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK)
}
expected := `["Best Practices","Convention"]`
if !strings.Contains(rr.Body.String(), expected) {
t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected)
}
})
t.Run("ClusterKindListHandler", func(t *testing.T) {
req, err := http.NewRequest("GET", "/v1/cluster-kinds", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
handler := v1.ClusterResourcesKindListHandler(store)
handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK)
}
expected := `["Namespace"]`
if !strings.Contains(rr.Body.String(), expected) {
t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected)
}
})
t.Run("NamespacedKindListHandler", func(t *testing.T) {
req, err := http.NewRequest("GET", "/v1/namespaced-kinds", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
handler := v1.NamespacedResourcesKindListHandler(store)
handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK)
}
expected := `["Deployment","Pod"]`
if !strings.Contains(rr.Body.String(), expected) {
t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected)
}
})
t.Run("ClusterSourceListHandler", func(t *testing.T) {
req, err := http.NewRequest("GET", "/v1/cluster-sources", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
handler := v1.ClusterResourcesSourceListHandler(store)
handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK)
}
expected := `["Kyverno"]`
if !strings.Contains(rr.Body.String(), expected) {
t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected)
}
})
t.Run("NamespacedSourceListHandler", func(t *testing.T) {
req, err := http.NewRequest("GET", "/v1/namspaced-sources", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
handler := v1.NamespacedSourceListHandler(store)
handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK)
}
expected := `["Kyverno"]`
if !strings.Contains(rr.Body.String(), expected) {
t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected)
}
})
t.Run("ClusterStatusCountHandler", func(t *testing.T) {
req, err := http.NewRequest("GET", "/v1/cluster-status-counts?status=pass", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
handler := v1.ClusterResourcesStatusCountHandler(store)
handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK)
}
expected := `[{"status":"pass","count":1}]`
if !strings.Contains(rr.Body.String(), expected) {
t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected)
}
})
t.Run("NamespacedStatusCountHandler", func(t *testing.T) {
req, err := http.NewRequest("GET", "/v1/namespaced-status-counts?status=pass", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
handler := v1.NamespacedResourcesStatusCountsHandler(store)
handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK)
}
expected := `[{"status":"pass","items":[{"namespace":"test","count":1}]}]`
if !strings.Contains(rr.Body.String(), expected) {
t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected)
}
})
t.Run("RuleStatusCountHandler", func(t *testing.T) {
req, err := http.NewRequest("GET", "/v1/rule-status-count?policy=require-requests-and-limits-required&rule=autogen-check-for-requests-and-limits", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
handler := v1.RuleStatusCountHandler(store)
handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK)
}
expected := `{"status":"fail","count":1}`
if !strings.Contains(rr.Body.String(), expected) {
t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected)
}
expected = `{"status":"pass","count":1}`
if !strings.Contains(rr.Body.String(), expected) {
t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected)
}
expected = `{"status":"warn","count":0}`
if !strings.Contains(rr.Body.String(), expected) {
t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected)
}
})
t.Run("NamespacedResultHandler", func(t *testing.T) {
req, err := http.NewRequest("GET", "/v1/namespaced-results", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
handler := v1.NamespacedResourcesResultHandler(store)
handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK)
}
expected := `[{"id":"123","namespace":"test","kind":"Deployment","name":"nginx","message":"validation error: requests and limits required. Rule autogen-check-for-requests-and-limits failed at path /spec/template/spec/containers/0/resources/requests/","policy":"require-requests-and-limits-required","rule":"autogen-check-for-requests-and-limits","status":"fail","severity":"high"},{"id":"124","namespace":"test","kind":"Pod","name":"nginx","message":"validation error: requests and limits required. Rule autogen-check-for-requests-and-limits failed at path /spec/template/spec/containers/0/resources/requests/","policy":"require-requests-and-limits-required","rule":"autogen-check-for-requests-and-limits","status":"pass"}]`
if !strings.Contains(rr.Body.String(), expected) {
t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected)
}
})
t.Run("ClusterResultHandler", func(t *testing.T) {
req, err := http.NewRequest("GET", "/v1/cluster-results", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
handler := v1.ClusterResourcesResultHandler(store)
handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK)
}
expected := "{\"id\":\"125\",\"kind\":\"Namespace\",\"name\":\"test\",\"message\":\"validation error: The label `test` is required. Rule check-for-labels-on-namespace\",\"policy\":\"require-ns-labels\",\"rule\":\"check-for-labels-on-namespace\",\"status\":\"pass\",\"severity\":\"medium\"}"
if !strings.Contains(rr.Body.String(), expected) {
t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected)
}
})
t.Run("NamespaceListHandler", func(t *testing.T) {
req, err := http.NewRequest("GET", "/v1/namespaces", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
handler := v1.NamespaceListHandler(store)
handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK)
}
expected := `["test"]`
if !strings.Contains(rr.Body.String(), expected) {
t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected)
}
})
}
func Test_TargetsAPI(t *testing.T) {
t.Run("Empty Respose", func(t *testing.T) {
req, err := http.NewRequest("GET", "/targets", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
handler := v1.TargetsHandler(make([]target.Client, 0))
handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK)
}
expected := "[]"
if !strings.Contains(rr.Body.String(), expected) {
t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected)
}
})
t.Run("Respose", func(t *testing.T) {
req, err := http.NewRequest("GET", "/targets", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
handler := v1.TargetsHandler([]target.Client{
loki.NewClient("", "", []string{}, true, &http.Client{}),
})
handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK)
}
expected := `[{"name":"Loki","minimumPriority":"debug","skipExistingOnStartup":true}]`
if !strings.Contains(rr.Body.String(), expected) {
t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected)
}
})
}

56
pkg/api/v1/model.go Normal file
View file

@ -0,0 +1,56 @@
package v1
import (
"github.com/kyverno/policy-reporter/pkg/report"
"github.com/kyverno/policy-reporter/pkg/target"
)
type StatusCount struct {
Status string `json:"status"`
Count int `json:"count"`
}
type NamespacedStatusCount struct {
Status string `json:"status"`
Items []NamespaceCount `json:"items"`
}
type NamespaceCount struct {
Namespace string `json:"namespace"`
Count int `json:"count"`
}
type ListResult struct {
ID string `json:"id"`
Namespace string `json:"namespace,omitempty"`
Kind string `json:"kind"`
Name string `json:"name"`
Message string `json:"message"`
Policy string `json:"policy"`
Rule string `json:"rule"`
Status string `json:"status"`
Severity string `json:"severity,omitempty"`
Properties map[string]string `json:"properties,omitempty"`
}
// Target API Model
type Target struct {
Name string `json:"name"`
MinimumPriority string `json:"minimumPriority"`
Sources []string `json:"sources,omitempty"`
SkipExistingOnStartup bool `json:"skipExistingOnStartup"`
}
func mapTarget(t target.Client) Target {
minPrio := t.MinimumPriority()
if minPrio == "" {
minPrio = report.Priority(report.DebugPriority).String()
}
return Target{
Name: t.Name(),
MinimumPriority: minPrio,
Sources: t.Sources(),
SkipExistingOnStartup: t.SkipExistingOnStartup(),
}
}

View file

@ -2,57 +2,64 @@ package config
// Loki configuration // Loki configuration
type Loki struct { type Loki struct {
Host string `mapstructure:"host"` Host string `mapstructure:"host"`
SkipExisting bool `mapstructure:"skipExistingOnStartup"` SkipExisting bool `mapstructure:"skipExistingOnStartup"`
MinimumPriority string `mapstructure:"minimumPriority"` MinimumPriority string `mapstructure:"minimumPriority"`
Sources []string `mapstructure:"sources"`
} }
// Elasticsearch configuration // Elasticsearch configuration
type Elasticsearch struct { type Elasticsearch struct {
Host string `mapstructure:"host"` Host string `mapstructure:"host"`
Index string `mapstructure:"index"` Index string `mapstructure:"index"`
Rotation string `mapstructure:"rotation"` Rotation string `mapstructure:"rotation"`
SkipExisting bool `mapstructure:"skipExistingOnStartup"` SkipExisting bool `mapstructure:"skipExistingOnStartup"`
MinimumPriority string `mapstructure:"minimumPriority"` MinimumPriority string `mapstructure:"minimumPriority"`
Sources []string `mapstructure:"sources"`
} }
// Slack configuration // Slack configuration
type Slack struct { type Slack struct {
Webhook string `mapstructure:"webhook"` Webhook string `mapstructure:"webhook"`
SkipExisting bool `mapstructure:"skipExistingOnStartup"` SkipExisting bool `mapstructure:"skipExistingOnStartup"`
MinimumPriority string `mapstructure:"minimumPriority"` MinimumPriority string `mapstructure:"minimumPriority"`
Sources []string `mapstructure:"sources"`
} }
// Discord configuration // Discord configuration
type Discord struct { type Discord struct {
Webhook string `mapstructure:"webhook"` Webhook string `mapstructure:"webhook"`
SkipExisting bool `mapstructure:"skipExistingOnStartup"` SkipExisting bool `mapstructure:"skipExistingOnStartup"`
MinimumPriority string `mapstructure:"minimumPriority"` MinimumPriority string `mapstructure:"minimumPriority"`
Sources []string `mapstructure:"sources"`
} }
// Teams configuration // Teams configuration
type Teams struct { type Teams struct {
Webhook string `mapstructure:"webhook"` Webhook string `mapstructure:"webhook"`
SkipExisting bool `mapstructure:"skipExistingOnStartup"` SkipExisting bool `mapstructure:"skipExistingOnStartup"`
MinimumPriority string `mapstructure:"minimumPriority"` MinimumPriority string `mapstructure:"minimumPriority"`
Sources []string `mapstructure:"sources"`
} }
// UI configuration // UI configuration
type UI struct { type UI struct {
Host string `mapstructure:"host"` Host string `mapstructure:"host"`
SkipExisting bool `mapstructure:"skipExistingOnStartup"` SkipExisting bool `mapstructure:"skipExistingOnStartup"`
MinimumPriority string `mapstructure:"minimumPriority"` MinimumPriority string `mapstructure:"minimumPriority"`
Sources []string `mapstructure:"sources"`
} }
type Yandex struct { type S3 struct {
AccessKeyID string `mapstructure:"accessKeyID"` AccessKeyID string `mapstructure:"accessKeyID"`
SecretAccessKey string `mapstructure:"secretAccessKey"` SecretAccessKey string `mapstructure:"secretAccessKey"`
Region string `mapstructure:"region"` Region string `mapstructure:"region"`
Endpoint string `mapstructure:"endpoint"` Endpoint string `mapstructure:"endpoint"`
Prefix string `mapstructure:"prefix"` Prefix string `mapstructure:"prefix"`
Bucket string `mapstructure:"bucket"` Bucket string `mapstructure:"bucket"`
SkipExisting bool `mapstructure:"skipExistingOnStartup"` SkipExisting bool `mapstructure:"skipExistingOnStartup"`
MinimumPriority string `mapstructure:"minimumPriority"` MinimumPriority string `mapstructure:"minimumPriority"`
Sources []string `mapstructure:"sources"`
} }
// API configuration // API configuration
@ -60,6 +67,18 @@ type API struct {
Port int `mapstructure:"port"` Port int `mapstructure:"port"`
} }
// REST configuration
type REST struct {
Enabled bool `mapstructure:"enabled"`
}
// Metrics configuration
type Metrics struct {
Enabled bool `mapstructure:"enabled"`
}
type PriorityMap = map[string]string
// Config of the PolicyReporter // Config of the PolicyReporter
type Config struct { type Config struct {
Loki Loki `mapstructure:"loki"` Loki Loki `mapstructure:"loki"`
@ -67,9 +86,12 @@ type Config struct {
Slack Slack `mapstructure:"slack"` Slack Slack `mapstructure:"slack"`
Discord Discord `mapstructure:"discord"` Discord Discord `mapstructure:"discord"`
Teams Teams `mapstructure:"teams"` Teams Teams `mapstructure:"teams"`
Yandex Yandex `mapstructure:"yandex"` S3 S3 `mapstructure:"s3"`
UI UI `mapstructure:"ui"` UI UI `mapstructure:"ui"`
API API `mapstructure:"api"` API API `mapstructure:"api"`
Kubeconfig string `mapstructure:"kubeconfig"` Kubeconfig string `mapstructure:"kubeconfig"`
Namespace string `mapstructure:"namespace"` DBFile string `mapstructure:"dbfile"`
Metrics Metrics `mapstructure:"metrics"`
REST REST `mapstructure:"rest"`
PriorityMap PriorityMap `mapstructure:"priorityMap"`
} }

View file

@ -1,28 +1,30 @@
package config package config
import ( import (
"context" "database/sql"
"log" "log"
"net/http" "net/http"
"time" "time"
"github.com/kyverno/policy-reporter/pkg/api" "github.com/kyverno/policy-reporter/pkg/api"
"github.com/kyverno/policy-reporter/pkg/helper"
"github.com/kyverno/policy-reporter/pkg/kubernetes" "github.com/kyverno/policy-reporter/pkg/kubernetes"
"github.com/kyverno/policy-reporter/pkg/listener"
"github.com/kyverno/policy-reporter/pkg/report" "github.com/kyverno/policy-reporter/pkg/report"
"github.com/kyverno/policy-reporter/pkg/sqlite3"
"github.com/kyverno/policy-reporter/pkg/target" "github.com/kyverno/policy-reporter/pkg/target"
"github.com/kyverno/policy-reporter/pkg/target/discord" "github.com/kyverno/policy-reporter/pkg/target/discord"
"github.com/kyverno/policy-reporter/pkg/target/elasticsearch" "github.com/kyverno/policy-reporter/pkg/target/elasticsearch"
"github.com/kyverno/policy-reporter/pkg/target/helper"
"github.com/kyverno/policy-reporter/pkg/target/loki" "github.com/kyverno/policy-reporter/pkg/target/loki"
"github.com/kyverno/policy-reporter/pkg/target/s3"
"github.com/kyverno/policy-reporter/pkg/target/slack" "github.com/kyverno/policy-reporter/pkg/target/slack"
"github.com/kyverno/policy-reporter/pkg/target/teams" "github.com/kyverno/policy-reporter/pkg/target/teams"
"github.com/kyverno/policy-reporter/pkg/target/ui" "github.com/kyverno/policy-reporter/pkg/target/ui"
"github.com/kyverno/policy-reporter/pkg/target/yandex"
"github.com/patrickmn/go-cache" "github.com/patrickmn/go-cache"
"k8s.io/client-go/dynamic" "k8s.io/client-go/dynamic"
v1 "k8s.io/client-go/kubernetes/typed/core/v1"
_ "github.com/mattn/go-sqlite3"
"k8s.io/client-go/rest" "k8s.io/client-go/rest"
) )
@ -31,88 +33,89 @@ type Resolver struct {
config *Config config *Config
k8sConfig *rest.Config k8sConfig *rest.Config
mapper kubernetes.Mapper mapper kubernetes.Mapper
policyAdapter kubernetes.PolicyReportAdapter publisher report.EventPublisher
policyStore *report.PolicyReportStore policyStore sqlite3.PolicyReportStore
policyClient report.PolicyResultClient policyReportClient report.PolicyReportClient
lokiClient target.Client lokiClient target.Client
elasticsearchClient target.Client elasticsearchClient target.Client
slackClient target.Client slackClient target.Client
discordClient target.Client discordClient target.Client
teamsClient target.Client teamsClient target.Client
uiClient target.Client uiClient target.Client
yandexClient target.Client s3Client target.Client
resultCache *cache.Cache resultCache *cache.Cache
} }
// APIServer resolver method // APIServer resolver method
func (r *Resolver) APIServer() api.Server { func (r *Resolver) APIServer(foundResources map[string]string) api.Server {
foundResources := make(map[string]string)
client := r.policyClient
if client != nil {
foundResources = client.GetFoundResources()
}
return api.NewServer( return api.NewServer(
r.PolicyReportStore(),
r.TargetClients(), r.TargetClients(),
r.config.API.Port, r.config.API.Port,
foundResources, foundResources,
) )
} }
// PolicyReportStore resolver method // Database resolver method
func (r *Resolver) PolicyReportStore() *report.PolicyReportStore { func (r *Resolver) Database() (*sql.DB, error) {
if r.policyStore != nil { return sqlite3.NewDatabase(r.config.DBFile)
return r.policyStore
}
r.policyStore = report.NewPolicyReportStore()
return r.policyStore
} }
// PolicyReportClient resolver method // PolicyReportStore resolver method
func (r *Resolver) PolicyReportClient(ctx context.Context) (report.PolicyResultClient, error) { func (r *Resolver) PolicyReportStore(db *sql.DB) (sqlite3.PolicyReportStore, error) {
if r.policyClient != nil { if r.policyStore != nil {
return r.policyClient, nil return r.policyStore, nil
} }
policyAPI, err := r.policyReportAPI(ctx) s, err := sqlite3.NewPolicyReportStore(db)
if err != nil { r.policyStore = s
return nil, err
return r.policyStore, err
}
// EventPublisher resolver method
func (r *Resolver) EventPublisher() report.EventPublisher {
if r.publisher != nil {
return r.publisher
} }
client := kubernetes.NewPolicyReportClient( s := report.NewEventPublisher()
policyAPI, r.publisher = s
r.PolicyReportStore(),
time.Now(),
r.ResultCache(),
)
r.policyClient = client return r.publisher
}
return client, nil // RegisterSendResultListener resolver method
func (r *Resolver) RegisterSendResultListener() {
targets := r.TargetClients()
if len(targets) > 0 {
newResultListener := listener.NewResultListener(r.SkipExistingOnStartup(), r.ResultCache(), time.Now())
newResultListener.RegisterListener(listener.NewSendResultListener(targets))
r.EventPublisher().RegisterListener(newResultListener.Listen)
}
}
// RegisterSendResultListener resolver method
func (r *Resolver) RegisterStoreListener(store report.PolicyReportStore) {
r.EventPublisher().RegisterListener(listener.NewStoreListener(store))
}
// RegisterMetricsListener resolver method
func (r *Resolver) RegisterMetricsListener() {
r.EventPublisher().RegisterListener(listener.NewMetricsListener())
} }
// Mapper resolver method // Mapper resolver method
func (r *Resolver) Mapper(ctx context.Context) (kubernetes.Mapper, error) { func (r *Resolver) Mapper() kubernetes.Mapper {
if r.mapper != nil { if r.mapper != nil {
return r.mapper, nil return r.mapper
} }
cmAPI, err := r.configMapAPI() mapper := kubernetes.NewMapper(r.config.PriorityMap)
if err != nil {
return nil, err
}
mapper := kubernetes.NewMapper(make(map[string]string), cmAPI)
mapper.FetchPriorities(ctx)
go mapper.SyncPriorities(ctx)
r.mapper = mapper r.mapper = mapper
return mapper, err return mapper
} }
// LokiClient resolver method // LokiClient resolver method
@ -128,6 +131,7 @@ func (r *Resolver) LokiClient() target.Client {
r.lokiClient = loki.NewClient( r.lokiClient = loki.NewClient(
r.config.Loki.Host, r.config.Loki.Host,
r.config.Loki.MinimumPriority, r.config.Loki.MinimumPriority,
r.config.Loki.Sources,
r.config.Loki.SkipExisting, r.config.Loki.SkipExisting,
&http.Client{}, &http.Client{},
) )
@ -158,6 +162,7 @@ func (r *Resolver) ElasticsearchClient() target.Client {
r.config.Elasticsearch.Index, r.config.Elasticsearch.Index,
r.config.Elasticsearch.Rotation, r.config.Elasticsearch.Rotation,
r.config.Elasticsearch.MinimumPriority, r.config.Elasticsearch.MinimumPriority,
r.config.Elasticsearch.Sources,
r.config.Elasticsearch.SkipExisting, r.config.Elasticsearch.SkipExisting,
&http.Client{}, &http.Client{},
) )
@ -180,6 +185,7 @@ func (r *Resolver) SlackClient() target.Client {
r.slackClient = slack.NewClient( r.slackClient = slack.NewClient(
r.config.Slack.Webhook, r.config.Slack.Webhook,
r.config.Slack.MinimumPriority, r.config.Slack.MinimumPriority,
r.config.Slack.Sources,
r.config.Slack.SkipExisting, r.config.Slack.SkipExisting,
&http.Client{}, &http.Client{},
) )
@ -202,6 +208,7 @@ func (r *Resolver) DiscordClient() target.Client {
r.discordClient = discord.NewClient( r.discordClient = discord.NewClient(
r.config.Discord.Webhook, r.config.Discord.Webhook,
r.config.Discord.MinimumPriority, r.config.Discord.MinimumPriority,
r.config.Discord.Sources,
r.config.Discord.SkipExisting, r.config.Discord.SkipExisting,
&http.Client{}, &http.Client{},
) )
@ -224,6 +231,7 @@ func (r *Resolver) TeamsClient() target.Client {
r.teamsClient = teams.NewClient( r.teamsClient = teams.NewClient(
r.config.Teams.Webhook, r.config.Teams.Webhook,
r.config.Teams.MinimumPriority, r.config.Teams.MinimumPriority,
r.config.Teams.Sources,
r.config.Teams.SkipExisting, r.config.Teams.SkipExisting,
&http.Client{}, &http.Client{},
) )
@ -246,6 +254,7 @@ func (r *Resolver) UIClient() target.Client {
r.uiClient = ui.NewClient( r.uiClient = ui.NewClient(
r.config.UI.Host, r.config.UI.Host,
r.config.UI.MinimumPriority, r.config.UI.MinimumPriority,
r.config.UI.Sources,
r.config.UI.SkipExisting, r.config.UI.SkipExisting,
&http.Client{}, &http.Client{},
) )
@ -255,49 +264,52 @@ func (r *Resolver) UIClient() target.Client {
return r.uiClient return r.uiClient
} }
func (r *Resolver) YandexClient() target.Client { func (r *Resolver) S3Client() target.Client {
if r.yandexClient != nil { if r.s3Client != nil {
return r.yandexClient return r.s3Client
} }
if r.config.Yandex.AccessKeyID == "" || r.config.Yandex.SecretAccessKey == "" { if r.config.S3.Endpoint == "" {
return nil return nil
} }
if r.config.S3.AccessKeyID == "" {
if r.config.Yandex.Region == "" { log.Printf("[ERROR] S3.AccessKeyID has not been declared")
log.Printf("[INFO] Yandex.Region has not been declared using ru-central1")
r.config.Yandex.Region = "ru-central1"
}
if r.config.Yandex.Endpoint == "" {
log.Printf("[INFO] Yandex.Endpoint has not been declared using https://storage.yandexcloud.net")
r.config.Yandex.Endpoint = "https://storage.yandexcloud.net"
}
if r.config.Yandex.Prefix == "" {
log.Printf("[INFO] Yandex.Prefix has not been declared using policy-reporter prefix")
r.config.Yandex.Prefix = "policy-reporter/"
}
if r.config.Yandex.Bucket == "" {
log.Printf("[ERROR] Yandex : Bucket has to be declared")
return nil return nil
} }
if r.config.S3.SecretAccessKey == "" {
log.Printf("[ERROR] S3.SecretAccessKey has not been declared")
return nil
}
if r.config.S3.Region == "" {
log.Printf("[ERROR] S3.Region has not been declared")
return nil
}
if r.config.S3.Bucket == "" {
log.Printf("[ERROR] S3.Bucket has to be declared")
return nil
}
if r.config.S3.Prefix == "" {
r.config.S3.Prefix = "policy-reporter/"
}
s3Client := helper.NewClient( s3Client := helper.NewClient(
r.config.Yandex.AccessKeyID, r.config.S3.AccessKeyID,
r.config.Yandex.SecretAccessKey, r.config.S3.SecretAccessKey,
r.config.Yandex.Region, r.config.S3.Region,
r.config.Yandex.Endpoint, r.config.S3.Endpoint,
r.config.Yandex.Bucket, r.config.S3.Bucket,
) )
r.yandexClient = yandex.NewClient( r.s3Client = s3.NewClient(
s3Client, s3Client,
r.config.Yandex.Prefix, r.config.S3.Prefix,
r.config.Yandex.MinimumPriority, r.config.S3.MinimumPriority,
r.config.Yandex.SkipExisting, r.config.S3.Sources,
r.config.S3.SkipExisting,
) )
log.Println("[INFO] Yandex configured") log.Println("[INFO] S3 configured")
return r.yandexClient return r.s3Client
} }
// TargetClients resolver method // TargetClients resolver method
@ -328,8 +340,8 @@ func (r *Resolver) TargetClients() []target.Client {
clients = append(clients, ui) clients = append(clients, ui)
} }
if yandex := r.YandexClient(); yandex != nil { if s3 := r.S3Client(); s3 != nil {
clients = append(clients, yandex) clients = append(clients, s3)
} }
return clients return clients
@ -346,44 +358,19 @@ func (r *Resolver) SkipExistingOnStartup() bool {
return true return true
} }
// ConfigMapClient resolver method func (r *Resolver) PolicyReportClient() (report.PolicyReportClient, error) {
func (r *Resolver) ConfigMapClient() (v1.ConfigMapInterface, error) { if r.policyReportClient != nil {
var err error return r.policyReportClient, nil
client, err := v1.NewForConfig(r.k8sConfig)
if err != nil {
return nil, err
}
return client.ConfigMaps(r.config.Namespace), nil
}
func (r *Resolver) configMapAPI() (kubernetes.ConfigMapAdapter, error) {
client, err := r.ConfigMapClient()
if err != nil {
return nil, err
}
return kubernetes.NewConfigMapAdapter(client), nil
}
func (r *Resolver) policyReportAPI(ctx context.Context) (kubernetes.PolicyReportAdapter, error) {
if r.policyAdapter != nil {
return r.policyAdapter, nil
} }
client, err := dynamic.NewForConfig(r.k8sConfig) client, err := dynamic.NewForConfig(r.k8sConfig)
if err != nil { if err != nil {
return nil, err return nil, err
} }
mapper, err := r.Mapper(ctx)
if err != nil {
return nil, err
}
r.policyAdapter = kubernetes.NewPolicyReportAdapter(client, mapper) r.policyReportClient = kubernetes.NewPolicyReportClient(client, r.Mapper(), 5*time.Second)
return r.policyAdapter, nil return r.policyReportClient, nil
} }
// ResultCache resolver method // ResultCache resolver method

View file

@ -1,10 +1,10 @@
package config_test package config_test
import ( import (
"context"
"testing" "testing"
"github.com/kyverno/policy-reporter/pkg/config" "github.com/kyverno/policy-reporter/pkg/config"
"github.com/kyverno/policy-reporter/pkg/report"
"k8s.io/client-go/rest" "k8s.io/client-go/rest"
) )
@ -41,12 +41,14 @@ var testConfig = &config.Config{
SkipExisting: true, SkipExisting: true,
MinimumPriority: "debug", MinimumPriority: "debug",
}, },
Yandex: config.Yandex{ S3: config.S3{
AccessKeyID: "AccessKey", AccessKeyID: "AccessKey",
SecretAccessKey: "SecretAccessKey", SecretAccessKey: "SecretAccessKey",
Bucket: "test", Bucket: "test",
SkipExisting: true, SkipExisting: true,
MinimumPriority: "debug", MinimumPriority: "debug",
Endpoint: "https://storage.yandexcloud.net",
Region: "ru-central1",
}, },
} }
@ -108,13 +110,13 @@ func Test_ResolveTarget(t *testing.T) {
t.Error("Error: Should reuse first instance") t.Error("Error: Should reuse first instance")
} }
}) })
t.Run("Yandex", func(t *testing.T) { t.Run("S3", func(t *testing.T) {
client := resolver.YandexClient() client := resolver.S3Client()
if client == nil { if client == nil {
t.Error("Expected Client, got nil") t.Error("Expected Client, got nil")
} }
client2 := resolver.YandexClient() client2 := resolver.S3Client()
if client != client2 { if client != client2 {
t.Error("Error: Should reuse first instance") t.Error("Error: Should reuse first instance")
} }
@ -194,10 +196,12 @@ func Test_ResolveTargetWithoutHost(t *testing.T) {
SkipExisting: true, SkipExisting: true,
MinimumPriority: "debug", MinimumPriority: "debug",
}, },
Yandex: config.Yandex{ S3: config.S3{
Endpoint: "",
Region: "",
AccessKeyID: "", AccessKeyID: "",
SecretAccessKey: "", SecretAccessKey: "",
Bucket: "test", Bucket: "",
SkipExisting: true, SkipExisting: true,
MinimumPriority: "debug", MinimumPriority: "debug",
}, },
@ -238,33 +242,94 @@ func Test_ResolveTargetWithoutHost(t *testing.T) {
t.Error("Expected Client to be nil if no host is configured") t.Error("Expected Client to be nil if no host is configured")
} }
}) })
t.Run("Yandex", func(t *testing.T) { t.Run("S3.Endoint", func(t *testing.T) {
resolver := config.NewResolver(config2, nil) resolver := config.NewResolver(config2, nil)
if resolver.YandexClient() != nil { if resolver.S3Client() != nil {
t.Error("Expected Client to be nil if no host is configured") t.Error("Expected Client to be nil if no endpoint is configured")
}
})
t.Run("S3.AccessKey", func(t *testing.T) {
config2.S3.Endpoint = "https://storage.yandexcloud.net"
resolver := config.NewResolver(config2, nil)
if resolver.S3Client() != nil {
t.Error("Expected Client to be nil if no accessKey is configured")
}
})
t.Run("S3.AccessKey", func(t *testing.T) {
config2.S3.Endpoint = "https://storage.yandexcloud.net"
resolver := config.NewResolver(config2, nil)
if resolver.S3Client() != nil {
t.Error("Expected Client to be nil if no accessKey is configured")
}
})
t.Run("S3.SecretAccessKey", func(t *testing.T) {
config2.S3.AccessKeyID = "access"
resolver := config.NewResolver(config2, nil)
if resolver.S3Client() != nil {
t.Error("Expected Client to be nil if no secretAccessKey is configured")
}
})
t.Run("S3.Region", func(t *testing.T) {
config2.S3.SecretAccessKey = "secret"
resolver := config.NewResolver(config2, nil)
if resolver.S3Client() != nil {
t.Error("Expected Client to be nil if no region is configured")
}
})
t.Run("S3.Bucket", func(t *testing.T) {
config2.S3.Region = "ru-central1"
resolver := config.NewResolver(config2, nil)
if resolver.S3Client() != nil {
t.Error("Expected Client to be nil if no bucket is configured")
} }
}) })
} }
func Test_ResolvePolicyClient(t *testing.T) { func Test_ResolvePolicyClient(t *testing.T) {
resolver := config.NewResolver(&config.Config{}, &rest.Config{}) resolver := config.NewResolver(&config.Config{DBFile: "test.db"}, &rest.Config{})
client1, err := resolver.PolicyReportClient(context.Background()) client1, err := resolver.PolicyReportClient()
if err != nil { if err != nil {
t.Errorf("Unexpected Error: %s", err) t.Errorf("Unexpected Error: %s", err)
} }
client2, _ := resolver.PolicyReportClient(context.Background()) client2, _ := resolver.PolicyReportClient()
if client1 != client2 { if client1 != client2 {
t.Error("A second call resolver.PolicyReportClient() should return the cached first client") t.Error("A second call resolver.PolicyReportClient() should return the cached first client")
} }
} }
func Test_ResolveAPIServer(t *testing.T) { func Test_ResolvePolicyStore(t *testing.T) {
resolver := config.NewResolver(testConfig, &rest.Config{}) resolver := config.NewResolver(&config.Config{DBFile: "test.db"}, &rest.Config{})
db, _ := resolver.Database()
defer db.Close()
server := resolver.APIServer() store1, err := resolver.PolicyReportStore(db)
if err != nil {
t.Errorf("Unexpected Error: %s", err)
}
store2, _ := resolver.PolicyReportStore(db)
if store1 != store2 {
t.Error("A second call resolver.PolicyReportClient() should return the cached first client")
}
}
func Test_ResolveAPIServer(t *testing.T) {
resolver := config.NewResolver(&config.Config{}, &rest.Config{})
server := resolver.APIServer(make(map[string]string))
if server == nil { if server == nil {
t.Error("Error: Should return API Server") t.Error("Error: Should return API Server")
} }
@ -284,14 +349,70 @@ func Test_ResolveCache(t *testing.T) {
} }
} }
func Test_ResolveMapper(t *testing.T) {
resolver := config.NewResolver(testConfig, &rest.Config{})
mapper1 := resolver.Mapper()
if mapper1 == nil {
t.Error("Error: Should return Mapper")
}
mapper2 := resolver.Mapper()
if mapper1 != mapper2 {
t.Error("A second call resolver.Mapper() should return the cached first cache")
}
}
func Test_ResolveClientWithInvalidK8sConfig(t *testing.T) { func Test_ResolveClientWithInvalidK8sConfig(t *testing.T) {
k8sConfig := &rest.Config{} k8sConfig := &rest.Config{}
k8sConfig.Host = "invalid/url" k8sConfig.Host = "invalid/url"
resolver := config.NewResolver(&config.Config{}, k8sConfig) resolver := config.NewResolver(testConfig, k8sConfig)
_, err := resolver.PolicyReportClient(context.Background()) _, err := resolver.PolicyReportClient()
if err == nil { if err == nil {
t.Error("Error: 'host must be a URL or a host:port pair' was expected") t.Error("Error: 'host must be a URL or a host:port pair' was expected")
} }
} }
func Test_RegisterStoreListener(t *testing.T) {
t.Run("Register StoreListener", func(t *testing.T) {
resolver := config.NewResolver(testConfig, &rest.Config{})
resolver.RegisterStoreListener(report.NewPolicyReportStore())
if len(resolver.EventPublisher().GetListener()) != 1 {
t.Error("Expected one Listener to be registered")
}
})
}
func Test_RegisterMetricsListener(t *testing.T) {
t.Run("Register MetricsListener", func(t *testing.T) {
resolver := config.NewResolver(testConfig, &rest.Config{})
resolver.RegisterMetricsListener()
if len(resolver.EventPublisher().GetListener()) != 1 {
t.Error("Expected one Listener to be registered")
}
})
}
func Test_RegisterSendResultListener(t *testing.T) {
t.Run("Register SendResultListener with Targets", func(t *testing.T) {
resolver := config.NewResolver(testConfig, &rest.Config{})
resolver.RegisterSendResultListener()
if len(resolver.EventPublisher().GetListener()) != 1 {
t.Error("Expected one Listener to be registered")
}
})
t.Run("Register SendResultListener without Targets", func(t *testing.T) {
resolver := config.NewResolver(&config.Config{}, &rest.Config{})
resolver.RegisterSendResultListener()
if len(resolver.EventPublisher().GetListener()) != 0 {
t.Error("Expected no Listener to be registered because no target exists")
}
})
}

64
pkg/helper/http.go Normal file
View file

@ -0,0 +1,64 @@
package helper
import (
"bytes"
"encoding/json"
"fmt"
"html"
"log"
"net/http"
)
func CreateJSONRequest(target, method, host string, payload interface{}) (*http.Request, error) {
body := new(bytes.Buffer)
if err := json.NewEncoder(body).Encode(payload); err != nil {
log.Printf("[ERROR] %s : %v\n", target, err.Error())
return nil, err
}
req, err := http.NewRequest(method, host, body)
if err != nil {
log.Printf("[ERROR] %s : %v\n", target, err.Error())
return nil, err
}
return req, nil
}
// ProcessHTTPResponse Logs Error or Success messages
func ProcessHTTPResponse(target string, resp *http.Response, err error) {
defer func() {
if resp != nil && resp.Body != nil {
resp.Body.Close()
}
}()
if err != nil {
log.Printf("[ERROR] %s PUSH failed: %s\n", target, err.Error())
} else if resp.StatusCode >= 400 {
fmt.Printf("StatusCode: %d\n", resp.StatusCode)
buf := new(bytes.Buffer)
buf.ReadFrom(resp.Body)
log.Printf("[ERROR] %s PUSH failed [%d]: %s\n", target, resp.StatusCode, buf.String())
} else {
log.Printf("[INFO] %s PUSH OK\n", target)
}
}
func SendJSONResponse(w http.ResponseWriter, list interface{}, err error) {
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(w, `{ "message": "%s" }`, html.EscapeString(err.Error()))
return
}
if err := json.NewEncoder(w).Encode(list); err != nil {
log.Println(err)
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(w, `{ "message": "%s" }`, html.EscapeString(err.Error()))
}
}

View file

@ -28,7 +28,7 @@ func (s *s3Client) Upload(body *bytes.Buffer, key string) error {
return err return err
} }
// NewClient creates a new Yandex.client to send Results to S3. It doesnt' work right now // NewClient creates a new S3.client to send Results to S3. It doesnt' work right now
func NewClient(accessKeyID, secretAccessKey, region, endpoint, bucket string) S3Client { func NewClient(accessKeyID, secretAccessKey, region, endpoint, bucket string) S3Client {
sess, err := session.NewSession(&aws.Config{ sess, err := session.NewSession(&aws.Config{
Region: aws.String(region), Region: aws.String(region),

View file

@ -1,53 +0,0 @@
package kubernetes
import (
"context"
apiv1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/watch"
v1 "k8s.io/client-go/kubernetes/typed/core/v1"
)
const (
prioriyConfig = "policy-reporter-priorities"
)
// ConfigMapAdapter provides simplified APIs for ConfigMap Resources
type ConfigMapAdapter interface {
// GetConfig return a single ConfigMap by name if exist
GetConfig(ctx context.Context, name string) (*apiv1.ConfigMap, error)
// WatchConfigs calls its ConfigMapCallback whenever a ConfigMap was added, modified or deleted
WatchConfigs(ctx context.Context, cb ConfigMapCallback) error
}
// ConfigMapCallback is used by WatchConfigs
type ConfigMapCallback = func(watch.EventType, *apiv1.ConfigMap)
type cmAdapter struct {
api v1.ConfigMapInterface
}
func (c cmAdapter) GetConfig(ctx context.Context, name string) (*apiv1.ConfigMap, error) {
return c.api.Get(ctx, name, metav1.GetOptions{})
}
func (c cmAdapter) WatchConfigs(ctx context.Context, cb ConfigMapCallback) error {
for {
watch, err := c.api.Watch(ctx, metav1.ListOptions{})
if err != nil {
return err
}
for event := range watch.ResultChan() {
if cm, ok := event.Object.(*apiv1.ConfigMap); ok {
cb(event.Type, cm)
}
}
}
}
// NewConfigMapAdapter creates a new ConfigMapClient
func NewConfigMapAdapter(api v1.ConfigMapInterface) ConfigMapAdapter {
return &cmAdapter{api}
}

View file

@ -1,92 +0,0 @@
package kubernetes_test
import (
"context"
"errors"
"sync"
"testing"
"github.com/kyverno/policy-reporter/pkg/kubernetes"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/kubernetes/fake"
clientv1 "k8s.io/client-go/kubernetes/typed/core/v1"
testcore "k8s.io/client-go/testing"
)
var configMap = &v1.ConfigMap{
TypeMeta: metav1.TypeMeta{
Kind: "ConfigMap",
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "policy-reporter-priorities",
},
Data: map[string]string{
"default": "critical",
},
}
func Test_GetConfigMap(t *testing.T) {
_, cmAPI := newFakeAPI()
cmAPI.Create(context.Background(), configMap, metav1.CreateOptions{})
cmClient := kubernetes.NewConfigMapAdapter(cmAPI)
cm, err := cmClient.GetConfig(context.Background(), "policy-reporter-priorities")
if err != nil {
t.Fatalf("Unexpected Error: %s", err)
}
if cm.Name != "policy-reporter-priorities" {
t.Error("Unexpted ConfigMapReturned")
}
if priority, ok := cm.Data["default"]; !ok || priority != "critical" {
t.Error("Unexpted default priority")
}
}
func Test_WatchConfigMap(t *testing.T) {
client, cmAPI := newFakeAPI()
watcher := watch.NewFake()
client.PrependWatchReactor("configmaps", testcore.DefaultWatchReactor(watcher, nil))
cmClient := kubernetes.NewConfigMapAdapter(cmAPI)
wg := sync.WaitGroup{}
wg.Add(1)
go cmClient.WatchConfigs(context.Background(), func(et watch.EventType, cm *v1.ConfigMap) {
defer wg.Done()
if cm.Name != "policy-reporter-priorities" {
t.Error("Unexpted ConfigMapReturned")
}
if priority, ok := cm.Data["default"]; !ok || priority != "critical" {
t.Error("Unexpted default priority")
}
})
watcher.Add(configMap)
wg.Wait()
}
func Test_WatchConfigMapError(t *testing.T) {
client, cmAPI := newFakeAPI()
client.PrependWatchReactor("configmaps", testcore.DefaultWatchReactor(watch.NewFake(), errors.New("")))
cmClient := kubernetes.NewConfigMapAdapter(cmAPI)
err := cmClient.WatchConfigs(context.Background(), func(et watch.EventType, cm *v1.ConfigMap) {})
if err == nil {
t.Error("Watch Error should stop execution")
}
}
func newFakeAPI() (*fake.Clientset, clientv1.ConfigMapInterface) {
client := fake.NewSimpleClientset()
return client, client.CoreV1().ConfigMaps("policy-reporter")
}

View file

@ -4,40 +4,47 @@ import (
"sync" "sync"
"time" "time"
"k8s.io/apimachinery/pkg/watch" "github.com/kyverno/policy-reporter/pkg/report"
) )
type debouncer struct { type Debouncer interface {
events map[string]WatchEvent Add(e report.LifecycleEvent)
channel chan WatchEvent ReportChan() <-chan report.LifecycleEvent
mutx *sync.Mutex Close()
} }
func (d *debouncer) Add(e WatchEvent) { type debouncer struct {
_, ok := d.events[e.Report.GetIdentifier()] waitDuration time.Duration
if e.Type != watch.Modified && ok { events map[string]report.LifecycleEvent
channel chan report.LifecycleEvent
mutx *sync.Mutex
}
func (d *debouncer) Add(event report.LifecycleEvent) {
_, ok := d.events[event.NewPolicyReport.GetIdentifier()]
if event.Type != report.Updated && ok {
d.mutx.Lock() d.mutx.Lock()
delete(d.events, e.Report.GetIdentifier()) delete(d.events, event.NewPolicyReport.GetIdentifier())
d.mutx.Unlock() d.mutx.Unlock()
} }
if e.Type != watch.Modified { if event.Type != report.Updated {
d.channel <- e d.channel <- event
return return
} }
if len(e.Report.Results) == 0 && !ok { if len(event.NewPolicyReport.Results) == 0 && !ok {
d.mutx.Lock() d.mutx.Lock()
d.events[e.Report.GetIdentifier()] = e d.events[event.NewPolicyReport.GetIdentifier()] = event
d.mutx.Unlock() d.mutx.Unlock()
go func() { go func() {
time.Sleep(1 * time.Minute) time.Sleep(d.waitDuration)
d.mutx.Lock() d.mutx.Lock()
if event, ok := d.events[e.Report.GetIdentifier()]; ok { if event, ok := d.events[event.NewPolicyReport.GetIdentifier()]; ok {
d.channel <- event d.channel <- event
delete(d.events, e.Report.GetIdentifier()) delete(d.events, event.NewPolicyReport.GetIdentifier())
} }
d.mutx.Unlock() d.mutx.Unlock()
}() }()
@ -47,23 +54,28 @@ func (d *debouncer) Add(e WatchEvent) {
if ok { if ok {
d.mutx.Lock() d.mutx.Lock()
d.events[e.Report.GetIdentifier()] = e d.events[event.NewPolicyReport.GetIdentifier()] = event
d.mutx.Unlock() d.mutx.Unlock()
return return
} }
d.channel <- e d.channel <- event
} }
func (d *debouncer) ReportChan() chan WatchEvent { func (d *debouncer) ReportChan() <-chan report.LifecycleEvent {
return d.channel return d.channel
} }
func newDebouncer() *debouncer { func (d *debouncer) Close() {
close(d.channel)
}
func NewDebouncer(waitDuration time.Duration) Debouncer {
return &debouncer{ return &debouncer{
events: make(map[string]WatchEvent), waitDuration: waitDuration,
mutx: new(sync.Mutex), events: make(map[string]report.LifecycleEvent),
channel: make(chan WatchEvent), mutx: new(sync.Mutex),
channel: make(chan report.LifecycleEvent),
} }
} }

View file

@ -0,0 +1,49 @@
package kubernetes_test
import (
"sync"
"testing"
"time"
"github.com/kyverno/policy-reporter/pkg/kubernetes"
"github.com/kyverno/policy-reporter/pkg/report"
)
func Test_Debouncer(t *testing.T) {
t.Run("Skip Empty Update", func(t *testing.T) {
debouncer := kubernetes.NewDebouncer(200 * time.Millisecond)
wg := sync.WaitGroup{}
wg.Add(2)
go func() {
for event := range debouncer.ReportChan() {
wg.Done()
if len(event.NewPolicyReport.Results) == 0 {
t.Error("Expected to skip the empty modify event")
}
}
}()
debouncer.Add(report.LifecycleEvent{
Type: report.Added,
NewPolicyReport: mapper.MapPolicyReport(policyMap),
})
debouncer.Add(report.LifecycleEvent{
Type: report.Updated,
NewPolicyReport: mapper.MapPolicyReport(minPolicyMap),
})
time.Sleep(10 * time.Millisecond)
debouncer.Add(report.LifecycleEvent{
Type: report.Updated,
NewPolicyReport: mapper.MapPolicyReport(policyMap),
})
wg.Wait()
debouncer.Close()
})
}

View file

@ -0,0 +1,166 @@
package kubernetes_test
import (
"sync"
"github.com/kyverno/policy-reporter/pkg/kubernetes"
"github.com/kyverno/policy-reporter/pkg/report"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/dynamic/fake"
)
var policyReportSchema = schema.GroupVersionResource{
Group: "wgpolicyk8s.io",
Version: "v1alpha2",
Resource: "policyreports",
}
var clusterPolicyReportSchema = schema.GroupVersionResource{
Group: "wgpolicyk8s.io",
Version: "v1alpha2",
Resource: "clusterpolicyreports",
}
var gvrToListKind = map[schema.GroupVersionResource]string{
policyReportSchema: "PolicyReportList",
clusterPolicyReportSchema: "ClusterPolicyReportList",
}
func NewFakeCilent() (dynamic.Interface, dynamic.ResourceInterface) {
client := fake.NewSimpleDynamicClientWithCustomListKinds(runtime.NewScheme(), gvrToListKind)
return client, client.Resource(policyReportSchema).Namespace("test")
}
func NewMapper() kubernetes.Mapper {
return kubernetes.NewMapper(make(map[string]string))
}
type store struct {
store []report.LifecycleEvent
rwm *sync.RWMutex
}
func (s *store) Add(r report.LifecycleEvent) {
s.rwm.Lock()
s.store = append(s.store, r)
s.rwm.Unlock()
}
func (s *store) Get(index int) report.LifecycleEvent {
return s.store[index]
}
func (s *store) List() []report.LifecycleEvent {
return s.store
}
func newStore(size int) *store {
return &store{
store: make([]report.LifecycleEvent, 0, size),
rwm: &sync.RWMutex{},
}
}
var policyMap = map[string]interface{}{
"metadata": map[string]interface{}{
"name": "policy-report",
"namespace": "test",
"creationTimestamp": "2021-02-23T15:00:00Z",
},
"summary": map[string]interface{}{
"pass": int64(1),
"skip": int64(2),
"warn": int64(3),
"fail": int64(4),
"error": int64(5),
},
"results": []interface{}{
map[string]interface{}{
"message": "message",
"result": "fail",
"scored": true,
"policy": "required-label",
"rule": "app-label-required",
"timestamp": map[string]interface{}{
"seconds": int64(1614093000),
},
"source": "test",
"category": "test",
"severity": "high",
"resources": []interface{}{
map[string]interface{}{
"apiVersion": "v1",
"kind": "Deployment",
"name": "nginx",
"namespace": "test",
"uid": "dfd57c50-f30c-4729-b63f-b1954d8988d1",
},
},
"properties": map[string]interface{}{
"version": "1.2.0",
},
},
map[string]interface{}{
"message": "message 2",
"result": "fail",
"scored": true,
"timestamp": map[string]interface{}{
"seconds": int64(1614093000),
},
"policy": "priority-test",
"resources": []interface{}{},
},
},
}
var minPolicyMap = map[string]interface{}{
"metadata": map[string]interface{}{
"name": "policy-report",
"namespace": "test",
},
"results": []interface{}{},
}
var clusterPolicyMap = map[string]interface{}{
"metadata": map[string]interface{}{
"name": "clusterpolicy-report",
"creationTimestamp": "2021-02-23T15:00:00Z",
},
"summary": map[string]interface{}{
"pass": int64(1),
"skip": int64(2),
"warn": int64(3),
"fail": int64(4),
"error": int64(5),
},
"results": []interface{}{
map[string]interface{}{
"message": "message",
"result": "fail",
"scored": true,
"policy": "cluster-required-label",
"rule": "ns-label-required",
"category": "test",
"severity": "high",
"timestamp": map[string]interface{}{"seconds": ""},
"resources": []interface{}{
map[string]interface{}{
"apiVersion": "v1",
"kind": "Namespace",
"name": "policy-reporter",
"uid": "dfd57c50-f30c-4729-b63f-b1954d8988d1",
},
},
},
},
}
var priorityMap = map[string]string{
"priority-test": "warning",
}
var result1ID string = report.GeneratePolicyReportResultID("dfd57c50-f30c-4729-b63f-b1954d8988d1", "required-label", "app-label-required", "fail", "message")
var result2ID string = report.GeneratePolicyReportResultID("", "priority-test", "", "fail", "message 2")

View file

@ -1,35 +1,24 @@
package kubernetes package kubernetes
import ( import (
"context"
"errors" "errors"
"log"
"time" "time"
"github.com/kyverno/policy-reporter/pkg/report" "github.com/kyverno/policy-reporter/pkg/report"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/watch"
) )
// Mapper converts maps into report structs // Mapper converts maps into report structs
type Mapper interface { type Mapper interface {
// MapPolicyReport maps a map into a PolicyReport // MapPolicyReport maps a map into a PolicyReport
MapPolicyReport(reportMap map[string]interface{}) report.PolicyReport MapPolicyReport(reportMap map[string]interface{}) *report.PolicyReport
// SetPriorityMap updates the policy/status to priority mapping
SetPriorityMap(map[string]string)
// SyncPriorities when ConfigMap has changed
SyncPriorities(ctx context.Context) error
// FetchPriorities from ConfigMap
FetchPriorities(ctx context.Context) error
} }
type mapper struct { type mapper struct {
priorityMap map[string]string priorityMap map[string]string
cmAdapter ConfigMapAdapter
} }
func (m *mapper) MapPolicyReport(reportMap map[string]interface{}) report.PolicyReport { func (m *mapper) MapPolicyReport(reportMap map[string]interface{}) *report.PolicyReport {
summary := report.Summary{} summary := &report.Summary{}
if s, ok := reportMap["summary"].(map[string]interface{}); ok { if s, ok := reportMap["summary"].(map[string]interface{}); ok {
summary.Pass = int(s["pass"].(int64)) summary.Pass = int(s["pass"].(int64))
@ -39,12 +28,15 @@ func (m *mapper) MapPolicyReport(reportMap map[string]interface{}) report.Policy
summary.Fail = int(s["fail"].(int64)) summary.Fail = int(s["fail"].(int64))
} }
metadata := reportMap["metadata"].(map[string]interface{}) metadata, ok := reportMap["metadata"].(map[string]interface{})
if !ok {
return &report.PolicyReport{}
}
r := report.PolicyReport{ r := &report.PolicyReport{
Name: metadata["name"].(string), Name: metadata["name"].(string),
Summary: summary, Summary: summary,
Results: make(map[string]report.Result), Results: make(map[string]*report.Result),
} }
if ns, ok := metadata["namespace"]; ok { if ns, ok := metadata["namespace"]; ok {
@ -60,13 +52,15 @@ func (m *mapper) MapPolicyReport(reportMap map[string]interface{}) report.Policy
if rs, ok := reportMap["results"].([]interface{}); ok { if rs, ok := reportMap["results"].([]interface{}); ok {
for _, resultItem := range rs { for _, resultItem := range rs {
resources := m.mapResult(resultItem.(map[string]interface{})) results := m.mapResult(resultItem.(map[string]interface{}))
for _, resource := range resources { for _, result := range results {
r.Results[resource.GetIdentifier()] = resource r.Results[result.GetIdentifier()] = result
} }
} }
} }
r.ID = report.GeneratePolicyReportID(r.Name, r.Namespace)
return r return r
} }
@ -75,24 +69,21 @@ func (m *mapper) SetPriorityMap(priorityMap map[string]string) {
} }
func (m *mapper) mapCreationTime(result map[string]interface{}) (time.Time, error) { func (m *mapper) mapCreationTime(result map[string]interface{}) (time.Time, error) {
if metadata, ok := result["metadata"].(map[string]interface{}); ok { metadata := result["metadata"].(map[string]interface{})
if created, ok2 := metadata["creationTimestamp"].(string); ok2 { if created, ok2 := metadata["creationTimestamp"].(string); ok2 {
return time.Parse("2006-01-02T15:04:05Z", created) return time.Parse("2006-01-02T15:04:05Z", created)
}
return time.Time{}, errors.New("no creationTimestamp provided")
} }
return time.Time{}, errors.New("no metadata provided") return time.Time{}, errors.New("no creationTimestamp provided")
} }
func (m *mapper) mapResult(result map[string]interface{}) []report.Result { func (m *mapper) mapResult(result map[string]interface{}) []*report.Result {
var resources []report.Resource var resources []*report.Resource
if ress, ok := result["resources"].([]interface{}); ok { if ress, ok := result["resources"].([]interface{}); ok {
for _, res := range ress { for _, res := range ress {
if resMap, ok := res.(map[string]interface{}); ok { if resMap, ok := res.(map[string]interface{}); ok {
r := report.Resource{ r := &report.Resource{
APIVersion: resMap["apiVersion"].(string), APIVersion: resMap["apiVersion"].(string),
Kind: resMap["kind"].(string), Kind: resMap["kind"].(string),
Name: resMap["name"].(string), Name: resMap["name"].(string),
@ -117,11 +108,10 @@ func (m *mapper) mapResult(result map[string]interface{}) []report.Result {
status = r.(report.Status) status = r.(report.Status)
} }
var results []report.Result var results []*report.Result
factory := func(res report.Resource) report.Result { factory := func(res *report.Resource) *report.Result {
r := report.Result{ r := &report.Result{
Message: result["message"].(string),
Policy: result["policy"].(string), Policy: result["policy"].(string),
Status: status, Status: status,
Priority: report.PriorityFromStatus(status), Priority: report.PriorityFromStatus(status),
@ -129,6 +119,10 @@ func (m *mapper) mapResult(result map[string]interface{}) []report.Result {
Properties: make(map[string]string, 0), Properties: make(map[string]string, 0),
} }
if message, ok := result["message"].(string); ok {
r.Message = message
}
if scored, ok := result["scored"]; ok { if scored, ok := result["scored"]; ok {
r.Scored = scored.(bool) r.Scored = scored.(bool)
} }
@ -137,7 +131,7 @@ func (m *mapper) mapResult(result map[string]interface{}) []report.Result {
r.Severity = severity.(report.Severity) r.Severity = severity.(report.Severity)
} }
if r.Status == report.Error || r.Status == report.Fail { if r.Status == report.Fail {
r.Priority = m.resolvePriority(r.Policy, r.Severity) r.Priority = m.resolvePriority(r.Policy, r.Severity)
} }
@ -166,6 +160,8 @@ func (m *mapper) mapResult(result map[string]interface{}) []report.Result {
} }
} }
r.ID = report.GeneratePolicyReportResultID(r.Resource.UID, r.Policy, r.Rule, r.Status, r.Message)
return r return r
} }
@ -174,7 +170,7 @@ func (m *mapper) mapResult(result map[string]interface{}) []report.Result {
} }
if len(results) == 0 { if len(results) == 0 {
results = append(results, factory(report.Resource{})) results = append(results, factory(&report.Resource{}))
} }
return results return results
@ -214,48 +210,9 @@ func (m *mapper) resolvePriority(policy string, severity report.Severity) report
return report.Priority(report.WarningPriority) return report.Priority(report.WarningPriority)
} }
func (m *mapper) FetchPriorities(ctx context.Context) error {
cm, err := m.cmAdapter.GetConfig(ctx, prioriyConfig)
if err != nil {
return err
}
if cm != nil {
m.SetPriorityMap(cm.Data)
log.Println("[INFO] Priorities loaded")
}
return nil
}
func (m *mapper) SyncPriorities(ctx context.Context) error {
err := m.cmAdapter.WatchConfigs(ctx, func(e watch.EventType, cm *v1.ConfigMap) {
if cm.Name != prioriyConfig {
return
}
switch e {
case watch.Added:
m.SetPriorityMap(cm.Data)
case watch.Modified:
m.SetPriorityMap(cm.Data)
case watch.Deleted:
m.SetPriorityMap(map[string]string{})
}
log.Println("[INFO] Priorities synchronized")
})
if err != nil {
log.Printf("[INFO] Unable to sync Priorities: %s", err.Error())
}
return err
}
// NewMapper creates an new Mapper instance // NewMapper creates an new Mapper instance
func NewMapper(priorities map[string]string, cmAdapter ConfigMapAdapter) Mapper { func NewMapper(priorities map[string]string) Mapper {
m := &mapper{cmAdapter: cmAdapter} m := &mapper{}
m.SetPriorityMap(priorities) m.SetPriorityMap(priorities)
return m return m

View file

@ -1,117 +1,13 @@
package kubernetes_test package kubernetes_test
import ( import (
"context"
"testing" "testing"
"time"
"github.com/kyverno/policy-reporter/pkg/kubernetes" "github.com/kyverno/policy-reporter/pkg/kubernetes"
"github.com/kyverno/policy-reporter/pkg/report" "github.com/kyverno/policy-reporter/pkg/report"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/watch"
testcore "k8s.io/client-go/testing"
) )
var policyMap = map[string]interface{}{ var mapper = kubernetes.NewMapper(priorityMap)
"metadata": map[string]interface{}{
"name": "policy-report",
"namespace": "test",
"creationTimestamp": "2021-02-23T15:00:00Z",
},
"summary": map[string]interface{}{
"pass": int64(1),
"skip": int64(2),
"warn": int64(3),
"fail": int64(4),
"error": int64(5),
},
"results": []interface{}{
map[string]interface{}{
"message": "message",
"status": "fail",
"scored": true,
"policy": "required-label",
"rule": "app-label-required",
"timestamp": map[string]interface{}{
"seconds": 1614093000,
},
"source": "test",
"category": "test",
"severity": "high",
"resources": []interface{}{
map[string]interface{}{
"apiVersion": "v1",
"kind": "Deployment",
"name": "nginx",
"namespace": "test",
"uid": "dfd57c50-f30c-4729-b63f-b1954d8988d1",
},
},
"properties": map[string]interface{}{
"version": "1.2.0",
},
},
map[string]interface{}{
"message": "message 2",
"status": "fail",
"scored": true,
"timestamp": map[string]interface{}{
"seconds": int64(1614093000),
},
"policy": "priority-test",
"resources": []interface{}{},
},
},
}
var minPolicyMap = map[string]interface{}{
"metadata": map[string]interface{}{
"name": "policy-report",
"namespace": "test",
},
"results": []interface{}{},
}
var clusterPolicyMap = map[string]interface{}{
"metadata": map[string]interface{}{
"name": "clusterpolicy-report",
"creationTimestamp": "2021-02-23T15:00:00Z",
},
"summary": map[string]interface{}{
"pass": int64(1),
"skip": int64(2),
"warn": int64(3),
"fail": int64(4),
"error": int64(5),
},
"results": []interface{}{
map[string]interface{}{
"message": "message",
"result": "fail",
"scored": true,
"policy": "cluster-required-label",
"rule": "ns-label-required",
"category": "test",
"severity": "high",
"timestamp": map[string]interface{}{"seconds": ""},
"resources": []interface{}{
map[string]interface{}{
"apiVersion": "v1",
"kind": "Namespace",
"name": "policy-reporter",
"uid": "dfd57c50-f30c-4729-b63f-b1954d8988d1",
},
},
},
},
}
var priorityMap = map[string]string{
"priority-test": "warning",
}
var mapper = kubernetes.NewMapper(priorityMap, nil)
func Test_MapPolicyReport(t *testing.T) { func Test_MapPolicyReport(t *testing.T) {
preport := mapper.MapPolicyReport(policyMap) preport := mapper.MapPolicyReport(policyMap)
@ -138,7 +34,7 @@ func Test_MapPolicyReport(t *testing.T) {
t.Errorf("Unexpected Summary.Error value %d (expected 5)", preport.Summary.Error) t.Errorf("Unexpected Summary.Error value %d (expected 5)", preport.Summary.Error)
} }
result1, ok := preport.Results["required-label__app-label-required__fail__dfd57c50-f30c-4729-b63f-b1954d8988d1"] result1, ok := preport.Results[result1ID]
if !ok { if !ok {
t.Error("Expected result not found") t.Error("Expected result not found")
} }
@ -194,7 +90,7 @@ func Test_MapPolicyReport(t *testing.T) {
t.Errorf("Expected Resource.Namespace 'dfd57c50-f30c-4729-b63f-b1954d8988d1' (acutal %s)", resource.UID) t.Errorf("Expected Resource.Namespace 'dfd57c50-f30c-4729-b63f-b1954d8988d1' (acutal %s)", resource.UID)
} }
result2, ok := preport.Results["priority-test____fail"] result2, ok := preport.Results[result2ID]
if !ok { if !ok {
t.Error("Expected result not found") t.Error("Expected result not found")
} }
@ -253,11 +149,11 @@ func Test_MapMinPolicyReport(t *testing.T) {
func Test_PriorityMap(t *testing.T) { func Test_PriorityMap(t *testing.T) {
t.Run("Test exact match, without default", func(t *testing.T) { t.Run("Test exact match, without default", func(t *testing.T) {
mapper := kubernetes.NewMapper(map[string]string{"required-label": "debug"}, nil) mapper := kubernetes.NewMapper(map[string]string{"required-label": "debug"})
preport := mapper.MapPolicyReport(policyMap) preport := mapper.MapPolicyReport(policyMap)
result := preport.Results["required-label__app-label-required__fail__dfd57c50-f30c-4729-b63f-b1954d8988d1"] result := preport.Results[result1ID]
if result.Priority != report.DebugPriority { if result.Priority != report.DebugPriority {
t.Errorf("Expected Policy '%d' (acutal %d)", report.DebugPriority, result.Priority) t.Errorf("Expected Policy '%d' (acutal %d)", report.DebugPriority, result.Priority)
@ -265,11 +161,11 @@ func Test_PriorityMap(t *testing.T) {
}) })
t.Run("Test exact match handled over default", func(t *testing.T) { t.Run("Test exact match handled over default", func(t *testing.T) {
mapper := kubernetes.NewMapper(map[string]string{"required-label": "debug", "default": "warning"}, nil) mapper := kubernetes.NewMapper(map[string]string{"required-label": "debug", "default": "warning"})
preport := mapper.MapPolicyReport(policyMap) preport := mapper.MapPolicyReport(policyMap)
result := preport.Results["required-label__app-label-required__fail__dfd57c50-f30c-4729-b63f-b1954d8988d1"] result := preport.Results[result1ID]
if result.Priority != report.DebugPriority { if result.Priority != report.DebugPriority {
t.Errorf("Expected Policy '%d' (acutal %d)", report.DebugPriority, result.Priority) t.Errorf("Expected Policy '%d' (acutal %d)", report.DebugPriority, result.Priority)
@ -277,11 +173,11 @@ func Test_PriorityMap(t *testing.T) {
}) })
t.Run("Test default expressions", func(t *testing.T) { t.Run("Test default expressions", func(t *testing.T) {
mapper := kubernetes.NewMapper(map[string]string{"default": "warning"}, nil) mapper := kubernetes.NewMapper(map[string]string{"default": "warning"})
preport := mapper.MapPolicyReport(policyMap) preport := mapper.MapPolicyReport(policyMap)
result := preport.Results["priority-test____fail"] result := preport.Results[result2ID]
if result.Priority != report.WarningPriority { if result.Priority != report.WarningPriority {
t.Errorf("Expected Policy '%d' (acutal %d)", report.WarningPriority, result.Priority) t.Errorf("Expected Policy '%d' (acutal %d)", report.WarningPriority, result.Priority)
@ -289,92 +185,67 @@ func Test_PriorityMap(t *testing.T) {
}) })
} }
func Test_PriorityFetch(t *testing.T) { func Test_MapWithoutMetadata(t *testing.T) {
_, cmAPI := newFakeAPI() mapper := kubernetes.NewMapper(make(map[string]string))
cmAPI.Create(context.Background(), configMap, metav1.CreateOptions{})
mapper := kubernetes.NewMapper(make(map[string]string), kubernetes.NewConfigMapAdapter(cmAPI))
preport1 := mapper.MapPolicyReport(policyMap) policyReport := map[string]interface{}{}
result1 := preport1.Results["priority-test____fail"]
if result1.Priority != report.WarningPriority { report := mapper.MapPolicyReport(policyReport)
t.Errorf("Default Priority should be Warning")
if report.Name != "" {
t.Errorf("Expected empty PolicyReport")
}
}
func Test_MapWithoutResultTimestamp(t *testing.T) {
mapper := kubernetes.NewMapper(make(map[string]string))
policyReport := map[string]interface{}{
"metadata": map[string]interface{}{
"name": "policy-report",
"namespace": "test",
"creationTimestamp": "2021-02-23T15:00:00Z",
},
"results": []interface{}{map[string]interface{}{
"message": "message 2",
"status": "fail",
"scored": true,
"policy": "priority-test",
"resources": []interface{}{},
}},
} }
mapper.FetchPriorities(context.Background()) report := mapper.MapPolicyReport(policyReport)
preport2 := mapper.MapPolicyReport(policyMap)
result2 := preport2.Results["priority-test____fail"] if report.Results[result2ID].Timestamp.IsZero() {
if result2.Priority != report.CriticalPriority { t.Errorf("Expected valid Timestamp")
t.Errorf("Default Priority should be Critical after ConigMap fetch")
} }
} }
func Test_PriorityFetchError(t *testing.T) { func Test_MapTimestamoAsInt(t *testing.T) {
_, cmAPI := newFakeAPI() mapper := kubernetes.NewMapper(make(map[string]string))
mapper := kubernetes.NewMapper(make(map[string]string), kubernetes.NewConfigMapAdapter(cmAPI))
mapper.FetchPriorities(context.Background()) policyReport := map[string]interface{}{
preport := mapper.MapPolicyReport(policyMap) "metadata": map[string]interface{}{
result := preport.Results["priority-test____fail"] "name": "policy-report",
if result.Priority != report.WarningPriority { "namespace": "test",
t.Errorf("Fetch Error should not effect the functionality and continue using Warning as default") "creationTimestamp": "2021-02-23T15:00:00Z",
} },
} "results": []interface{}{map[string]interface{}{
"message": "message 2",
func Test_PrioritySync(t *testing.T) { "status": "fail",
client, cmAPI := newFakeAPI() "scored": true,
watcher := watch.NewFake() "timestamp": map[string]interface{}{
client.PrependWatchReactor("configmaps", testcore.DefaultWatchReactor(watcher, nil)) "seconds": 1614093000,
},
mapper := kubernetes.NewMapper(make(map[string]string), kubernetes.NewConfigMapAdapter(cmAPI)) "policy": "priority-test",
"resources": []interface{}{},
preport1 := mapper.MapPolicyReport(policyMap) }},
result1 := preport1.Results["priority-test____fail"] }
if result1.Priority != report.WarningPriority { r := mapper.MapPolicyReport(policyReport)
t.Errorf("Default Priority should be Warning") id := report.GeneratePolicyReportResultID("", "priority-test", "", "fail", "message 2")
}
if r.Results[id].Timestamp.IsZero() {
go mapper.SyncPriorities(context.Background()) t.Errorf("Expected valid Timestamp")
watcher.Add(configMap)
preport2 := mapper.MapPolicyReport(policyMap)
result2 := preport2.Results["priority-test____fail"]
if result2.Priority != report.CriticalPriority {
t.Errorf("Default Priority should be Critical after ConigMap add sync")
}
configMap2 := &v1.ConfigMap{
TypeMeta: metav1.TypeMeta{
Kind: "ConfigMap",
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "policy-reporter-priorities",
},
Data: map[string]string{
"default": "debug",
},
}
watcher.Modify(configMap2)
time.Sleep(100 * time.Millisecond)
preport3 := mapper.MapPolicyReport(policyMap)
result3 := preport3.Results["priority-test____fail"]
if result3.Priority != report.DebugPriority {
t.Errorf("Default Priority should be Debug after ConigMap modify sync")
}
watcher.Delete(configMap2)
time.Sleep(100 * time.Millisecond)
preport4 := mapper.MapPolicyReport(policyMap)
result4 := preport4.Results["priority-test____fail"]
if result4.Priority != report.WarningPriority {
t.Errorf("Default Priority should be fallback to Warning after ConigMap delete sync")
} }
} }

View file

@ -2,188 +2,164 @@ package kubernetes
import ( import (
"context" "context"
"errors" "log"
"sync" "sync"
"time" "time"
"github.com/kyverno/policy-reporter/pkg/report" "github.com/kyverno/policy-reporter/pkg/report"
"github.com/patrickmn/go-cache" corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/watch" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/dynamic/dynamicinformer"
"k8s.io/client-go/tools/cache"
) )
type policyReportClient struct { var (
policyAPI PolicyReportAdapter policyReportAlphaV1 = schema.GroupVersionResource{
store *report.PolicyReportStore Group: "wgpolicyk8s.io",
callbacks []report.PolicyReportCallback Version: "v1alpha1",
resultCallbacks []report.PolicyResultCallback Resource: "policyreports",
debouncer *debouncer }
startUp time.Time policyReportAlphaV2 = schema.GroupVersionResource{
skipExisting bool Group: "wgpolicyk8s.io",
started bool Version: "v1alpha2",
resultCache *cache.Cache Resource: "policyreports",
}
clusterPolicyReportAlphaV1 = schema.GroupVersionResource{
Group: "wgpolicyk8s.io",
Version: "v1alpha1",
Resource: "clusterpolicyreports",
}
clusterPolicyReportAlphaV2 = schema.GroupVersionResource{
Group: "wgpolicyk8s.io",
Version: "v1alpha2",
Resource: "clusterpolicyreports",
}
)
type k8sPolicyReportClient struct {
debouncer Debouncer
client dynamic.Interface
found map[string]string
mapper Mapper
mx *sync.Mutex
restartWatchOnFailure time.Duration
} }
func (c *policyReportClient) RegisterCallback(cb report.PolicyReportCallback) { func (k *k8sPolicyReportClient) GetFoundResources() map[string]string {
c.callbacks = append(c.callbacks, cb) return k.found
} }
func (c *policyReportClient) RegisterPolicyResultCallback(cb report.PolicyResultCallback) { func (k *k8sPolicyReportClient) WatchPolicyReports(ctx context.Context) <-chan report.LifecycleEvent {
c.resultCallbacks = append(c.resultCallbacks, cb) pr := []schema.GroupVersionResource{
} policyReportAlphaV2,
policyReportAlphaV1,
func (c *policyReportClient) GetFoundResources() map[string]string {
return c.policyAPI.GetFoundResources()
}
func (c *policyReportClient) StartWatching(ctx context.Context) error {
if c.started {
return errors.New("StartWatching was already started")
} }
c.started = true cpor := []schema.GroupVersionResource{
clusterPolicyReportAlphaV2,
events, err := c.policyAPI.WatchPolicyReports(ctx) clusterPolicyReportAlphaV1,
if err != nil {
c.started = false
return err
} }
go func() { for _, versions := range [][]schema.GroupVersionResource{pr, cpor} {
for event := range events { go func(vs []schema.GroupVersionResource) {
c.debouncer.Add(event) for {
} factory := dynamicinformer.NewFilteredDynamicSharedInformerFactory(k.client, 30*time.Minute, corev1.NamespaceAll, nil)
for _, resource := range vs {
close(c.debouncer.channel) k.watchCRD(ctx, resource, factory)
}()
for event := range c.debouncer.ReportChan() {
c.executeReportHandler(event.Type, event.Report)
}
c.started = false
return errors.New("watching stopped")
}
func (c *policyReportClient) cacheResults(opr report.PolicyReport) {
for id := range opr.Results {
c.resultCache.SetDefault(id, true)
}
}
func (c *policyReportClient) executeReportHandler(e watch.EventType, pr report.PolicyReport) {
opr, ok := c.store.Get(pr.GetType(), pr.GetIdentifier())
if !ok {
opr = report.PolicyReport{}
}
if len(opr.Results) > 0 {
c.cacheResults(opr)
}
wg := sync.WaitGroup{}
wg.Add(len(c.callbacks))
for _, cb := range c.callbacks {
go func(
callback report.PolicyReportCallback,
event watch.EventType,
creport report.PolicyReport,
oreport report.PolicyReport,
) {
callback(event, creport, oreport)
wg.Done()
}(cb, e, pr, opr)
}
wg.Wait()
if e == watch.Deleted {
c.store.Remove(pr.GetType(), pr.GetIdentifier())
return
}
c.store.Add(pr)
}
func (c *policyReportClient) RegisterPolicyResultWatcher(skipExisting bool) {
c.skipExisting = skipExisting
c.RegisterCallback(
func(e watch.EventType, pr report.PolicyReport, or report.PolicyReport) {
switch e {
case watch.Added:
if len(pr.Results) == 0 {
break
} }
preExisted := pr.CreationTimestamp.Before(c.startUp) time.Sleep(2 * time.Second)
if c.skipExisting && preExisted {
break
}
diff := pr.GetNewResults(or)
wg := sync.WaitGroup{}
for _, r := range diff {
if _, found := c.resultCache.Get(r.GetIdentifier()); found {
continue
}
wg.Add(len(c.resultCallbacks))
for _, cb := range c.resultCallbacks {
go func(callback report.PolicyResultCallback, result report.Result) {
callback(result, preExisted)
wg.Done()
}(cb, r)
}
}
wg.Wait()
case watch.Modified:
if len(pr.Results) == 0 {
break
}
diff := pr.GetNewResults(or)
wg := sync.WaitGroup{}
for _, r := range diff {
if _, found := c.resultCache.Get(r.GetIdentifier()); found {
continue
}
wg.Add(len(c.resultCallbacks))
for _, cb := range c.resultCallbacks {
go func(callback report.PolicyResultCallback, result report.Result) {
callback(result, false)
wg.Done()
}(cb, r)
}
}
wg.Wait()
} }
})
}(versions)
}
for {
if len(k.found) == 2 {
break
}
}
return k.debouncer.ReportChan()
} }
// NewPolicyReportClient creates a new PolicyReportClient based on the kubernetes go-client func (k *k8sPolicyReportClient) watchCRD(ctx context.Context, r schema.GroupVersionResource, factory dynamicinformer.DynamicSharedInformerFactory) {
func NewPolicyReportClient( informer := factory.ForResource(r).Informer()
client PolicyReportAdapter,
store *report.PolicyReportStore, ctx, cancel := context.WithCancel(ctx)
startUp time.Time,
resultCache *cache.Cache, informer.SetWatchErrorHandler(func(c *cache.Reflector, err error) {
) report.PolicyResultClient { k.mx.Lock()
return &policyReportClient{ delete(k.found, r.String())
policyAPI: client, k.mx.Unlock()
store: store, cancel()
startUp: startUp,
resultCache: resultCache, log.Printf("[WARNING] Resource registration failed: %s\n", r.String())
debouncer: newDebouncer(), })
go k.handleCRDRegistration(ctx, informer, r)
informer.AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) {
if item, ok := obj.(*unstructured.Unstructured); ok {
preport := k.mapper.MapPolicyReport(item.Object)
k.debouncer.Add(report.LifecycleEvent{NewPolicyReport: preport, OldPolicyReport: &report.PolicyReport{}, Type: report.Added})
}
},
DeleteFunc: func(obj interface{}) {
if item, ok := obj.(*unstructured.Unstructured); ok {
preport := k.mapper.MapPolicyReport(item.Object)
k.debouncer.Add(report.LifecycleEvent{NewPolicyReport: preport, OldPolicyReport: &report.PolicyReport{}, Type: report.Deleted})
}
},
UpdateFunc: func(oldObj, newObj interface{}) {
if item, ok := newObj.(*unstructured.Unstructured); ok {
preport := k.mapper.MapPolicyReport(item.Object)
var oreport *report.PolicyReport
if oldItem, ok := oldObj.(*unstructured.Unstructured); ok {
oreport = k.mapper.MapPolicyReport(oldItem.Object)
}
k.debouncer.Add(report.LifecycleEvent{NewPolicyReport: preport, OldPolicyReport: oreport, Type: report.Updated})
}
},
})
informer.Run(ctx.Done())
}
func (k *k8sPolicyReportClient) handleCRDRegistration(ctx context.Context, informer cache.SharedIndexInformer, r schema.GroupVersionResource) {
ticker := time.NewTicker(k.restartWatchOnFailure)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
if informer.HasSynced() {
k.mx.Lock()
k.found[r.String()] = r.String()
k.mx.Unlock()
log.Printf("[INFO] Resource registered: %s\n", r.String())
return
}
}
}
}
// NewPolicyReportAdapter new Adapter for Policy Report Kubernetes API
func NewPolicyReportClient(dynamic dynamic.Interface, mapper Mapper, restartWatchOnFailure time.Duration) report.PolicyReportClient {
return &k8sPolicyReportClient{
client: dynamic,
mapper: mapper,
mx: &sync.Mutex{},
found: make(map[string]string),
debouncer: NewDebouncer(time.Minute),
restartWatchOnFailure: restartWatchOnFailure,
} }
} }

View file

@ -2,300 +2,59 @@ package kubernetes_test
import ( import (
"context" "context"
"errors"
"sync" "sync"
"testing" "testing"
"time" "time"
"github.com/kyverno/policy-reporter/pkg/kubernetes" "github.com/kyverno/policy-reporter/pkg/kubernetes"
"github.com/kyverno/policy-reporter/pkg/report"
"github.com/patrickmn/go-cache"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/watch"
) )
func Test_PolicyWatcher(t *testing.T) { func Test_PolicyWatcher(t *testing.T) {
ctx := context.Background() ctx := context.Background()
_, k8sCMClient := newFakeAPI() kclient, rclient := NewFakeCilent()
k8sCMClient.Create(ctx, configMap, metav1.CreateOptions{}) client := kubernetes.NewPolicyReportClient(kclient, NewMapper(), 100*time.Millisecond)
fakeAdapter := NewPolicyReportAdapter(NewMapper(k8sCMClient)) eventChan := client.WatchPolicyReports(ctx)
client := kubernetes.NewPolicyReportClient(fakeAdapter, report.NewPolicyReportStore(), time.Now(), cache.New(cache.DefaultExpiration, time.Minute*5))
client.RegisterPolicyResultWatcher(false) store := newStore(3)
wg := sync.WaitGroup{}
wg.Add(2)
results := make([]report.Result, 0, 3)
client.RegisterPolicyResultCallback(func(r report.Result, b bool) {
results = append(results, r)
wg.Done()
})
go client.StartWatching(ctx)
fakeAdapter.Watcher.Add(&unstructured.Unstructured{Object: policyMap})
wg.Wait()
if len(results) != 2 {
t.Error("Should receive 2 Results from the Policy")
}
}
func Test_PolicyWatcherTwice(t *testing.T) {
ctx := context.Background()
_, k8sCMClient := newFakeAPI()
k8sCMClient.Create(ctx, configMap, metav1.CreateOptions{})
fakeAdapter := NewPolicyReportAdapter(NewMapper(k8sCMClient))
client := kubernetes.NewPolicyReportClient(fakeAdapter, report.NewPolicyReportStore(), time.Now(), cache.New(cache.DefaultExpiration, time.Minute*5))
go client.StartWatching(ctx)
time.Sleep(10 * time.Millisecond)
err := client.StartWatching(ctx)
if err == nil {
t.Error("Second StartWatching call should return immediately with error")
}
}
var notSkippedPolicyMap = map[string]interface{}{
"metadata": map[string]interface{}{
"name": "policy-report",
"namespace": "test",
"creationTimestamp": time.Now().Add(10 * time.Minute).Format("2006-01-02T15:04:05Z"),
},
"summary": map[string]interface{}{
"pass": int64(1),
"skip": int64(2),
"warn": int64(3),
"fail": int64(4),
"error": int64(5),
},
"results": []interface{}{
map[string]interface{}{
"message": "message",
"status": "fail",
"scored": true,
"policy": "not-skiped-policy-result",
"rule": "app-label-required",
"category": "test",
"severity": "low",
"resources": []interface{}{
map[string]interface{}{
"apiVersion": "v1",
"kind": "Deployment",
"name": "nginx",
"namespace": "test",
"uid": "dfd57c50-f30c-4729-b63f-b1954d8988d1",
},
},
},
},
}
func Test_PolicySkipExisting(t *testing.T) {
ctx := context.Background()
_, k8sCMClient := newFakeAPI()
k8sCMClient.Create(ctx, configMap, metav1.CreateOptions{})
fakeAdapter := NewPolicyReportAdapter(NewMapper(k8sCMClient))
client := kubernetes.NewPolicyReportClient(fakeAdapter, report.NewPolicyReportStore(), time.Now(), cache.New(cache.DefaultExpiration, time.Minute*5))
client.RegisterPolicyResultWatcher(true)
wg := sync.WaitGroup{}
wg.Add(1)
results := make([]report.Result, 0, 1)
client.RegisterPolicyResultCallback(func(r report.Result, b bool) {
results = append(results, r)
wg.Done()
})
go client.StartWatching(ctx)
fakeAdapter.Watcher.Add(&unstructured.Unstructured{Object: policyMap})
fakeAdapter.Watcher.Add(&unstructured.Unstructured{Object: notSkippedPolicyMap})
wg.Wait()
if len(results) != 1 {
t.Error("Should receive one not skipped Result form notSkippedPolicyMap")
}
if results[0].Policy != "not-skiped-policy-result" {
t.Error("Should be 'not-skiped-policy-result'")
}
}
func Test_PolicyWatcherError(t *testing.T) {
ctx := context.Background()
_, k8sCMClient := newFakeAPI()
k8sCMClient.Create(ctx, configMap, metav1.CreateOptions{})
fakeAdapter := NewPolicyReportAdapter(NewMapper(k8sCMClient))
fakeAdapter.Error = errors.New("")
client := kubernetes.NewPolicyReportClient(fakeAdapter, report.NewPolicyReportStore(), time.Now(), cache.New(cache.DefaultExpiration, time.Minute*5))
client.RegisterPolicyResultWatcher(false)
err := client.StartWatching(ctx)
if err == nil {
t.Error("Shoud stop execution when error is returned")
}
}
func Test_PolicyWatchDeleteEvent(t *testing.T) {
ctx := context.Background()
_, k8sCMClient := newFakeAPI()
k8sCMClient.Create(ctx, configMap, metav1.CreateOptions{})
fakeAdapter := NewPolicyReportAdapter(NewMapper(k8sCMClient))
client := kubernetes.NewPolicyReportClient(fakeAdapter, report.NewPolicyReportStore(), time.Now(), cache.New(cache.DefaultExpiration, time.Minute*5))
client.RegisterPolicyResultWatcher(false)
wg := sync.WaitGroup{}
wg.Add(2)
results := make([]report.Result, 0, 2)
client.RegisterPolicyResultCallback(func(r report.Result, b bool) {
results = append(results, r)
wg.Done()
})
go client.StartWatching(ctx)
fakeAdapter.Watcher.Add(&unstructured.Unstructured{Object: policyMap})
fakeAdapter.Watcher.Delete(&unstructured.Unstructured{Object: policyMap})
wg.Wait()
if len(results) != 2 {
t.Error("Should receive initial 2 and no result from deletion")
}
}
func Test_PolicyWatchModifiedEvent(t *testing.T) {
ctx := context.Background()
_, k8sCMClient := newFakeAPI()
k8sCMClient.Create(ctx, configMap, metav1.CreateOptions{})
fakeAdapter := NewPolicyReportAdapter(NewMapper(k8sCMClient))
client := kubernetes.NewPolicyReportClient(fakeAdapter, report.NewPolicyReportStore(), time.Now(), cache.New(cache.DefaultExpiration, time.Minute*5))
client.RegisterPolicyResultWatcher(false)
wg := sync.WaitGroup{} wg := sync.WaitGroup{}
wg.Add(3) wg.Add(3)
results := make([]report.Result, 0, 3) go func() {
client.RegisterPolicyResultCallback(func(r report.Result, b bool) { for event := range eventChan {
results = append(results, r) store.Add(event)
wg.Done() wg.Done()
}) }
}()
go client.StartWatching(ctx) rclient.Create(ctx, &unstructured.Unstructured{Object: policyMap}, metav1.CreateOptions{})
time.Sleep(10 * time.Millisecond)
fakeAdapter.Watcher.Add(&unstructured.Unstructured{Object: policyMap}) rclient.Update(ctx, &unstructured.Unstructured{Object: policyMap}, metav1.UpdateOptions{})
time.Sleep(10 * time.Millisecond)
var policyMap2 = map[string]interface{}{ rclient.Delete(ctx, policyMap["metadata"].(map[string]interface{})["name"].(string), metav1.DeleteOptions{})
"metadata": map[string]interface{}{
"name": "policy-report",
"namespace": "test",
"creationTimestamp": "2021-02-23T15:00:00Z",
},
"summary": map[string]interface{}{
"pass": int64(1),
"skip": int64(2),
"warn": int64(3),
"fail": int64(4),
"error": int64(5),
},
"results": []interface{}{
map[string]interface{}{
"message": "message",
"status": "fail",
"scored": true,
"policy": "required-label",
"rule": "app-label-required",
"category": "test",
"severity": "medium",
"resources": []interface{}{
map[string]interface{}{
"apiVersion": "v1",
"kind": "Deployment",
"name": "nginx",
"namespace": "test",
"uid": "dfd57c50-f30c-4729-b63f-b1954d8988d1",
},
},
},
map[string]interface{}{
"message": "message 2",
"status": "fail",
"scored": true,
"policy": "priority-test",
"resources": []interface{}{},
},
map[string]interface{}{
"message": "message 3",
"status": "pass",
"scored": true,
"policy": "priority-test",
"resources": []interface{}{},
},
},
}
fakeAdapter.Watcher.Modify(&unstructured.Unstructured{Object: policyMap2})
wg.Wait() wg.Wait()
if len(results) != 3 { if len(store.List()) != 3 {
t.Error("Should receive initial 2 and 1 modification") t.Error("Should receive the Added, Updated and Deleted Event")
} }
} }
func Test_PolicyDelayReset(t *testing.T) { func Test_GetFoundResources(t *testing.T) {
ctx := context.Background() ctx := context.Background()
_, k8sCMClient := newFakeAPI() kclient, _ := NewFakeCilent()
k8sCMClient.Create(ctx, configMap, metav1.CreateOptions{}) client := kubernetes.NewPolicyReportClient(kclient, NewMapper(), 100*time.Millisecond)
fakeAdapter := NewPolicyReportAdapter(NewMapper(k8sCMClient)) client.WatchPolicyReports(ctx)
client := kubernetes.NewPolicyReportClient(fakeAdapter, report.NewPolicyReportStore(), time.Now(), cache.New(cache.DefaultExpiration, time.Minute*5))
client.RegisterPolicyResultWatcher(false) time.Sleep(1 * time.Second)
wg := sync.WaitGroup{} if len(client.GetFoundResources()) != 2 {
wg.Add(2) t.Errorf("Should find PolicyReport and ClusterPolicyReport Resource")
}
client.RegisterCallback(func(e watch.EventType, r report.PolicyReport, o report.PolicyReport) {
wg.Done()
})
go client.StartWatching(ctx)
fakeAdapter.Watcher.Add(&unstructured.Unstructured{Object: policyMap})
fakeAdapter.Watcher.Modify(&unstructured.Unstructured{Object: minPolicyMap})
fakeAdapter.Watcher.Modify(&unstructured.Unstructured{Object: policyMap})
fakeAdapter.Watcher.Delete(&unstructured.Unstructured{Object: policyMap})
wg.Wait()
} }

View file

@ -1,124 +0,0 @@
package kubernetes
import (
"context"
"log"
"sync"
"github.com/kyverno/policy-reporter/pkg/report"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/dynamic"
)
var (
policyReportAlphaV1 = schema.GroupVersionResource{
Group: "wgpolicyk8s.io",
Version: "v1alpha1",
Resource: "policyreports",
}
policyReportAlphaV2 = schema.GroupVersionResource{
Group: "wgpolicyk8s.io",
Version: "v1alpha2",
Resource: "policyreports",
}
clusterPolicyReportAlphaV1 = schema.GroupVersionResource{
Group: "wgpolicyk8s.io",
Version: "v1alpha1",
Resource: "clusterpolicyreports",
}
clusterPolicyReportAlphaV2 = schema.GroupVersionResource{
Group: "wgpolicyk8s.io",
Version: "v1alpha2",
Resource: "clusterpolicyreports",
}
)
// WatchEvent of PolicyReports
type WatchEvent struct {
Report report.PolicyReport
Type watch.EventType
}
// PolicyReportAdapter translates API responses to an internal struct
type PolicyReportAdapter interface {
WatchPolicyReports(ctx context.Context) (chan WatchEvent, error)
GetFoundResources() map[string]string
}
type k8sPolicyReportAdapter struct {
client dynamic.Interface
found map[string]string
mapper Mapper
mx *sync.Mutex
}
func (k *k8sPolicyReportAdapter) GetFoundResources() map[string]string {
return k.found
}
func (k *k8sPolicyReportAdapter) WatchPolicyReports(ctx context.Context) (chan WatchEvent, error) {
events := make(chan WatchEvent)
pr := []schema.GroupVersionResource{
policyReportAlphaV2,
policyReportAlphaV1,
}
cpor := []schema.GroupVersionResource{
clusterPolicyReportAlphaV2,
clusterPolicyReportAlphaV1,
}
for _, versions := range [][]schema.GroupVersionResource{pr, cpor} {
go func(vs []schema.GroupVersionResource) {
for {
for _, resource := range vs {
k.WatchCRD(ctx, resource, events)
}
}
}(versions)
}
return events, nil
}
func (k *k8sPolicyReportAdapter) WatchCRD(ctx context.Context, r schema.GroupVersionResource, events chan WatchEvent) {
for {
w, err := k.client.Resource(r).Watch(ctx, metav1.ListOptions{})
if err != nil {
k.mx.Lock()
delete(k.found, r.String())
k.mx.Unlock()
return
}
log.Printf("[INFO] Resource registered: %s\n", r.String())
k.mx.Lock()
k.found[r.String()] = r.String()
k.mx.Unlock()
for result := range w.ResultChan() {
if item, ok := result.Object.(*unstructured.Unstructured); ok {
preport := k.mapper.MapPolicyReport(item.Object)
events <- WatchEvent{preport, result.Type}
}
}
}
}
// NewPolicyReportAdapter new Adapter for Policy Report Kubernetes API
func NewPolicyReportAdapter(dynamic dynamic.Interface, mapper Mapper) PolicyReportAdapter {
return &k8sPolicyReportAdapter{
client: dynamic,
mapper: mapper,
mx: &sync.Mutex{},
found: make(map[string]string),
}
}

View file

@ -1,29 +0,0 @@
package kubernetes_test
import (
"context"
"testing"
"github.com/kyverno/policy-reporter/pkg/kubernetes"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/dynamic/fake"
)
func NewFakeClient(items ...runtime.Object) *fake.FakeDynamicClient {
return fake.NewSimpleDynamicClient(runtime.NewScheme(), items...)
}
func Test_WatchPolicyReports(t *testing.T) {
ctx := context.Background()
dynamic := NewFakeClient()
_, k8sCMClient := newFakeAPI()
k8sCMClient.Create(ctx, configMap, metav1.CreateOptions{})
client := kubernetes.NewPolicyReportAdapter(dynamic, NewMapper(k8sCMClient))
_, err := client.WatchPolicyReports(ctx)
if err != nil {
t.Error("Unexpected WatchError")
}
}

View file

@ -1,323 +0,0 @@
package kubernetes_test
import (
"context"
"sync"
"testing"
"time"
"github.com/kyverno/policy-reporter/pkg/kubernetes"
"github.com/kyverno/policy-reporter/pkg/report"
"github.com/patrickmn/go-cache"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/watch"
v1 "k8s.io/client-go/kubernetes/typed/core/v1"
)
type fakeClient struct {
List []report.PolicyReport
Watcher *watch.FakeWatcher
Error error
mapper kubernetes.Mapper
}
func (f *fakeClient) GetFoundResources() map[string]string {
return make(map[string]string)
}
func (f *fakeClient) ListPolicyReports() ([]report.PolicyReport, error) {
return f.List, f.Error
}
func (f *fakeClient) ListClusterPolicyReports() ([]report.PolicyReport, error) {
return f.List, f.Error
}
func (f *fakeClient) WatchPolicyReports(_ context.Context) (chan kubernetes.WatchEvent, error) {
channel := make(chan kubernetes.WatchEvent)
go func() {
for result := range f.Watcher.ResultChan() {
if item, ok := result.Object.(*unstructured.Unstructured); ok {
channel <- kubernetes.WatchEvent{
Report: f.mapper.MapPolicyReport(item.Object),
Type: result.Type,
}
}
}
}()
return channel, f.Error
}
func NewPolicyReportAdapter(mapper kubernetes.Mapper) *fakeClient {
return &fakeClient{
List: make([]report.PolicyReport, 0),
Watcher: watch.NewFake(),
mapper: mapper,
}
}
func NewMapper(k8sCMClient v1.ConfigMapInterface) kubernetes.Mapper {
return kubernetes.NewMapper(make(map[string]string), kubernetes.NewConfigMapAdapter(k8sCMClient))
}
func Test_ResultClient_RegisterPolicyResultWatcher(t *testing.T) {
ctx := context.Background()
_, k8sCMClient := newFakeAPI()
k8sCMClient.Create(ctx, configMap, metav1.CreateOptions{})
fakeAdapter := NewPolicyReportAdapter(NewMapper(k8sCMClient))
client := kubernetes.NewPolicyReportClient(fakeAdapter, report.NewPolicyReportStore(), time.Now(), cache.New(cache.DefaultExpiration, time.Minute*5))
client.RegisterPolicyResultWatcher(false)
wg := sync.WaitGroup{}
wg.Add(3)
results := make([]report.Result, 0, 3)
client.RegisterPolicyResultCallback(func(r report.Result, b bool) {
results = append(results, r)
wg.Done()
})
go client.StartWatching(ctx)
fakeAdapter.Watcher.Add(&unstructured.Unstructured{Object: clusterPolicyMap})
fakeAdapter.Watcher.Add(&unstructured.Unstructured{Object: policyMap})
wg.Wait()
if len(results) != 3 {
t.Error("Should receive 3 Result from all PolicyReports")
}
}
func Test_ResultClient_SkipCachedResults(t *testing.T) {
ctx := context.Background()
_, k8sCMClient := newFakeAPI()
k8sCMClient.Create(ctx, configMap, metav1.CreateOptions{})
fakeAdapter := NewPolicyReportAdapter(NewMapper(k8sCMClient))
client := kubernetes.NewPolicyReportClient(fakeAdapter, report.NewPolicyReportStore(), time.Now(), cache.New(cache.DefaultExpiration, time.Minute*5))
client.RegisterPolicyResultWatcher(false)
wg := sync.WaitGroup{}
wg.Add(3)
results := make([]report.Result, 0, 3)
client.RegisterPolicyResultCallback(func(r report.Result, b bool) {
results = append(results, r)
wg.Done()
})
go client.StartWatching(ctx)
var policyMap1 = map[string]interface{}{
"metadata": map[string]interface{}{
"name": "policy-report",
"namespace": "test",
"creationTimestamp": "2021-02-23T15:00:00Z",
},
"summary": map[string]interface{}{
"pass": int64(1),
"skip": int64(2),
"warn": int64(3),
"fail": int64(4),
"error": int64(5),
},
"results": []interface{}{
map[string]interface{}{
"message": "message",
"status": "fail",
"scored": true,
"policy": "required-label",
"rule": "app-label-required",
"timestamp": map[string]interface{}{
"seconds": 1614093000,
},
"category": "test",
"severity": "high",
"resources": []interface{}{
map[string]interface{}{
"apiVersion": "v1",
"kind": "Deployment",
"name": "nginx",
"namespace": "test",
"uid": "dfd57c50-f30c-4729-b63f-b1954d8988d1",
},
},
"properties": map[string]interface{}{
"version": "1.2.0",
},
},
map[string]interface{}{
"message": "message 2",
"status": "fail",
"scored": true,
"timestamp": map[string]interface{}{
"seconds": int64(1614093000),
},
"policy": "priority-test",
"resources": []interface{}{},
},
},
}
var policyMap2 = map[string]interface{}{
"metadata": map[string]interface{}{
"name": "policy-report",
"namespace": "test",
"creationTimestamp": "2021-02-23T15:00:00Z",
},
"summary": map[string]interface{}{
"pass": int64(1),
"skip": int64(2),
"warn": int64(3),
"fail": int64(4),
"error": int64(5),
},
"results": []interface{}{},
}
var clusterPolicyMap2 = map[string]interface{}{
"metadata": map[string]interface{}{
"name": "clusterpolicy-report",
"creationTimestamp": "2021-02-23T15:00:00Z",
},
"summary": map[string]interface{}{
"pass": int64(0),
"skip": int64(0),
"warn": int64(0),
"fail": int64(0),
"error": int64(0),
},
"results": []interface{}{},
}
fakeAdapter.Watcher.Add(&unstructured.Unstructured{Object: clusterPolicyMap})
fakeAdapter.Watcher.Modify(&unstructured.Unstructured{Object: clusterPolicyMap2})
fakeAdapter.Watcher.Modify(&unstructured.Unstructured{Object: clusterPolicyMap})
fakeAdapter.Watcher.Add(&unstructured.Unstructured{Object: policyMap})
fakeAdapter.Watcher.Modify(&unstructured.Unstructured{Object: policyMap2})
fakeAdapter.Watcher.Modify(&unstructured.Unstructured{Object: policyMap1})
fakeAdapter.Watcher.Modify(&unstructured.Unstructured{Object: policyMap})
wg.Wait()
if len(results) != 3 {
t.Error("Should receive 3 Result from none empty PolicyReport and ClusterPolicyReport Modify")
}
}
func Test_ResultClient_SkipReportsCleanUpEvents(t *testing.T) {
ctx := context.Background()
_, k8sCMClient := newFakeAPI()
k8sCMClient.Create(ctx, configMap, metav1.CreateOptions{})
fakeAdapter := NewPolicyReportAdapter(NewMapper(k8sCMClient))
client := kubernetes.NewPolicyReportClient(fakeAdapter, report.NewPolicyReportStore(), time.Now(), cache.New(cache.DefaultExpiration, time.Minute*5))
client.RegisterPolicyResultWatcher(false)
wg := sync.WaitGroup{}
wg.Add(3)
results := make([]report.Result, 0, 3)
client.RegisterPolicyResultCallback(func(r report.Result, b bool) {
results = append(results, r)
wg.Done()
})
go client.StartWatching(ctx)
var policyMap2 = map[string]interface{}{
"metadata": map[string]interface{}{
"name": "policy-report",
"namespace": "test",
"creationTimestamp": "2021-02-23T15:00:00Z",
},
"summary": map[string]interface{}{
"pass": int64(0),
"skip": int64(0),
"warn": int64(0),
"fail": int64(0),
"error": int64(0),
},
"results": []interface{}{},
}
var clusterPolicyMap2 = map[string]interface{}{
"metadata": map[string]interface{}{
"name": "clusterpolicy-report",
"creationTimestamp": "2021-02-23T15:00:00Z",
},
"summary": map[string]interface{}{
"pass": int64(0),
"skip": int64(0),
"warn": int64(0),
"fail": int64(0),
"error": int64(0),
},
"results": []interface{}{},
}
fakeAdapter.Watcher.Add(&unstructured.Unstructured{Object: clusterPolicyMap})
fakeAdapter.Watcher.Modify(&unstructured.Unstructured{Object: clusterPolicyMap2})
fakeAdapter.Watcher.Modify(&unstructured.Unstructured{Object: clusterPolicyMap})
fakeAdapter.Watcher.Add(&unstructured.Unstructured{Object: policyMap})
fakeAdapter.Watcher.Modify(&unstructured.Unstructured{Object: policyMap2})
fakeAdapter.Watcher.Modify(&unstructured.Unstructured{Object: policyMap})
wg.Wait()
if len(results) != 3 {
t.Error("Should receive 3 Results from the initial add events, not from the cleanup modify events")
}
}
func Test_ResultClient_SkipReportsReconnectEvents(t *testing.T) {
ctx := context.Background()
_, k8sCMClient := newFakeAPI()
k8sCMClient.Create(ctx, configMap, metav1.CreateOptions{})
fakeAdapter := NewPolicyReportAdapter(NewMapper(k8sCMClient))
client := kubernetes.NewPolicyReportClient(fakeAdapter, report.NewPolicyReportStore(), time.Now(), cache.New(cache.DefaultExpiration, time.Minute*5))
client.RegisterPolicyResultWatcher(false)
wg := sync.WaitGroup{}
wg.Add(3)
results := make([]report.Result, 0, 3)
client.RegisterPolicyResultCallback(func(r report.Result, b bool) {
results = append(results, r)
wg.Done()
})
go client.StartWatching(ctx)
fakeAdapter.Watcher.Add(&unstructured.Unstructured{Object: clusterPolicyMap})
fakeAdapter.Watcher.Add(&unstructured.Unstructured{Object: clusterPolicyMap})
fakeAdapter.Watcher.Add(&unstructured.Unstructured{Object: policyMap})
fakeAdapter.Watcher.Add(&unstructured.Unstructured{Object: policyMap})
wg.Wait()
if len(results) != 3 {
t.Error("Should receive 3 Results from the initial add events, not from the restart events")
}
}

View file

@ -0,0 +1,75 @@
package listener_test
import (
"time"
"github.com/kyverno/policy-reporter/pkg/report"
)
var result1 = &report.Result{
ID: "123",
Message: "validation error: requests and limits required. Rule autogen-check-for-requests-and-limits failed at path /spec/template/spec/containers/0/resources/requests/",
Policy: "require-requests-and-limits-required",
Rule: "autogen-check-for-requests-and-limits",
Priority: report.ErrorPriority,
Status: report.Fail,
Category: "Best Practices",
Severity: report.High,
Scored: true,
Source: "Kyverno",
Resource: &report.Resource{
APIVersion: "v1",
Kind: "Deployment",
Name: "nginx",
Namespace: "test",
UID: "536ab69f-1b3c-4bd9-9ba4-274a56188409",
},
}
var result2 = &report.Result{
ID: "124",
Message: "validation error: requests and limits required. Rule autogen-check-for-requests-and-limits failed at path /spec/template/spec/containers/0/resources/requests/",
Policy: "require-requests-and-limits-required",
Rule: "autogen-check-for-requests-and-limits",
Priority: report.WarningPriority,
Status: report.Pass,
Category: "Best Practices",
Scored: true,
Source: "Kyverno",
Resource: &report.Resource{
APIVersion: "v1",
Kind: "Pod",
Name: "nginx",
Namespace: "test",
UID: "536ab69f-1b3c-4bd9-9ba4-274a56188419",
},
}
var preport1 = &report.PolicyReport{
ID: report.GeneratePolicyReportID("polr-test", "test"),
Name: "polr-test",
Namespace: "test",
Results: map[string]*report.Result{
result1.GetIdentifier(): result1,
},
Summary: &report.Summary{Fail: 1},
CreationTimestamp: time.Now(),
}
var preport2 = &report.PolicyReport{
ID: report.GeneratePolicyReportID("polr-test", "test"),
Name: "polr-test",
Namespace: "test",
Results: map[string]*report.Result{
result1.GetIdentifier(): result1,
result2.GetIdentifier(): result2,
},
Summary: &report.Summary{Fail: 1, Pass: 1},
CreationTimestamp: time.Now(),
}
var creport = &report.PolicyReport{
Name: "cpolr-test",
Summary: &report.Summary{},
CreationTimestamp: time.Now(),
}

20
pkg/listener/metrics.go Normal file
View file

@ -0,0 +1,20 @@
package listener
import (
"github.com/kyverno/policy-reporter/pkg/listener/metrics"
"github.com/kyverno/policy-reporter/pkg/report"
)
// NewMetricsListener for PolicyReport watch.Events
func NewMetricsListener() report.PolicyReportListener {
pCallback := metrics.CreatePolicyReportMetricsListener()
cCallback := metrics.CreateClusterPolicyReportMetricsListener()
return func(event report.LifecycleEvent) {
if event.NewPolicyReport.Namespace == "" {
cCallback(event)
} else {
pCallback(event)
}
}
}

View file

@ -0,0 +1,97 @@
package metrics
import (
"github.com/kyverno/policy-reporter/pkg/report"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
var clusterPolicyGauge = promauto.NewGaugeVec(prometheus.GaugeOpts{
Name: "cluster_policy_report_summary",
Help: "Summary of all ClusterPolicyReports",
}, []string{"name", "status"})
var clusterRuleGauge = promauto.NewGaugeVec(prometheus.GaugeOpts{
Name: "cluster_policy_report_result",
Help: "List of all ClusterPolicyReport Results",
}, []string{"rule", "policy", "report", "kind", "name", "status", "severity", "category"})
func CreateClusterPolicyReportMetricsListener() report.PolicyReportListener {
prometheus.Register(clusterPolicyGauge)
prometheus.Register(clusterRuleGauge)
var newReport *report.PolicyReport
var oldReport *report.PolicyReport
return func(event report.LifecycleEvent) {
newReport = event.NewPolicyReport
oldReport = event.OldPolicyReport
switch event.Type {
case report.Added:
updateClusterPolicyGauge(newReport)
for _, result := range newReport.Results {
clusterRuleGauge.With(generateClusterResultLabels(newReport, result)).Set(1)
}
case report.Updated:
updateClusterPolicyGauge(newReport)
for _, result := range oldReport.Results {
clusterRuleGauge.Delete(generateClusterResultLabels(oldReport, result))
}
for _, result := range newReport.Results {
clusterRuleGauge.With(generateClusterResultLabels(newReport, result)).Set(1)
}
case report.Deleted:
clusterPolicyGauge.DeleteLabelValues(newReport.Name, "Pass")
clusterPolicyGauge.DeleteLabelValues(newReport.Name, "Fail")
clusterPolicyGauge.DeleteLabelValues(newReport.Name, "Warn")
clusterPolicyGauge.DeleteLabelValues(newReport.Name, "Error")
clusterPolicyGauge.DeleteLabelValues(newReport.Name, "Skip")
for _, result := range newReport.Results {
clusterRuleGauge.Delete(generateClusterResultLabels(newReport, result))
}
}
}
}
func generateClusterResultLabels(newReport *report.PolicyReport, result *report.Result) prometheus.Labels {
labels := prometheus.Labels{
"rule": result.Rule,
"policy": result.Policy,
"report": newReport.Name,
"kind": "",
"name": "",
"status": result.Status,
"severity": result.Severity,
"category": result.Category,
}
if result.HasResource() {
labels["kind"] = result.Resource.Kind
labels["name"] = result.Resource.Name
}
return labels
}
func updateClusterPolicyGauge(newReport *report.PolicyReport) {
clusterPolicyGauge.
WithLabelValues(newReport.Name, "Pass").
Set(float64(newReport.Summary.Pass))
clusterPolicyGauge.
WithLabelValues(newReport.Name, "Fail").
Set(float64(newReport.Summary.Fail))
clusterPolicyGauge.
WithLabelValues(newReport.Name, "Warn").
Set(float64(newReport.Summary.Warn))
clusterPolicyGauge.
WithLabelValues(newReport.Name, "Error").
Set(float64(newReport.Summary.Error))
clusterPolicyGauge.
WithLabelValues(newReport.Name, "Skip").
Set(float64(newReport.Summary.Skip))
}

View file

@ -5,38 +5,43 @@ import (
"testing" "testing"
"time" "time"
"github.com/kyverno/policy-reporter/pkg/metrics" "github.com/kyverno/policy-reporter/pkg/listener/metrics"
"github.com/kyverno/policy-reporter/pkg/report" "github.com/kyverno/policy-reporter/pkg/report"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
ioprometheusclient "github.com/prometheus/client_model/go" ioprometheusclient "github.com/prometheus/client_model/go"
"k8s.io/apimachinery/pkg/watch"
) )
var creport = report.PolicyReport{ var creport = &report.PolicyReport{
Name: "cpolr-test", Name: "cpolr-test",
Results: make(map[string]report.Result, 0), Results: make(map[string]*report.Result),
Summary: report.Summary{}, Summary: &report.Summary{},
CreationTimestamp: time.Now(), CreationTimestamp: time.Now(),
} }
func Test_ClusterPolicyReportMetricGeneration(t *testing.T) { func Test_ClusterPolicyReportMetricGeneration(t *testing.T) {
report1 := creport report1 := &report.PolicyReport{
report1.Summary = report.Summary{Pass: 1, Fail: 1} Name: "cpolr-test",
report1.Results = map[string]report.Result{ Summary: &report.Summary{Pass: 1, Fail: 1},
result1.GetIdentifier(): result1, CreationTimestamp: time.Now(),
result2.GetIdentifier(): result2, Results: map[string]*report.Result{
result1.GetIdentifier(): result1,
result2.GetIdentifier(): result2,
},
} }
report2 := creport report2 := &report.PolicyReport{
report2.Summary = report.Summary{Pass: 0, Fail: 1} Name: "cpolr-test",
report2.Results = map[string]report.Result{ Summary: &report.Summary{Pass: 0, Fail: 1},
result1.GetIdentifier(): result1, CreationTimestamp: time.Now(),
Results: map[string]*report.Result{
result1.GetIdentifier(): result1,
},
} }
handler := metrics.CreateMetricsCallback() handler := metrics.CreateClusterPolicyReportMetricsListener()
t.Run("Added Metric", func(t *testing.T) { t.Run("Added Metric", func(t *testing.T) {
handler(watch.Added, report1, report.PolicyReport{}) handler(report.LifecycleEvent{Type: report.Added, NewPolicyReport: report1, OldPolicyReport: &report.PolicyReport{}})
metricFam, err := prometheus.DefaultGatherer.Gather() metricFam, err := prometheus.DefaultGatherer.Gather()
if err != nil { if err != nil {
@ -81,8 +86,8 @@ func Test_ClusterPolicyReportMetricGeneration(t *testing.T) {
}) })
t.Run("Modified Metric", func(t *testing.T) { t.Run("Modified Metric", func(t *testing.T) {
handler(watch.Added, report1, report.PolicyReport{}) handler(report.LifecycleEvent{Type: report.Added, NewPolicyReport: report1, OldPolicyReport: &report.PolicyReport{}})
handler(watch.Modified, report2, report1) handler(report.LifecycleEvent{Type: report.Updated, NewPolicyReport: report2, OldPolicyReport: report1})
metricFam, err := prometheus.DefaultGatherer.Gather() metricFam, err := prometheus.DefaultGatherer.Gather()
if err != nil { if err != nil {
@ -127,9 +132,9 @@ func Test_ClusterPolicyReportMetricGeneration(t *testing.T) {
}) })
t.Run("Deleted Metric", func(t *testing.T) { t.Run("Deleted Metric", func(t *testing.T) {
handler(watch.Added, report1, report.PolicyReport{}) handler(report.LifecycleEvent{Type: report.Added, NewPolicyReport: report1, OldPolicyReport: &report.PolicyReport{}})
handler(watch.Modified, report2, report1) handler(report.LifecycleEvent{Type: report.Updated, NewPolicyReport: report2, OldPolicyReport: report1})
handler(watch.Deleted, report2, report2) handler(report.LifecycleEvent{Type: report.Deleted, NewPolicyReport: report2, OldPolicyReport: &report.PolicyReport{}})
metricFam, err := prometheus.DefaultGatherer.Gather() metricFam, err := prometheus.DefaultGatherer.Gather()
if err != nil { if err != nil {
@ -151,7 +156,7 @@ func Test_ClusterPolicyReportMetricGeneration(t *testing.T) {
func testClusterSummaryMetricLabels( func testClusterSummaryMetricLabels(
metric *ioprometheusclient.Metric, metric *ioprometheusclient.Metric,
preport report.PolicyReport, preport *report.PolicyReport,
status string, status string,
gauge float64, gauge float64,
) error { ) error {
@ -176,7 +181,7 @@ func testClusterSummaryMetricLabels(
return nil return nil
} }
func testClusterResultMetricLabels(metric *ioprometheusclient.Metric, result report.Result) error { func testClusterResultMetricLabels(metric *ioprometheusclient.Metric, result *report.Result) error {
if name := *metric.Label[0].Name; name != "category" { if name := *metric.Label[0].Name; name != "category" {
return fmt.Errorf("unexpected Name Label: %s", name) return fmt.Errorf("unexpected Name Label: %s", name)
} }

View file

@ -0,0 +1,98 @@
package metrics
import (
"github.com/kyverno/policy-reporter/pkg/report"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
var policyGauge = promauto.NewGaugeVec(prometheus.GaugeOpts{
Name: "policy_report_summary",
Help: "Summary of all PolicyReports",
}, []string{"namespace", "name", "status"})
var ruleGauge = promauto.NewGaugeVec(prometheus.GaugeOpts{
Name: "policy_report_result",
Help: "List of all PolicyReport Results",
}, []string{"namespace", "rule", "policy", "report", "kind", "name", "status", "severity", "category"})
func CreatePolicyReportMetricsListener() report.PolicyReportListener {
prometheus.Register(policyGauge)
prometheus.Register(ruleGauge)
var newReport *report.PolicyReport
var oldReport *report.PolicyReport
return func(event report.LifecycleEvent) {
newReport = event.NewPolicyReport
oldReport = event.OldPolicyReport
switch event.Type {
case report.Added:
updatePolicyGauge(newReport)
for _, result := range newReport.Results {
ruleGauge.With(generateResultLabels(newReport, result)).Set(1)
}
case report.Updated:
updatePolicyGauge(newReport)
for _, result := range oldReport.Results {
ruleGauge.Delete(generateResultLabels(oldReport, result))
}
for _, result := range newReport.Results {
ruleGauge.With(generateResultLabels(newReport, result)).Set(1)
}
case report.Deleted:
policyGauge.DeleteLabelValues(newReport.Namespace, newReport.Name, "Pass")
policyGauge.DeleteLabelValues(newReport.Namespace, newReport.Name, "Fail")
policyGauge.DeleteLabelValues(newReport.Namespace, newReport.Name, "Warn")
policyGauge.DeleteLabelValues(newReport.Namespace, newReport.Name, "Error")
policyGauge.DeleteLabelValues(newReport.Namespace, newReport.Name, "Skip")
for _, result := range newReport.Results {
ruleGauge.Delete(generateResultLabels(newReport, result))
}
}
}
}
func generateResultLabels(report *report.PolicyReport, result *report.Result) prometheus.Labels {
labels := prometheus.Labels{
"namespace": report.Namespace,
"rule": result.Rule,
"policy": result.Policy,
"report": report.Name,
"kind": "",
"name": "",
"status": result.Status,
"severity": result.Severity,
"category": result.Category,
}
if result.HasResource() {
labels["kind"] = result.Resource.Kind
labels["name"] = result.Resource.Name
}
return labels
}
func updatePolicyGauge(newReport *report.PolicyReport) {
policyGauge.
WithLabelValues(newReport.Namespace, newReport.Name, "Pass").
Set(float64(newReport.Summary.Pass))
policyGauge.
WithLabelValues(newReport.Namespace, newReport.Name, "Fail").
Set(float64(newReport.Summary.Fail))
policyGauge.
WithLabelValues(newReport.Namespace, newReport.Name, "Warn").
Set(float64(newReport.Summary.Warn))
policyGauge.
WithLabelValues(newReport.Namespace, newReport.Name, "Error").
Set(float64(newReport.Summary.Error))
policyGauge.
WithLabelValues(newReport.Namespace, newReport.Name, "Skip").
Set(float64(newReport.Summary.Skip))
}

View file

@ -5,14 +5,14 @@ import (
"testing" "testing"
"time" "time"
"github.com/kyverno/policy-reporter/pkg/metrics" "github.com/kyverno/policy-reporter/pkg/listener/metrics"
"github.com/kyverno/policy-reporter/pkg/report" "github.com/kyverno/policy-reporter/pkg/report"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
ioprometheusclient "github.com/prometheus/client_model/go" ioprometheusclient "github.com/prometheus/client_model/go"
"k8s.io/apimachinery/pkg/watch"
) )
var result1 = report.Result{ var result1 = &report.Result{
ID: "1",
Message: "validation error: requests and limits required. Rule autogen-check-for-requests-and-limits failed at path /spec/template/spec/containers/0/resources/requests/", Message: "validation error: requests and limits required. Rule autogen-check-for-requests-and-limits failed at path /spec/template/spec/containers/0/resources/requests/",
Policy: "require-requests-and-limits-required", Policy: "require-requests-and-limits-required",
Rule: "autogen-check-for-requests-and-limits", Rule: "autogen-check-for-requests-and-limits",
@ -21,7 +21,7 @@ var result1 = report.Result{
Severity: report.High, Severity: report.High,
Category: "resources", Category: "resources",
Scored: true, Scored: true,
Resource: report.Resource{ Resource: &report.Resource{
APIVersion: "v1", APIVersion: "v1",
Kind: "Deployment", Kind: "Deployment",
Name: "nginx", Name: "nginx",
@ -30,7 +30,8 @@ var result1 = report.Result{
}, },
} }
var result2 = report.Result{ var result2 = &report.Result{
ID: "2",
Message: "validation error: requests and limits required. Rule autogen-check-for-requests-and-limits failed at path /spec/template/spec/containers/0/resources/requests/", Message: "validation error: requests and limits required. Rule autogen-check-for-requests-and-limits failed at path /spec/template/spec/containers/0/resources/requests/",
Policy: "check-requests-and-limits-required", Policy: "check-requests-and-limits-required",
Rule: "check-for-requests-and-limits", Rule: "check-for-requests-and-limits",
@ -38,7 +39,7 @@ var result2 = report.Result{
Status: report.Pass, Status: report.Pass,
Category: "resources", Category: "resources",
Scored: true, Scored: true,
Resource: report.Resource{ Resource: &report.Resource{
APIVersion: "v1", APIVersion: "v1",
Kind: "Deployment", Kind: "Deployment",
Name: "nginx", Name: "nginx",
@ -47,32 +48,43 @@ var result2 = report.Result{
}, },
} }
var preport = report.PolicyReport{ var preport = &report.PolicyReport{
ID: "1",
Name: "polr-test", Name: "polr-test",
Namespace: "test", Namespace: "test",
Results: make(map[string]report.Result, 0), Results: make(map[string]*report.Result),
Summary: report.Summary{}, Summary: &report.Summary{},
CreationTimestamp: time.Now(), CreationTimestamp: time.Now(),
} }
func Test_PolicyReportMetricGeneration(t *testing.T) { func Test_PolicyReportMetricGeneration(t *testing.T) {
report1 := preport report1 := &report.PolicyReport{
report1.Summary = report.Summary{Pass: 1, Fail: 1} ID: "1",
report1.Results = map[string]report.Result{ Name: "polr-test",
result1.GetIdentifier(): result1, Namespace: "test",
result2.GetIdentifier(): result2, Summary: &report.Summary{Pass: 1, Fail: 1},
CreationTimestamp: time.Now(),
Results: map[string]*report.Result{
result1.GetIdentifier(): result1,
result2.GetIdentifier(): result2,
},
} }
report2 := preport report2 := &report.PolicyReport{
report2.Summary = report.Summary{Pass: 0, Fail: 1} ID: "1",
report2.Results = map[string]report.Result{ Name: "polr-test",
result1.GetIdentifier(): result1, Namespace: "test",
Summary: &report.Summary{Pass: 0, Fail: 1},
CreationTimestamp: time.Now(),
Results: map[string]*report.Result{
result1.GetIdentifier(): result1,
},
} }
handler := metrics.CreateMetricsCallback() handler := metrics.CreatePolicyReportMetricsListener()
t.Run("Added Metric", func(t *testing.T) { t.Run("Added Metric", func(t *testing.T) {
handler(watch.Added, report1, report.PolicyReport{}) handler(report.LifecycleEvent{Type: report.Added, NewPolicyReport: report1, OldPolicyReport: &report.PolicyReport{}})
metricFam, err := prometheus.DefaultGatherer.Gather() metricFam, err := prometheus.DefaultGatherer.Gather()
if err != nil { if err != nil {
@ -117,8 +129,8 @@ func Test_PolicyReportMetricGeneration(t *testing.T) {
}) })
t.Run("Modified Metric", func(t *testing.T) { t.Run("Modified Metric", func(t *testing.T) {
handler(watch.Added, report1, report.PolicyReport{}) handler(report.LifecycleEvent{Type: report.Added, NewPolicyReport: report1, OldPolicyReport: &report.PolicyReport{}})
handler(watch.Modified, report2, report1) handler(report.LifecycleEvent{Type: report.Updated, NewPolicyReport: report2, OldPolicyReport: report1})
metricFam, err := prometheus.DefaultGatherer.Gather() metricFam, err := prometheus.DefaultGatherer.Gather()
if err != nil { if err != nil {
@ -163,9 +175,9 @@ func Test_PolicyReportMetricGeneration(t *testing.T) {
}) })
t.Run("Deleted Metric", func(t *testing.T) { t.Run("Deleted Metric", func(t *testing.T) {
handler(watch.Added, report1, report.PolicyReport{}) handler(report.LifecycleEvent{Type: report.Added, NewPolicyReport: report1, OldPolicyReport: &report.PolicyReport{}})
handler(watch.Modified, report2, report1) handler(report.LifecycleEvent{Type: report.Updated, NewPolicyReport: report2, OldPolicyReport: report1})
handler(watch.Deleted, report2, report2) handler(report.LifecycleEvent{Type: report.Deleted, NewPolicyReport: report2, OldPolicyReport: &report.PolicyReport{}})
metricFam, err := prometheus.DefaultGatherer.Gather() metricFam, err := prometheus.DefaultGatherer.Gather()
if err != nil { if err != nil {
@ -186,7 +198,7 @@ func Test_PolicyReportMetricGeneration(t *testing.T) {
func testSummaryMetricLabels( func testSummaryMetricLabels(
metric *ioprometheusclient.Metric, metric *ioprometheusclient.Metric,
preport report.PolicyReport, preport *report.PolicyReport,
status string, status string,
gauge float64, gauge float64,
) error { ) error {
@ -218,7 +230,7 @@ func testSummaryMetricLabels(
return nil return nil
} }
func testResultMetricLabels(metric *ioprometheusclient.Metric, result report.Result) error { func testResultMetricLabels(metric *ioprometheusclient.Metric, result *report.Result) error {
if name := *metric.Label[0].Name; name != "category" { if name := *metric.Label[0].Name; name != "category" {
return fmt.Errorf("unexpected Name Label: %s", name) return fmt.Errorf("unexpected Name Label: %s", name)
} }

View file

@ -0,0 +1,52 @@
package listener_test
import (
"testing"
"github.com/kyverno/policy-reporter/pkg/listener"
"github.com/kyverno/policy-reporter/pkg/report"
"github.com/prometheus/client_golang/prometheus"
ioprometheusclient "github.com/prometheus/client_model/go"
)
func Test_MetricsListener(t *testing.T) {
slistener := listener.NewMetricsListener()
t.Run("Add ClusterPolicyReport Metric", func(t *testing.T) {
slistener(report.LifecycleEvent{Type: report.Added, NewPolicyReport: creport, OldPolicyReport: &report.PolicyReport{}})
metricFam, err := prometheus.DefaultGatherer.Gather()
if err != nil {
t.Errorf("unexpected Error: %s", err)
}
summary := findMetric(metricFam, "cluster_policy_report_summary")
if summary == nil {
t.Fatalf("Metric not found: cluster_policy_report_summary")
}
})
t.Run("Add PolicyReport Metric", func(t *testing.T) {
slistener(report.LifecycleEvent{Type: report.Added, NewPolicyReport: preport1, OldPolicyReport: &report.PolicyReport{}})
metricFam, err := prometheus.DefaultGatherer.Gather()
if err != nil {
t.Errorf("unexpected Error: %s", err)
}
summary := findMetric(metricFam, "policy_report_summary")
if summary == nil {
t.Fatalf("Metric not found: policy_report_summary")
}
})
}
func findMetric(metrics []*ioprometheusclient.MetricFamily, name string) *ioprometheusclient.MetricFamily {
for _, metric := range metrics {
if *metric.Name == name {
return metric
}
}
return nil
}

View file

@ -0,0 +1,76 @@
package listener
import (
"sync"
"time"
"github.com/kyverno/policy-reporter/pkg/report"
"github.com/patrickmn/go-cache"
)
type ResultListener struct {
skipExisting bool
listener []report.PolicyReportResultListener
cache *cache.Cache
startUp time.Time
}
func (l *ResultListener) RegisterListener(listener report.PolicyReportResultListener) {
l.listener = append(l.listener, listener)
}
func (l *ResultListener) Listen(event report.LifecycleEvent) {
if len(event.OldPolicyReport.Results) > 0 {
for id := range event.OldPolicyReport.Results {
l.cache.SetDefault(id, true)
}
}
if event.Type != report.Added && event.Type != report.Updated {
return
}
var preExisted bool
if event.Type == report.Added {
preExisted = event.NewPolicyReport.CreationTimestamp.Before(l.startUp)
if l.skipExisting && preExisted {
return
}
}
if len(event.NewPolicyReport.Results) == 0 {
return
}
diff := event.NewPolicyReport.GetNewResults(event.OldPolicyReport)
wg := sync.WaitGroup{}
for _, r := range diff {
if _, found := l.cache.Get(r.GetIdentifier()); found {
continue
}
wg.Add(len(l.listener))
for _, cb := range l.listener {
go func(callback report.PolicyReportResultListener, result *report.Result) {
callback(result, preExisted)
wg.Done()
}(cb, r)
}
}
wg.Wait()
}
func NewResultListener(skipExisting bool, rcache *cache.Cache, startUp time.Time) *ResultListener {
return &ResultListener{
skipExisting: skipExisting,
cache: rcache,
startUp: startUp,
listener: make([]report.PolicyReportResultListener, 0),
}
}

View file

@ -0,0 +1,93 @@
package listener_test
import (
"testing"
"time"
"github.com/kyverno/policy-reporter/pkg/listener"
"github.com/kyverno/policy-reporter/pkg/report"
"github.com/patrickmn/go-cache"
)
func Test_ResultListener(t *testing.T) {
t.Run("Publish Result", func(t *testing.T) {
var called *report.Result
slistener := listener.NewResultListener(true, cache.New(cache.DefaultExpiration, 5*time.Minute), time.Now())
slistener.RegisterListener(func(r *report.Result, b bool) {
called = r
})
slistener.Listen(report.LifecycleEvent{Type: report.Updated, NewPolicyReport: preport2, OldPolicyReport: preport1})
if called.GetIdentifier() != result2.GetIdentifier() {
t.Error("Expected Listener to be called with Result2")
}
})
t.Run("Ignore Delete Event", func(t *testing.T) {
var called bool
slistener := listener.NewResultListener(true, cache.New(cache.DefaultExpiration, 5*time.Minute), time.Now())
slistener.RegisterListener(func(r *report.Result, b bool) {
called = true
})
slistener.Listen(report.LifecycleEvent{Type: report.Deleted, NewPolicyReport: preport2, OldPolicyReport: preport1})
if called {
t.Error("Expected Listener not be called on Deleted event")
}
})
t.Run("Ignore Added Results created before startup", func(t *testing.T) {
var called bool
slistener := listener.NewResultListener(true, cache.New(cache.DefaultExpiration, 5*time.Minute), time.Now())
slistener.RegisterListener(func(r *report.Result, b bool) {
called = true
})
slistener.Listen(report.LifecycleEvent{Type: report.Added, NewPolicyReport: preport2, OldPolicyReport: preport1})
if called {
t.Error("Expected Listener not be called on Deleted event")
}
})
t.Run("Ignore CacheResults", func(t *testing.T) {
var called bool
rcache := cache.New(cache.DefaultExpiration, 5*time.Minute)
rcache.SetDefault(result2.ID, true)
slistener := listener.NewResultListener(true, rcache, time.Now())
slistener.RegisterListener(func(r *report.Result, b bool) {
called = true
})
slistener.Listen(report.LifecycleEvent{Type: report.Updated, NewPolicyReport: preport2, OldPolicyReport: preport1})
if called {
t.Error("Expected Listener not be called on cached results")
}
})
t.Run("Early Return if Rsults are empty", func(t *testing.T) {
var called bool
rcache := cache.New(cache.DefaultExpiration, 5*time.Minute)
rcache.SetDefault(result2.ID, true)
slistener := listener.NewResultListener(true, rcache, time.Now())
slistener.RegisterListener(func(r *report.Result, b bool) {
called = true
})
slistener.Listen(report.LifecycleEvent{Type: report.Updated, NewPolicyReport: preport2, OldPolicyReport: preport1})
if called {
t.Error("Expected Listener not be called with empty results")
}
})
}

View file

@ -0,0 +1,29 @@
package listener
import (
"sync"
"github.com/kyverno/policy-reporter/pkg/report"
"github.com/kyverno/policy-reporter/pkg/target"
)
func NewSendResultListener(clients []target.Client) report.PolicyReportResultListener {
return func(r *report.Result, e bool) {
wg := &sync.WaitGroup{}
wg.Add(len(clients))
for _, t := range clients {
go func(target target.Client, result *report.Result, preExisted bool) {
defer wg.Done()
if (preExisted && target.SkipExistingOnStartup()) || !target.Validate(result) {
return
}
target.Send(result)
}(t, r, e)
}
wg.Wait()
}
}

View file

@ -0,0 +1,69 @@
package listener_test
import (
"testing"
"github.com/kyverno/policy-reporter/pkg/listener"
"github.com/kyverno/policy-reporter/pkg/report"
"github.com/kyverno/policy-reporter/pkg/target"
)
type client struct {
Called bool
skipExistingOnStartup bool
validated bool
}
func (c *client) Send(result *report.Result) {
c.Called = true
}
func (c *client) MinimumPriority() string {
return report.InfoPriority.String()
}
func (c *client) Name() string {
return "test"
}
func (c *client) Sources() []string {
return []string{}
}
func (c *client) SkipExistingOnStartup() bool {
return c.skipExistingOnStartup
}
func (c client) Validate(result *report.Result) bool {
return c.validated
}
func Test_SendResultListener(t *testing.T) {
t.Run("Send Result", func(t *testing.T) {
c := &client{validated: true}
slistener := listener.NewSendResultListener([]target.Client{c})
slistener(result1, false)
if !c.Called {
t.Error("Expected Send to be called")
}
})
t.Run("Don't Send Result when validation fails", func(t *testing.T) {
c := &client{validated: false}
slistener := listener.NewSendResultListener([]target.Client{c})
slistener(result1, false)
if c.Called {
t.Error("Expected Send not to be called")
}
})
t.Run("Don't Send pre existing Result when skipExistingOnStartup is true", func(t *testing.T) {
c := &client{skipExistingOnStartup: true}
slistener := listener.NewSendResultListener([]target.Client{c})
slistener(result1, true)
if c.Called {
t.Error("Expected Send not to be called")
}
})
}

29
pkg/listener/store.go Normal file
View file

@ -0,0 +1,29 @@
package listener
import (
"log"
"github.com/kyverno/policy-reporter/pkg/report"
)
func NewStoreListener(store report.PolicyReportStore) report.PolicyReportListener {
return func(event report.LifecycleEvent) {
if event.Type == report.Deleted {
logOnError("remove", event.NewPolicyReport.Name, store.Remove(event.NewPolicyReport.GetIdentifier()))
return
}
if event.Type == report.Updated {
logOnError("update", event.NewPolicyReport.Name, store.Update(event.NewPolicyReport))
return
}
logOnError("add", event.NewPolicyReport.Name, store.Add(event.NewPolicyReport))
}
}
func logOnError(operation, name string, err error) {
if err != nil {
log.Printf("[ERROR] Failed to %s Policy Report %s (%s)\n", operation, name, err.Error())
}
}

View file

@ -0,0 +1,37 @@
package listener_test
import (
"testing"
"github.com/kyverno/policy-reporter/pkg/listener"
"github.com/kyverno/policy-reporter/pkg/report"
)
func Test_StoreListener(t *testing.T) {
store := report.NewPolicyReportStore()
t.Run("Save New Report", func(t *testing.T) {
slistener := listener.NewStoreListener(store)
slistener(report.LifecycleEvent{Type: report.Added, NewPolicyReport: preport1, OldPolicyReport: &report.PolicyReport{}})
if _, ok := store.Get(preport1.ID); !ok {
t.Error("Expected Report to be stored")
}
})
t.Run("Update Modified Report", func(t *testing.T) {
slistener := listener.NewStoreListener(store)
slistener(report.LifecycleEvent{Type: report.Updated, NewPolicyReport: preport2, OldPolicyReport: preport1})
if preport, ok := store.Get(preport2.ID); !ok && len(preport.Results) == 2 {
t.Error("Expected Report to be updated")
}
})
t.Run("Remove Deleted Report", func(t *testing.T) {
slistener := listener.NewStoreListener(store)
slistener(report.LifecycleEvent{Type: report.Deleted, NewPolicyReport: preport2, OldPolicyReport: &report.PolicyReport{}})
if _, ok := store.Get(preport2.ID); ok {
t.Error("Expected Report to be removed")
}
})
}

View file

@ -1,92 +0,0 @@
package metrics
import (
"github.com/kyverno/policy-reporter/pkg/report"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"k8s.io/apimachinery/pkg/watch"
)
func createClusterPolicyReportMetricsCallback() report.PolicyReportCallback {
policyGauge := promauto.NewGaugeVec(prometheus.GaugeOpts{
Name: "cluster_policy_report_summary",
Help: "Summary of all ClusterPolicyReports",
}, []string{"name", "status"})
ruleGauge := promauto.NewGaugeVec(prometheus.GaugeOpts{
Name: "cluster_policy_report_result",
Help: "List of all ClusterPolicyReport Results",
}, []string{"rule", "policy", "report", "kind", "name", "status", "severity", "category"})
prometheus.Register(policyGauge)
prometheus.Register(ruleGauge)
return func(event watch.EventType, report report.PolicyReport, oldReport report.PolicyReport) {
switch event {
case watch.Added:
updateClusterPolicyGauge(policyGauge, report)
for _, rule := range report.Results {
ruleGauge.With(generateClusterResultLabels(report, rule)).Set(1)
}
case watch.Modified:
updateClusterPolicyGauge(policyGauge, report)
for _, rule := range oldReport.Results {
ruleGauge.Delete(generateClusterResultLabels(oldReport, rule))
}
for _, rule := range report.Results {
ruleGauge.With(generateClusterResultLabels(report, rule)).Set(1)
}
case watch.Deleted:
policyGauge.DeleteLabelValues(report.Name, "Pass")
policyGauge.DeleteLabelValues(report.Name, "Fail")
policyGauge.DeleteLabelValues(report.Name, "Warn")
policyGauge.DeleteLabelValues(report.Name, "Error")
policyGauge.DeleteLabelValues(report.Name, "Skip")
for _, rule := range report.Results {
ruleGauge.Delete(generateClusterResultLabels(report, rule))
}
}
}
}
func generateClusterResultLabels(report report.PolicyReport, result report.Result) prometheus.Labels {
labels := prometheus.Labels{
"rule": result.Rule,
"policy": result.Policy,
"report": report.Name,
"kind": "",
"name": "",
"status": result.Status,
"severity": result.Severity,
"category": result.Category,
}
if result.HasResource() {
labels["kind"] = result.Resource.Kind
labels["name"] = result.Resource.Name
}
return labels
}
func updateClusterPolicyGauge(policyGauge *prometheus.GaugeVec, report report.PolicyReport) {
policyGauge.
WithLabelValues(report.Name, "Pass").
Set(float64(report.Summary.Pass))
policyGauge.
WithLabelValues(report.Name, "Fail").
Set(float64(report.Summary.Fail))
policyGauge.
WithLabelValues(report.Name, "Warn").
Set(float64(report.Summary.Warn))
policyGauge.
WithLabelValues(report.Name, "Error").
Set(float64(report.Summary.Error))
policyGauge.
WithLabelValues(report.Name, "Skip").
Set(float64(report.Summary.Skip))
}

View file

@ -1,29 +0,0 @@
package metrics
import (
"github.com/kyverno/policy-reporter/pkg/report"
"k8s.io/apimachinery/pkg/watch"
)
var (
pCallback report.PolicyReportCallback
cCallback report.PolicyReportCallback
)
// CreateMetricsCallback for PolicyReport watch.Events
func CreateMetricsCallback() report.PolicyReportCallback {
if pCallback == nil {
pCallback = createPolicyReportMetricsCallback()
}
if cCallback == nil {
cCallback = createClusterPolicyReportMetricsCallback()
}
return func(et watch.EventType, pr, opr report.PolicyReport) {
if pr.Namespace == "" {
cCallback(et, pr, opr)
} else {
pCallback(et, pr, opr)
}
}
}

View file

@ -1,93 +0,0 @@
package metrics
import (
"github.com/kyverno/policy-reporter/pkg/report"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"k8s.io/apimachinery/pkg/watch"
)
func createPolicyReportMetricsCallback() report.PolicyReportCallback {
policyGauge := promauto.NewGaugeVec(prometheus.GaugeOpts{
Name: "policy_report_summary",
Help: "Summary of all PolicyReports",
}, []string{"namespace", "name", "status"})
ruleGauge := promauto.NewGaugeVec(prometheus.GaugeOpts{
Name: "policy_report_result",
Help: "List of all PolicyReport Results",
}, []string{"namespace", "rule", "policy", "report", "kind", "name", "status", "severity", "category"})
prometheus.Register(policyGauge)
prometheus.Register(ruleGauge)
return func(event watch.EventType, report report.PolicyReport, oldReport report.PolicyReport) {
switch event {
case watch.Added:
updatePolicyGauge(policyGauge, report)
for _, rule := range report.Results {
ruleGauge.With(generateResultLabels(report, rule)).Set(1)
}
case watch.Modified:
updatePolicyGauge(policyGauge, report)
for _, rule := range oldReport.Results {
ruleGauge.Delete(generateResultLabels(oldReport, rule))
}
for _, rule := range report.Results {
ruleGauge.With(generateResultLabels(report, rule)).Set(1)
}
case watch.Deleted:
policyGauge.DeleteLabelValues(report.Namespace, report.Name, "Pass")
policyGauge.DeleteLabelValues(report.Namespace, report.Name, "Fail")
policyGauge.DeleteLabelValues(report.Namespace, report.Name, "Warn")
policyGauge.DeleteLabelValues(report.Namespace, report.Name, "Error")
policyGauge.DeleteLabelValues(report.Namespace, report.Name, "Skip")
for _, rule := range report.Results {
ruleGauge.Delete(generateResultLabels(report, rule))
}
}
}
}
func generateResultLabels(report report.PolicyReport, result report.Result) prometheus.Labels {
labels := prometheus.Labels{
"namespace": report.Namespace,
"rule": result.Rule,
"policy": result.Policy,
"report": report.Name,
"kind": "",
"name": "",
"status": result.Status,
"severity": result.Severity,
"category": result.Category,
}
if result.HasResource() {
labels["kind"] = result.Resource.Kind
labels["name"] = result.Resource.Name
}
return labels
}
func updatePolicyGauge(policyGauge *prometheus.GaugeVec, report report.PolicyReport) {
policyGauge.
WithLabelValues(report.Namespace, report.Name, "Pass").
Set(float64(report.Summary.Pass))
policyGauge.
WithLabelValues(report.Namespace, report.Name, "Fail").
Set(float64(report.Summary.Fail))
policyGauge.
WithLabelValues(report.Namespace, report.Name, "Warn").
Set(float64(report.Summary.Warn))
policyGauge.
WithLabelValues(report.Namespace, report.Name, "Error").
Set(float64(report.Summary.Error))
policyGauge.
WithLabelValues(report.Namespace, report.Name, "Skip").
Set(float64(report.Summary.Skip))
}

View file

@ -2,25 +2,18 @@ package report
import ( import (
"context" "context"
"k8s.io/apimachinery/pkg/watch"
) )
// PolicyReportCallback is called whenever a new PolicyReport comes in // PolicyReportListener is called whenever a new PolicyReport comes in
type PolicyReportCallback = func(watch.EventType, PolicyReport, PolicyReport) type PolicyReportListener = func(LifecycleEvent)
// PolicyResultCallback is called whenever a new PolicyResult comes in // PolicyReportResultListener is called whenever a new PolicyResult comes in
type PolicyResultCallback = func(Result, bool) type PolicyReportResultListener = func(*Result, bool)
// PolicyResultClient watches for PolicyReport Events and executes registered callback // PolicyReportClient watches for PolicyReport Events and executes registered callback
type PolicyResultClient interface { type PolicyReportClient interface {
// RegisterCallback register Handlers called on each PolicyReport watch.Event // WatchPolicyReports starts to watch for PolicyReport LifecycleEvent events
RegisterCallback(PolicyReportCallback) WatchPolicyReports(ctx context.Context) <-chan LifecycleEvent
// RegisterPolicyResultCallback register Handlers called on each PolicyReport watch.Event for each changed PolicyResult
RegisterPolicyResultCallback(PolicyResultCallback)
// RegisterPolicyResultWatcher register a handler for ClusterPolicyReports and PolicyReports who call the registered PolicyResultCallbacks
RegisterPolicyResultWatcher(skipExisting bool)
// StartWatching calls the WatchAPI, waiting for incoming PolicyReport watch.Events and call the registered Handlers
StartWatching(ctx context.Context) error
// GetFoundResources as Map of Names // GetFoundResources as Map of Names
GetFoundResources() map[string]string GetFoundResources() map[string]string
} }

View file

@ -3,13 +3,27 @@ package report
import ( import (
"bytes" "bytes"
"crypto/sha1" "crypto/sha1"
"encoding/hex"
"fmt" "fmt"
"sort"
"strings"
"time" "time"
) )
// Event Enum
type Event = int
// Possible PolicyReport Event Enums
const (
Added Event = iota
Updated
Deleted
)
// LifecycleEvent of PolicyReports
type LifecycleEvent struct {
Type Event
NewPolicyReport *PolicyReport
OldPolicyReport *PolicyReport
}
// Status Enum defined for PolicyReport // Status Enum defined for PolicyReport
type Status = string type Status = string
@ -36,18 +50,18 @@ const (
criticalString = "critical" criticalString = "critical"
) )
// Type Enum defined for PolicyReport // ResourceType Enum defined for PolicyReport
type Type = string type ResourceType = string
// ReportType Enum // ReportType Enum
const ( const (
PolicyReportType Type = "PolicyReport" PolicyReportType ResourceType = "PolicyReport"
ClusterPolicyReportType Type = "ClusterPolicyReport" ClusterPolicyReportType ResourceType = "ClusterPolicyReport"
) )
// Internal Priority definitions and weighting // Internal Priority definitions and weighting
const ( const (
DefaultPriority = iota DefaultPriority Priority = iota
DebugPriority DebugPriority
InfoPriority InfoPriority
WarningPriority WarningPriority
@ -142,6 +156,7 @@ type Resource struct {
// Result from the PolicyReport spec wgpolicyk8s.io/v1alpha1.PolicyReportResult // Result from the PolicyReport spec wgpolicyk8s.io/v1alpha1.PolicyReportResult
type Result struct { type Result struct {
ID string `json:"-"`
Message string Message string
Policy string Policy string
Rule string Rule string
@ -149,25 +164,24 @@ type Result struct {
Status Status Status Status
Severity Severity `json:",omitempty"` Severity Severity `json:",omitempty"`
Category string `json:",omitempty"` Category string `json:",omitempty"`
Source string `json:"source,omitempty"` Source string `json:",omitempty"`
Scored bool Scored bool
Timestamp time.Time Timestamp time.Time
Resource Resource Resource *Resource
Properties map[string]string Properties map[string]string
} }
// GetIdentifier returns a global unique Result identifier // GetIdentifier returns a global unique Result identifier
func (r Result) GetIdentifier() string { func (r Result) GetIdentifier() string {
suffix := "" return r.ID
if r.Resource.UID != "" {
suffix = "__" + r.Resource.UID
}
return fmt.Sprintf("%s__%s__%s%s", r.Policy, r.Rule, r.Status, suffix)
} }
// HasResource checks if the result has an valid Resource // HasResource checks if the result has an valid Resource
func (r Result) HasResource() bool { func (r Result) HasResource() bool {
if r.Resource == nil {
return false
}
return r.Resource.UID != "" return r.Resource.UID != ""
} }
@ -182,36 +196,17 @@ type Summary struct {
// PolicyReport from the PolicyReport spec wgpolicyk8s.io/v1alpha1.PolicyReport // PolicyReport from the PolicyReport spec wgpolicyk8s.io/v1alpha1.PolicyReport
type PolicyReport struct { type PolicyReport struct {
ID string
Name string Name string
Namespace string Namespace string
Results map[string]Result Results map[string]*Result
Summary Summary Summary *Summary
CreationTimestamp time.Time CreationTimestamp time.Time
} }
// GetIdentifier returns a global unique PolicyReport identifier // GetIdentifier returns a global unique PolicyReport identifier
func (pr PolicyReport) GetIdentifier() string { func (pr PolicyReport) GetIdentifier() string {
if pr.Namespace == "" { return pr.ID
return pr.Name
}
return fmt.Sprintf("%s__%s", pr.Namespace, pr.Name)
}
// ResultHash generates a has of the current result set
func (pr PolicyReport) ResultHash() string {
list := make([]string, 0, len(pr.Results))
for id := range pr.Results {
list = append(list, id)
}
sort.Strings(list)
h := sha1.New()
h.Write([]byte(strings.Join(list, "")))
return hex.EncodeToString(h.Sum(nil))
} }
// HasResult returns if the Report has an Rusult with the given ID // HasResult returns if the Report has an Rusult with the given ID
@ -222,7 +217,7 @@ func (pr PolicyReport) HasResult(id string) bool {
} }
// GetType returns the Type of the Report // GetType returns the Type of the Report
func (pr PolicyReport) GetType() Type { func (pr PolicyReport) GetType() ResourceType {
if pr.Namespace == "" { if pr.Namespace == "" {
return ClusterPolicyReportType return ClusterPolicyReportType
} }
@ -231,8 +226,8 @@ func (pr PolicyReport) GetType() Type {
} }
// GetNewResults filters already existing Results from the old PolicyReport and returns only the diff with new Results // GetNewResults filters already existing Results from the old PolicyReport and returns only the diff with new Results
func (pr PolicyReport) GetNewResults(or PolicyReport) []Result { func (pr PolicyReport) GetNewResults(or *PolicyReport) []*Result {
diff := make([]Result, 0) diff := make([]*Result, 0)
for _, r := range pr.Results { for _, r := range pr.Results {
if or.HasResult(r.GetIdentifier()) { if or.HasResult(r.GetIdentifier()) {
@ -244,3 +239,30 @@ func (pr PolicyReport) GetNewResults(or PolicyReport) []Result {
return diff return diff
} }
func GeneratePolicyReportID(name, namespace string) string {
id := name
if namespace != "" {
id = fmt.Sprintf("%s__%s", namespace, name)
}
h := sha1.New()
h.Write([]byte(id))
return fmt.Sprintf("%x", h.Sum(nil))
}
func GeneratePolicyReportResultID(uid, policy, rule, status, suffix string) string {
if uid != "" {
suffix = "__" + uid
}
id := fmt.Sprintf("%s__%s__%s%s", policy, rule, status, suffix)
h := sha1.New()
h.Write([]byte(id))
return fmt.Sprintf("%x", h.Sum(nil))
}

View file

@ -1,14 +1,14 @@
package report_test package report_test
import ( import (
"fmt"
"testing" "testing"
"time" "time"
"github.com/kyverno/policy-reporter/pkg/report" "github.com/kyverno/policy-reporter/pkg/report"
) )
var result1 = report.Result{ var result1 = &report.Result{
ID: "e0659854c6ee5c1a4df9242b2eb8b40919967842",
Message: "validation error: requests and limits required. Rule autogen-check-for-requests-and-limits failed at path /spec/template/spec/containers/0/resources/requests/", Message: "validation error: requests and limits required. Rule autogen-check-for-requests-and-limits failed at path /spec/template/spec/containers/0/resources/requests/",
Policy: "require-requests-and-limits-required", Policy: "require-requests-and-limits-required",
Rule: "autogen-check-for-requests-and-limits", Rule: "autogen-check-for-requests-and-limits",
@ -17,7 +17,7 @@ var result1 = report.Result{
Category: "resources", Category: "resources",
Severity: report.High, Severity: report.High,
Scored: true, Scored: true,
Resource: report.Resource{ Resource: &report.Resource{
APIVersion: "v1", APIVersion: "v1",
Kind: "Deployment", Kind: "Deployment",
Name: "nginx", Name: "nginx",
@ -26,7 +26,8 @@ var result1 = report.Result{
}, },
} }
var result2 = report.Result{ var result2 = &report.Result{
ID: "2",
Message: "validation error: requests and limits required. Rule autogen-check-for-requests-and-limits failed at path /spec/template/spec/containers/0/resources/requests/", Message: "validation error: requests and limits required. Rule autogen-check-for-requests-and-limits failed at path /spec/template/spec/containers/0/resources/requests/",
Policy: "require-requests-and-limits-required", Policy: "require-requests-and-limits-required",
Rule: "autogen-check-for-requests-and-limits", Rule: "autogen-check-for-requests-and-limits",
@ -34,7 +35,7 @@ var result2 = report.Result{
Status: report.Fail, Status: report.Fail,
Category: "resources", Category: "resources",
Scored: true, Scored: true,
Resource: report.Resource{ Resource: &report.Resource{
APIVersion: "v1", APIVersion: "v1",
Kind: "Deployment", Kind: "Deployment",
Name: "nginx", Name: "nginx",
@ -43,24 +44,26 @@ var result2 = report.Result{
}, },
} }
var preport = report.PolicyReport{ var preport = &report.PolicyReport{
ID: "24cfa233af033d104cd6ce0ff9a5a875c71a5844",
Name: "polr-test", Name: "polr-test",
Namespace: "test", Namespace: "test",
Results: make(map[string]report.Result, 0), Results: make(map[string]*report.Result),
Summary: report.Summary{}, Summary: &report.Summary{},
CreationTimestamp: time.Now(), CreationTimestamp: time.Now(),
} }
var creport = report.PolicyReport{ var creport = &report.PolicyReport{
ID: "57e1551475e17740bacc3640d2412b1a6aad6a93",
Name: "cpolr-test", Name: "cpolr-test",
Results: make(map[string]report.Result, 0), Results: make(map[string]*report.Result),
Summary: report.Summary{}, Summary: &report.Summary{},
CreationTimestamp: time.Now(), CreationTimestamp: time.Now(),
} }
func Test_PolicyReport(t *testing.T) { func Test_PolicyReport(t *testing.T) {
t.Run("Check PolicyReport.GetIdentifier", func(t *testing.T) { t.Run("Check PolicyReport.GetIdentifier", func(t *testing.T) {
expected := fmt.Sprintf("%s__%s", preport.Namespace, preport.Name) expected := report.GeneratePolicyReportID(preport.Name, preport.Namespace)
if preport.GetIdentifier() != expected { if preport.GetIdentifier() != expected {
t.Errorf("Expected PolicyReport.GetIdentifier() to be %s (actual: %s)", expected, preport.GetIdentifier()) t.Errorf("Expected PolicyReport.GetIdentifier() to be %s (actual: %s)", expected, preport.GetIdentifier())
@ -68,45 +71,36 @@ func Test_PolicyReport(t *testing.T) {
}) })
t.Run("Check PolicyReport.GetNewResults", func(t *testing.T) { t.Run("Check PolicyReport.GetNewResults", func(t *testing.T) {
preport1 := preport preport1 := &report.PolicyReport{
preport2 := preport ID: "24cfa233af033d104cd6ce0ff9a5a875c71a5844",
Name: "polr-test",
preport1.Results = map[string]report.Result{result1.GetIdentifier(): result1} Namespace: "test",
preport2.Results = map[string]report.Result{result1.GetIdentifier(): result1, result2.GetIdentifier(): result2} Summary: &report.Summary{},
CreationTimestamp: time.Now(),
Results: map[string]*report.Result{result1.GetIdentifier(): result1},
}
preport2 := &report.PolicyReport{
ID: "24cfa233af033d104cd6ce0ff9a5a875c71a5844",
Name: "polr-test",
Namespace: "test",
Summary: &report.Summary{},
CreationTimestamp: time.Now(),
Results: map[string]*report.Result{result1.GetIdentifier(): result1, result2.GetIdentifier(): result2},
}
diff := preport2.GetNewResults(preport1) diff := preport2.GetNewResults(preport1)
if len(diff) != 1 { if len(diff) != 1 {
t.Error("Expected 1 new result in diff") t.Error("Expected 1 new result in diff")
} }
}) })
t.Run("Check PolicyReport.ResultHash", func(t *testing.T) {
preport := preport
preport.Results = map[string]report.Result{result1.GetIdentifier(): result1, result2.GetIdentifier(): result2}
hash := preport.ResultHash()
if hash != "cd4a0ebefa915f33649db99063c182488403bb4c" {
t.Errorf("Expected 'cd4a0ebefa915f33649db99063c182488403bb4c', got %s", hash)
}
})
t.Run("Check PolicyReport.ResultHash same with different order", func(t *testing.T) {
preport1 := preport
preport2 := preport
preport1.Results = map[string]report.Result{result2.GetIdentifier(): result2, result1.GetIdentifier(): result1}
preport2.Results = map[string]report.Result{result1.GetIdentifier(): result1, result2.GetIdentifier(): result2}
if preport2.ResultHash() != preport1.ResultHash() {
t.Error("Expected same hash with different order")
}
})
} }
func Test_ClusterPolicyReport(t *testing.T) { func Test_ClusterPolicyReport(t *testing.T) {
t.Run("Check ClusterPolicyReport.GetIdentifier", func(t *testing.T) { t.Run("Check ClusterPolicyReport.GetIdentifier", func(t *testing.T) {
if creport.GetIdentifier() != creport.Name { expected := report.GeneratePolicyReportID(creport.Name, creport.Namespace)
t.Errorf("Expected ClusterPolicyReport.GetIdentifier() to be %s (actual: %s)", creport.Name, creport.GetIdentifier())
if creport.GetIdentifier() != expected {
t.Errorf("Expected ClusterPolicyReport.GetIdentifier() to be %s (actual: %s)", expected, creport.GetIdentifier())
} }
}) })
t.Run("Check ClusterPolicyReport.GetType", func(t *testing.T) { t.Run("Check ClusterPolicyReport.GetType", func(t *testing.T) {
@ -116,54 +110,49 @@ func Test_ClusterPolicyReport(t *testing.T) {
}) })
t.Run("Check ClusterPolicyReport.GetNewResults", func(t *testing.T) { t.Run("Check ClusterPolicyReport.GetNewResults", func(t *testing.T) {
creport1 := creport creport1 := &report.PolicyReport{
creport2 := creport ID: "57e1551475e17740bacc3640d2412b1a6aad6a93",
Name: "cpolr-test",
Summary: &report.Summary{},
CreationTimestamp: time.Now(),
Results: map[string]*report.Result{result1.GetIdentifier(): result1},
}
creport1.Results = map[string]report.Result{result1.GetIdentifier(): result1} creport2 := &report.PolicyReport{
creport2.Results = map[string]report.Result{result1.GetIdentifier(): result1, result2.GetIdentifier(): result2} ID: "57e1551475e17740bacc3640d2412b1a6aad6a93",
Name: "cpolr-test",
Summary: &report.Summary{},
CreationTimestamp: time.Now(),
Results: map[string]*report.Result{result1.GetIdentifier(): result1, result2.GetIdentifier(): result2},
}
diff := creport2.GetNewResults(creport1) diff := creport2.GetNewResults(creport1)
if len(diff) != 1 { if len(diff) != 1 {
t.Error("Expected 1 new result in diff") t.Error("Expected 1 new result in diff")
} }
}) })
t.Run("Check PolicyReport.ResultHash", func(t *testing.T) {
report1 := creport
report1.Results = map[string]report.Result{result1.GetIdentifier(): result1, result2.GetIdentifier(): result2}
hash := report1.ResultHash()
if hash != "cd4a0ebefa915f33649db99063c182488403bb4c" {
t.Errorf("Expected 'cd4a0ebefa915f33649db99063c182488403bb4c', got %s", hash)
}
})
t.Run("Check PolicyReport.ResultHash same with different order", func(t *testing.T) {
report1 := creport
report2 := creport
report1.Results = map[string]report.Result{result2.GetIdentifier(): result2, result1.GetIdentifier(): result1}
report2.Results = map[string]report.Result{result1.GetIdentifier(): result1, result2.GetIdentifier(): result2}
if report2.ResultHash() != report1.ResultHash() {
t.Error("Expected same hash with different order")
}
})
} }
func Test_Result(t *testing.T) { func Test_Result(t *testing.T) {
t.Run("Check Result.GetIdentifier", func(t *testing.T) { t.Run("Check Result.GetIdentifier", func(t *testing.T) {
expected := fmt.Sprintf("%s__%s__%s__%s", result1.Policy, result1.Rule, result1.Status, result1.Resource.UID) expected := report.GeneratePolicyReportResultID(result1.Resource.UID, result1.Policy, result1.Rule, result1.Status, "")
if result1.GetIdentifier() != expected { if result1.GetIdentifier() != expected {
t.Errorf("Expected ClusterPolicyReport.GetIdentifier() to be %s (actual: %s)", expected, creport.GetIdentifier()) t.Errorf("Expected ClusterPolicyReport.GetIdentifier() to be %s (actual: %s)", expected, creport.GetIdentifier())
} }
}) })
t.Run("Check Result.HasResource", func(t *testing.T) { t.Run("Check Result.HasResource with Resource", func(t *testing.T) {
if result1.HasResource() == false { if result1.HasResource() == false {
t.Errorf("Expected result1.HasResource() to be true (actual: %v)", result1.HasResource()) t.Errorf("Expected result1.HasResource() to be true (actual: %v)", result1.HasResource())
} }
}) })
t.Run("Check Result.HasResource without Resource", func(t *testing.T) {
result := report.Result{}
if result.HasResource() == true {
t.Errorf("Expected result.HasResource() to be false without a Resource (actual: %v)", result1.HasResource())
}
})
} }

44
pkg/report/publisher.go Normal file
View file

@ -0,0 +1,44 @@
package report
import "sync"
type EventPublisher interface {
// RegisterListener register Handlers called on each PolicyReport watch.Event
RegisterListener(PolicyReportListener)
// GetListener returns a list of all registered Listeners
GetListener() []PolicyReportListener
// Publish events to the registered listeners
Publish(eventChan <-chan LifecycleEvent)
}
type lifecycleEventPublisher struct {
listeners []PolicyReportListener
}
func (p *lifecycleEventPublisher) RegisterListener(listener PolicyReportListener) {
p.listeners = append(p.listeners, listener)
}
func (p *lifecycleEventPublisher) GetListener() []PolicyReportListener {
return p.listeners
}
func (p *lifecycleEventPublisher) Publish(eventChan <-chan LifecycleEvent) {
for event := range eventChan {
wg := sync.WaitGroup{}
wg.Add(len(p.listeners))
for _, listener := range p.listeners {
go func(li PolicyReportListener, ev LifecycleEvent) {
li(event)
wg.Done()
}(listener, event)
}
wg.Wait()
}
}
func NewEventPublisher() EventPublisher {
return &lifecycleEventPublisher{}
}

View file

@ -0,0 +1,46 @@
package report_test
import (
"sync"
"testing"
"github.com/kyverno/policy-reporter/pkg/report"
)
func Test_PublishLifecycleEvents(t *testing.T) {
eventChan := make(chan report.LifecycleEvent)
var event report.LifecycleEvent
wg := sync.WaitGroup{}
wg.Add(1)
publisher := report.NewEventPublisher()
publisher.RegisterListener(func(le report.LifecycleEvent) {
event = le
wg.Done()
})
go func() {
eventChan <- report.LifecycleEvent{Type: report.Updated, NewPolicyReport: &report.PolicyReport{}, OldPolicyReport: &report.PolicyReport{}}
close(eventChan)
}()
publisher.Publish(eventChan)
wg.Wait()
if event.Type != report.Updated {
t.Error("Expected Event to be published to the listener")
}
}
func Test_GetReisteredListeners(t *testing.T) {
publisher := report.NewEventPublisher()
publisher.RegisterListener(func(le report.LifecycleEvent) {})
if len(publisher.GetListener()) != 1 {
t.Error("Expected to get one registered listener back")
}
}

View file

@ -2,52 +2,87 @@ package report
import "sync" import "sync"
type PolicyReportStore interface {
// CreateSchemas for PolicyReports and PolicyReportResults
CreateSchemas() error
// Get an PolicyReport by Type and ID
Get(id string) (*PolicyReport, bool)
// Add a PolicyReport to the Store
Add(r *PolicyReport) error
// Add a PolicyReport to the Store
Update(r *PolicyReport) error
// Remove a PolicyReport with the given Type and ID from the Store
Remove(id string) error
// CleanUp removes all items in the store
CleanUp() error
}
// PolicyReportStore caches the latest version of an PolicyReport // PolicyReportStore caches the latest version of an PolicyReport
type PolicyReportStore struct { type policyReportStore struct {
store map[string]map[string]PolicyReport store map[string]map[string]*PolicyReport
rwm *sync.RWMutex rwm *sync.RWMutex
} }
// Get an PolicyReport by Type and ID func (s *policyReportStore) CreateSchemas() error {
func (s *PolicyReportStore) Get(rType Type, id string) (PolicyReport, bool) { return nil
}
func (s *policyReportStore) Get(id string) (*PolicyReport, bool) {
s.rwm.RLock() s.rwm.RLock()
r, ok := s.store[rType][id] r, ok := s.store[PolicyReportType][id]
s.rwm.RUnlock()
if ok {
return r, ok
}
s.rwm.RLock()
r, ok = s.store[ClusterPolicyReportType][id]
s.rwm.RUnlock() s.rwm.RUnlock()
return r, ok return r, ok
} }
// List all PolicyReports of the given Type func (s *policyReportStore) Add(r *PolicyReport) error {
func (s *PolicyReportStore) List(rType Type) []PolicyReport {
s.rwm.RLock()
list := make([]PolicyReport, 0, len(s.store))
for _, r := range s.store[rType] {
list = append(list, r)
}
s.rwm.RUnlock()
return list
}
// Add a PolicyReport to the Store
func (s *PolicyReportStore) Add(r PolicyReport) {
s.rwm.Lock() s.rwm.Lock()
s.store[r.GetType()][r.GetIdentifier()] = r s.store[r.GetType()][r.GetIdentifier()] = r
s.rwm.Unlock() s.rwm.Unlock()
return nil
} }
// Remove a PolicyReport with the given Type and ID from the Store func (s *policyReportStore) Update(r *PolicyReport) error {
func (s *PolicyReportStore) Remove(rType Type, id string) {
s.rwm.Lock() s.rwm.Lock()
delete(s.store[rType], id) s.store[r.GetType()][r.GetIdentifier()] = r
s.rwm.Unlock() s.rwm.Unlock()
return nil
}
func (s *policyReportStore) Remove(id string) error {
if r, ok := s.Get(id); ok {
s.rwm.Lock()
delete(s.store[r.GetType()], id)
s.rwm.Unlock()
}
return nil
}
func (s *policyReportStore) CleanUp() error {
s.rwm.Lock()
s.store = map[ResourceType]map[string]*PolicyReport{
PolicyReportType: {},
ClusterPolicyReportType: {},
}
s.rwm.Unlock()
return nil
} }
// NewPolicyReportStore construct a PolicyReportStore // NewPolicyReportStore construct a PolicyReportStore
func NewPolicyReportStore() *PolicyReportStore { func NewPolicyReportStore() PolicyReportStore {
return &PolicyReportStore{ return &policyReportStore{
store: map[Type]map[string]PolicyReport{ store: map[ResourceType]map[string]*PolicyReport{
PolicyReportType: {}, PolicyReportType: {},
ClusterPolicyReportType: {}, ClusterPolicyReportType: {},
}, },

View file

@ -2,43 +2,71 @@ package report_test
import ( import (
"testing" "testing"
"time"
"github.com/kyverno/policy-reporter/pkg/report" "github.com/kyverno/policy-reporter/pkg/report"
) )
func Test_PolicyReportStore(t *testing.T) { func Test_PolicyReportStore(t *testing.T) {
store := report.NewPolicyReportStore() store := report.NewPolicyReportStore()
store.CreateSchemas()
t.Run("Add/Get", func(t *testing.T) { t.Run("Add/Get", func(t *testing.T) {
_, ok := store.Get(preport.GetType(), preport.GetIdentifier()) _, ok := store.Get(preport.GetIdentifier())
if ok == true { if ok == true {
t.Fatalf("Should not be found in empty Store") t.Fatalf("Should not be found in empty Store")
} }
store.Add(preport) store.Add(preport)
_, ok = store.Get(preport.GetType(), preport.GetIdentifier()) _, ok = store.Get(preport.GetIdentifier())
if ok == false { if ok == false {
t.Errorf("Should be found in Store after adding report to the store") t.Errorf("Should be found in Store after adding report to the store")
} }
}) })
t.Run("List", func(t *testing.T) { t.Run("Update/Get", func(t *testing.T) {
items := store.List(preport.GetType()) ureport := &report.PolicyReport{
if len(items) != 1 { ID: "24cfa233af033d104cd6ce0ff9a5a875c71a5844",
t.Errorf("Should return List with the added Report") Name: "polr-test",
Namespace: "test",
Results: make(map[string]*report.Result),
Summary: &report.Summary{Skip: 1},
CreationTimestamp: time.Now(),
}
store.Add(preport)
r, _ := store.Get(preport.GetIdentifier())
if r.Summary.Skip != 0 {
t.Errorf("Expected Summary.Skip to be 0")
}
store.Update(ureport)
r2, _ := store.Get(preport.GetIdentifier())
if r2.Summary.Skip != 1 {
t.Errorf("Expected Summary.Skip to be 1 after update")
} }
}) })
t.Run("Delete/Get", func(t *testing.T) { t.Run("Delete/Get", func(t *testing.T) {
_, ok := store.Get(preport.GetType(), preport.GetIdentifier()) _, ok := store.Get(preport.GetIdentifier())
if ok == false { if ok == false {
t.Errorf("Should be found in Store after adding report to the store") t.Errorf("Should be found in Store after adding report to the store")
} }
store.Remove(preport.GetType(), preport.GetIdentifier()) store.Remove(preport.GetIdentifier())
_, ok = store.Get(preport.GetType(), preport.GetIdentifier()) _, ok = store.Get(preport.GetIdentifier())
if ok == true { if ok == true {
t.Fatalf("Should not be found after Remove report from Store") t.Fatalf("Should not be found after Remove report from Store")
} }
}) })
t.Run("CleanUp", func(t *testing.T) {
store.Add(preport)
store.CleanUp()
_, ok := store.Get(preport.GetIdentifier())
if ok == true {
t.Fatalf("Should have no results after CleanUp")
}
})
} }

846
pkg/sqlite3/store.go Normal file
View file

@ -0,0 +1,846 @@
package sqlite3
import (
"database/sql"
"encoding/json"
"fmt"
"log"
"os"
"strings"
"time"
api "github.com/kyverno/policy-reporter/pkg/api/v1"
"github.com/kyverno/policy-reporter/pkg/report"
_ "github.com/mattn/go-sqlite3"
)
const (
reportSQL = `CREATE TABLE policy_report (
"id" TEXT NOT NULL PRIMARY KEY,
"type" TEXT,
"namespace" TEXT,
"name" TEXT NOT NULL,
"skip" INTEGER DEFAULT 0,
"pass" INTEGER DEFAULT 0,
"warn" INTEGER DEFAULT 0,
"fail" INTEGER DEFAULT 0,
"error" INTEGER DEFAULT 0,
"created" INTEGER
);`
resultSQL = `CREATE TABLE policy_report_result (
"policy_report_id" TEXT NOT NULL,
"id" TEXT NOT NULL PRIMARY KEY,
"policy" TEXT,
"rule" TEXT,
"message" TEXT,
"scored" INTEGER,
"priority" TEXT,
"status" TEXT,
"severity" TEXT,
"category" TEXT,
"source" TEXT,
"resource_api_version" TEXT,
"resource_kind" TEXT,
"resource_name" TEXT,
"resource_namespace" TEXT,
"resource_uid" TEXT,
"properties" TEXT,
"timestamp" INTEGER,
FOREIGN KEY (policy_report_id) REFERENCES policy_report(id) ON DELETE CASCADE
);`
)
type PolicyReportStore interface {
report.PolicyReportStore
api.PolicyReportFinder
}
// policyReportStore caches the latest version of an PolicyReport
type policyReportStore struct {
db *sql.DB
}
func (s *policyReportStore) CreateSchemas() error {
_, err := s.db.Exec("PRAGMA foreign_keys = ON")
if err != nil {
return err
}
_, err = s.db.Exec(reportSQL)
if err != nil {
return err
}
_, err = s.db.Exec(resultSQL)
return err
}
// Get an PolicyReport by Type and ID
func (s *policyReportStore) Get(id string) (*report.PolicyReport, bool) {
var created int64
r := &report.PolicyReport{Summary: &report.Summary{}}
row := s.db.QueryRow("SELECT namespace, name, pass, skip, warn, fail, error, created FROM policy_report WHERE id=$1", id)
err := row.Scan(&r.Namespace, &r.Name, &r.Summary.Pass, &r.Summary.Skip, &r.Summary.Warn, &r.Summary.Fail, &r.Summary.Error, &created)
if err == sql.ErrNoRows {
return r, false
} else if err != nil {
log.Printf("[ERROR] Failed to select PolicyReport: %s", err)
return r, false
}
r.CreationTimestamp = time.Unix(created, 0)
results, err := s.fetchResults(id)
if err != nil {
log.Printf("Failed to fetch Reports: %s\n", err)
return r, false
}
r.Results = results
return r, true
}
// Add a PolicyReport to the Store
func (s *policyReportStore) Add(r *report.PolicyReport) error {
stmt, err := s.db.Prepare("INSERT INTO policy_report(id, type, namespace, name, pass, skip, warn, fail, error, created) values(?,?,?,?,?,?,?,?,?,?)")
if err != nil {
return err
}
defer stmt.Close()
_, err = stmt.Exec(r.GetIdentifier(), r.GetType(), r.Namespace, r.Name, r.Summary.Pass, r.Summary.Skip, r.Summary.Warn, r.Summary.Fail, r.Summary.Error, r.CreationTimestamp.Unix())
if err != nil {
return err
}
return s.persistResults(r)
}
func (s *policyReportStore) Update(r *report.PolicyReport) error {
stmt, err := s.db.Prepare("UPDATE policy_report SET pass=?, skip=?, warn=?, fail=?, error=?, created=? WHERE id=?")
if err != nil {
return err
}
defer stmt.Close()
_, err = stmt.Exec(r.Summary.Pass, r.Summary.Skip, r.Summary.Warn, r.Summary.Fail, r.Summary.Error, r.CreationTimestamp.Unix(), r.GetIdentifier())
if err != nil {
return err
}
dstmt, err := s.db.Prepare("DELETE FROM policy_report_result WHERE policy_report_id=?")
if err != nil {
return err
}
defer dstmt.Close()
_, err = dstmt.Exec(r.GetIdentifier())
if err != nil {
return err
}
return s.persistResults(r)
}
// Remove a PolicyReport with the given Type and ID from the Store
func (s *policyReportStore) Remove(id string) error {
stmt, err := s.db.Prepare("DELETE FROM policy_report WHERE id=?")
if err != nil {
return err
}
defer stmt.Close()
_, err = stmt.Exec(id)
if err != nil {
return err
}
stmt, err = s.db.Prepare("DELETE FROM policy_report_result WHERE policy_report_id=?")
if err != nil {
return err
}
defer stmt.Close()
_, err = stmt.Exec(id)
return err
}
func (s *policyReportStore) CleanUp() error {
stmt, err := s.db.Prepare("DELETE FROM policy_report")
if err != nil {
return err
}
defer stmt.Close()
_, err = stmt.Exec()
if err != nil {
return err
}
dstmt, err := s.db.Prepare("DELETE FROM policy_report_result")
if err != nil {
return err
}
defer dstmt.Close()
_, err = dstmt.Exec()
return err
}
func (s *policyReportStore) FetchClusterPolicies(source string) ([]string, error) {
list := make([]string, 0)
where, args := appendSourceWhere(source)
if where != "" {
where = " AND " + where
}
rows, err := s.db.Query(`SELECT DISTINCT policy FROM policy_report_result WHERE resource_namespace == ""`+where+` ORDER BY policy ASC`, args...)
if err != nil {
return list, err
}
defer rows.Close()
for rows.Next() {
var item string
err := rows.Scan(&item)
if err != nil {
return list, err
}
list = append(list, item)
}
return list, nil
}
func (s *policyReportStore) FetchNamespacedPolicies(source string) ([]string, error) {
list := make([]string, 0)
where, args := appendSourceWhere(source)
if where != "" {
where = " AND " + where
}
rows, err := s.db.Query(`SELECT DISTINCT policy FROM policy_report_result WHERE resource_namespace != ""`+where+` ORDER BY policy ASC`, args...)
if err != nil {
return list, err
}
defer rows.Close()
for rows.Next() {
var item string
err := rows.Scan(&item)
if err != nil {
return list, err
}
list = append(list, item)
}
return list, nil
}
func (s *policyReportStore) FetchCategories(source string) ([]string, error) {
list := make([]string, 0)
where, args := appendSourceWhere(source)
if where != "" {
where = " AND " + where
}
rows, err := s.db.Query(`SELECT DISTINCT category FROM policy_report_result WHERE category != ""`+where+` ORDER BY category ASC`, args...)
if err != nil {
return list, err
}
defer rows.Close()
for rows.Next() {
var item string
err := rows.Scan(&item)
if err != nil {
return list, err
}
list = append(list, item)
}
return list, nil
}
func (s *policyReportStore) FetchNamespacedKinds(source string) ([]string, error) {
list := make([]string, 0)
where, args := appendSourceWhere(source)
if where != "" {
where = " AND " + where
}
rows, err := s.db.Query(`SELECT DISTINCT resource_kind FROM policy_report_result WHERE resource_kind != "" AND resource_namespace != ""`+where+` ORDER BY resource_kind ASC`, args...)
if err != nil {
return list, err
}
defer rows.Close()
for rows.Next() {
var item string
err := rows.Scan(&item)
if err != nil {
return list, err
}
list = append(list, item)
}
return list, nil
}
func (s *policyReportStore) FetchClusterKinds(source string) ([]string, error) {
list := make([]string, 0)
where, args := appendSourceWhere(source)
if where != "" {
where = " AND " + where
}
rows, err := s.db.Query(`SELECT DISTINCT resource_kind FROM policy_report_result WHERE resource_kind != "" AND resource_namespace == ""`+where+` ORDER BY resource_kind ASC`, args...)
if err != nil {
return list, err
}
defer rows.Close()
for rows.Next() {
var item string
err := rows.Scan(&item)
if err != nil {
return list, err
}
list = append(list, item)
}
return list, nil
}
func (s *policyReportStore) FetchClusterSources() ([]string, error) {
list := make([]string, 0)
rows, err := s.db.Query(`SELECT DISTINCT source FROM policy_report_result WHERE source != "" AND resource_namespace == "" ORDER BY source ASC`)
if err != nil {
return list, err
}
defer rows.Close()
for rows.Next() {
var item string
err := rows.Scan(&item)
if err != nil {
return list, err
}
list = append(list, item)
}
return list, nil
}
func (s *policyReportStore) FetchNamespacedSources() ([]string, error) {
list := make([]string, 0)
rows, err := s.db.Query(`SELECT DISTINCT source FROM policy_report_result WHERE source != "" AND resource_namespace != "" ORDER BY source ASC`)
if err != nil {
return list, err
}
defer rows.Close()
for rows.Next() {
var item string
err := rows.Scan(&item)
if err != nil {
return list, err
}
list = append(list, item)
}
return list, nil
}
func (s *policyReportStore) FetchNamespaces(source string) ([]string, error) {
list := make([]string, 0)
where, args := appendSourceWhere(source)
if where != "" {
where = " AND " + where
}
rows, err := s.db.Query(`SELECT DISTINCT resource_namespace FROM policy_report_result WHERE resource_namespace != ""`+where+` ORDER BY resource_namespace ASC`, args...)
if err != nil {
return list, err
}
defer rows.Close()
for rows.Next() {
var item string
err := rows.Scan(&item)
if err != nil {
return list, err
}
list = append(list, item)
}
return list, nil
}
func (s *policyReportStore) FetchNamespacedStatusCounts(filter api.Filter) ([]api.NamespacedStatusCount, error) {
var list map[string][]api.NamespaceCount
if len(filter.Status) == 0 {
list = map[string][]api.NamespaceCount{
report.Pass: make([]api.NamespaceCount, 0),
report.Fail: make([]api.NamespaceCount, 0),
report.Warn: make([]api.NamespaceCount, 0),
report.Error: make([]api.NamespaceCount, 0),
report.Skip: make([]api.NamespaceCount, 0),
}
} else {
list = map[string][]api.NamespaceCount{}
for _, status := range filter.Status {
list[status] = make([]api.NamespaceCount, 0)
}
}
statusCounts := make([]api.NamespacedStatusCount, 0, 5)
where := make([]string, 0)
args := make([]interface{}, 0)
var argCounter int
argCounter, where, args = appendWhere(filter.Policies, "policy", where, args, argCounter)
argCounter, where, args = appendWhere(filter.Kinds, "resource_kind", where, args, argCounter)
argCounter, where, args = appendWhere(filter.Sources, "source", where, args, argCounter)
argCounter, where, args = appendWhere(filter.Categories, "category", where, args, argCounter)
argCounter, where, args = appendWhere(filter.Severities, "severity", where, args, argCounter)
argCounter, where, args = appendWhere(filter.Status, "status", where, args, argCounter)
_, where, args = appendWhere(filter.Namespaces, "resource_namespace", where, args, argCounter)
whereClause := ""
if len(where) > 0 {
whereClause = " AND " + strings.Join(where, " AND ")
}
rows, err := s.db.Query(`
SELECT COUNT(id) as counter, resource_namespace, status
FROM policy_report_result WHERE resource_namespace != ""`+whereClause+`
GROUP BY resource_namespace, status
ORDER BY resource_namespace ASC`, args...)
if err != nil {
return statusCounts, err
}
defer rows.Close()
for rows.Next() {
count := api.NamespaceCount{}
var status string
err := rows.Scan(&count.Count, &count.Namespace, &status)
if err != nil {
return statusCounts, err
}
list[status] = append(list[status], count)
}
for status, items := range list {
statusCounts = append(statusCounts, api.NamespacedStatusCount{
Status: status,
Items: items,
})
}
return statusCounts, nil
}
func (s *policyReportStore) FetchRuleStatusCounts(policy, rule string) ([]api.StatusCount, error) {
list := map[string]api.StatusCount{
report.Pass: {Status: report.Pass},
report.Fail: {Status: report.Fail},
report.Warn: {Status: report.Warn},
report.Error: {Status: report.Error},
report.Skip: {Status: report.Skip},
}
statusCounts := make([]api.StatusCount, 0, len(list))
where := make([]string, 0)
args := make([]interface{}, 0)
var argCounter int
argCounter, where, args = appendWhere([]string{policy}, "policy", where, args, argCounter)
argCounter, where, args = appendWhere([]string{rule}, "rule", where, args, argCounter)
whereClause := ""
if len(where) > 0 {
whereClause = " WHERE " + strings.Join(where, " AND ")
}
rows, err := s.db.Query(`
SELECT COUNT(id) as counter, status
FROM policy_report_result`+whereClause+`
GROUP BY status`, args...)
if err != nil {
return statusCounts, err
}
defer rows.Close()
for rows.Next() {
count := api.StatusCount{}
err := rows.Scan(&count.Count, &count.Status)
if err != nil {
return statusCounts, err
}
list[count.Status] = count
}
for _, count := range list {
statusCounts = append(statusCounts, count)
}
return statusCounts, nil
}
func (s *policyReportStore) FetchStatusCounts(filter api.Filter) ([]api.StatusCount, error) {
var list map[string]api.StatusCount
if len(filter.Status) == 0 {
list = map[string]api.StatusCount{
report.Pass: {Status: report.Pass},
report.Fail: {Status: report.Fail},
report.Warn: {Status: report.Warn},
report.Error: {Status: report.Error},
report.Skip: {Status: report.Skip},
}
} else {
list = map[string]api.StatusCount{}
for _, status := range filter.Status {
list[status] = api.StatusCount{Status: status}
}
}
statusCounts := make([]api.StatusCount, 0, len(list))
where := make([]string, 0)
args := make([]interface{}, 0)
var argCounter int
argCounter, where, args = appendWhere(filter.Policies, "policy", where, args, argCounter)
argCounter, where, args = appendWhere(filter.Kinds, "resource_kind", where, args, argCounter)
argCounter, where, args = appendWhere(filter.Sources, "source", where, args, argCounter)
argCounter, where, args = appendWhere(filter.Categories, "category", where, args, argCounter)
argCounter, where, args = appendWhere(filter.Severities, "severity", where, args, argCounter)
_, where, args = appendWhere(filter.Status, "status", where, args, argCounter)
whereClause := ""
if len(where) > 0 {
whereClause = " AND " + strings.Join(where, " AND ")
}
rows, err := s.db.Query(`
SELECT COUNT(id) as counter, status
FROM policy_report_result WHERE resource_namespace = ""`+whereClause+`
GROUP BY status`, args...)
if err != nil {
return statusCounts, err
}
defer rows.Close()
for rows.Next() {
count := api.StatusCount{}
err := rows.Scan(&count.Count, &count.Status)
if err != nil {
return statusCounts, err
}
list[count.Status] = count
}
for _, count := range list {
statusCounts = append(statusCounts, count)
}
return statusCounts, nil
}
func (s *policyReportStore) FetchNamespacedResults(filter api.Filter) ([]*api.ListResult, error) {
list := []*api.ListResult{}
where := make([]string, 0)
args := make([]interface{}, 0)
var argCounter int
argCounter, where, args = appendWhere(filter.Policies, "policy", where, args, argCounter)
argCounter, where, args = appendWhere(filter.Kinds, "resource_kind", where, args, argCounter)
argCounter, where, args = appendWhere(filter.Sources, "source", where, args, argCounter)
argCounter, where, args = appendWhere(filter.Categories, "category", where, args, argCounter)
argCounter, where, args = appendWhere(filter.Severities, "severity", where, args, argCounter)
argCounter, where, args = appendWhere(filter.Status, "status", where, args, argCounter)
_, where, args = appendWhere(filter.Namespaces, "resource_namespace", where, args, argCounter)
whereClause := ""
if len(where) > 0 {
whereClause = " AND " + strings.Join(where, " AND ")
}
rows, err := s.db.Query(`
SELECT id, resource_namespace, resource_kind, resource_name, message, policy, rule, severity, properties, status
FROM policy_report_result WHERE resource_namespace != ""`+whereClause+`
ORDER BY resource_namespace, resource_name, resource_uid ASC`, args...)
if err != nil {
return list, err
}
defer rows.Close()
for rows.Next() {
result := api.ListResult{}
var props []byte
err := rows.Scan(&result.ID, &result.Namespace, &result.Kind, &result.Name, &result.Message, &result.Policy, &result.Rule, &result.Severity, &props, &result.Status)
if err != nil {
return list, err
}
json.Unmarshal(props, &result.Properties)
list = append(list, &result)
}
return list, nil
}
func (s *policyReportStore) FetchClusterResults(filter api.Filter) ([]*api.ListResult, error) {
list := []*api.ListResult{}
where := make([]string, 0)
args := make([]interface{}, 0)
var argCounter int
argCounter, where, args = appendWhere(filter.Policies, "policy", where, args, argCounter)
argCounter, where, args = appendWhere(filter.Kinds, "resource_kind", where, args, argCounter)
argCounter, where, args = appendWhere(filter.Sources, "source", where, args, argCounter)
argCounter, where, args = appendWhere(filter.Categories, "category", where, args, argCounter)
argCounter, where, args = appendWhere(filter.Severities, "severity", where, args, argCounter)
_, where, args = appendWhere(filter.Status, "status", where, args, argCounter)
whereClause := ""
if len(where) > 0 {
whereClause = " AND " + strings.Join(where, " AND ")
}
rows, err := s.db.Query(`
SELECT id, resource_namespace, resource_kind, resource_name, message, policy, rule, severity, properties, status
FROM policy_report_result WHERE resource_namespace =""`+whereClause+`
ORDER BY resource_namespace, resource_name, resource_uid ASC`, args...)
if err != nil {
return list, err
}
defer rows.Close()
for rows.Next() {
result := api.ListResult{}
var props []byte
err := rows.Scan(&result.ID, &result.Namespace, &result.Kind, &result.Name, &result.Message, &result.Policy, &result.Rule, &result.Severity, &props, &result.Status)
if err != nil {
return list, err
}
json.Unmarshal(props, &result.Properties)
list = append(list, &result)
}
return list, nil
}
func (s *policyReportStore) persistResults(report *report.PolicyReport) error {
for _, result := range report.Results {
rstmt, err := s.db.Prepare("INSERT INTO policy_report_result(policy_report_id, id, policy, rule, message, scored, priority, status, severity, category, source, resource_api_version, resource_kind, resource_name, resource_namespace, resource_uid, properties, timestamp) values(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)")
if err != nil {
return err
}
defer rstmt.Close()
var props string
b, err := json.Marshal(result.Properties)
if err == nil {
props = string(b)
}
_, err = rstmt.Exec(
report.GetIdentifier(),
result.GetIdentifier(),
result.Policy,
result.Rule,
result.Message,
result.Scored,
result.Priority,
result.Status,
result.Severity,
result.Category,
result.Source,
result.Resource.APIVersion,
result.Resource.Kind,
result.Resource.Name,
result.Resource.Namespace,
result.Resource.UID,
props,
result.Timestamp.Unix(),
)
if err != nil {
return err
}
}
return nil
}
func (s *policyReportStore) fetchResults(reportID string) (map[string]*report.Result, error) {
results := make(map[string]*report.Result)
rows, err := s.db.Query(`
SELECT
id,
policy,
rule,
message,
scored,
priority,
status,
severity,
category,
source,
resource_api_version,
resource_kind,
resource_name,
resource_namespace,
resource_uid,
properties,
timestamp
FROM policy_report_result
WHERE policy_report_id=$1
`, reportID)
if err != nil {
return results, err
}
defer rows.Close()
var props []byte
var timestamp int64
for rows.Next() {
result := &report.Result{
Resource: &report.Resource{},
}
err = rows.Scan(
&result.ID,
&result.Policy,
&result.Rule,
&result.Message,
&result.Scored,
&result.Priority,
&result.Status,
&result.Severity,
&result.Category,
&result.Source,
&result.Resource.APIVersion,
&result.Resource.Kind,
&result.Resource.Name,
&result.Resource.Namespace,
&result.Resource.UID,
&props,
&timestamp,
)
if err != nil {
return results, err
}
err = json.Unmarshal(props, &result.Properties)
if err != nil {
result.Properties = make(map[string]string)
}
result.Timestamp = time.Unix(timestamp, 0)
results[result.GetIdentifier()] = result
}
return results, nil
}
func appendWhere(options []string, field string, where []string, args []interface{}, argCounter int) (int, []string, []interface{}) {
length := len(options)
if length == 0 {
return argCounter, where, args
}
if length == 1 {
option := options[0]
argCounter++
args = append(args, strings.ToLower(option))
where = append(where, fmt.Sprintf("LOWER(%s)=$%d", field, argCounter))
return argCounter + length, where, args
}
arguments := make([]string, 0, length)
for _, option := range options {
argCounter++
arguments = append(arguments, fmt.Sprintf("$%d", argCounter))
args = append(args, strings.ToLower(option))
}
where = append(where, "LOWER("+field+") IN ("+strings.Join(arguments, ",")+")")
return argCounter + length, where, args
}
func appendSourceWhere(source string) (string, []interface{}) {
if source == "" {
return "", make([]interface{}, 0)
}
return "LOWER(source)=$1", []interface{}{strings.ToLower(source)}
}
// NewPolicyReportStore construct a PolicyReportStore
func NewPolicyReportStore(db *sql.DB) (PolicyReportStore, error) {
var err error
s := &policyReportStore{db}
if db != nil {
err = s.CreateSchemas()
}
return s, err
}
func NewDatabase(dbFile string) (*sql.DB, error) {
os.Remove(dbFile)
file, err := os.Create(dbFile)
if err != nil {
return nil, err
}
file.Close()
return sql.Open("sqlite3", dbFile)
}

476
pkg/sqlite3/store_test.go Normal file
View file

@ -0,0 +1,476 @@
package sqlite3_test
import (
"testing"
"time"
v1 "github.com/kyverno/policy-reporter/pkg/api/v1"
"github.com/kyverno/policy-reporter/pkg/report"
"github.com/kyverno/policy-reporter/pkg/sqlite3"
)
var result1 = &report.Result{
ID: "123",
Message: "validation error: requests and limits required. Rule autogen-check-for-requests-and-limits failed at path /spec/template/spec/containers/0/resources/requests/",
Policy: "require-requests-and-limits-required",
Rule: "autogen-check-for-requests-and-limits",
Priority: report.ErrorPriority,
Status: report.Fail,
Category: "resources",
Severity: report.High,
Scored: true,
Source: "Kyverno",
Resource: &report.Resource{
APIVersion: "v1",
Kind: "Deployment",
Name: "nginx",
Namespace: "test",
UID: "536ab69f-1b3c-4bd9-9ba4-274a56188409",
},
}
var result2 = &report.Result{
ID: "124",
Message: "validation error: requests and limits required. Rule autogen-check-for-requests-and-limits failed at path /spec/template/spec/containers/0/resources/requests/",
Policy: "require-requests-and-limits-required",
Rule: "autogen-check-for-requests-and-limits",
Priority: report.WarningPriority,
Status: report.Pass,
Category: "Best Practices",
Scored: true,
Source: "Kyverno",
Resource: &report.Resource{
APIVersion: "v1",
Kind: "Pod",
Name: "nginx",
Namespace: "test",
UID: "536ab69f-1b3c-4bd9-9ba4-274a56188419",
},
}
var cresult1 = &report.Result{
ID: "125",
Message: "validation error: The label `test` is required. Rule check-for-labels-on-namespace",
Policy: "require-ns-labels",
Rule: "check-for-labels-on-namespace",
Priority: report.ErrorPriority,
Status: report.Pass,
Category: "namespaces",
Severity: report.Medium,
Scored: true,
Source: "Kyverno",
Resource: &report.Resource{
APIVersion: "v1",
Kind: "Namespace",
Name: "test",
UID: "536ab69f-1b3c-4bd9-9ba4-274a56188411",
},
}
var cresult2 = &report.Result{
ID: "126",
Message: "validation error: The label `test` is required. Rule check-for-labels-on-namespace",
Policy: "require-ns-labels",
Rule: "check-for-labels-on-namespace",
Priority: report.WarningPriority,
Status: report.Fail,
Category: "namespaces",
Severity: report.High,
Scored: true,
Source: "Kyverno",
Resource: &report.Resource{
APIVersion: "v1",
Kind: "Namespace",
Name: "dev",
UID: "536ab69f-1b3c-4bd9-9ba4-274a56188412",
},
}
var preport = &report.PolicyReport{
ID: report.GeneratePolicyReportID("polr-test", "test"),
Name: "polr-test",
Namespace: "test",
Results: map[string]*report.Result{
result1.GetIdentifier(): result1,
},
Summary: &report.Summary{Fail: 1},
CreationTimestamp: time.Now(),
}
var ureport = &report.PolicyReport{
ID: report.GeneratePolicyReportID("polr-test", "test"),
Name: "polr-test",
Namespace: "test",
Results: map[string]*report.Result{
result1.GetIdentifier(): result1,
result2.GetIdentifier(): result2,
},
Summary: &report.Summary{Fail: 1, Pass: 1},
CreationTimestamp: time.Now(),
}
var creport = &report.PolicyReport{
ID: report.GeneratePolicyReportID("cpolr", ""),
Name: "cpolr",
Results: map[string]*report.Result{
cresult1.GetIdentifier(): cresult1,
cresult2.GetIdentifier(): cresult2,
},
Summary: &report.Summary{},
CreationTimestamp: time.Now(),
}
func Test_PolicyReportStore(t *testing.T) {
db, _ := sqlite3.NewDatabase("test.db")
defer db.Close()
store, _ := sqlite3.NewPolicyReportStore(db)
t.Run("Add/Get/Update PolicyReport", func(t *testing.T) {
_, ok := store.Get(preport.GetIdentifier())
if ok == true {
t.Fatalf("Should not be found in empty Store")
}
store.Add(preport)
r1, ok := store.Get(preport.GetIdentifier())
if ok == false {
t.Errorf("Should be found in Store after adding report to the store")
}
if r1.Summary.Pass != 0 {
t.Errorf("Expected 0 Passed Results in Summary")
}
store.Update(ureport)
r2, _ := store.Get(preport.GetIdentifier())
if r2.Summary.Pass != 1 {
t.Errorf("Expected 1 Passed Results in Summary after Update")
}
})
t.Run("Add/Get ClusterPolicyReport", func(t *testing.T) {
_, ok := store.Get(creport.GetIdentifier())
if ok == true {
t.Fatalf("Should not be found in empty Store")
}
store.Add(creport)
_, ok = store.Get(creport.GetIdentifier())
if ok == false {
t.Errorf("Should be found in Store after adding report to the store")
}
})
t.Run("FetchNamespacedKinds", func(t *testing.T) {
items, err := store.FetchNamespacedKinds("kyverno")
if err != nil {
t.Fatalf("Unexpected Error: %s", err)
}
if len(items) != 2 {
t.Fatalf("Should Find 2 Kinds with Namespace Scope")
}
if items[0] != "Deployment" {
t.Errorf("Should return 'Deployment' as first result")
}
if items[1] != "Pod" {
t.Errorf("Should return 'Pod' as second result")
}
})
t.Run("FetchClusterKinds", func(t *testing.T) {
items, err := store.FetchClusterKinds("kyverno")
if err != nil {
t.Fatalf("Unexpected Error: %s", err)
}
if len(items) != 1 {
t.Fatalf("Should find 1 kind with cluster scope")
}
if items[0] != "Namespace" {
t.Errorf("Should return 'Namespace' as first result")
}
})
t.Run("FetchNamespacedStatusCounts", func(t *testing.T) {
items, err := store.FetchNamespacedStatusCounts(v1.Filter{})
if err != nil {
t.Fatalf("Unexpected Error: %s", err)
}
if len(items) != 5 {
t.Fatalf("Should include 1 item per possible status")
}
var passed v1.NamespacedStatusCount
var failed v1.NamespacedStatusCount
for _, item := range items {
if item.Status == report.Pass {
passed = item
}
if item.Status == report.Fail {
failed = item
}
}
if passed.Status != report.Pass {
t.Errorf("Expected Pass Counts as first item")
}
if passed.Items[0].Count != 1 {
t.Errorf("Expected count to be one for pass")
}
if failed.Status != report.Fail {
t.Errorf("Expected Pass Counts as first item")
}
if failed.Items[0].Count != 1 {
t.Errorf("Expected count to be one for fail")
}
})
t.Run("FetchNamespacedStatusCounts with StatusFilter", func(t *testing.T) {
items, err := store.FetchNamespacedStatusCounts(v1.Filter{Status: []string{report.Pass}})
if err != nil {
t.Fatalf("Unexpected Error: %s", err)
}
if len(items) != 1 {
t.Fatalf("Should have only 1 item for pass counts")
}
if items[0].Status != report.Pass {
t.Errorf("Expected Pass Counts")
}
if items[0].Items[0].Count != 1 {
t.Errorf("Expected count to be one for pass")
}
})
t.Run("FetchRuleStatusCounts", func(t *testing.T) {
items, err := store.FetchRuleStatusCounts("require-requests-and-limits-required", "autogen-check-for-requests-and-limits")
var passed v1.StatusCount
var failed v1.StatusCount
for _, item := range items {
if item.Status == report.Pass {
passed = item
}
if item.Status == report.Fail {
failed = item
}
}
if err != nil {
t.Fatalf("Unexpected Error: %s", err)
}
if passed.Count != 1 {
t.Errorf("Expected count to be one for pass")
}
if failed.Count != 1 {
t.Errorf("Expected count to be one for fail")
}
})
t.Run("FetchStatusCounts", func(t *testing.T) {
items, err := store.FetchStatusCounts(v1.Filter{})
if err != nil {
t.Fatalf("Unexpected Error: %s", err)
}
var passed v1.StatusCount
var failed v1.StatusCount
for _, item := range items {
if item.Status == report.Pass {
passed = item
}
if item.Status == report.Fail {
failed = item
}
}
if len(items) != 5 {
t.Fatalf("Should include 1 item per possible status")
}
if passed.Count != 1 {
t.Errorf("Expected count to be one for pass")
}
if failed.Count != 1 {
t.Errorf("Expected count to be one for fail")
}
})
t.Run("FetchStatusCounts with StatusFilter", func(t *testing.T) {
items, err := store.FetchStatusCounts(v1.Filter{Status: []string{report.Pass}})
if err != nil {
t.Fatalf("Unexpected Error: %s", err)
}
if len(items) != 1 {
t.Fatalf("Should have only 1 item for pass counts")
}
if items[0].Status != report.Pass {
t.Errorf("Expected Pass Counts")
}
if items[0].Count != 1 {
t.Errorf("Expected count to be one for pass")
}
})
t.Run("FetchNamespacedResults", func(t *testing.T) {
items, err := store.FetchNamespacedResults(v1.Filter{Namespaces: []string{"test"}})
if err != nil {
t.Fatalf("Unexpected Error: %s", err)
}
if len(items) != 2 {
t.Fatalf("Should return 2 namespaced results")
}
})
t.Run("FetchNamespacedResults with SeverityFilter", func(t *testing.T) {
items, err := store.FetchNamespacedResults(v1.Filter{Severities: []string{report.High}})
if err != nil {
t.Fatalf("Unexpected Error: %s", err)
}
if len(items) != 1 {
t.Fatalf("Should return 1 namespaced result")
}
if items[0].Severity != report.High {
t.Fatalf("result with severity high")
}
})
t.Run("FetchClusterResults", func(t *testing.T) {
items, err := store.FetchClusterResults(v1.Filter{Status: []string{report.Pass, report.Fail}})
if err != nil {
t.Fatalf("Unexpected Error: %s", err)
}
if len(items) != 2 {
t.Fatalf("Should return 2 cluster results")
}
})
t.Run("FetchClusterResults with SeverityFilter", func(t *testing.T) {
items, err := store.FetchClusterResults(v1.Filter{Severities: []string{report.High}})
if err != nil {
t.Fatalf("Unexpected Error: %s", err)
}
if len(items) != 1 {
t.Fatalf("Should return 1 namespaced result")
}
if items[0].Severity != report.High {
t.Fatalf("result with severity high")
}
})
t.Run("FetchStatusCounts with StatusFilter", func(t *testing.T) {
items, err := store.FetchStatusCounts(v1.Filter{Status: []string{report.Pass}})
if err != nil {
t.Fatalf("Unexpected Error: %s", err)
}
if len(items) != 1 {
t.Fatalf("Should have only 1 item for pass counts")
}
if items[0].Status != report.Pass {
t.Errorf("Expected Pass Counts")
}
if items[0].Count != 1 {
t.Errorf("Expected count to be one for pass")
}
})
t.Run("FetchNamespaces", func(t *testing.T) {
items, err := store.FetchNamespaces("kyverno")
if err != nil {
t.Fatalf("Unexpected Error: %s", err)
}
if len(items) != 1 {
t.Errorf("Should find 1 Namespace")
}
if items[0] != "test" {
t.Errorf("Should return test namespace")
}
})
t.Run("FetchCategories", func(t *testing.T) {
items, err := store.FetchCategories("kyverno")
if err != nil {
t.Fatalf("Unexpected Error: %s", err)
}
if len(items) != 3 {
t.Errorf("Should Find 2 Categories")
}
if items[0] != "Best Practices" {
t.Errorf("Should return 'Best Practices' as first category")
}
})
t.Run("FetchClusterPolicies", func(t *testing.T) {
items, err := store.FetchClusterPolicies("kyverno")
if err != nil {
t.Fatalf("Unexpected Error: %s", err)
}
if len(items) != 1 {
t.Errorf("Should Find 1 cluster scoped Policy")
}
if items[0] != "require-ns-labels" {
t.Errorf("Should return 'require-ns-labels' policy")
}
})
t.Run("FetchNamespacedPolicies", func(t *testing.T) {
items, err := store.FetchNamespacedPolicies("kyverno")
if err != nil {
t.Fatalf("Unexpected Error: %s", err)
}
if len(items) != 1 {
t.Errorf("Should find 1 namespace scoped policy")
}
if items[0] != "require-requests-and-limits-required" {
t.Errorf("Should return 'require-requests-and-limits-required' policy")
}
})
t.Run("FetchClusterSources", func(t *testing.T) {
items, err := store.FetchClusterSources()
if err != nil {
t.Fatalf("Unexpected Error: %s", err)
}
if len(items) != 1 {
t.Errorf("Should find 1 Source")
}
if items[0] != "Kyverno" {
t.Errorf("Should return Kyverno")
}
})
t.Run("FetchNamespacedSources", func(t *testing.T) {
items, err := store.FetchNamespacedSources()
if err != nil {
t.Fatalf("Unexpected Error: %s", err)
}
if len(items) != 1 {
t.Errorf("Should find 1 Source")
}
if items[0] != "Kyverno" {
t.Errorf("Should return Kyverno")
}
})
t.Run("Delete/Get", func(t *testing.T) {
_, ok := store.Get(preport.GetIdentifier())
if ok == false {
t.Errorf("Should be found in Store after adding report to the store")
}
store.Remove(preport.GetIdentifier())
_, ok = store.Get(preport.GetIdentifier())
if ok == true {
t.Fatalf("Should not be found after Remove report from Store")
}
})
t.Run("CleanUp", func(t *testing.T) {
store.Add(preport)
store.CleanUp()
list, _ := store.FetchNamespacedResults(v1.Filter{})
if len(list) == 1 {
t.Fatalf("Should have no results after CleanUp")
}
})
}

View file

@ -1,17 +1,67 @@
package target package target
import ( import (
"strings"
"github.com/kyverno/policy-reporter/pkg/report" "github.com/kyverno/policy-reporter/pkg/report"
) )
// Client for a provided Target // Client for a provided Target
type Client interface { type Client interface {
// Send the given Result to the configured Target // Send the given Result to the configured Target
Send(result report.Result) Send(result *report.Result)
// SkipExistingOnStartup skips already existing PolicyReportResults on startup // SkipExistingOnStartup skips already existing PolicyReportResults on startup
SkipExistingOnStartup() bool SkipExistingOnStartup() bool
// Name is a unique identifier for each Target // Name is a unique identifier for each Target
Name() string Name() string
// Validate is a result should send
Validate(result *report.Result) bool
// MinimumPriority for a triggered Result to send to this target // MinimumPriority for a triggered Result to send to this target
MinimumPriority() string MinimumPriority() string
// Sources of the Results which should send to this target, empty means all sources
Sources() []string
}
type BaseClient struct {
minimumPriority string
sources []string
skipExistingOnStartup bool
}
func (c *BaseClient) MinimumPriority() string {
return c.minimumPriority
}
func (c *BaseClient) Sources() []string {
return c.sources
}
func (c *BaseClient) SkipExistingOnStartup() bool {
return c.skipExistingOnStartup
}
func (c *BaseClient) Validate(result *report.Result) bool {
if result.Priority < report.NewPriority(c.minimumPriority) {
return false
}
if len(c.sources) > 0 && !contains(result.Source, c.sources) {
return false
}
return true
}
func contains(source string, sources []string) bool {
for _, s := range sources {
if strings.EqualFold(s, source) {
return true
}
}
return false
}
func NewBaseClient(minimumPriority string, sources []string, skipExistingOnStartup bool) BaseClient {
return BaseClient{minimumPriority, sources, skipExistingOnStartup}
} }

75
pkg/target/client_test.go Normal file
View file

@ -0,0 +1,75 @@
package target_test
import (
"testing"
"github.com/kyverno/policy-reporter/pkg/report"
"github.com/kyverno/policy-reporter/pkg/target"
)
var result = &report.Result{
Message: "validation error: requests and limits required. Rule autogen-check-for-requests-and-limits failed at path /spec/template/spec/containers/0/resources/requests/",
Policy: "require-requests-and-limits-required",
Rule: "autogen-check-for-requests-and-limits",
Priority: report.WarningPriority,
Status: report.Fail,
Severity: report.High,
Category: "resources",
Scored: true,
Source: "Kyverno",
Resource: &report.Resource{
APIVersion: "v1",
Kind: "Deployment",
Name: "nginx",
Namespace: "default",
UID: "536ab69f-1b3c-4bd9-9ba4-274a56188409",
},
}
func Test_BaseClient(t *testing.T) {
t.Run("Validate Default", func(t *testing.T) {
client := target.NewBaseClient("", []string{}, false)
if !client.Validate(result) {
t.Errorf("Unexpected Validation Result")
}
})
t.Run("Validate MinimumPriority", func(t *testing.T) {
client := target.NewBaseClient("error", []string{}, false)
if client.Validate(result) {
t.Errorf("Unexpected Validation Result")
}
})
t.Run("Validate Source", func(t *testing.T) {
client := target.NewBaseClient("", []string{"jsPolicy"}, false)
if client.Validate(result) {
t.Errorf("Unexpected Validation Result")
}
})
t.Run("SkipExistingOnStartup", func(t *testing.T) {
client := target.NewBaseClient("", []string{}, true)
if !client.SkipExistingOnStartup() {
t.Error("Should return configured SkipExistingOnStartup")
}
})
t.Run("MinimumPriority", func(t *testing.T) {
client := target.NewBaseClient("error", []string{}, true)
if client.MinimumPriority() != "error" {
t.Error("Should return configured MinimumPriority")
}
})
t.Run("Sources", func(t *testing.T) {
client := target.NewBaseClient("", []string{"Kyverno"}, true)
if len(client.Sources()) != 1 {
t.Fatal("Unexpected length of Sources")
}
if client.Sources()[0] != "Kyverno" {
t.Error("Unexptected Source returned")
}
})
}

View file

@ -1,15 +1,12 @@
package discord package discord
import ( import (
"bytes"
"encoding/json"
"log"
"net/http" "net/http"
"strings" "strings"
"github.com/kyverno/policy-reporter/pkg/helper"
"github.com/kyverno/policy-reporter/pkg/report" "github.com/kyverno/policy-reporter/pkg/report"
"github.com/kyverno/policy-reporter/pkg/target" "github.com/kyverno/policy-reporter/pkg/target"
"github.com/kyverno/policy-reporter/pkg/target/helper"
) )
type payload struct { type payload struct {
@ -30,20 +27,16 @@ type embedField struct {
Inline bool `json:"inline"` Inline bool `json:"inline"`
} }
func newPayload(result report.Result) payload { var colors = map[report.Priority]string{
var color string report.DebugPriority: "12370112",
switch result.Priority { report.InfoPriority: "3066993",
case report.CriticalPriority: report.WarningPriority: "15105570",
color = "15158332" report.CriticalPriority: "15158332",
case report.ErrorPriority: report.ErrorPriority: "15158332",
color = "15158332" }
case report.WarningPriority:
color = "15105570" func newPayload(result *report.Result) payload {
case report.InfoPriority: color := colors[result.Priority]
color = "3066993"
case report.DebugPriority:
color = "12370112"
}
embedFields := make([]embedField, 0) embedFields := make([]embedField, 0)
@ -94,28 +87,14 @@ type httpClient interface {
} }
type client struct { type client struct {
webhook string target.BaseClient
minimumPriority string webhook string
skipExistingOnStartup bool client httpClient
client httpClient
} }
func (d *client) Send(result report.Result) { func (d *client) Send(result *report.Result) {
if result.Priority < report.NewPriority(d.minimumPriority) { req, err := helper.CreateJSONRequest(d.Name(), "POST", d.webhook, newPayload(result))
return
}
payload := newPayload(result)
body := new(bytes.Buffer)
if err := json.NewEncoder(body).Encode(payload); err != nil {
log.Printf("[ERROR] DISCORD : %v\n", err.Error())
return
}
req, err := http.NewRequest("POST", d.webhook, body)
if err != nil { if err != nil {
log.Printf("[ERROR] DISCORD : %v\n", err.Error())
return return
} }
@ -123,27 +102,18 @@ func (d *client) Send(result report.Result) {
req.Header.Add("User-Agent", "Policy-Reporter") req.Header.Add("User-Agent", "Policy-Reporter")
resp, err := d.client.Do(req) resp, err := d.client.Do(req)
helper.HandleHTTPResponse("DISCORD", resp, err) helper.ProcessHTTPResponse(d.Name(), resp, err)
}
func (d *client) SkipExistingOnStartup() bool {
return d.skipExistingOnStartup
} }
func (d *client) Name() string { func (d *client) Name() string {
return "Discord" return "Discord"
} }
func (d *client) MinimumPriority() string {
return d.minimumPriority
}
// NewClient creates a new loki.client to send Results to Discord // NewClient creates a new loki.client to send Results to Discord
func NewClient(webhook, minimumPriority string, skipExistingOnStartup bool, httpClient httpClient) target.Client { func NewClient(webhook, minimumPriority string, sources []string, skipExistingOnStartup bool, httpClient httpClient) target.Client {
return &client{ return &client{
target.NewBaseClient(minimumPriority, sources, skipExistingOnStartup),
webhook, webhook,
minimumPriority,
skipExistingOnStartup,
httpClient, httpClient,
} }
} }

View file

@ -9,7 +9,7 @@ import (
"github.com/kyverno/policy-reporter/pkg/target/discord" "github.com/kyverno/policy-reporter/pkg/target/discord"
) )
var completeResult = report.Result{ var completeResult = &report.Result{
Message: "validation error: requests and limits required. Rule autogen-check-for-requests-and-limits failed at path /spec/template/spec/containers/0/resources/requests/", Message: "validation error: requests and limits required. Rule autogen-check-for-requests-and-limits failed at path /spec/template/spec/containers/0/resources/requests/",
Policy: "require-requests-and-limits-required", Policy: "require-requests-and-limits-required",
Rule: "autogen-check-for-requests-and-limits", Rule: "autogen-check-for-requests-and-limits",
@ -19,7 +19,8 @@ var completeResult = report.Result{
Severity: report.High, Severity: report.High,
Category: "resources", Category: "resources",
Scored: true, Scored: true,
Resource: report.Resource{ Source: "Kyverno",
Resource: &report.Resource{
APIVersion: "v1", APIVersion: "v1",
Kind: "Deployment", Kind: "Deployment",
Name: "nginx", Name: "nginx",
@ -29,10 +30,10 @@ var completeResult = report.Result{
Properties: map[string]string{"version": "1.2.0"}, Properties: map[string]string{"version": "1.2.0"},
} }
var minimalResult = report.Result{ var minimalResult = &report.Result{
Message: "validation error: label required. Rule app-label-required failed at path /spec/template/spec/containers/0/resources/requests/", Message: "validation error: label required. Rule app-label-required failed at path /spec/template/spec/containers/0/resources/requests/",
Policy: "app-label-requirement", Policy: "app-label-requirement",
Priority: report.WarningPriority, Priority: report.CriticalPriority,
Status: report.Fail, Status: report.Fail,
Scored: true, Scored: true,
} }
@ -66,7 +67,7 @@ func Test_LokiTarget(t *testing.T) {
} }
} }
client := discord.NewClient("http://hook.discord:80", "", false, testClient{callback, 200}) client := discord.NewClient("http://hook.discord:80", "", []string{}, false, testClient{callback, 200})
client.Send(completeResult) client.Send(completeResult)
}) })
@ -85,40 +86,14 @@ func Test_LokiTarget(t *testing.T) {
} }
} }
client := discord.NewClient("http://hook.discord:80", "", false, testClient{callback, 200}) client := discord.NewClient("http://hook.discord:80", "", []string{}, false, testClient{callback, 200})
client.Send(minimalResult) client.Send(minimalResult)
}) })
t.Run("Send with ingored Priority", func(t *testing.T) {
callback := func(req *http.Request) {
t.Errorf("Unexpected Call")
}
client := discord.NewClient("http://localhost:9200", "error", false, testClient{callback, 200})
client.Send(completeResult)
})
t.Run("SkipExistingOnStartup", func(t *testing.T) {
callback := func(req *http.Request) {
t.Errorf("Unexpected Call")
}
client := discord.NewClient("http://localhost:9200", "", true, testClient{callback, 200})
if !client.SkipExistingOnStartup() {
t.Error("Should return configured SkipExistingOnStartup")
}
})
t.Run("Name", func(t *testing.T) { t.Run("Name", func(t *testing.T) {
client := discord.NewClient("http://localhost:9200", "", true, testClient{}) client := discord.NewClient("http://localhost:9200", "", []string{}, true, testClient{})
if client.Name() != "Discord" { if client.Name() != "Discord" {
t.Errorf("Unexpected Name %s", client.Name()) t.Errorf("Unexpected Name %s", client.Name())
} }
}) })
t.Run("MinimumPriority", func(t *testing.T) {
client := discord.NewClient("http://localhost:9200", "debug", true, testClient{})
if client.MinimumPriority() != "debug" {
t.Errorf("Unexpected MinimumPriority %s", client.MinimumPriority())
}
})
} }

View file

@ -1,15 +1,12 @@
package elasticsearch package elasticsearch
import ( import (
"bytes"
"encoding/json"
"log"
"net/http" "net/http"
"time" "time"
"github.com/kyverno/policy-reporter/pkg/helper"
"github.com/kyverno/policy-reporter/pkg/report" "github.com/kyverno/policy-reporter/pkg/report"
"github.com/kyverno/policy-reporter/pkg/target" "github.com/kyverno/policy-reporter/pkg/target"
"github.com/kyverno/policy-reporter/pkg/target/helper"
) )
// Rotation Enum // Rotation Enum
@ -28,26 +25,14 @@ type httpClient interface {
} }
type client struct { type client struct {
host string target.BaseClient
index string host string
rotation Rotation index string
minimumPriority string rotation Rotation
skipExistingOnStartup bool client httpClient
client httpClient
} }
func (e *client) Send(result report.Result) { func (e *client) Send(result *report.Result) {
if result.Priority < report.NewPriority(e.minimumPriority) {
return
}
body := new(bytes.Buffer)
if err := json.NewEncoder(body).Encode(result); err != nil {
log.Printf("[ERROR] ELASTICSEARCH : %v\n", err.Error())
return
}
var host string var host string
switch e.rotation { switch e.rotation {
case None: case None:
@ -60,9 +45,8 @@ func (e *client) Send(result report.Result) {
host = e.host + "/" + e.index + "-" + time.Now().Format("2006.01.02") + "/event" host = e.host + "/" + e.index + "-" + time.Now().Format("2006.01.02") + "/event"
} }
req, err := http.NewRequest("POST", host, body) req, err := helper.CreateJSONRequest(e.Name(), "POST", host, result)
if err != nil { if err != nil {
log.Printf("[ERROR] ELASTICSEARCH : %v\n", err.Error())
return return
} }
@ -70,29 +54,20 @@ func (e *client) Send(result report.Result) {
req.Header.Add("User-Agent", "Policy-Reporter") req.Header.Add("User-Agent", "Policy-Reporter")
resp, err := e.client.Do(req) resp, err := e.client.Do(req)
helper.HandleHTTPResponse("ELASTICSEARCH", resp, err) helper.ProcessHTTPResponse(e.Name(), resp, err)
}
func (e *client) SkipExistingOnStartup() bool {
return e.skipExistingOnStartup
} }
func (e *client) Name() string { func (e *client) Name() string {
return "Elasticsearch" return "Elasticsearch"
} }
func (e *client) MinimumPriority() string {
return e.minimumPriority
}
// NewClient creates a new loki.client to send Results to Elasticsearch // NewClient creates a new loki.client to send Results to Elasticsearch
func NewClient(host, index, rotation, minimumPriority string, skipExistingOnStartup bool, httpClient httpClient) target.Client { func NewClient(host, index, rotation, minimumPriority string, sources []string, skipExistingOnStartup bool, httpClient httpClient) target.Client {
return &client{ return &client{
target.NewBaseClient(minimumPriority, sources, skipExistingOnStartup),
host, host,
index, index,
rotation, rotation,
minimumPriority,
skipExistingOnStartup,
httpClient, httpClient,
} }
} }

View file

@ -9,7 +9,7 @@ import (
"github.com/kyverno/policy-reporter/pkg/target/elasticsearch" "github.com/kyverno/policy-reporter/pkg/target/elasticsearch"
) )
var completeResult = report.Result{ var completeResult = &report.Result{
Message: "validation error: requests and limits required. Rule autogen-check-for-requests-and-limits failed at path /spec/template/spec/containers/0/resources/requests/", Message: "validation error: requests and limits required. Rule autogen-check-for-requests-and-limits failed at path /spec/template/spec/containers/0/resources/requests/",
Policy: "require-requests-and-limits-required", Policy: "require-requests-and-limits-required",
Rule: "autogen-check-for-requests-and-limits", Rule: "autogen-check-for-requests-and-limits",
@ -18,8 +18,9 @@ var completeResult = report.Result{
Status: report.Fail, Status: report.Fail,
Severity: report.High, Severity: report.High,
Category: "resources", Category: "resources",
Source: "Kyverno",
Scored: true, Scored: true,
Resource: report.Resource{ Resource: &report.Resource{
APIVersion: "v1", APIVersion: "v1",
Kind: "Deployment", Kind: "Deployment",
Name: "nginx", Name: "nginx",
@ -58,7 +59,7 @@ func Test_ElasticsearchTarget(t *testing.T) {
} }
} }
client := elasticsearch.NewClient("http://localhost:9200", "policy-reporter", "annually", "", false, testClient{callback, 200}) client := elasticsearch.NewClient("http://localhost:9200", "policy-reporter", "annually", "", []string{}, false, testClient{callback, 200})
client.Send(completeResult) client.Send(completeResult)
}) })
t.Run("Send with Monthly Result", func(t *testing.T) { t.Run("Send with Monthly Result", func(t *testing.T) {
@ -68,7 +69,7 @@ func Test_ElasticsearchTarget(t *testing.T) {
} }
} }
client := elasticsearch.NewClient("http://localhost:9200", "policy-reporter", "monthly", "", false, testClient{callback, 200}) client := elasticsearch.NewClient("http://localhost:9200", "policy-reporter", "monthly", "", []string{}, false, testClient{callback, 200})
client.Send(completeResult) client.Send(completeResult)
}) })
t.Run("Send with Monthly Result", func(t *testing.T) { t.Run("Send with Monthly Result", func(t *testing.T) {
@ -78,7 +79,7 @@ func Test_ElasticsearchTarget(t *testing.T) {
} }
} }
client := elasticsearch.NewClient("http://localhost:9200", "policy-reporter", "daily", "", false, testClient{callback, 200}) client := elasticsearch.NewClient("http://localhost:9200", "policy-reporter", "daily", "", []string{}, false, testClient{callback, 200})
client.Send(completeResult) client.Send(completeResult)
}) })
t.Run("Send with None Result", func(t *testing.T) { t.Run("Send with None Result", func(t *testing.T) {
@ -88,40 +89,14 @@ func Test_ElasticsearchTarget(t *testing.T) {
} }
} }
client := elasticsearch.NewClient("http://localhost:9200", "policy-reporter", "none", "", false, testClient{callback, 200}) client := elasticsearch.NewClient("http://localhost:9200", "policy-reporter", "none", "", []string{}, false, testClient{callback, 200})
client.Send(completeResult) client.Send(completeResult)
}) })
t.Run("Send with ignored Priority", func(t *testing.T) {
callback := func(req *http.Request) {
t.Errorf("Unexpected Call")
}
client := elasticsearch.NewClient("http://localhost:9200", "policy-reporter", "none", "error", false, testClient{callback, 200})
client.Send(completeResult)
})
t.Run("SkipExistingOnStartup", func(t *testing.T) {
callback := func(req *http.Request) {
t.Errorf("Unexpected Call")
}
client := elasticsearch.NewClient("http://localhost:9200", "policy-reporter", "none", "", true, testClient{callback, 200})
if !client.SkipExistingOnStartup() {
t.Error("Should return configured SkipExistingOnStartup")
}
})
t.Run("Name", func(t *testing.T) { t.Run("Name", func(t *testing.T) {
client := elasticsearch.NewClient("http://localhost:9200", "policy-reporter", "none", "", true, testClient{}) client := elasticsearch.NewClient("http://localhost:9200", "policy-reporter", "none", "", []string{}, true, testClient{})
if client.Name() != "Elasticsearch" { if client.Name() != "Elasticsearch" {
t.Errorf("Unexpected Name %s", client.Name()) t.Errorf("Unexpected Name %s", client.Name())
} }
}) })
t.Run("MinimumPriority", func(t *testing.T) {
client := elasticsearch.NewClient("http://localhost:9200", "policy-reporter", "none", "debug", true, testClient{})
if client.MinimumPriority() != "debug" {
t.Errorf("Unexpected MinimumPriority %s", client.MinimumPriority())
}
})
} }

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