mirror of
https://github.com/kyverno/policy-reporter.git
synced 2024-12-14 11:57:32 +00:00
parent
2872a259ec
commit
cf69957a5a
12 changed files with 441 additions and 68 deletions
|
@ -1,5 +1,9 @@
|
|||
# Changelog
|
||||
|
||||
## 0.8.0
|
||||
|
||||
* Implement Slack as Target for PolicyReportResults
|
||||
|
||||
## 0.7.0
|
||||
|
||||
* Implement Elasticsearch as Target for PolicyReportResults
|
||||
|
|
19
README.md
19
README.md
|
@ -3,7 +3,7 @@
|
|||
|
||||
## Motivation
|
||||
|
||||
Kyverno ships with two types of validation. You can either enforce a rule or audit it. If you don't want to block developers or if you want to try out a new rule, you can use the audit functionality. The audit configuration creates [PolicyReports](https://kyverno.io/docs/policy-reports/) which you can access with `kubectl`. Because I can't find a simple solution to get a general overview of this PolicyReports and PolicyReportResults, I created this tool to send information from PolicyReports to different targets like [Grafana Loki](https://grafana.com/oss/loki/). This tool provides by default an HTTP server with Prometheus Metrics on `http://localhost:2112/metrics` about ReportPolicy Summaries and ReportPolicyRules.
|
||||
Kyverno ships with two types of validation. You can either enforce a rule or audit it. If you don't want to block developers or if you want to try out a new rule, you can use the audit functionality. The audit configuration creates [PolicyReports](https://kyverno.io/docs/policy-reports/) which you can access with `kubectl`. Because I can't find a simple solution to get a general overview of this PolicyReports and PolicyReportResults, I created this tool to send information from PolicyReports to different targets like [Grafana Loki](https://grafana.com/oss/loki/), [Elasticsearch](https://www.elastic.co/de/elasticsearch/) or [Slack](https://slack.com/). This tool provides by default an HTTP server with Prometheus Metrics on `http://localhost:2112/metrics` about ReportPolicy Summaries and ReportPolicyRules.
|
||||
|
||||
This project is in an early stage. Please let me know if anything did not work as expected or if you want to send your audits to other targets then Loki.
|
||||
|
||||
|
@ -35,6 +35,12 @@ helm install policy-reporter policy-reporter/policy-reporter --set loki.host=htt
|
|||
helm install policy-reporter policy-reporter/policy-reporter --set elasticsearch.host=http://elasticsearch:3100 -n policy-reporter --create-namespace
|
||||
```
|
||||
|
||||
### Installation with Slack
|
||||
|
||||
```bash
|
||||
helm install policy-reporter policy-reporter/policy-reporter --set slack.webhook=http://hook.slack -n policy-reporter --create-namespace
|
||||
```
|
||||
|
||||
You can also customize the `./charts/policy-reporter/values.yaml` to change the default configurations.
|
||||
|
||||
### Additional configurations for Loki
|
||||
|
@ -63,6 +69,17 @@ elasticsearch:
|
|||
skipExistingOnStartup: true
|
||||
```
|
||||
|
||||
### Additional configurations for Slack
|
||||
|
||||
* Configure `slack.minimumPriority` to send only results with the configured minimumPriority or above, empty means all results. (info < warning < error)
|
||||
* Configure `slack.skipExistingOnStartup` to skip all results who already existed before the PolicyReporter started (default: `true`).
|
||||
|
||||
```yaml
|
||||
slack:
|
||||
minimumPriority: ""
|
||||
skipExistingOnStartup: true
|
||||
```
|
||||
|
||||
### Configure Policy Priorities
|
||||
|
||||
By default kyverno PolicyReports has no priority or severity for policies. So every passed rule validation will be processed as notice, a failed validation is processed as error. To customize this you can configure a mapping from policies to fail priorities. So you can send them as warnings instead of errors. To configure the priorities create a ConfigMap in the `policy-reporter` namespace with the name `policy-reporter-priorities`. Configure each priority as value with the Policyname as key and the Priority as value. This Configuration is loaded and synchronized during runtime. Any change to this configmap will automaticly synchronized, no new deployment needed.
|
||||
|
|
|
@ -3,5 +3,5 @@ name: policy-reporter
|
|||
description: K8s PolicyReporter watches for wgpolicyk8s.io/v1alpha1.PolicyReport resources. It creates Prometheus Metrics and can send rule validation events to Loki
|
||||
|
||||
type: application
|
||||
version: 0.8.0
|
||||
appVersion: 0.7.0
|
||||
version: 0.9.0
|
||||
appVersion: 0.8.0
|
||||
|
|
|
@ -17,3 +17,8 @@ data:
|
|||
rotation: {{ .Values.elasticsearch.rotation | default "dayli" | quote }}
|
||||
minimumPriority: {{ .Values.elasticsearch.minimumPriority | quote }}
|
||||
skipExistingOnStartup: {{ .Values.elasticsearch.skipExistingOnStartup }}
|
||||
|
||||
slack:
|
||||
webhook: {{ .Values.slack.webhook | quote }}
|
||||
minimumPriority: {{ .Values.loki.minimumPriority | quote }}
|
||||
skipExistingOnStartup: {{ .Values.loki.skipExistingOnStartup }}
|
||||
|
|
|
@ -19,6 +19,13 @@ elasticsearch:
|
|||
# Skip already existing PolicyReportResults on startup
|
||||
skipExistingOnStartup: true
|
||||
|
||||
slack:
|
||||
# slack app webhook address
|
||||
webhook: ""
|
||||
# minimum priority "" < info < warning < error
|
||||
minimumPriority: ""
|
||||
# Skip already existing PolicyReportResults on startup
|
||||
skipExistingOnStartup: true
|
||||
|
||||
metrics:
|
||||
serviceMonitor: false
|
||||
|
@ -29,7 +36,7 @@ metrics:
|
|||
image:
|
||||
repository: fjogeleit/policy-reporter
|
||||
pullPolicy: IfNotPresent
|
||||
tag: 0.7.0
|
||||
tag: 0.8.0
|
||||
|
||||
imagePullSecrets: []
|
||||
|
||||
|
|
|
@ -1,19 +1,33 @@
|
|||
package config
|
||||
|
||||
// Loki configuration
|
||||
type Loki struct {
|
||||
Host string `mapstructure:"host"`
|
||||
SkipExisting bool `mapstructure:"skipExistingOnStartup"`
|
||||
MinimumPriority string `mapstructure:"minimumPriority"`
|
||||
}
|
||||
|
||||
// Elasticsearch configuration
|
||||
type Elasticsearch struct {
|
||||
Host string `mapstructure:"host"`
|
||||
Index string `mapstructure:"index"`
|
||||
Rotation string `mapstructure:"rotation"`
|
||||
SkipExisting bool `mapstructure:"skipExistingOnStartup"`
|
||||
MinimumPriority string `mapstructure:"minimumPriority"`
|
||||
}
|
||||
|
||||
// Slack configuration
|
||||
type Slack struct {
|
||||
Webhook string `mapstructure:"webhook"`
|
||||
SkipExisting bool `mapstructure:"skipExistingOnStartup"`
|
||||
MinimumPriority string `mapstructure:"minimumPriority"`
|
||||
}
|
||||
|
||||
// Config of the PolicyReporter
|
||||
type Config struct {
|
||||
Loki struct {
|
||||
Host string `mapstructure:"host"`
|
||||
SkipExisting bool `mapstructure:"skipExistingOnStartup"`
|
||||
MinimumPriority string `mapstructure:"minimumPriority"`
|
||||
} `mapstructure:"loki"`
|
||||
Elasticsearch struct {
|
||||
Host string `mapstructure:"host"`
|
||||
Index string `mapstructure:"index"`
|
||||
Rotation string `mapstructure:"rotation"`
|
||||
SkipExisting bool `mapstructure:"skipExistingOnStartup"`
|
||||
MinimumPriority string `mapstructure:"minimumPriority"`
|
||||
} `mapstructure:"elasticsearch"`
|
||||
Kubeconfig string `mapstructure:"kubeconfig"`
|
||||
Namespace string `mapstructure:"namespace"`
|
||||
Loki Loki `mapstructure:"loki"`
|
||||
Elasticsearch Elasticsearch `mapstructure:"elasticsearch"`
|
||||
Slack Slack `mapstructure:"slack"`
|
||||
Kubeconfig string `mapstructure:"kubeconfig"`
|
||||
Namespace string `mapstructure:"namespace"`
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
"github.com/fjogeleit/policy-reporter/pkg/target"
|
||||
"github.com/fjogeleit/policy-reporter/pkg/target/elasticsearch"
|
||||
"github.com/fjogeleit/policy-reporter/pkg/target/loki"
|
||||
"github.com/fjogeleit/policy-reporter/pkg/target/slack"
|
||||
)
|
||||
|
||||
// Resolver manages dependencies
|
||||
|
@ -19,6 +20,7 @@ type Resolver struct {
|
|||
kubeClient report.Client
|
||||
lokiClient target.Client
|
||||
elasticsearchClient target.Client
|
||||
slackClient target.Client
|
||||
policyReportMetrics metrics.Metrics
|
||||
clusterPolicyReportMetrics metrics.Metrics
|
||||
}
|
||||
|
@ -89,6 +91,26 @@ func (r *Resolver) ElasticsearchClient() target.Client {
|
|||
return r.elasticsearchClient
|
||||
}
|
||||
|
||||
// ElasticsearchClient resolver method
|
||||
func (r *Resolver) SlackClient() target.Client {
|
||||
if r.slackClient != nil {
|
||||
return r.slackClient
|
||||
}
|
||||
|
||||
if r.config.Slack.Webhook == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
r.slackClient = slack.NewClient(
|
||||
r.config.Slack.Webhook,
|
||||
r.config.Slack.MinimumPriority,
|
||||
r.config.Slack.SkipExisting,
|
||||
&http.Client{},
|
||||
)
|
||||
|
||||
return r.slackClient
|
||||
}
|
||||
|
||||
// PolicyReportMetrics resolver method
|
||||
func (r *Resolver) PolicyReportMetrics() (metrics.Metrics, error) {
|
||||
if r.policyReportMetrics != nil {
|
||||
|
@ -132,6 +154,10 @@ func (r *Resolver) TargetClients() []target.Client {
|
|||
clients = append(clients, elasticsearch)
|
||||
}
|
||||
|
||||
if slack := r.SlackClient(); slack != nil {
|
||||
clients = append(clients, slack)
|
||||
}
|
||||
|
||||
return clients
|
||||
}
|
||||
|
||||
|
|
|
@ -7,28 +7,23 @@ import (
|
|||
)
|
||||
|
||||
var testConfig = &config.Config{
|
||||
Loki: struct {
|
||||
Host string "mapstructure:\"host\""
|
||||
SkipExisting bool "mapstructure:\"skipExistingOnStartup\""
|
||||
MinimumPriority string "mapstructure:\"minimumPriority\""
|
||||
}{
|
||||
Loki: config.Loki{
|
||||
Host: "http://localhost:3100",
|
||||
SkipExisting: true,
|
||||
MinimumPriority: "debug",
|
||||
},
|
||||
Elasticsearch: struct {
|
||||
Host string "mapstructure:\"host\""
|
||||
Index string "mapstructure:\"index\""
|
||||
Rotation string "mapstructure:\"rotation\""
|
||||
SkipExisting bool "mapstructure:\"skipExistingOnStartup\""
|
||||
MinimumPriority string "mapstructure:\"minimumPriority\""
|
||||
}{
|
||||
Elasticsearch: config.Elasticsearch{
|
||||
Host: "http://localhost:9200",
|
||||
Index: "policy-reporter",
|
||||
Rotation: "dayli",
|
||||
SkipExisting: true,
|
||||
MinimumPriority: "debug",
|
||||
},
|
||||
Slack: config.Slack{
|
||||
Webhook: "http://localhost:80",
|
||||
SkipExisting: true,
|
||||
MinimumPriority: "debug",
|
||||
},
|
||||
}
|
||||
|
||||
func Test_ResolveLokiClient(t *testing.T) {
|
||||
|
@ -59,33 +54,37 @@ func Test_ResolveElasticSearchClient(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func Test_ResolveSlackClient(t *testing.T) {
|
||||
resolver := config.NewResolver(testConfig)
|
||||
|
||||
client := resolver.SlackClient()
|
||||
if client == nil {
|
||||
t.Error("Expected Client, got nil")
|
||||
}
|
||||
|
||||
client2 := resolver.SlackClient()
|
||||
if client != client2 {
|
||||
t.Error("Error: Should reuse first instance")
|
||||
}
|
||||
}
|
||||
|
||||
func Test_ResolveTargets(t *testing.T) {
|
||||
resolver := config.NewResolver(testConfig)
|
||||
|
||||
clients := resolver.TargetClients()
|
||||
if count := len(clients); count != 2 {
|
||||
t.Errorf("Expected 2 Clients, got %d", count)
|
||||
if count := len(clients); count != 3 {
|
||||
t.Errorf("Expected 3 Clients, got %d", count)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_ResolveSkipExistingOnStartup(t *testing.T) {
|
||||
var testConfig = &config.Config{
|
||||
Loki: struct {
|
||||
Host string "mapstructure:\"host\""
|
||||
SkipExisting bool "mapstructure:\"skipExistingOnStartup\""
|
||||
MinimumPriority string "mapstructure:\"minimumPriority\""
|
||||
}{
|
||||
Loki: config.Loki{
|
||||
Host: "http://localhost:3100",
|
||||
SkipExisting: true,
|
||||
MinimumPriority: "debug",
|
||||
},
|
||||
Elasticsearch: struct {
|
||||
Host string "mapstructure:\"host\""
|
||||
Index string "mapstructure:\"index\""
|
||||
Rotation string "mapstructure:\"rotation\""
|
||||
SkipExisting bool "mapstructure:\"skipExistingOnStartup\""
|
||||
MinimumPriority string "mapstructure:\"minimumPriority\""
|
||||
}{
|
||||
Elasticsearch: config.Elasticsearch{
|
||||
Host: "http://localhost:9200",
|
||||
Index: "policy-reporter",
|
||||
Rotation: "dayli",
|
||||
|
@ -117,11 +116,7 @@ func Test_ResolveSkipExistingOnStartup(t *testing.T) {
|
|||
|
||||
func Test_ResolveLokiClientWithoutHost(t *testing.T) {
|
||||
config2 := &config.Config{
|
||||
Loki: struct {
|
||||
Host string "mapstructure:\"host\""
|
||||
SkipExisting bool "mapstructure:\"skipExistingOnStartup\""
|
||||
MinimumPriority string "mapstructure:\"minimumPriority\""
|
||||
}{
|
||||
Loki: config.Loki{
|
||||
Host: "",
|
||||
SkipExisting: true,
|
||||
MinimumPriority: "debug",
|
||||
|
@ -138,13 +133,7 @@ func Test_ResolveLokiClientWithoutHost(t *testing.T) {
|
|||
|
||||
func Test_ResolveElasticsearchClientWithoutHost(t *testing.T) {
|
||||
config2 := &config.Config{
|
||||
Elasticsearch: struct {
|
||||
Host string "mapstructure:\"host\""
|
||||
Index string "mapstructure:\"index\""
|
||||
Rotation string "mapstructure:\"rotation\""
|
||||
SkipExisting bool "mapstructure:\"skipExistingOnStartup\""
|
||||
MinimumPriority string "mapstructure:\"minimumPriority\""
|
||||
}{
|
||||
Elasticsearch: config.Elasticsearch{
|
||||
Host: "",
|
||||
Index: "policy-reporter",
|
||||
Rotation: "dayli",
|
||||
|
@ -159,3 +148,19 @@ func Test_ResolveElasticsearchClientWithoutHost(t *testing.T) {
|
|||
t.Error("Expected Client to be nil if no host is configured")
|
||||
}
|
||||
}
|
||||
|
||||
func Test_ResolveSlackClientWithoutHost(t *testing.T) {
|
||||
config2 := &config.Config{
|
||||
Slack: config.Slack{
|
||||
Webhook: "",
|
||||
SkipExisting: true,
|
||||
MinimumPriority: "debug",
|
||||
},
|
||||
}
|
||||
|
||||
resolver := config.NewResolver(config2)
|
||||
|
||||
if resolver.SlackClient() != nil {
|
||||
t.Error("Expected Client to be nil if no host is configured")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,7 +43,7 @@ func (e *client) Send(result report.Result) {
|
|||
body := new(bytes.Buffer)
|
||||
|
||||
if err := json.NewEncoder(body).Encode(result); err != nil {
|
||||
log.Printf("[ERROR] : %v\n", err.Error())
|
||||
log.Printf("[ERROR] ELASTICSEARCH : %v\n", err.Error())
|
||||
}
|
||||
|
||||
var host string
|
||||
|
@ -60,7 +60,7 @@ func (e *client) Send(result report.Result) {
|
|||
|
||||
req, err := http.NewRequest("POST", host, body)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] : %v\n", err.Error())
|
||||
log.Printf("[ERROR] ELASTICSEARCH : %v\n", err.Error())
|
||||
}
|
||||
|
||||
req.Header.Add("Content-Type", "application/json; charset=utf-8")
|
||||
|
@ -74,15 +74,15 @@ func (e *client) Send(result report.Result) {
|
|||
}()
|
||||
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] PUSH failed: %s\n", err.Error())
|
||||
} else if resp.StatusCode > 400 {
|
||||
log.Printf("[ERROR] ELASTICSEARCH PUSH failed: %s\n", err.Error())
|
||||
} else if resp.StatusCode >= 400 {
|
||||
fmt.Printf("StatusCode: %d\n", resp.StatusCode)
|
||||
buf := new(bytes.Buffer)
|
||||
buf.ReadFrom(resp.Body)
|
||||
|
||||
log.Printf("[ERROR] PUSH failed [%d]: %s\n", resp.StatusCode, buf.String())
|
||||
log.Printf("[ERROR] ELASTICSEARCH PUSH failed [%d]: %s\n", resp.StatusCode, buf.String())
|
||||
} else {
|
||||
log.Println("[INFO] PUSH OK")
|
||||
log.Println("[INFO] ELASTICSEARCH PUSH OK")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -85,12 +85,12 @@ func (l *client) Send(result report.Result) {
|
|||
body := new(bytes.Buffer)
|
||||
|
||||
if err := json.NewEncoder(body).Encode(payload); err != nil {
|
||||
log.Printf("[ERROR] : %v\n", err.Error())
|
||||
log.Printf("[ERROR] LOKI : %v\n", err.Error())
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", l.host, body)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] : %v\n", err.Error())
|
||||
log.Printf("[ERROR] LOKI : %v\n", err.Error())
|
||||
}
|
||||
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
|
@ -104,15 +104,15 @@ func (l *client) Send(result report.Result) {
|
|||
}()
|
||||
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] PUSH failed: %s\n", err.Error())
|
||||
} else if resp.StatusCode > 400 {
|
||||
log.Printf("[ERROR] LOKI PUSH failed: %s\n", err.Error())
|
||||
} else if resp.StatusCode >= 400 {
|
||||
fmt.Printf("StatusCode: %d\n", resp.StatusCode)
|
||||
buf := new(bytes.Buffer)
|
||||
buf.ReadFrom(resp.Body)
|
||||
|
||||
log.Printf("[ERROR] PUSH failed [%d]: %s\n", resp.StatusCode, buf.String())
|
||||
log.Printf("[ERROR] LOKI PUSH failed [%d]: %s\n", resp.StatusCode, buf.String())
|
||||
} else {
|
||||
log.Println("[INFO] PUSH OK")
|
||||
log.Println("[INFO] LOKI PUSH OK")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
205
pkg/target/slack/slack.go
Normal file
205
pkg/target/slack/slack.go
Normal file
|
@ -0,0 +1,205 @@
|
|||
package slack
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/fjogeleit/policy-reporter/pkg/report"
|
||||
"github.com/fjogeleit/policy-reporter/pkg/target"
|
||||
)
|
||||
|
||||
type httpClient interface {
|
||||
Do(req *http.Request) (*http.Response, error)
|
||||
}
|
||||
|
||||
type text struct {
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
type block struct {
|
||||
Type string `json:"type"`
|
||||
Text *text `json:"text,omitempty"`
|
||||
Fields []field `json:"fields,omitempty"`
|
||||
}
|
||||
|
||||
type field struct {
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
type attachment struct {
|
||||
Color string `json:"color"`
|
||||
Blocks []block `json:"blocks"`
|
||||
}
|
||||
|
||||
type payload struct {
|
||||
Username string `json:"username,omitempty"`
|
||||
Attachments []attachment `json:"attachments,omitempty"`
|
||||
}
|
||||
|
||||
type client struct {
|
||||
webhook string
|
||||
minimumPriority string
|
||||
skipExistingOnStartup bool
|
||||
client httpClient
|
||||
}
|
||||
|
||||
func colorFromPriority(p report.Priority) string {
|
||||
if p == report.ErrorPriority {
|
||||
return "#e20b0b"
|
||||
}
|
||||
if p == report.WarningPriority {
|
||||
return "#f2c744"
|
||||
}
|
||||
if p == report.InfoPriority {
|
||||
return "#36a64f"
|
||||
}
|
||||
|
||||
return "#68c2ff"
|
||||
}
|
||||
|
||||
func (s *client) newPayload(result report.Result) payload {
|
||||
p := payload{
|
||||
Attachments: make([]attachment, 0, 1),
|
||||
}
|
||||
|
||||
att := attachment{
|
||||
Color: colorFromPriority(result.Priority),
|
||||
Blocks: make([]block, 0),
|
||||
}
|
||||
|
||||
policyBlock := block{
|
||||
Type: "section",
|
||||
Fields: []field{{Type: "mrkdwn", Text: "*Policy*\n" + result.Policy}},
|
||||
}
|
||||
|
||||
if result.Rule != "" {
|
||||
policyBlock.Fields = append(policyBlock.Fields, field{Type: "mrkdwn", Text: "*Rule*\n" + result.Rule})
|
||||
}
|
||||
|
||||
att.Blocks = append(
|
||||
att.Blocks,
|
||||
block{Type: "header", Text: &text{Type: "plain_text", Text: "New Policy Report Result"}},
|
||||
policyBlock,
|
||||
)
|
||||
|
||||
att.Blocks = append(
|
||||
att.Blocks,
|
||||
block{Type: "section", Text: &text{Type: "mrkdwn", Text: "*Message*\n" + result.Message}},
|
||||
block{
|
||||
Type: "section",
|
||||
Fields: []field{
|
||||
{Type: "mrkdwn", Text: "*Priority*\n" + result.Priority.String()},
|
||||
{Type: "mrkdwn", Text: "*Status*\n" + result.Status},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
b := block{
|
||||
Type: "section",
|
||||
Fields: make([]field, 0, 2),
|
||||
}
|
||||
|
||||
if result.Category != "" {
|
||||
b.Fields = append(b.Fields, field{Type: "mrkdwn", Text: "*Category*\n" + result.Category})
|
||||
}
|
||||
if result.Severity != "" {
|
||||
b.Fields = append(b.Fields, field{Type: "mrkdwn", Text: "*Severity*\n" + result.Severity})
|
||||
}
|
||||
|
||||
if len(b.Fields) > 0 {
|
||||
att.Blocks = append(att.Blocks, b)
|
||||
}
|
||||
|
||||
res := report.Resource{}
|
||||
|
||||
if len(result.Resources) > 0 {
|
||||
res = result.Resources[0]
|
||||
}
|
||||
if res.Kind != "" {
|
||||
att.Blocks = append(
|
||||
att.Blocks,
|
||||
block{Type: "section", Text: &text{Type: "mrkdwn", Text: "*Resource*"}},
|
||||
block{
|
||||
Type: "section",
|
||||
Fields: []field{
|
||||
{Type: "mrkdwn", Text: "*Kind*\n" + res.Kind},
|
||||
{Type: "mrkdwn", Text: "*API Version*\n" + res.APIVersion},
|
||||
},
|
||||
},
|
||||
block{
|
||||
Type: "section",
|
||||
Fields: []field{
|
||||
{Type: "mrkdwn", Text: "*Name*\n" + res.Name},
|
||||
{Type: "mrkdwn", Text: "*UID*\n" + res.UID},
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if res.Namespace != "" {
|
||||
att.Blocks = append(att.Blocks, block{Type: "section", Fields: []field{{Type: "mrkdwn", Text: "*Namespace*\n" + res.Namespace}}})
|
||||
}
|
||||
|
||||
p.Attachments = append(p.Attachments, att)
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
func (s *client) Send(result report.Result) {
|
||||
if result.Priority < report.NewPriority(s.minimumPriority) {
|
||||
return
|
||||
}
|
||||
|
||||
payload := s.newPayload(result)
|
||||
body := new(bytes.Buffer)
|
||||
|
||||
if err := json.NewEncoder(body).Encode(payload); err != nil {
|
||||
log.Printf("[ERROR] SLACK : %v\n", err.Error())
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", s.webhook, body)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] SLACK : %v\n", err.Error())
|
||||
}
|
||||
|
||||
req.Header.Add("Content-Type", "application/json; charset=utf-8")
|
||||
req.Header.Add("User-Agent", "Policy-Reporter")
|
||||
|
||||
resp, err := s.client.Do(req)
|
||||
defer func() {
|
||||
if resp != nil && resp.Body != nil {
|
||||
resp.Body.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] SLACK PUSH failed: %s\n", err.Error())
|
||||
} else if resp.StatusCode >= 400 {
|
||||
fmt.Printf("StatusCode: %d\n", resp.StatusCode)
|
||||
buf := new(bytes.Buffer)
|
||||
buf.ReadFrom(resp.Body)
|
||||
|
||||
log.Printf("[ERROR] PUSH failed [%d]: %s\n", resp.StatusCode, buf.String())
|
||||
} else {
|
||||
log.Println("[INFO] SLACK PUSH OK")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *client) SkipExistingOnStartup() bool {
|
||||
return s.skipExistingOnStartup
|
||||
}
|
||||
|
||||
// NewClient creates a new slack.client to send Results to Loki
|
||||
func NewClient(host, minimumPriority string, skipExistingOnStartup bool, httpClient httpClient) target.Client {
|
||||
return &client{
|
||||
host,
|
||||
minimumPriority,
|
||||
skipExistingOnStartup,
|
||||
httpClient,
|
||||
}
|
||||
}
|
90
pkg/target/slack/slack_test.go
Normal file
90
pkg/target/slack/slack_test.go
Normal file
|
@ -0,0 +1,90 @@
|
|||
package slack_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/fjogeleit/policy-reporter/pkg/report"
|
||||
"github.com/fjogeleit/policy-reporter/pkg/target/slack"
|
||||
)
|
||||
|
||||
var completeResult = report.Result{
|
||||
Message: "validation error: requests and limits required. Rule autogen-check-for-requests-and-limits failed at path /spec/template/spec/containers/0/resources/requests/",
|
||||
Policy: "require-requests-and-limits-required",
|
||||
Rule: "autogen-check-for-requests-and-limits",
|
||||
Priority: report.ErrorPriority,
|
||||
Status: report.Fail,
|
||||
Severity: report.Heigh,
|
||||
Category: "resources",
|
||||
Scored: true,
|
||||
Resources: []report.Resource{
|
||||
{
|
||||
APIVersion: "v1",
|
||||
Kind: "Deployment",
|
||||
Name: "nginx",
|
||||
Namespace: "default",
|
||||
UID: "536ab69f-1b3c-4bd9-9ba4-274a56188409",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var minimalResult = report.Result{
|
||||
Message: "validation error: label required. Rule app-label-required failed at path /spec/template/spec/containers/0/resources/requests/",
|
||||
Policy: "app-label-requirement",
|
||||
Priority: report.WarningPriority,
|
||||
Status: report.Fail,
|
||||
Scored: true,
|
||||
}
|
||||
|
||||
type testClient struct {
|
||||
callback func(req *http.Request)
|
||||
statusCode int
|
||||
}
|
||||
|
||||
func (c testClient) Do(req *http.Request) (*http.Response, error) {
|
||||
c.callback(req)
|
||||
|
||||
return &http.Response{
|
||||
StatusCode: c.statusCode,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func Test_LokiTarget(t *testing.T) {
|
||||
t.Run("Send Complete Result", func(t *testing.T) {
|
||||
callback := func(req *http.Request) {
|
||||
if contentType := req.Header.Get("Content-Type"); contentType != "application/json; charset=utf-8" {
|
||||
t.Errorf("Unexpected Content-Type: %s", contentType)
|
||||
}
|
||||
|
||||
if agend := req.Header.Get("User-Agent"); agend != "Policy-Reporter" {
|
||||
t.Errorf("Unexpected Host: %s", agend)
|
||||
}
|
||||
|
||||
if url := req.URL.String(); url != "http://hook.slack:80" {
|
||||
t.Errorf("Unexpected Host: %s", url)
|
||||
}
|
||||
}
|
||||
|
||||
slack := slack.NewClient("http://hook.slack:80", "", false, testClient{callback, 200})
|
||||
slack.Send(minimalResult)
|
||||
})
|
||||
|
||||
t.Run("Send Minimal Result", func(t *testing.T) {
|
||||
callback := func(req *http.Request) {
|
||||
if contentType := req.Header.Get("Content-Type"); contentType != "application/json; charset=utf-8" {
|
||||
t.Errorf("Unexpected Content-Type: %s", contentType)
|
||||
}
|
||||
|
||||
if agend := req.Header.Get("User-Agent"); agend != "Policy-Reporter" {
|
||||
t.Errorf("Unexpected Host: %s", agend)
|
||||
}
|
||||
|
||||
if url := req.URL.String(); url != "http://hook.slack:80" {
|
||||
t.Errorf("Unexpected Host: %s", url)
|
||||
}
|
||||
}
|
||||
|
||||
slack := slack.NewClient("http://hook.slack:80", "", false, testClient{callback, 200})
|
||||
slack.Send(minimalResult)
|
||||
})
|
||||
}
|
Loading…
Reference in a new issue