mirror of
https://github.com/kyverno/policy-reporter.git
synced 2024-12-14 11:57:32 +00:00
parent
92c0e51a89
commit
22f68c788d
38 changed files with 1092 additions and 42 deletions
|
@ -1,5 +1,10 @@
|
|||
# Changelog
|
||||
|
||||
## 0.16.0
|
||||
|
||||
* New Optional REST API
|
||||
* New Optional Policy Reporter UI Helm SubChart
|
||||
|
||||
## 0.15.1
|
||||
|
||||
* Add a checksum for the target configuration secret to the deployment. This enforces a pod recreation when the configuration changed by a Helm upgrade.
|
||||
|
|
31
README.md
31
README.md
|
@ -16,6 +16,7 @@ This project is in an early stage. Please let me know if anything did not work a
|
|||
* [Customization](#customization)
|
||||
* [Configure Policy Priorities](#configure-policy-priorities)
|
||||
* [Configure Monitoring](#monitoring)
|
||||
* [Policy Report UI](#policy-report-ui)
|
||||
|
||||
## Installation with Helm v3
|
||||
|
||||
|
@ -236,6 +237,36 @@ If you are not using the MonitoringStack you can import the dashboards from [Gra
|
|||
|
||||
![ClusterPolicyReporter Details Grafana Dashboard](https://github.com/fjogeleit/policy-reporter/blob/main/docs/images/cluster-policy-details.png?raw=true)
|
||||
|
||||
## Policy Report UI
|
||||
|
||||
If you don't have any supported Monitoring solution running, you can use the standalone Policy Report UI.
|
||||
|
||||
The UI is provided as optional Helm Sub Chart and can be enabled by setting `ui.enabled` to `true`.
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
helm install policy-reporter policy-reporter/policy-reporter --set ui.enabled=true -n policy-reporter --create-namespace
|
||||
```
|
||||
|
||||
### Access it with Port Forward on localhost
|
||||
|
||||
```bash
|
||||
kubectl port-forward service/policy-reporter-ui 8082:8080 -n policy-reporter
|
||||
```
|
||||
|
||||
Open `http://localhost:8082/` in your browser.
|
||||
|
||||
### Exmaple
|
||||
|
||||
The UI is an optional application and provides three different views with informations about the validation status of your audit policies.
|
||||
|
||||
![Dashboard](https://github.com/fjogeleit/policy-reporter-ui/blob/main/docs/images/dashboard.png?raw=true)
|
||||
|
||||
![Policy Reports](https://github.com/fjogeleit/policy-reporter-ui/blob/main/docs/images/policy-report.png?raw=true)
|
||||
|
||||
![ClusterPolicyReports](https://github.com/fjogeleit/policy-reporter-ui/blob/main/docs/images/cluster-policy-report.png?raw=true)
|
||||
|
||||
# Todos
|
||||
* ~~Support for ClusterPolicyReports~~
|
||||
* ~~Additional Targets~~
|
||||
|
|
|
@ -3,9 +3,11 @@ name: policy-reporter
|
|||
description: K8s PolicyReporter watches for wgpolicyk8s.io/v1alpha1.PolicyReport resources. It creates Prometheus Metrics and can send rule validation events to different targets like Loki, Elasticsearch, Slack or Discord
|
||||
|
||||
type: application
|
||||
version: 0.15.1
|
||||
appVersion: 0.11.1
|
||||
version: 0.16.0
|
||||
appVersion: 0.12.0
|
||||
|
||||
dependencies:
|
||||
- name: monitoring
|
||||
condition: monitoring.enabled
|
||||
condition: monitoring.enabled
|
||||
- name: policy-reporter-ui
|
||||
condition: ui.enabled
|
|
@ -0,0 +1,7 @@
|
|||
apiVersion: v2
|
||||
name: policy-reporter-ui
|
||||
description: Policy Reporter UI
|
||||
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: 0.1.0
|
|
@ -0,0 +1,51 @@
|
|||
{{/*
|
||||
Create a default fully qualified app name.
|
||||
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||
If release name contains chart name it will be used as a full name.
|
||||
*/}}
|
||||
{{- define "ui.fullname" -}}
|
||||
{{- $name := .Chart.Name }}
|
||||
{{- if contains .Release.Name $name }}
|
||||
{{- $name | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "ui.name" -}}
|
||||
{{- "policy-reporter-ui" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create chart name and version as used by the chart label.
|
||||
*/}}
|
||||
{{- define "ui.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Common labels
|
||||
*/}}
|
||||
{{- define "ui.labels" -}}
|
||||
helm.sh/chart: {{ include "ui.chart" . }}
|
||||
{{ include "ui.selectorLabels" . }}
|
||||
{{- if .Chart.AppVersion }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
{{- end }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Selector labels
|
||||
*/}}
|
||||
{{- define "ui.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "ui.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create the name of the service account to use
|
||||
*/}}
|
||||
{{- define "ui.serviceAccountName" -}}
|
||||
{{ include "ui.fullname" . }}-sa
|
||||
{{- end }}
|
|
@ -0,0 +1,42 @@
|
|||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "ui.fullname" . }}
|
||||
labels:
|
||||
{{- include "ui.labels" . | nindent 4 }}
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "ui.selectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "ui.selectorLabels" . | nindent 8 }}
|
||||
spec:
|
||||
{{- with .Values.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
serviceAccountName: {{ include "ui.serviceAccountName" . }}
|
||||
automountServiceAccountToken: true
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
args:
|
||||
- -backend={{ .Values.backend }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 8080
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
|
@ -0,0 +1,15 @@
|
|||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "ui.fullname" . }}
|
||||
labels:
|
||||
{{- include "ui.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: ClusterIP
|
||||
ports:
|
||||
- port: 8080
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector:
|
||||
{{- include "ui.selectorLabels" . | nindent 4 }}
|
|
@ -0,0 +1,6 @@
|
|||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: {{ include "ui.serviceAccountName" . }}
|
||||
labels:
|
||||
{{- include "ui.labels" . | nindent 4 }}
|
18
charts/policy-reporter/charts/policy-reporter-ui/values.yaml
Normal file
18
charts/policy-reporter/charts/policy-reporter-ui/values.yaml
Normal file
|
@ -0,0 +1,18 @@
|
|||
enabled: false
|
||||
|
||||
image:
|
||||
repository: fjogeleit/policy-reporter-ui
|
||||
pullPolicy: IfNotPresent
|
||||
tag: 0.1.0
|
||||
|
||||
imagePullSecrets: []
|
||||
|
||||
backend: http://policy-reporter:8080
|
||||
|
||||
resources:
|
||||
requests:
|
||||
memory: 50Mi
|
||||
cpu: 10m
|
||||
limits:
|
||||
memory: 100Mi
|
||||
cpu: 50m
|
|
@ -32,10 +32,18 @@ spec:
|
|||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
args:
|
||||
- --config=/app/config.yaml
|
||||
{{- if or .Values.api.enabled .Values.ui.enabled }}
|
||||
- --apiPort=8080
|
||||
{{- end }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 2112
|
||||
protocol: TCP
|
||||
{{- if or .Values.api.enabled .Values.ui.enabled }}
|
||||
- name: rest
|
||||
containerPort: 8080
|
||||
protocol: TCP
|
||||
{{- end }}
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /metrics
|
||||
|
|
|
@ -5,11 +5,17 @@ metadata:
|
|||
labels:
|
||||
{{- include "policyreporter.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
type: ClusterIP
|
||||
ports:
|
||||
- port: {{ .Values.service.port }}
|
||||
- port: 2112
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
name: http
|
||||
{{- if or .Values.api.enabled .Values.ui.enabled }}
|
||||
- port: 8080
|
||||
targetPort: rest
|
||||
protocol: TCP
|
||||
name: rest
|
||||
{{- end }}
|
||||
selector:
|
||||
{{- include "policyreporter.selectorLabels" . | nindent 4 }}
|
||||
|
|
|
@ -1,24 +1,20 @@
|
|||
image:
|
||||
repository: fjogeleit/policy-reporter
|
||||
pullPolicy: IfNotPresent
|
||||
tag: 0.11.1
|
||||
tag: 0.12.0
|
||||
|
||||
imagePullSecrets: []
|
||||
|
||||
deployment:
|
||||
annotations: {}
|
||||
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 2112
|
||||
|
||||
resources:
|
||||
requests:
|
||||
memory: 50Mi
|
||||
cpu: 10m
|
||||
memory: 20Mi
|
||||
cpu: 5m
|
||||
limits:
|
||||
memory: 100Mi
|
||||
cpu: 50m
|
||||
memory: 30Mi
|
||||
cpu: 10m
|
||||
|
||||
monitoring:
|
||||
enabled: false
|
||||
|
@ -28,6 +24,9 @@ monitoring:
|
|||
# labels to match the serviceMonitorSelector of the Prometheus Resource
|
||||
labels: {}
|
||||
|
||||
api:
|
||||
enabled: false
|
||||
|
||||
loki:
|
||||
# loki host address
|
||||
host: ""
|
||||
|
|
|
@ -27,6 +27,7 @@ func loadConfig(cmd *cobra.Command) (*config.Config, error) {
|
|||
v := viper.New()
|
||||
|
||||
v.SetDefault("namespace", "policy-reporter")
|
||||
v.SetDefault("api.port", 8080)
|
||||
|
||||
cfgFile := ""
|
||||
|
||||
|
@ -62,6 +63,11 @@ func loadConfig(cmd *cobra.Command) (*config.Config, error) {
|
|||
v.BindPFlag("kubeconfig", flag)
|
||||
}
|
||||
|
||||
if flag := cmd.Flags().Lookup("apiPort"); flag != nil {
|
||||
v.BindPFlag("api.port", flag)
|
||||
v.BindPFlag("api.enabled", flag)
|
||||
}
|
||||
|
||||
c := &config.Config{}
|
||||
|
||||
err := v.Unmarshal(c)
|
||||
|
|
|
@ -75,6 +75,11 @@ func newRunCMD() *cobra.Command {
|
|||
}
|
||||
|
||||
g := new(errgroup.Group)
|
||||
|
||||
if c.API.Enabled {
|
||||
g.Go(resolver.APIServer().Start)
|
||||
}
|
||||
|
||||
g.Go(cpClient.StartWatching)
|
||||
g.Go(pClient.StartWatching)
|
||||
g.Go(func() error {
|
||||
|
@ -90,6 +95,7 @@ func newRunCMD() *cobra.Command {
|
|||
// For local usage
|
||||
cmd.PersistentFlags().StringP("kubeconfig", "k", "", "absolute path to the kubeconfig file")
|
||||
cmd.PersistentFlags().StringP("config", "c", "", "target configuration file")
|
||||
cmd.PersistentFlags().IntP("apiPort", "a", 0, "http port for the optional rest api")
|
||||
|
||||
cmd.PersistentFlags().String("loki", "", "loki host: http://loki:3100")
|
||||
cmd.PersistentFlags().String("loki-minimum-priority", "", "Minimum Priority to send Results to Loki (info < warning < error)")
|
||||
|
|
78
pkg/api/handler.go
Normal file
78
pkg/api/handler.go
Normal file
|
@ -0,0 +1,78 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/fjogeleit/policy-reporter/pkg/report"
|
||||
)
|
||||
|
||||
// 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()
|
||||
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.ClusterPolicyReportStore) 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()
|
||||
if len(reports) == 0 {
|
||||
fmt.Fprint(w, "[]")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
apiReports := make([]ClusterPolicyReport, 0, len(reports))
|
||||
for _, r := range reports {
|
||||
apiReports = append(apiReports, mapClusterPolicyReport(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())
|
||||
}
|
||||
}
|
||||
}
|
201
pkg/api/handler_test.go
Normal file
201
pkg/api/handler_test.go
Normal file
|
@ -0,0 +1,201 @@
|
|||
package api_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/fjogeleit/policy-reporter/pkg/api"
|
||||
"github.com/fjogeleit/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 := http.HandlerFunc(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 := http.HandlerFunc(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 := http.HandlerFunc(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,
|
||||
Resources: []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 := http.HandlerFunc(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 := http.HandlerFunc(api.ClusterPolicyReportHandler(report.NewClusterPolicyReportStore()))
|
||||
|
||||
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,
|
||||
Resources: []report.Resource{
|
||||
{
|
||||
APIVersion: "v1",
|
||||
Kind: "Namespace",
|
||||
Name: "dev",
|
||||
UID: "536ab69f-1b3c-4bd9-9ba4-274a56188409",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
creport := report.ClusterPolicyReport{
|
||||
Name: "cpolr-test",
|
||||
Summary: report.Summary{},
|
||||
CreationTimestamp: time.Now(),
|
||||
Results: map[string]report.Result{"": result},
|
||||
}
|
||||
|
||||
store := report.NewClusterPolicyReportStore()
|
||||
store.Add(creport)
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
handler := http.HandlerFunc(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)
|
||||
}
|
||||
})
|
||||
}
|
152
pkg/api/model.go
Normal file
152
pkg/api/model.go
Normal file
|
@ -0,0 +1,152 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/fjogeleit/policy-reporter/pkg/report"
|
||||
"github.com/fjogeleit/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"`
|
||||
Resource Resource `json:"resource"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
Results []Result `json:"results"`
|
||||
Summary Summary `json:"summary"`
|
||||
CreationTimestamp time.Time `json:"creationTimestamp"`
|
||||
}
|
||||
|
||||
// ClusterPolicyReport API Model
|
||||
type ClusterPolicyReport struct {
|
||||
Name string `json:"name"`
|
||||
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 {
|
||||
|
||||
results = append(results, 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,
|
||||
Resource: Resource{
|
||||
Namespace: r.Resources[0].Namespace,
|
||||
APIVersion: r.Resources[0].APIVersion,
|
||||
Kind: r.Resources[0].Kind,
|
||||
Name: r.Resources[0].Name,
|
||||
UID: r.Resources[0].UID,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
func mapClusterPolicyReport(c report.ClusterPolicyReport) ClusterPolicyReport {
|
||||
results := make([]Result, 0, len(c.Results))
|
||||
|
||||
for _, r := range c.Results {
|
||||
results = append(results, 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,
|
||||
Resource: Resource{
|
||||
Namespace: r.Resources[0].Namespace,
|
||||
APIVersion: r.Resources[0].APIVersion,
|
||||
Kind: r.Resources[0].Kind,
|
||||
Name: r.Resources[0].Name,
|
||||
UID: r.Resources[0].UID,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return ClusterPolicyReport{
|
||||
Name: c.Name,
|
||||
CreationTimestamp: c.CreationTimestamp,
|
||||
Summary: Summary{
|
||||
Skip: c.Summary.Skip,
|
||||
Pass: c.Summary.Pass,
|
||||
Warn: c.Summary.Warn,
|
||||
Fail: c.Summary.Fail,
|
||||
Error: c.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(),
|
||||
}
|
||||
}
|
58
pkg/api/server.go
Normal file
58
pkg/api/server.go
Normal file
|
@ -0,0 +1,58 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/fjogeleit/policy-reporter/pkg/report"
|
||||
"github.com/fjogeleit/policy-reporter/pkg/target"
|
||||
)
|
||||
|
||||
// Server for the optional HTTP REST API
|
||||
type Server interface {
|
||||
// Start the HTTP REST API
|
||||
Start() error
|
||||
}
|
||||
|
||||
type httpServer struct {
|
||||
port int
|
||||
mux *http.ServeMux
|
||||
pStore *report.PolicyReportStore
|
||||
cStore *report.ClusterPolicyReportStore
|
||||
targets []Target
|
||||
}
|
||||
|
||||
func (s *httpServer) registerHandler() {
|
||||
s.mux.HandleFunc("/policy-reports", PolicyReportHandler(s.pStore))
|
||||
s.mux.HandleFunc("/cluster-policy-reports", ClusterPolicyReportHandler(s.cStore))
|
||||
s.mux.HandleFunc("/targets", TargetsHandler(s.targets))
|
||||
}
|
||||
|
||||
func (s *httpServer) Start() error {
|
||||
server := http.Server{
|
||||
Addr: fmt.Sprintf(":%d", s.port),
|
||||
Handler: s.mux,
|
||||
}
|
||||
|
||||
return server.ListenAndServe()
|
||||
}
|
||||
|
||||
// NewServer constructor for a new API Server
|
||||
func NewServer(pStore *report.PolicyReportStore, cStore *report.ClusterPolicyReportStore, targets []target.Client, port int) Server {
|
||||
apiTargets := make([]Target, 0, len(targets))
|
||||
for _, t := range targets {
|
||||
apiTargets = append(apiTargets, mapTarget(t))
|
||||
}
|
||||
|
||||
s := &httpServer{
|
||||
port: port,
|
||||
targets: apiTargets,
|
||||
cStore: cStore,
|
||||
pStore: pStore,
|
||||
mux: http.NewServeMux(),
|
||||
}
|
||||
|
||||
s.registerHandler()
|
||||
|
||||
return s
|
||||
}
|
26
pkg/api/server_test.go
Normal file
26
pkg/api/server_test.go
Normal file
|
@ -0,0 +1,26 @@
|
|||
package api_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/fjogeleit/policy-reporter/pkg/api"
|
||||
"github.com/fjogeleit/policy-reporter/pkg/report"
|
||||
"github.com/fjogeleit/policy-reporter/pkg/target"
|
||||
"github.com/fjogeleit/policy-reporter/pkg/target/discord"
|
||||
"github.com/fjogeleit/policy-reporter/pkg/target/loki"
|
||||
)
|
||||
|
||||
func Test_NewServer(t *testing.T) {
|
||||
server := api.NewServer(
|
||||
report.NewPolicyReportStore(),
|
||||
report.NewClusterPolicyReportStore(),
|
||||
[]target.Client{
|
||||
loki.NewClient("http://localhost:3100", "debug", true, &http.Client{}),
|
||||
discord.NewClient("http://webhook:2000", "", false, &http.Client{}),
|
||||
},
|
||||
8080,
|
||||
)
|
||||
|
||||
go server.Start()
|
||||
}
|
|
@ -30,12 +30,19 @@ type Discord struct {
|
|||
MinimumPriority string `mapstructure:"minimumPriority"`
|
||||
}
|
||||
|
||||
// Server configuration
|
||||
type API struct {
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
Port int `mapstructure:"port"`
|
||||
}
|
||||
|
||||
// Config of the PolicyReporter
|
||||
type Config struct {
|
||||
Loki Loki `mapstructure:"loki"`
|
||||
Elasticsearch Elasticsearch `mapstructure:"elasticsearch"`
|
||||
Slack Slack `mapstructure:"slack"`
|
||||
Discord Discord `mapstructure:"discord"`
|
||||
API API `mapstructure:"api"`
|
||||
Kubeconfig string `mapstructure:"kubeconfig"`
|
||||
Namespace string `mapstructure:"namespace"`
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/fjogeleit/policy-reporter/pkg/api"
|
||||
"github.com/fjogeleit/policy-reporter/pkg/kubernetes"
|
||||
"github.com/fjogeleit/policy-reporter/pkg/report"
|
||||
"github.com/fjogeleit/policy-reporter/pkg/target"
|
||||
|
@ -23,6 +24,8 @@ type Resolver struct {
|
|||
config *Config
|
||||
k8sConfig *rest.Config
|
||||
mapper kubernetes.Mapper
|
||||
policyStore *report.PolicyReportStore
|
||||
clusterPolicyStore *report.ClusterPolicyReportStore
|
||||
resultClient report.ResultClient
|
||||
policyClient report.PolicyClient
|
||||
clusterPolicyClient report.ClusterPolicyClient
|
||||
|
@ -32,6 +35,16 @@ type Resolver struct {
|
|||
discordClient target.Client
|
||||
}
|
||||
|
||||
// APIServer resolver method
|
||||
func (r *Resolver) APIServer() api.Server {
|
||||
return api.NewServer(
|
||||
r.PolicyReportStore(),
|
||||
r.ClusterPolicyReportStore(),
|
||||
r.TargetClients(),
|
||||
r.config.API.Port,
|
||||
)
|
||||
}
|
||||
|
||||
// PolicyResultClient resolver method
|
||||
func (r *Resolver) PolicyResultClient(ctx context.Context) (report.ResultClient, error) {
|
||||
if r.resultClient != nil {
|
||||
|
@ -48,11 +61,31 @@ func (r *Resolver) PolicyResultClient(ctx context.Context) (report.ResultClient,
|
|||
return nil, err
|
||||
}
|
||||
|
||||
client := kubernetes.NewPolicyResultClient(pClient, cpClient)
|
||||
r.resultClient = kubernetes.NewPolicyResultClient(pClient, cpClient)
|
||||
|
||||
r.resultClient = client
|
||||
return r.resultClient, nil
|
||||
}
|
||||
|
||||
return client, nil
|
||||
// PolicyReportStore resolver method
|
||||
func (r *Resolver) PolicyReportStore() *report.PolicyReportStore {
|
||||
if r.policyStore != nil {
|
||||
return r.policyStore
|
||||
}
|
||||
|
||||
r.policyStore = report.NewPolicyReportStore()
|
||||
|
||||
return r.policyStore
|
||||
}
|
||||
|
||||
// PolicyReportStore resolver method
|
||||
func (r *Resolver) ClusterPolicyReportStore() *report.ClusterPolicyReportStore {
|
||||
if r.clusterPolicyStore != nil {
|
||||
return r.clusterPolicyStore
|
||||
}
|
||||
|
||||
r.clusterPolicyStore = report.NewClusterPolicyReportStore()
|
||||
|
||||
return r.clusterPolicyStore
|
||||
}
|
||||
|
||||
// PolicyReportClient resolver method
|
||||
|
@ -73,6 +106,7 @@ func (r *Resolver) PolicyReportClient(ctx context.Context) (report.PolicyClient,
|
|||
|
||||
client := kubernetes.NewPolicyReportClient(
|
||||
policyAPI,
|
||||
r.PolicyReportStore(),
|
||||
mapper,
|
||||
time.Now(),
|
||||
)
|
||||
|
@ -98,15 +132,14 @@ func (r *Resolver) ClusterPolicyReportClient(ctx context.Context) (report.Cluste
|
|||
return nil, err
|
||||
}
|
||||
|
||||
client := kubernetes.NewClusterPolicyReportClient(
|
||||
r.clusterPolicyClient = kubernetes.NewClusterPolicyReportClient(
|
||||
policyAPI,
|
||||
r.ClusterPolicyReportStore(),
|
||||
mapper,
|
||||
time.Now(),
|
||||
)
|
||||
|
||||
r.clusterPolicyClient = client
|
||||
|
||||
return client, nil
|
||||
return r.clusterPolicyClient, nil
|
||||
}
|
||||
|
||||
// Mapper resolver method
|
||||
|
|
|
@ -224,6 +224,15 @@ func Test_ResolveClusterPolicyClient(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func Test_ResolveAPIServer(t *testing.T) {
|
||||
resolver := config.NewResolver(testConfig, &rest.Config{})
|
||||
|
||||
server := resolver.APIServer()
|
||||
if server == nil {
|
||||
t.Error("Error: Should return API Server")
|
||||
}
|
||||
}
|
||||
|
||||
func Test_ResolveClientWithInvalidK8sConfig(t *testing.T) {
|
||||
k8sConfig := &rest.Config{}
|
||||
k8sConfig.Host = "invalid/url"
|
||||
|
|
|
@ -13,7 +13,7 @@ import (
|
|||
|
||||
type clusterPolicyReportClient struct {
|
||||
policyAPI PolicyReportAdapter
|
||||
cache map[string]report.ClusterPolicyReport
|
||||
store *report.ClusterPolicyReportStore
|
||||
callbacks []report.ClusterPolicyReportCallback
|
||||
resultCallbacks []report.PolicyResultCallback
|
||||
mapper Mapper
|
||||
|
@ -91,7 +91,7 @@ func (c *clusterPolicyReportClient) StartWatching() error {
|
|||
func (c *clusterPolicyReportClient) executeClusterPolicyReportHandler(e watch.EventType, cpr report.ClusterPolicyReport) {
|
||||
opr := report.ClusterPolicyReport{}
|
||||
if e != watch.Added {
|
||||
opr = c.cache[cpr.GetIdentifier()]
|
||||
opr, _ = c.store.Get(cpr.GetIdentifier())
|
||||
}
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
|
@ -112,11 +112,11 @@ func (c *clusterPolicyReportClient) executeClusterPolicyReportHandler(e watch.Ev
|
|||
wg.Wait()
|
||||
|
||||
if e == watch.Deleted {
|
||||
delete(c.cache, cpr.GetIdentifier())
|
||||
c.store.Remove(cpr.GetIdentifier())
|
||||
return
|
||||
}
|
||||
|
||||
c.cache[cpr.GetIdentifier()] = cpr
|
||||
c.store.Add(cpr)
|
||||
}
|
||||
|
||||
func (c *clusterPolicyReportClient) RegisterPolicyResultWatcher(skipExisting bool) {
|
||||
|
@ -145,7 +145,7 @@ func (c *clusterPolicyReportClient) RegisterPolicyResultWatcher(skipExisting boo
|
|||
|
||||
wg.Wait()
|
||||
case watch.Modified:
|
||||
diff := cpr.GetNewResults(c.cache[cpr.GetIdentifier()])
|
||||
diff := cpr.GetNewResults(opr)
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(len(diff) * len(c.resultCallbacks))
|
||||
|
@ -165,10 +165,10 @@ func (c *clusterPolicyReportClient) RegisterPolicyResultWatcher(skipExisting boo
|
|||
}
|
||||
|
||||
// NewPolicyReportClient creates a new PolicyReportClient based on the kubernetes go-client
|
||||
func NewClusterPolicyReportClient(client PolicyReportAdapter, mapper Mapper, startUp time.Time) report.ClusterPolicyClient {
|
||||
func NewClusterPolicyReportClient(client PolicyReportAdapter, store *report.ClusterPolicyReportStore, mapper Mapper, startUp time.Time) report.ClusterPolicyClient {
|
||||
return &clusterPolicyReportClient{
|
||||
policyAPI: client,
|
||||
cache: make(map[string]report.ClusterPolicyReport),
|
||||
store: store,
|
||||
mapper: mapper,
|
||||
startUp: startUp,
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ func Test_FetchClusterPolicyReports(t *testing.T) {
|
|||
|
||||
client := kubernetes.NewClusterPolicyReportClient(
|
||||
fakeAdapter,
|
||||
report.NewClusterPolicyReportStore(),
|
||||
NewMapper(k8sCMClient),
|
||||
time.Now(),
|
||||
)
|
||||
|
@ -54,6 +55,7 @@ func Test_FetchClusterPolicyReportsError(t *testing.T) {
|
|||
|
||||
client := kubernetes.NewClusterPolicyReportClient(
|
||||
fakeAdapter,
|
||||
report.NewClusterPolicyReportStore(),
|
||||
NewMapper(k8sCMClient),
|
||||
time.Now(),
|
||||
)
|
||||
|
@ -75,6 +77,7 @@ func Test_FetchClusterPolicyResults(t *testing.T) {
|
|||
|
||||
client := kubernetes.NewClusterPolicyReportClient(
|
||||
fakeAdapter,
|
||||
report.NewClusterPolicyReportStore(),
|
||||
NewMapper(k8sCMClient),
|
||||
time.Now(),
|
||||
)
|
||||
|
@ -99,6 +102,7 @@ func Test_FetchClusterPolicyResultsError(t *testing.T) {
|
|||
|
||||
client := kubernetes.NewClusterPolicyReportClient(
|
||||
fakeAdapter,
|
||||
report.NewClusterPolicyReportStore(),
|
||||
NewMapper(k8sCMClient),
|
||||
time.Now(),
|
||||
)
|
||||
|
@ -116,6 +120,7 @@ func Test_ClusterPolicyWatcher(t *testing.T) {
|
|||
|
||||
client := kubernetes.NewClusterPolicyReportClient(
|
||||
fakeAdapter,
|
||||
report.NewClusterPolicyReportStore(),
|
||||
NewMapper(k8sCMClient),
|
||||
time.Now(),
|
||||
)
|
||||
|
@ -150,6 +155,7 @@ func Test_ClusterPolicyWatcherTwice(t *testing.T) {
|
|||
|
||||
client := kubernetes.NewClusterPolicyReportClient(
|
||||
fakeAdapter,
|
||||
report.NewClusterPolicyReportStore(),
|
||||
NewMapper(k8sCMClient),
|
||||
time.Now(),
|
||||
)
|
||||
|
@ -204,6 +210,7 @@ func Test_SkipExisting(t *testing.T) {
|
|||
|
||||
client := kubernetes.NewClusterPolicyReportClient(
|
||||
fakeAdapter,
|
||||
report.NewClusterPolicyReportStore(),
|
||||
NewMapper(k8sCMClient),
|
||||
time.Now(),
|
||||
)
|
||||
|
@ -243,6 +250,7 @@ func Test_WatcherError(t *testing.T) {
|
|||
|
||||
client := kubernetes.NewClusterPolicyReportClient(
|
||||
fakeAdapter,
|
||||
report.NewClusterPolicyReportStore(),
|
||||
NewMapper(k8sCMClient),
|
||||
time.Now(),
|
||||
)
|
||||
|
@ -262,6 +270,7 @@ func Test_WatchDeleteEvent(t *testing.T) {
|
|||
|
||||
client := kubernetes.NewClusterPolicyReportClient(
|
||||
fakeAdapter,
|
||||
report.NewClusterPolicyReportStore(),
|
||||
NewMapper(k8sCMClient),
|
||||
time.Now(),
|
||||
)
|
||||
|
@ -297,6 +306,7 @@ func Test_WatchModifiedEvent(t *testing.T) {
|
|||
|
||||
client := kubernetes.NewClusterPolicyReportClient(
|
||||
fakeAdapter,
|
||||
report.NewClusterPolicyReportStore(),
|
||||
NewMapper(k8sCMClient),
|
||||
time.Now(),
|
||||
)
|
||||
|
|
|
@ -13,7 +13,7 @@ import (
|
|||
|
||||
type policyReportClient struct {
|
||||
policyAPI PolicyReportAdapter
|
||||
cache map[string]report.PolicyReport
|
||||
store *report.PolicyReportStore
|
||||
callbacks []report.PolicyReportCallback
|
||||
resultCallbacks []report.PolicyResultCallback
|
||||
mapper Mapper
|
||||
|
@ -91,7 +91,7 @@ func (c *policyReportClient) StartWatching() error {
|
|||
func (c *policyReportClient) executePolicyReportHandler(e watch.EventType, pr report.PolicyReport) {
|
||||
opr := report.PolicyReport{}
|
||||
if e != watch.Added {
|
||||
opr = c.cache[pr.GetIdentifier()]
|
||||
opr, _ = c.store.Get(pr.GetIdentifier())
|
||||
}
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
|
@ -112,11 +112,11 @@ func (c *policyReportClient) executePolicyReportHandler(e watch.EventType, pr re
|
|||
wg.Wait()
|
||||
|
||||
if e == watch.Deleted {
|
||||
delete(c.cache, pr.GetIdentifier())
|
||||
c.store.Remove(pr.GetIdentifier())
|
||||
return
|
||||
}
|
||||
|
||||
c.cache[pr.GetIdentifier()] = pr
|
||||
c.store.Add(pr)
|
||||
}
|
||||
|
||||
func (c *policyReportClient) RegisterPolicyResultWatcher(skipExisting bool) {
|
||||
|
@ -149,10 +149,10 @@ func (c *policyReportClient) RegisterPolicyResultWatcher(skipExisting bool) {
|
|||
}
|
||||
|
||||
// NewPolicyReportClient creates a new PolicyReportClient based on the kubernetes go-client
|
||||
func NewPolicyReportClient(client PolicyReportAdapter, mapper Mapper, startUp time.Time) report.PolicyClient {
|
||||
func NewPolicyReportClient(client PolicyReportAdapter, store *report.PolicyReportStore, mapper Mapper, startUp time.Time) report.PolicyClient {
|
||||
return &policyReportClient{
|
||||
policyAPI: client,
|
||||
cache: make(map[string]report.PolicyReport),
|
||||
store: store,
|
||||
mapper: mapper,
|
||||
startUp: startUp,
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ func Test_FetchPolicyReports(t *testing.T) {
|
|||
|
||||
client := kubernetes.NewPolicyReportClient(
|
||||
fakeAdapter,
|
||||
report.NewPolicyReportStore(),
|
||||
NewMapper(k8sCMClient),
|
||||
time.Now(),
|
||||
)
|
||||
|
@ -53,6 +54,7 @@ func Test_FetchPolicyReportsError(t *testing.T) {
|
|||
|
||||
client := kubernetes.NewPolicyReportClient(
|
||||
fakeAdapter,
|
||||
report.NewPolicyReportStore(),
|
||||
NewMapper(k8sCMClient),
|
||||
time.Now(),
|
||||
)
|
||||
|
@ -71,6 +73,7 @@ func Test_FetchPolicyResults(t *testing.T) {
|
|||
|
||||
client := kubernetes.NewPolicyReportClient(
|
||||
fakeAdapter,
|
||||
report.NewPolicyReportStore(),
|
||||
NewMapper(k8sCMClient),
|
||||
time.Now(),
|
||||
)
|
||||
|
@ -95,6 +98,7 @@ func Test_FetchPolicyResultsError(t *testing.T) {
|
|||
|
||||
client := kubernetes.NewPolicyReportClient(
|
||||
fakeAdapter,
|
||||
report.NewPolicyReportStore(),
|
||||
NewMapper(k8sCMClient),
|
||||
time.Now(),
|
||||
)
|
||||
|
@ -112,6 +116,7 @@ func Test_PolicyWatcher(t *testing.T) {
|
|||
|
||||
client := kubernetes.NewPolicyReportClient(
|
||||
fakeAdapter,
|
||||
report.NewPolicyReportStore(),
|
||||
NewMapper(k8sCMClient),
|
||||
time.Now(),
|
||||
)
|
||||
|
@ -146,6 +151,7 @@ func Test_PolicyWatcherTwice(t *testing.T) {
|
|||
|
||||
client := kubernetes.NewPolicyReportClient(
|
||||
fakeAdapter,
|
||||
report.NewPolicyReportStore(),
|
||||
NewMapper(k8sCMClient),
|
||||
time.Now(),
|
||||
)
|
||||
|
@ -202,6 +208,7 @@ func Test_PolicySkipExisting(t *testing.T) {
|
|||
|
||||
client := kubernetes.NewPolicyReportClient(
|
||||
fakeAdapter,
|
||||
report.NewPolicyReportStore(),
|
||||
NewMapper(k8sCMClient),
|
||||
time.Now(),
|
||||
)
|
||||
|
@ -242,6 +249,7 @@ func Test_PolicyWatcherError(t *testing.T) {
|
|||
|
||||
client := kubernetes.NewPolicyReportClient(
|
||||
fakeAdapter,
|
||||
report.NewPolicyReportStore(),
|
||||
NewMapper(k8sCMClient),
|
||||
time.Now(),
|
||||
)
|
||||
|
@ -261,6 +269,7 @@ func Test_PolicyWatchDeleteEvent(t *testing.T) {
|
|||
|
||||
client := kubernetes.NewPolicyReportClient(
|
||||
fakeAdapter,
|
||||
report.NewPolicyReportStore(),
|
||||
NewMapper(k8sCMClient),
|
||||
time.Now(),
|
||||
)
|
||||
|
@ -296,6 +305,7 @@ func Test_PolicyWatchModifiedEvent(t *testing.T) {
|
|||
|
||||
client := kubernetes.NewPolicyReportClient(
|
||||
fakeAdapter,
|
||||
report.NewPolicyReportStore(),
|
||||
NewMapper(k8sCMClient),
|
||||
time.Now(),
|
||||
)
|
||||
|
|
|
@ -65,8 +65,8 @@ func Test_ResultClient_FetchPolicyResults(t *testing.T) {
|
|||
mapper := NewMapper(k8sCMClient)
|
||||
|
||||
client := kubernetes.NewPolicyResultClient(
|
||||
kubernetes.NewPolicyReportClient(fakeAdapter, mapper, time.Now()),
|
||||
kubernetes.NewClusterPolicyReportClient(fakeAdapter, mapper, time.Now()),
|
||||
kubernetes.NewPolicyReportClient(fakeAdapter, report.NewPolicyReportStore(), mapper, time.Now()),
|
||||
kubernetes.NewClusterPolicyReportClient(fakeAdapter, report.NewClusterPolicyReportStore(), mapper, time.Now()),
|
||||
)
|
||||
|
||||
fakeAdapter.policies = append(fakeAdapter.policies, unstructured.Unstructured{Object: policyMap})
|
||||
|
@ -92,8 +92,8 @@ func Test_ResultClient_FetchPolicyResultsPolicyReportError(t *testing.T) {
|
|||
mapper := NewMapper(k8sCMClient)
|
||||
|
||||
client := kubernetes.NewPolicyResultClient(
|
||||
kubernetes.NewPolicyReportClient(fakeAdapter, mapper, time.Now()),
|
||||
kubernetes.NewClusterPolicyReportClient(fakeAdapter, mapper, time.Now()),
|
||||
kubernetes.NewPolicyReportClient(fakeAdapter, report.NewPolicyReportStore(), mapper, time.Now()),
|
||||
kubernetes.NewClusterPolicyReportClient(fakeAdapter, report.NewClusterPolicyReportStore(), mapper, time.Now()),
|
||||
)
|
||||
|
||||
_, err := client.FetchPolicyResults()
|
||||
|
@ -112,8 +112,8 @@ func Test_ResultClient_FetchPolicyResultsClusterPolicyReportError(t *testing.T)
|
|||
mapper := NewMapper(k8sCMClient)
|
||||
|
||||
client := kubernetes.NewPolicyResultClient(
|
||||
kubernetes.NewPolicyReportClient(fakeAdapter, mapper, time.Now()),
|
||||
kubernetes.NewClusterPolicyReportClient(fakeAdapter, mapper, time.Now()),
|
||||
kubernetes.NewPolicyReportClient(fakeAdapter, report.NewPolicyReportStore(), mapper, time.Now()),
|
||||
kubernetes.NewClusterPolicyReportClient(fakeAdapter, report.NewClusterPolicyReportStore(), mapper, time.Now()),
|
||||
)
|
||||
|
||||
_, err := client.FetchPolicyResults()
|
||||
|
@ -129,8 +129,8 @@ func Test_ResultClient_RegisterPolicyResultWatcher(t *testing.T) {
|
|||
|
||||
mapper := NewMapper(k8sCMClient)
|
||||
|
||||
pClient := kubernetes.NewPolicyReportClient(fakeAdapter, mapper, time.Now())
|
||||
cpClient := kubernetes.NewClusterPolicyReportClient(fakeAdapter, mapper, time.Now())
|
||||
pClient := kubernetes.NewPolicyReportClient(fakeAdapter, report.NewPolicyReportStore(), mapper, time.Now())
|
||||
cpClient := kubernetes.NewClusterPolicyReportClient(fakeAdapter, report.NewClusterPolicyReportStore(), mapper, time.Now())
|
||||
|
||||
client := kubernetes.NewPolicyResultClient(pClient, cpClient)
|
||||
|
||||
|
|
91
pkg/report/store.go
Normal file
91
pkg/report/store.go
Normal file
|
@ -0,0 +1,91 @@
|
|||
package report
|
||||
|
||||
import "sync"
|
||||
|
||||
type PolicyReportStore struct {
|
||||
store map[string]PolicyReport
|
||||
rwm *sync.RWMutex
|
||||
}
|
||||
|
||||
func (s *PolicyReportStore) Get(id string) (PolicyReport, bool) {
|
||||
s.rwm.RLock()
|
||||
r, ok := s.store[id]
|
||||
s.rwm.RUnlock()
|
||||
|
||||
return r, ok
|
||||
}
|
||||
|
||||
func (s *PolicyReportStore) List() []PolicyReport {
|
||||
s.rwm.RLock()
|
||||
list := make([]PolicyReport, 0, len(s.store))
|
||||
|
||||
for _, r := range s.store {
|
||||
list = append(list, r)
|
||||
}
|
||||
s.rwm.RUnlock()
|
||||
|
||||
return list
|
||||
}
|
||||
|
||||
func (s *PolicyReportStore) Add(r PolicyReport) {
|
||||
s.rwm.Lock()
|
||||
s.store[r.GetIdentifier()] = r
|
||||
s.rwm.Unlock()
|
||||
}
|
||||
|
||||
func (s *PolicyReportStore) Remove(id string) {
|
||||
s.rwm.Lock()
|
||||
delete(s.store, id)
|
||||
s.rwm.Unlock()
|
||||
}
|
||||
|
||||
func NewPolicyReportStore() *PolicyReportStore {
|
||||
return &PolicyReportStore{
|
||||
store: map[string]PolicyReport{},
|
||||
rwm: new(sync.RWMutex),
|
||||
}
|
||||
}
|
||||
|
||||
type ClusterPolicyReportStore struct {
|
||||
store map[string]ClusterPolicyReport
|
||||
rwm *sync.RWMutex
|
||||
}
|
||||
|
||||
func (s *ClusterPolicyReportStore) Get(id string) (ClusterPolicyReport, bool) {
|
||||
s.rwm.RLock()
|
||||
r, ok := s.store[id]
|
||||
s.rwm.RUnlock()
|
||||
|
||||
return r, ok
|
||||
}
|
||||
|
||||
func (s *ClusterPolicyReportStore) List() []ClusterPolicyReport {
|
||||
s.rwm.RLock()
|
||||
list := make([]ClusterPolicyReport, 0, len(s.store))
|
||||
|
||||
for _, r := range s.store {
|
||||
list = append(list, r)
|
||||
}
|
||||
s.rwm.RUnlock()
|
||||
|
||||
return list
|
||||
}
|
||||
|
||||
func (s *ClusterPolicyReportStore) Add(r ClusterPolicyReport) {
|
||||
s.rwm.Lock()
|
||||
s.store[r.GetIdentifier()] = r
|
||||
s.rwm.Unlock()
|
||||
}
|
||||
|
||||
func (s *ClusterPolicyReportStore) Remove(id string) {
|
||||
s.rwm.Lock()
|
||||
delete(s.store, id)
|
||||
s.rwm.Unlock()
|
||||
}
|
||||
|
||||
func NewClusterPolicyReportStore() *ClusterPolicyReportStore {
|
||||
return &ClusterPolicyReportStore{
|
||||
store: map[string]ClusterPolicyReport{},
|
||||
rwm: new(sync.RWMutex),
|
||||
}
|
||||
}
|
81
pkg/report/store_test.go
Normal file
81
pkg/report/store_test.go
Normal file
|
@ -0,0 +1,81 @@
|
|||
package report_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/fjogeleit/policy-reporter/pkg/report"
|
||||
)
|
||||
|
||||
func Test_PolicyReportStore(t *testing.T) {
|
||||
store := report.NewPolicyReportStore()
|
||||
|
||||
t.Run("Add/Get", func(t *testing.T) {
|
||||
_, ok := store.Get(preport.GetIdentifier())
|
||||
if ok == true {
|
||||
t.Fatalf("Should not be found in empty Store")
|
||||
}
|
||||
|
||||
store.Add(preport)
|
||||
_, ok = store.Get(preport.GetIdentifier())
|
||||
if ok == false {
|
||||
t.Errorf("Should be found in Store after adding report to the store")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("List", func(t *testing.T) {
|
||||
items := store.List()
|
||||
if len(items) != 1 {
|
||||
t.Errorf("Should return List with the added Report")
|
||||
}
|
||||
})
|
||||
|
||||
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")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func Test_ClusterPolicyReportStore(t *testing.T) {
|
||||
store := report.NewClusterPolicyReportStore()
|
||||
|
||||
t.Run("Add/Get", 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("List", func(t *testing.T) {
|
||||
items := store.List()
|
||||
if len(items) != 1 {
|
||||
t.Errorf("Should return List with the added Report")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Delete/Get", func(t *testing.T) {
|
||||
_, ok := store.Get(creport.GetIdentifier())
|
||||
if ok == false {
|
||||
t.Errorf("Should be found in Store after adding report to the store")
|
||||
}
|
||||
|
||||
store.Remove(creport.GetIdentifier())
|
||||
_, ok = store.Get(creport.GetIdentifier())
|
||||
if ok == true {
|
||||
t.Fatalf("Should not be found after Remove report from Store")
|
||||
}
|
||||
})
|
||||
}
|
|
@ -10,4 +10,8 @@ type Client interface {
|
|||
Send(result report.Result)
|
||||
// SkipExistingOnStartup skips already existing PolicyReportResults on startup
|
||||
SkipExistingOnStartup() bool
|
||||
// Name is a unique identifier for each Target
|
||||
Name() string
|
||||
// MinimumPriority for a triggered Result to send to this target
|
||||
MinimumPriority() string
|
||||
}
|
||||
|
|
|
@ -127,6 +127,14 @@ func (d *client) SkipExistingOnStartup() bool {
|
|||
return d.skipExistingOnStartup
|
||||
}
|
||||
|
||||
func (d *client) Name() string {
|
||||
return "Discord"
|
||||
}
|
||||
|
||||
func (d *client) MinimumPriority() string {
|
||||
return d.minimumPriority
|
||||
}
|
||||
|
||||
// NewClient creates a new loki.client to send Results to Loki
|
||||
func NewClient(webhook, minimumPriority string, skipExistingOnStartup bool, httpClient httpClient) target.Client {
|
||||
return &client{
|
||||
|
|
|
@ -106,4 +106,18 @@ func Test_LokiTarget(t *testing.T) {
|
|||
t.Error("Should return configured SkipExistingOnStartup")
|
||||
}
|
||||
})
|
||||
t.Run("Name", func(t *testing.T) {
|
||||
client := discord.NewClient("http://localhost:9200", "", true, testClient{})
|
||||
|
||||
if client.Name() != "Discord" {
|
||||
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())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -76,6 +76,14 @@ func (e *client) SkipExistingOnStartup() bool {
|
|||
return e.skipExistingOnStartup
|
||||
}
|
||||
|
||||
func (e *client) Name() string {
|
||||
return "Elasticsearch"
|
||||
}
|
||||
|
||||
func (e *client) MinimumPriority() string {
|
||||
return e.minimumPriority
|
||||
}
|
||||
|
||||
// NewClient creates a new loki.client to send Results to Loki
|
||||
func NewClient(host, index, rotation, minimumPriority string, skipExistingOnStartup bool, httpClient httpClient) target.Client {
|
||||
return &client{
|
||||
|
|
|
@ -110,4 +110,18 @@ func Test_ElasticsearchTarget(t *testing.T) {
|
|||
t.Error("Should return configured SkipExistingOnStartup")
|
||||
}
|
||||
})
|
||||
t.Run("Name", func(t *testing.T) {
|
||||
client := elasticsearch.NewClient("http://localhost:9200", "policy-reporter", "none", "", true, testClient{})
|
||||
|
||||
if client.Name() != "Elasticsearch" {
|
||||
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())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -107,6 +107,14 @@ func (l *client) SkipExistingOnStartup() bool {
|
|||
return l.skipExistingOnStartup
|
||||
}
|
||||
|
||||
func (l *client) Name() string {
|
||||
return "Loki"
|
||||
}
|
||||
|
||||
func (l *client) MinimumPriority() string {
|
||||
return l.minimumPriority
|
||||
}
|
||||
|
||||
// NewClient creates a new loki.client to send Results to Loki
|
||||
func NewClient(host, minimumPriority string, skipExistingOnStartup bool, httpClient httpClient) target.Client {
|
||||
return &client{
|
||||
|
|
|
@ -190,6 +190,20 @@ func Test_LokiTarget(t *testing.T) {
|
|||
t.Error("Should return configured SkipExistingOnStartup")
|
||||
}
|
||||
})
|
||||
t.Run("Name", func(t *testing.T) {
|
||||
client := loki.NewClient("http://localhost:9200", "", true, testClient{})
|
||||
|
||||
if client.Name() != "Loki" {
|
||||
t.Errorf("Unexpected Name %s", client.Name())
|
||||
}
|
||||
})
|
||||
t.Run("MinimumPriority", func(t *testing.T) {
|
||||
client := loki.NewClient("http://localhost:9200", "debug", true, testClient{})
|
||||
|
||||
if client.MinimumPriority() != "debug" {
|
||||
t.Errorf("Unexpected MinimumPriority %s", client.MinimumPriority())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func convertAndValidateBody(req *http.Request, t *testing.T) (string, string) {
|
||||
|
|
|
@ -180,6 +180,14 @@ func (s *client) SkipExistingOnStartup() bool {
|
|||
return s.skipExistingOnStartup
|
||||
}
|
||||
|
||||
func (s *client) Name() string {
|
||||
return "Slack"
|
||||
}
|
||||
|
||||
func (s *client) MinimumPriority() string {
|
||||
return s.minimumPriority
|
||||
}
|
||||
|
||||
// NewClient creates a new slack.client to send Results to Loki
|
||||
func NewClient(host, minimumPriority string, skipExistingOnStartup bool, httpClient httpClient) target.Client {
|
||||
return &client{
|
||||
|
|
|
@ -106,4 +106,18 @@ func Test_LokiTarget(t *testing.T) {
|
|||
t.Error("Should return configured SkipExistingOnStartup")
|
||||
}
|
||||
})
|
||||
t.Run("Name", func(t *testing.T) {
|
||||
client := slack.NewClient("http://localhost:9200", "", true, testClient{})
|
||||
|
||||
if client.Name() != "Slack" {
|
||||
t.Errorf("Unexpected Name %s", client.Name())
|
||||
}
|
||||
})
|
||||
t.Run("MinimumPriority", func(t *testing.T) {
|
||||
client := slack.NewClient("http://localhost:9200", "debug", true, testClient{})
|
||||
|
||||
if client.MinimumPriority() != "debug" {
|
||||
t.Errorf("Unexpected MinimumPriority %s", client.MinimumPriority())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue