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

Development (#12)

* New REST API
* New UI SubChart
This commit is contained in:
Frank Jogeleit 2021-03-13 19:56:38 +01:00 committed by GitHub
parent 92c0e51a89
commit 22f68c788d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 1092 additions and 42 deletions

View file

@ -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.

View file

@ -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~~

View file

@ -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

View file

@ -0,0 +1,7 @@
apiVersion: v2
name: policy-reporter-ui
description: Policy Reporter UI
type: application
version: 0.1.0
appVersion: 0.1.0

View file

@ -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 }}

View file

@ -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 }}

View file

@ -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 }}

View file

@ -0,0 +1,6 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "ui.serviceAccountName" . }}
labels:
{{- include "ui.labels" . | nindent 4 }}

View 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

View file

@ -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

View file

@ -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 }}

View file

@ -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: ""

View file

@ -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)

View file

@ -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
View 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
View 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
View 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
View 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
View 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()
}

View file

@ -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"`
}

View file

@ -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

View file

@ -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"

View file

@ -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,
}

View file

@ -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(),
)

View file

@ -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,
}

View file

@ -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(),
)

View file

@ -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
View 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
View 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")
}
})
}

View file

@ -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
}

View file

@ -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{

View file

@ -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())
}
})
}

View file

@ -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{

View file

@ -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())
}
})
}

View file

@ -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{

View file

@ -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) {

View file

@ -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{

View file

@ -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())
}
})
}