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

Improve CI

This commit is contained in:
Frank Jogeleit 2021-02-24 01:06:58 +01:00
parent 04c220d3e3
commit 1a1ab1786c
21 changed files with 1052 additions and 171 deletions

50
.github/workflows/ci.yaml vendored Normal file
View file

@ -0,0 +1,50 @@
name: CI
on:
push:
# Publish `master` as Docker `latest` image.
branches:
- main
# Publish `v1.2.3` tags as releases.
tags:
- v*
paths-ignore:
- README.md
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Set up Go 1.16
uses: actions/setup-go@v1
with:
go-version: 1.16
- name: Check out code into the Go module directory
uses: actions/checkout@v2
- name: Get dependencies
run: go get -v -t -d ./...
- name: Test
run: make test
coverage:
runs-on: ubuntu-latest
steps:
- name: Set up Go 1.16
uses: actions/setup-go@v2
with:
go-version: 1.16
- name: Checkout code
uses: actions/checkout@v2
- name: Get dependencies
run: go get -v -t -d ./...
- name: Calc coverage
run: make coverage
- name: Convert coverage to lcov
uses: jandelgado/gcov2lcov-action@v1.0.8
- name: Coveralls
uses: coverallsapp/github-action@v1.1.2
with:
github-token: ${{ secrets.github_token }}
path-to-lcov: coverage.lcov

View file

@ -12,6 +12,14 @@ clean:
prepare:
mkdir -p $(BUILD)
.PHONY: test
test:
go test -v ./...
.PHONY: coverage
coverage:
go test -v ./... -covermode=count -coverprofile=coverage.out
.PHONY: build
build: prepare
CGO_ENABLED=0 $(GO) build -v -ldflags="-s -w" $(GOFLAGS) -o $(BUILD)/policyreporter .

View file

@ -1,4 +1,5 @@
# PolicyReporter
[![CI](https://github.com/fjogeleit/policy-reporter/actions/workflows/ci.yaml/badge.svg)](https://github.com/fjogeleit/policy-reporter/actions/workflows/ci.yaml)
## Motivation

View file

@ -31,7 +31,7 @@ spec:
- --loki-minimum-priority={{ .Values.loki.minimumPriority }}
{{- end }}
{{- if .Values.loki.skipExistingOnStartup }}
- --loki-skip-exising-on-startup
- --loki-skip-existing-on-startup
{{- end }}
ports:
- name: http

View file

@ -12,23 +12,14 @@ import (
"golang.org/x/sync/errgroup"
)
type PolicySeverity = string
const (
Fail PolicySeverity = "fail"
Warn PolicySeverity = "warn"
Error PolicySeverity = "error"
Pass PolicySeverity = "pass"
Skip PolicySeverity = "skip"
)
// NewCLI creates a new instance of the root CLI
func NewCLI() *cobra.Command {
rootCmd := &cobra.Command{
Use: "run",
Short: "Kyverno Policy API",
Long: `Kyverno Policy API and Monitoring`,
RunE: func(cmd *cobra.Command, args []string) error {
c, err := LoadConfig(cmd)
c, err := loadConfig(cmd)
if err != nil {
return err
}
@ -80,14 +71,14 @@ func NewCLI() *cobra.Command {
rootCmd.PersistentFlags().String("loki", "", "loki host: http://loki:3100")
rootCmd.PersistentFlags().String("loki-minimum-priority", "", "Minimum Priority to send Results to Loki (info < warning < error)")
rootCmd.PersistentFlags().Bool("loki-skip-exising-on-startup", false, "Skip Results created before PolicyReporter started. Prevent duplicated sending after new deployment")
rootCmd.PersistentFlags().Bool("loki-skip-existing-on-startup", false, "Skip Results created before PolicyReporter started. Prevent duplicated sending after new deployment")
flag.Parse()
return rootCmd
}
func LoadConfig(cmd *cobra.Command) (*config.Config, error) {
func loadConfig(cmd *cobra.Command) (*config.Config, error) {
v := viper.New()
v.SetDefault("namespace", "policy-reporter")
@ -100,7 +91,7 @@ func LoadConfig(cmd *cobra.Command) (*config.Config, error) {
if flag := cmd.Flags().Lookup("loki-minimum-priority"); flag != nil {
v.BindPFlag("loki.minimumPriority", flag)
}
if flag := cmd.Flags().Lookup("loki-skip-exising-on-startup"); flag != nil {
if flag := cmd.Flags().Lookup("loki-skip-existing-on-startup"); flag != nil {
v.BindPFlag("loki.skipExistingOnStartup", flag)
}

View file

@ -1,5 +1,6 @@
package config
// Config of the PolicyReporter
type Config struct {
Loki struct {
Host string `mapstructure:"host"`

View file

@ -2,6 +2,7 @@ package config
import (
"context"
"net/http"
"time"
"github.com/fjogeleit/policy-reporter/pkg/kubernetes"
@ -18,10 +19,12 @@ var (
clusterPolicyReportMetrics metrics.Metrics
)
// Resolver manages dependencies
type Resolver struct {
config *Config
}
// PolicyReportClient resolver method
func (r *Resolver) PolicyReportClient() (report.Client, error) {
if kubeClient != nil {
return kubeClient, nil
@ -39,6 +42,7 @@ func (r *Resolver) PolicyReportClient() (report.Client, error) {
return client, err
}
// LokiClient resolver method
func (r *Resolver) LokiClient() target.Client {
if lokiClient != nil {
return lokiClient
@ -51,11 +55,13 @@ func (r *Resolver) LokiClient() target.Client {
lokiClient = loki.NewClient(
r.config.Loki.Host,
r.config.Loki.MinimumPriority,
&http.Client{},
)
return lokiClient
}
// PolicyReportMetrics resolver method
func (r *Resolver) PolicyReportMetrics() (metrics.Metrics, error) {
if policyReportMetrics != nil {
return policyReportMetrics, nil
@ -71,6 +77,7 @@ func (r *Resolver) PolicyReportMetrics() (metrics.Metrics, error) {
return policyReportMetrics, nil
}
// ClusterPolicyReportMetrics resolver method
func (r *Resolver) ClusterPolicyReportMetrics() (metrics.Metrics, error) {
if clusterPolicyReportMetrics != nil {
return clusterPolicyReportMetrics, nil
@ -86,6 +93,15 @@ func (r *Resolver) ClusterPolicyReportMetrics() (metrics.Metrics, error) {
return clusterPolicyReportMetrics, nil
}
// Reset all cached dependencies
func (r *Resolver) Reset() {
kubeClient = nil
lokiClient = nil
policyReportMetrics = nil
clusterPolicyReportMetrics = nil
}
// NewResolver constructor function
func NewResolver(config *Config) Resolver {
return Resolver{config}
}

View file

@ -0,0 +1,54 @@
package config_test
import (
"testing"
"github.com/fjogeleit/policy-reporter/pkg/config"
)
var testConfig = &config.Config{
Loki: struct {
Host string "mapstructure:\"host\""
SkipExisting bool "mapstructure:\"skipExistingOnStartup\""
MinimumPriority string "mapstructure:\"minimumPriority\""
}{
Host: "http://localhost:3100",
SkipExisting: true,
MinimumPriority: "debug",
},
}
func Test_ResolveLokiClient(t *testing.T) {
resolver := config.NewResolver(testConfig)
client := resolver.LokiClient()
if client == nil {
t.Error("Expected Client, got nil")
}
client2 := resolver.LokiClient()
if client != client2 {
t.Error("Error: Should reuse first instance")
}
}
func Test_ResolveLokiClientWithoutHost(t *testing.T) {
config2 := &config.Config{
Loki: struct {
Host string "mapstructure:\"host\""
SkipExisting bool "mapstructure:\"skipExistingOnStartup\""
MinimumPriority string "mapstructure:\"minimumPriority\""
}{
Host: "",
SkipExisting: true,
MinimumPriority: "debug",
},
}
resolver := config.NewResolver(config2)
resolver.Reset()
if resolver.LokiClient() != nil {
t.Error("Expected Client to be nil if no host is configured")
}
}

View file

@ -11,11 +11,15 @@ import (
"k8s.io/client-go/tools/clientcmd"
)
// CoreClient provides simplified APIs for ConfigMap Resources
type CoreClient interface {
// GetConfig return a single ConfigMap by name if exist
GetConfig(ctx context.Context, name string) (*apiv1.ConfigMap, error)
// WatchConfigs calls its ConfigMapCallback whenever a ConfigMap was added, modified or deleted
WatchConfigs(ctx context.Context, cb ConfigMapCallback) error
}
// ConfigMapCallback is used by WatchConfigs
type ConfigMapCallback = func(watch.EventType, *apiv1.ConfigMap)
type coreClient struct {
@ -41,6 +45,7 @@ func (c coreClient) WatchConfigs(ctx context.Context, cb ConfigMapCallback) erro
}
}
// NewCoreClient creates a new CoreClient with the provided kubeconfig or InCluster configuration if kubeconfig is empty
func NewCoreClient(kubeconfig, namespace string) (CoreClient, error) {
var config *rest.Config
var err error

166
pkg/kubernetes/mapper.go Normal file
View file

@ -0,0 +1,166 @@
package kubernetes
import (
"errors"
"time"
"github.com/fjogeleit/policy-reporter/pkg/report"
)
// Mapper converts maps into report structs
type Mapper interface {
// MapPolicyReport maps a map into a PolicyReport
MapPolicyReport(reportMap map[string]interface{}) report.PolicyReport
// MapClusterPolicyReport maps a map into a ClusterPolicyReport
MapClusterPolicyReport(reportMap map[string]interface{}) report.ClusterPolicyReport
// SetPriorityMap updates the policy/status to priority mapping
SetPriorityMap(map[string]string)
}
type mapper struct {
priorityMap map[string]string
}
func (m *mapper) MapPolicyReport(reportMap map[string]interface{}) report.PolicyReport {
summary := report.Summary{}
if s, ok := reportMap["summary"].(map[string]interface{}); ok {
summary.Pass = int(s["pass"].(int64))
summary.Skip = int(s["skip"].(int64))
summary.Warn = int(s["warn"].(int64))
summary.Error = int(s["error"].(int64))
summary.Fail = int(s["fail"].(int64))
}
r := report.PolicyReport{
Name: reportMap["metadata"].(map[string]interface{})["name"].(string),
Namespace: reportMap["metadata"].(map[string]interface{})["namespace"].(string),
Summary: summary,
Results: make(map[string]report.Result),
}
creationTimestamp, err := m.mapCreationTime(reportMap)
if err == nil {
r.CreationTimestamp = creationTimestamp
} else {
r.CreationTimestamp = time.Now()
}
if rs, ok := reportMap["results"].([]interface{}); ok {
for _, resultItem := range rs {
res := m.mapResult(resultItem.(map[string]interface{}))
r.Results[res.GetIdentifier()] = res
}
}
return r
}
func (m *mapper) MapClusterPolicyReport(reportMap map[string]interface{}) report.ClusterPolicyReport {
summary := report.Summary{}
if s, ok := reportMap["summary"].(map[string]interface{}); ok {
summary.Pass = int(s["pass"].(int64))
summary.Skip = int(s["skip"].(int64))
summary.Warn = int(s["warn"].(int64))
summary.Error = int(s["error"].(int64))
summary.Fail = int(s["fail"].(int64))
}
r := report.ClusterPolicyReport{
Name: reportMap["metadata"].(map[string]interface{})["name"].(string),
Summary: summary,
Results: make(map[string]report.Result),
}
creationTimestamp, err := m.mapCreationTime(reportMap)
if err == nil {
r.CreationTimestamp = creationTimestamp
} else {
r.CreationTimestamp = time.Now()
}
if rs, ok := reportMap["results"].([]interface{}); ok {
for _, resultItem := range rs {
res := m.mapResult(resultItem.(map[string]interface{}))
r.Results[res.GetIdentifier()] = res
}
}
return r
}
func (m *mapper) SetPriorityMap(priorityMap map[string]string) {
m.priorityMap = priorityMap
}
func (m *mapper) mapCreationTime(result map[string]interface{}) (time.Time, error) {
if metadata, ok := result["metadata"].(map[string]interface{}); ok {
if created, ok2 := metadata["creationTimestamp"].(string); ok2 {
return time.Parse("2006-01-02T15:04:05Z", created)
}
return time.Time{}, errors.New("No creationTimestamp provided")
}
return time.Time{}, errors.New("No metadata provided")
}
func (m *mapper) mapResult(result map[string]interface{}) report.Result {
var resources []report.Resource
if ress, ok := result["resources"].([]interface{}); ok {
for _, res := range ress {
if resMap, ok := res.(map[string]interface{}); ok {
r := report.Resource{
APIVersion: resMap["apiVersion"].(string),
Kind: resMap["kind"].(string),
Name: resMap["name"].(string),
UID: resMap["uid"].(string),
}
if ns, ok := resMap["namespace"]; ok {
r.Namespace = ns.(string)
}
resources = append(resources, r)
}
}
}
status := result["status"].(report.Status)
r := report.Result{
Message: result["message"].(string),
Policy: result["policy"].(string),
Status: status,
Scored: result["scored"].(bool),
Priority: report.PriorityFromStatus(status),
Resources: resources,
}
if r.Status == report.Error || r.Status == report.Fail {
if priority, ok := m.priorityMap[r.Policy]; ok {
r.Priority = report.NewPriority(priority)
}
}
if rule, ok := result["rule"]; ok {
r.Rule = rule.(string)
}
if category, ok := result["category"]; ok {
r.Category = category.(string)
}
if severity, ok := result["severity"]; ok {
r.Severity = severity.(report.Severity)
}
return r
}
// NewMapper creates an new Mapper instance
func NewMapper(priorityMap map[string]string) Mapper {
return &mapper{priorityMap}
}

View file

@ -0,0 +1,292 @@
package kubernetes_test
import (
"testing"
"github.com/fjogeleit/policy-reporter/pkg/kubernetes"
"github.com/fjogeleit/policy-reporter/pkg/report"
)
var policyMap = map[string]interface{}{
"metadata": map[string]interface{}{
"name": "policy-report",
"namespace": "test",
"creationTimestamp": "2021-02-23T15:00:00Z",
},
"summary": map[string]interface{}{
"pass": int64(1),
"skip": int64(2),
"warn": int64(3),
"fail": int64(4),
"error": int64(5),
},
"results": []interface{}{
map[string]interface{}{
"message": "message",
"status": "fail",
"scored": true,
"policy": "required-label",
"rule": "app-label-required",
"category": "test",
"severity": "low",
"resources": []interface{}{
map[string]interface{}{
"apiVersion": "v1",
"kind": "Deployment",
"name": "nginx",
"namespace": "test",
"uid": "dfd57c50-f30c-4729-b63f-b1954d8988d1",
},
},
},
map[string]interface{}{
"message": "message 2",
"status": "fail",
"scored": true,
"policy": "priority-test",
"resources": []interface{}{},
},
},
}
var minPolicyMap = map[string]interface{}{
"metadata": map[string]interface{}{
"name": "policy-report",
"namespace": "test",
},
"results": []interface{}{},
}
var clusterPolicyMap = map[string]interface{}{
"metadata": map[string]interface{}{
"name": "clusterpolicy-report",
"creationTimestamp": "2021-02-23T15:00:00Z",
},
"summary": map[string]interface{}{
"pass": int64(1),
"skip": int64(2),
"warn": int64(3),
"fail": int64(4),
"error": int64(5),
},
"results": []interface{}{
map[string]interface{}{
"message": "message",
"status": "fail",
"scored": true,
"policy": "required-label",
"rule": "app-label-required",
"category": "test",
"severity": "low",
"resources": []interface{}{
map[string]interface{}{
"apiVersion": "v1",
"kind": "Namespace",
"name": "policy-reporter",
"uid": "dfd57c50-f30c-4729-b63f-b1954d8988d1",
},
},
},
},
}
var minClusterPolicyMap = map[string]interface{}{
"metadata": map[string]interface{}{
"name": "clusterpolicy-report",
},
"results": []interface{}{},
}
var priorityMap = map[string]string{
"priority-test": "warning",
}
var mapper = kubernetes.NewMapper(priorityMap)
func Test_MapPolicyReport(t *testing.T) {
preport := mapper.MapPolicyReport(policyMap)
if preport.Name != "policy-report" {
t.Errorf("Expected Name 'policy-report' (acutal %s)", preport.Name)
}
if preport.Namespace != "test" {
t.Errorf("Expected Name 'test' (acutal %s)", preport.Namespace)
}
if preport.Summary.Pass != 1 {
t.Errorf("Unexpected Summary.Pass value %d (expected 1)", preport.Summary.Pass)
}
if preport.Summary.Skip != 2 {
t.Errorf("Unexpected Summary.Skip value %d (expected 2)", preport.Summary.Skip)
}
if preport.Summary.Warn != 3 {
t.Errorf("Unexpected Summary.Warn value %d (expected 3)", preport.Summary.Warn)
}
if preport.Summary.Fail != 4 {
t.Errorf("Unexpected Summary.Fail value %d (expected 4)", preport.Summary.Fail)
}
if preport.Summary.Error != 5 {
t.Errorf("Unexpected Summary.Error value %d (expected 5)", preport.Summary.Error)
}
result1, ok := preport.Results["required-label__app-label-required__fail__dfd57c50-f30c-4729-b63f-b1954d8988d1"]
if !ok {
t.Error("Expected result not found")
}
if result1.Message != "message" {
t.Errorf("Expected Message 'message' (acutal %s)", result1.Message)
}
if result1.Status != report.Fail {
t.Errorf("Expected Message '%s' (acutal %s)", report.Fail, result1.Status)
}
if result1.Priority != report.ErrorPriority {
t.Errorf("Expected Priority '%d' (acutal %d)", report.ErrorPriority, result1.Priority)
}
if !result1.Scored {
t.Errorf("Expected Scored to be true")
}
if result1.Policy != "required-label" {
t.Errorf("Expected Policy 'required-label' (acutal %s)", result1.Policy)
}
if result1.Rule != "app-label-required" {
t.Errorf("Expected Rule 'app-label-required' (acutal %s)", result1.Rule)
}
if result1.Category != "test" {
t.Errorf("Expected Category 'test' (acutal %s)", result1.Category)
}
if result1.Severity != report.Low {
t.Errorf("Expected Severity '%s' (acutal %s)", report.Low, result1.Severity)
}
resource := result1.Resources[0]
if resource.APIVersion != "v1" {
t.Errorf("Expected Resource.APIVersion 'v1' (acutal %s)", resource.APIVersion)
}
if resource.Kind != "Deployment" {
t.Errorf("Expected Resource.Kind 'Deployment' (acutal %s)", resource.Kind)
}
if resource.Name != "nginx" {
t.Errorf("Expected Resource.Name 'nginx' (acutal %s)", resource.Name)
}
if resource.Namespace != "test" {
t.Errorf("Expected Resource.Namespace 'test' (acutal %s)", resource.Namespace)
}
if resource.UID != "dfd57c50-f30c-4729-b63f-b1954d8988d1" {
t.Errorf("Expected Resource.Namespace 'dfd57c50-f30c-4729-b63f-b1954d8988d1' (acutal %s)", resource.UID)
}
result2, ok := preport.Results["priority-test____fail"]
if !ok {
t.Error("Expected result not found")
}
if result2.Message != "message 2" {
t.Errorf("Expected Message 'message' (acutal %s)", result1.Message)
}
if result2.Status != report.Fail {
t.Errorf("Expected Message '%s' (acutal %s)", report.Fail, result2.Status)
}
if result2.Priority != report.WarningPriority {
t.Errorf("Expected Priority '%d' (acutal %s)", report.WarningPriority, result2.Priority)
}
if !result2.Scored {
t.Errorf("Expected Scored to be true")
}
if result2.Policy != "priority-test" {
t.Errorf("Expected Policy 'priority-test' (acutal %s)", result2.Policy)
}
if result2.Rule != "" {
t.Errorf("Expected Rule to be empty (acutal %s)", result2.Rule)
}
if result2.Category != "" {
t.Errorf("Expected Category to be empty (acutal %s)", result2.Category)
}
if result2.Severity != "" {
t.Errorf("Expected Severity to be empty (acutal %s)", report.Low)
}
}
func Test_MapMinPolicyReport(t *testing.T) {
report := mapper.MapPolicyReport(minPolicyMap)
if report.Name != "policy-report" {
t.Errorf("Expected Name 'policy-report' (acutal %s)", report.Name)
}
if report.Namespace != "test" {
t.Errorf("Expected Name 'test' (acutal %s)", report.Namespace)
}
if report.Summary.Pass != 0 {
t.Errorf("Unexpected Summary.Pass value %d (expected 0)", report.Summary.Pass)
}
if report.Summary.Skip != 0 {
t.Errorf("Unexpected Summary.Skip value %d (expected 0)", report.Summary.Skip)
}
if report.Summary.Warn != 0 {
t.Errorf("Unexpected Summary.Warn value %d (expected 0)", report.Summary.Warn)
}
if report.Summary.Fail != 0 {
t.Errorf("Unexpected Summary.Fail value %d (expected 0)", report.Summary.Fail)
}
if report.Summary.Error != 0 {
t.Errorf("Unexpected Summary.Error value %d (expected 0)", report.Summary.Error)
}
}
func Test_MapClusterPolicyReport(t *testing.T) {
report := mapper.MapClusterPolicyReport(clusterPolicyMap)
if report.Name != "clusterpolicy-report" {
t.Errorf("Expected Name 'clusterpolicy-report' (acutal %s)", report.Name)
}
if report.Summary.Pass != 1 {
t.Errorf("Unexpected Summary.Pass value %d (expected 1)", report.Summary.Pass)
}
if report.Summary.Skip != 2 {
t.Errorf("Unexpected Summary.Skip value %d (expected 2)", report.Summary.Skip)
}
if report.Summary.Warn != 3 {
t.Errorf("Unexpected Summary.Warn value %d (expected 3)", report.Summary.Warn)
}
if report.Summary.Fail != 4 {
t.Errorf("Unexpected Summary.Fail value %d (expected 4)", report.Summary.Fail)
}
if report.Summary.Error != 5 {
t.Errorf("Unexpected Summary.Error value %d (expected 5)", report.Summary.Error)
}
}
func Test_MapMinClusterPolicyReport(t *testing.T) {
report := mapper.MapClusterPolicyReport(minClusterPolicyMap)
if report.Name != "clusterpolicy-report" {
t.Errorf("Expected Name 'clusterpolicy-report' (acutal %s)", report.Name)
}
if report.Summary.Pass != 0 {
t.Errorf("Unexpected Summary.Pass value %d (expected 0)", report.Summary.Pass)
}
if report.Summary.Skip != 0 {
t.Errorf("Unexpected Summary.Skip value %d (expected 0)", report.Summary.Skip)
}
if report.Summary.Warn != 0 {
t.Errorf("Unexpected Summary.Warn value %d (expected 0)", report.Summary.Warn)
}
if report.Summary.Fail != 0 {
t.Errorf("Unexpected Summary.Fail value %d (expected 0)", report.Summary.Fail)
}
if report.Summary.Error != 0 {
t.Errorf("Unexpected Summary.Error value %d (expected 0)", report.Summary.Error)
}
}
func Test_MapperSetPriorityMap(t *testing.T) {
mapper := kubernetes.NewMapper(make(map[string]string))
mapper.SetPriorityMap(map[string]string{"required-label": "debug"})
preport := mapper.MapPolicyReport(policyMap)
result1 := preport.Results["required-label__app-label-required__fail__dfd57c50-f30c-4729-b63f-b1954d8988d1"]
if result1.Priority != report.DebugPriority {
t.Errorf("Expected Policy '%d' (acutal %d)", report.DebugPriority, result1.Priority)
}
}

View file

@ -2,7 +2,6 @@ package kubernetes
import (
"context"
"errors"
"log"
"time"
@ -32,7 +31,7 @@ type policyReportClient struct {
coreClient CoreClient
policyCache map[string]report.PolicyReport
clusterPolicyCache map[string]report.ClusterPolicyReport
priorityMap map[string]string
mapper Mapper
startUp time.Time
}
@ -46,7 +45,7 @@ func (c *policyReportClient) FetchPolicyReports() ([]report.PolicyReport, error)
}
for _, item := range result.Items {
reports = append(reports, c.mapPolicyReport(item.Object))
reports = append(reports, c.mapper.MapPolicyReport(item.Object))
}
return reports, nil
@ -61,7 +60,7 @@ func (c *policyReportClient) WatchClusterPolicyReports(cb report.WatchClusterPol
for result := range result.ResultChan() {
if item, ok := result.Object.(*unstructured.Unstructured); ok {
cb(result.Type, c.mapClusterPolicyReport(item.Object))
cb(result.Type, c.mapper.MapClusterPolicyReport(item.Object))
}
}
}
@ -76,7 +75,7 @@ func (c *policyReportClient) WatchPolicyReports(cb report.WatchPolicyReportCallb
for result := range result.ResultChan() {
if item, ok := result.Object.(*unstructured.Unstructured); ok {
cb(result.Type, c.mapPolicyReport(item.Object))
cb(result.Type, c.mapper.MapPolicyReport(item.Object))
}
}
}
@ -100,7 +99,7 @@ func (c *policyReportClient) WatchRuleValidation(cb report.WatchPolicyResultCall
c.policyCache[pr.GetIdentifier()] = pr
case watch.Modified:
diff := pr.GetNewValidation(c.policyCache[pr.GetIdentifier()])
diff := pr.GetNewResults(c.policyCache[pr.GetIdentifier()])
for _, result := range diff {
cb(result)
}
@ -127,7 +126,7 @@ func (c *policyReportClient) WatchRuleValidation(cb report.WatchPolicyResultCall
c.clusterPolicyCache[cpr.GetIdentifier()] = cpr
case watch.Modified:
diff := cpr.GetNewValidation(c.clusterPolicyCache[cpr.GetIdentifier()])
diff := cpr.GetNewResults(c.clusterPolicyCache[cpr.GetIdentifier()])
for _, result := range diff {
cb(result)
}
@ -149,7 +148,7 @@ func (c *policyReportClient) fetchPriorities(ctx context.Context) error {
}
if cm != nil {
c.priorityMap = cm.Data
c.mapper.SetPriorityMap(cm.Data)
log.Println("[INFO] Priorities loaded")
}
@ -164,11 +163,11 @@ func (c *policyReportClient) syncPriorities(ctx context.Context) error {
switch e {
case watch.Added:
c.priorityMap = cm.Data
c.mapper.SetPriorityMap(cm.Data)
case watch.Modified:
c.priorityMap = cm.Data
c.mapper.SetPriorityMap(cm.Data)
case watch.Deleted:
c.priorityMap = map[string]string{}
c.mapper.SetPriorityMap(map[string]string{})
}
log.Println("[INFO] Priorities synchronized")
@ -181,134 +180,7 @@ func (c *policyReportClient) syncPriorities(ctx context.Context) error {
return err
}
func (c *policyReportClient) mapPolicyReport(reportMap map[string]interface{}) report.PolicyReport {
summary := report.Summary{}
if s, ok := reportMap["summary"].(map[string]interface{}); ok {
summary.Pass = int(s["pass"].(int64))
summary.Skip = int(s["skip"].(int64))
summary.Warn = int(s["warn"].(int64))
summary.Error = int(s["error"].(int64))
summary.Fail = int(s["fail"].(int64))
}
r := report.PolicyReport{
Name: reportMap["metadata"].(map[string]interface{})["name"].(string),
Namespace: reportMap["metadata"].(map[string]interface{})["namespace"].(string),
Summary: summary,
Results: make(map[string]report.Result),
}
if rs, ok := reportMap["results"].([]interface{}); ok {
for _, resultItem := range rs {
res := c.mapResult(resultItem.(map[string]interface{}))
r.Results[res.GetIdentifier()] = res
}
}
return r
}
func (c *policyReportClient) mapClusterPolicyReport(reportMap map[string]interface{}) report.ClusterPolicyReport {
summary := report.Summary{}
if s, ok := reportMap["summary"].(map[string]interface{}); ok {
summary.Pass = int(s["pass"].(int64))
summary.Skip = int(s["skip"].(int64))
summary.Warn = int(s["warn"].(int64))
summary.Error = int(s["error"].(int64))
summary.Fail = int(s["fail"].(int64))
}
r := report.ClusterPolicyReport{
Name: reportMap["metadata"].(map[string]interface{})["name"].(string),
Summary: summary,
Results: make(map[string]report.Result),
}
creationTimestamp, err := c.mapCreationTime(reportMap)
if err == nil {
r.CreationTimestamp = creationTimestamp
} else {
r.CreationTimestamp = time.Now()
}
if rs, ok := reportMap["results"].([]interface{}); ok {
for _, resultItem := range rs {
res := c.mapResult(resultItem.(map[string]interface{}))
r.Results[res.GetIdentifier()] = res
}
}
return r
}
func (c *policyReportClient) mapCreationTime(result map[string]interface{}) (time.Time, error) {
if metadata, ok := result["metadata"].(map[string]interface{}); ok {
if created, ok2 := metadata["creationTimestamp"].(string); ok2 {
return time.Parse("2006-01-02T15:04:05Z", created)
}
return time.Time{}, errors.New("No creationTimestamp provided")
}
return time.Time{}, errors.New("No metadata provided")
}
func (c *policyReportClient) mapResult(result map[string]interface{}) report.Result {
var resources []report.Resource
if ress, ok := result["resources"].([]interface{}); ok {
for _, res := range ress {
if resMap, ok := res.(map[string]interface{}); ok {
r := report.Resource{
APIVersion: resMap["apiVersion"].(string),
Kind: resMap["kind"].(string),
Name: resMap["name"].(string),
UID: resMap["uid"].(string),
}
if ns, ok := result["namespace"]; ok {
r.Namespace = ns.(string)
}
resources = append(resources, r)
}
}
}
status := result["status"].(report.Status)
r := report.Result{
Message: result["message"].(string),
Policy: result["policy"].(string),
Status: status,
Scored: result["scored"].(bool),
Priority: report.PriorityFromStatus(status),
Resources: resources,
}
if r.Status == report.Error || r.Status == report.Fail {
if priority, ok := c.priorityMap[r.Policy]; ok {
r.Priority = report.NewPriority(priority)
}
}
if rule, ok := result["rule"]; ok {
r.Rule = rule.(string)
}
if category, ok := result["category"]; ok {
r.Category = category.(string)
}
if severity, ok := result["severity"]; ok {
r.Severity = severity.(report.Severity)
}
return r
}
// NewPolicyReportClient creates a new ReportClient based on the kubernetes go-client
func NewPolicyReportClient(ctx context.Context, kubeconfig, namespace string, startUp time.Time) (report.Client, error) {
var config *rest.Config
var err error
@ -337,7 +209,7 @@ func NewPolicyReportClient(ctx context.Context, kubeconfig, namespace string, st
coreClient: coreClient,
policyCache: make(map[string]report.PolicyReport),
clusterPolicyCache: make(map[string]report.ClusterPolicyReport),
priorityMap: make(map[string]string),
mapper: NewMapper(make(map[string]string)),
startUp: startUp,
}

View file

@ -9,6 +9,7 @@ import (
"k8s.io/apimachinery/pkg/watch"
)
// ClusterPolicyReportMetrics creates ClusterPolicy Metrics
type ClusterPolicyReportMetrics struct {
client report.Client
cache map[string]report.ClusterPolicyReport
@ -33,6 +34,7 @@ func (m ClusterPolicyReportMetrics) removeCachedReport(i string) {
m.rwmutex.Unlock()
}
// GenerateMetrics for ClusterPolicyReport Summaries and PolicyResults
func (m ClusterPolicyReportMetrics) GenerateMetrics() error {
policyGauge := promauto.NewGaugeVec(prometheus.GaugeOpts{
Name: "cluster_policy_report_summary",
@ -132,6 +134,7 @@ func updateClusterPolicyGauge(policyGauge *prometheus.GaugeVec, report report.Cl
Set(float64(report.Summary.Skip))
}
// NewClusterPolicyMetrics creates a new ClusterPolicyReportMetrics pointer
func NewClusterPolicyMetrics(client report.Client) *ClusterPolicyReportMetrics {
return &ClusterPolicyReportMetrics{
client: client,

View file

@ -1,5 +1,7 @@
package metrics
// Metrics interface for Prometheus Metrics used for the Resolver
type Metrics interface {
// GenerateMetrics for Prometheus
GenerateMetrics() error
}

View file

@ -9,6 +9,7 @@ import (
"k8s.io/apimachinery/pkg/watch"
)
// PolicyReportMetrics creates ClusterPolicy Metrics
type PolicyReportMetrics struct {
client report.Client
cache map[string]report.PolicyReport
@ -33,6 +34,7 @@ func (m PolicyReportMetrics) removeCachedReport(i string) {
m.rwmutex.Unlock()
}
// GenerateMetrics for PolicyReport Summaries and PolicyResults
func (m PolicyReportMetrics) GenerateMetrics() error {
policyGauge := promauto.NewGaugeVec(prometheus.GaugeOpts{
Name: "policy_report_summary",
@ -146,6 +148,7 @@ func updatePolicyGauge(policyGauge *prometheus.GaugeVec, report report.PolicyRep
Set(float64(report.Summary.Skip))
}
// NewPolicyReportMetrics creates a new PolicyReportMetrics pointer
func NewPolicyReportMetrics(client report.Client) *PolicyReportMetrics {
return &PolicyReportMetrics{
client: client,

View file

@ -2,13 +2,23 @@ package report
import "k8s.io/apimachinery/pkg/watch"
// WatchPolicyReportCallback is called whenver a new PolicyReport comes in
type WatchPolicyReportCallback = func(watch.EventType, PolicyReport)
// WatchClusterPolicyReportCallback is called whenver a new ClusterPolicyReport comes in
type WatchClusterPolicyReportCallback = func(watch.EventType, ClusterPolicyReport)
// WatchPolicyResultCallback is called whenver a new PolicyResult comes in
type WatchPolicyResultCallback = func(Result)
// Client interface for interacting with the Kubernetes API
type Client interface {
// FetchPolicyReports from the unterlying API
FetchPolicyReports() ([]PolicyReport, error)
// WatchPolicyReports blocking API to watch for PolicyReport changes
WatchPolicyReports(WatchPolicyReportCallback) error
// WatchRuleValidation blocking API to watch for PolicyResult changes from PolicyReports and ClusterPolicyReports
WatchRuleValidation(WatchPolicyResultCallback, bool) error
// WatchClusterPolicyReports blocking API to watch for ClusterPolicyReport changes
WatchClusterPolicyReports(WatchClusterPolicyReportCallback) error
}

View file

@ -5,9 +5,13 @@ import (
"time"
)
// Status Enum defined for PolicyReport
type Status = string
// Severity Enum defined for PolicyReport
type Severity = string
// Enums for predefined values from the PolicyReport spec
const (
Fail Status = "fail"
Warn Status = "warn"
@ -26,6 +30,7 @@ const (
errorString = "error"
)
// Internal Priority definitions and weighting
const (
DefaultPriority = iota
DebugPriority
@ -34,8 +39,10 @@ const (
ErrorPriority
)
// Priority Enum for internal Result weighting
type Priority int
// String maps the internal weighting of Priorities to a String representation
func (p Priority) String() string {
switch p {
case DebugPriority:
@ -51,6 +58,7 @@ func (p Priority) String() string {
}
}
// PriorityFromStatus creates a Priority based on a Status
func PriorityFromStatus(p Status) Priority {
switch p {
case Fail:
@ -66,6 +74,7 @@ func PriorityFromStatus(p Status) Priority {
}
}
// NewPriority creates a new Priority based an its string representation
func NewPriority(p string) Priority {
switch p {
case debugString:
@ -81,6 +90,7 @@ func NewPriority(p string) Priority {
}
}
// Resource from the Kubernetes spec k8s.io/api/core/v1.ObjectReference
type Resource struct {
APIVersion string
Kind string
@ -89,6 +99,7 @@ type Resource struct {
UID string
}
// Result from the PolicyReport spec wgpolicyk8s.io/v1alpha1.PolicyReportResult
type Result struct {
Message string
Policy string
@ -101,19 +112,17 @@ type Result struct {
Resources []Resource
}
// GetIdentifier returns a global unique Result identifier
func (r Result) GetIdentifier() string {
res := Resource{}
suffix := ""
if len(r.Resources) > 0 {
res = r.Resources[0]
suffix = "__" + r.Resources[0].UID
}
return fmt.Sprintf("%s__%s__%s__%s", r.Policy, r.Rule, r.Status, res.UID)
}
type Report interface {
GetIdentifier() string
return fmt.Sprintf("%s__%s__%s%s", r.Policy, r.Rule, r.Status, suffix)
}
// Summary from the PolicyReport spec wgpolicyk8s.io/v1alpha1.PolicyReportSummary
type Summary struct {
Pass int
Skip int
@ -122,6 +131,7 @@ type Summary struct {
Fail int
}
// PolicyReport from the PolicyReport spec wgpolicyk8s.io/v1alpha1.PolicyReport
type PolicyReport struct {
Name string
Namespace string
@ -130,11 +140,13 @@ type PolicyReport struct {
CreationTimestamp time.Time
}
// GetIdentifier returns a global unique PolicyReport identifier
func (pr PolicyReport) GetIdentifier() string {
return fmt.Sprintf("%s__%s", pr.Namespace, pr.Name)
}
func (pr PolicyReport) GetNewValidation(or PolicyReport) []Result {
// GetNewResults filters already existing Results from the old PolicyReport and returns only the diff with new Results
func (pr PolicyReport) GetNewResults(or PolicyReport) []Result {
diff := make([]Result, 0)
for _, r := range pr.Results {
@ -148,6 +160,7 @@ func (pr PolicyReport) GetNewValidation(or PolicyReport) []Result {
return diff
}
// ClusterPolicyReport from the PolicyReport spec wgpolicyk8s.io/v1alpha1.ClusterPolicyReport
type ClusterPolicyReport struct {
Name string
Results map[string]Result
@ -155,11 +168,13 @@ type ClusterPolicyReport struct {
CreationTimestamp time.Time
}
// GetIdentifier returns a global unique ClusterPolicyReport identifier
func (cr ClusterPolicyReport) GetIdentifier() string {
return cr.Name
}
func (cr ClusterPolicyReport) GetNewValidation(cor ClusterPolicyReport) []Result {
// GetNewResults filters already existing Results from the old PolicyReport and returns only the diff with new Results
func (cr ClusterPolicyReport) GetNewResults(cor ClusterPolicyReport) []Result {
diff := make([]Result, 0)
for _, r := range cr.Results {

170
pkg/report/model_test.go Normal file
View file

@ -0,0 +1,170 @@
package report_test
import (
"fmt"
"testing"
"time"
"github.com/fjogeleit/policy-reporter/pkg/report"
)
var result1 = 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",
},
},
}
var result2 = 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-274a56188419",
},
},
}
var preport = report.PolicyReport{
Name: "polr-test",
Namespace: "test",
Results: make(map[string]report.Result, 0),
Summary: report.Summary{},
CreationTimestamp: time.Now(),
}
var creport = report.ClusterPolicyReport{
Name: "cpolr-test",
Results: make(map[string]report.Result, 0),
Summary: report.Summary{},
CreationTimestamp: time.Now(),
}
func Test_PolicyReport(t *testing.T) {
t.Run("Check PolicyReport.GetIdentifier", func(t *testing.T) {
expected := fmt.Sprintf("%s__%s", preport.Namespace, preport.Name)
if preport.GetIdentifier() != expected {
t.Errorf("Expected PolicyReport.GetIdentifier() to be %s (actual: %s)", expected, preport.GetIdentifier())
}
})
t.Run("Check PolicyReport.GetNewResults", func(t *testing.T) {
preport1 := preport
preport2 := preport
preport1.Results = map[string]report.Result{result1.GetIdentifier(): result1}
preport2.Results = map[string]report.Result{result1.GetIdentifier(): result1, result2.GetIdentifier(): result2}
diff := preport2.GetNewResults(preport1)
if len(diff) != 1 {
t.Error("Expected 1 new result in diff")
}
})
}
func Test_ClusterPolicyReport(t *testing.T) {
t.Run("Check ClusterPolicyReport.GetIdentifier", func(t *testing.T) {
if creport.GetIdentifier() != creport.Name {
t.Errorf("Expected ClusterPolicyReport.GetIdentifier() to be %s (actual: %s)", creport.Name, creport.GetIdentifier())
}
})
t.Run("Check ClusterPolicyReport.GetNewResults", func(t *testing.T) {
creport1 := creport
creport2 := creport
creport1.Results = map[string]report.Result{result1.GetIdentifier(): result1}
creport2.Results = map[string]report.Result{result1.GetIdentifier(): result1, result2.GetIdentifier(): result2}
diff := creport2.GetNewResults(creport1)
if len(diff) != 1 {
t.Error("Expected 1 new result in diff")
}
})
}
func Test_Result(t *testing.T) {
t.Run("Check Result.GetIdentifier", func(t *testing.T) {
expected := fmt.Sprintf("%s__%s__%s__%s", result1.Policy, result1.Rule, result1.Status, result1.Resources[0].UID)
if result1.GetIdentifier() != expected {
t.Errorf("Expected ClusterPolicyReport.GetIdentifier() to be %s (actual: %s)", expected, creport.GetIdentifier())
}
})
}
func Test_Priorities(t *testing.T) {
t.Run("Priority.String", func(t *testing.T) {
if prio := report.Priority(0).String(); prio != "" {
t.Errorf("Expected Priority to be '' (actual %s)", prio)
}
if prio := report.Priority(1).String(); prio != "debug" {
t.Errorf("Expected Priority to be debug (actual %s)", prio)
}
if prio := report.Priority(2).String(); prio != "info" {
t.Errorf("Expected Priority to be debug (actual %s)", prio)
}
if prio := report.Priority(3).String(); prio != "warning" {
t.Errorf("Expected Priority to be debug (actual %s)", prio)
}
if prio := report.Priority(4).String(); prio != "error" {
t.Errorf("Expected Priority to be debug (actual %s)", prio)
}
})
t.Run("PriorityFromStatus", func(t *testing.T) {
if prio := report.PriorityFromStatus(report.Fail); prio != report.ErrorPriority {
t.Errorf("Expected Priority to be %d (actual %d)", report.ErrorPriority, prio)
}
if prio := report.PriorityFromStatus(report.Error); prio != report.ErrorPriority {
t.Errorf("Expected Priority to be %d (actual %d)", report.ErrorPriority, prio)
}
if prio := report.PriorityFromStatus(report.Pass); prio != report.InfoPriority {
t.Errorf("Expected Priority to be %d (actual %d)", report.InfoPriority, prio)
}
if prio := report.PriorityFromStatus(report.Skip); prio != report.DefaultPriority {
t.Errorf("Expected Priority to be %d (actual %d)", report.DefaultPriority, prio)
}
if prio := report.PriorityFromStatus(report.Warn); prio != report.WarningPriority {
t.Errorf("Expected Priority to be %d (actual %d)", report.WarningPriority, prio)
}
})
t.Run("PriorityFromStatus", func(t *testing.T) {
if prio := report.NewPriority(""); prio != report.DefaultPriority {
t.Errorf("Expected Priority to be %d (actual %d)", report.DefaultPriority, prio)
}
if prio := report.NewPriority("error"); prio != report.ErrorPriority {
t.Errorf("Expected Priority to be %d (actual %d)", report.ErrorPriority, prio)
}
if prio := report.NewPriority("warning"); prio != report.WarningPriority {
t.Errorf("Expected Priority to be %d (actual %d)", report.WarningPriority, prio)
}
if prio := report.NewPriority("info"); prio != report.InfoPriority {
t.Errorf("Expected Priority to be %d (actual %d)", report.InfoPriority, prio)
}
if prio := report.NewPriority("debug"); prio != report.DebugPriority {
t.Errorf("Expected Priority to be %d (actual %d)", report.DebugPriority, prio)
}
})
}

View file

@ -4,6 +4,8 @@ import (
"github.com/fjogeleit/policy-reporter/pkg/report"
)
// Client for a provided Target
type Client interface {
// Send the given Result to the configured Target
Send(result report.Result)
}

View file

@ -13,6 +13,10 @@ import (
"github.com/fjogeleit/policy-reporter/pkg/target"
)
type httpClient interface {
Do(req *http.Request) (*http.Response, error)
}
type payload struct {
Streams []stream `json:"streams"`
}
@ -65,13 +69,13 @@ func newLokiPayload(result report.Result) payload {
return payload{Streams: []stream{ls}}
}
type Client struct {
type client struct {
host string
minimumPriority string
client *http.Client
client httpClient
}
func (l *Client) Send(result report.Result) {
func (l *client) Send(result report.Result) {
if result.Priority < report.NewPriority(l.minimumPriority) {
return
}
@ -93,7 +97,7 @@ func (l *Client) Send(result report.Result) {
resp, err := l.client.Do(req)
defer func() {
if resp != nil {
if resp != nil && resp.Body != nil {
resp.Body.Close()
}
}()
@ -111,10 +115,11 @@ func (l *Client) Send(result report.Result) {
}
}
func NewClient(host, minimumPriority string) target.Client {
return &Client{
// NewClient creates a new loki.client to send Results to Loki
func NewClient(host, minimumPriority string, httpClient httpClient) target.Client {
return &client{
host + "/api/prom/push",
minimumPriority,
&http.Client{},
httpClient,
}
}

View file

@ -0,0 +1,215 @@
package loki_test
import (
"encoding/json"
"fmt"
"net/http"
"strings"
"testing"
"github.com/fjogeleit/policy-reporter/pkg/report"
"github.com/fjogeleit/policy-reporter/pkg/target/loki"
)
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" {
t.Errorf("Unexpected Content-Type: %s", contentType)
}
if agend := req.Header.Get("User-Agent"); agend != "Policy-API" {
t.Errorf("Unexpected Host: %s", agend)
}
if url := req.URL.String(); url != "http://localhost:3100/api/prom/push" {
t.Errorf("Unexpected Host: %s", url)
}
expectedLine := fmt.Sprintf("[%s] %s", strings.ToUpper(completeResult.Priority.String()), completeResult.Message)
labels, line := convertAndValidateBody(req, t)
if line != expectedLine {
t.Errorf("Unexpected LineContent: %s", line)
}
if !strings.Contains(labels, "policy=\""+completeResult.Policy+"\"") {
t.Error("Missing Content for Label 'policy'")
}
if !strings.Contains(labels, "status=\""+completeResult.Status+"\"") {
t.Error("Missing Content for Label 'status'")
}
if !strings.Contains(labels, "priority=\""+completeResult.Priority.String()+"\"") {
t.Error("Missing Content for Label 'priority'")
}
if !strings.Contains(labels, "source=\"policy-reporter\"") {
t.Error("Missing Content for Label 'policy-reporter'")
}
if !strings.Contains(labels, "rule=\""+completeResult.Rule+"\"") {
t.Error("Missing Content for Label 'rule'")
}
if !strings.Contains(labels, "category=\""+completeResult.Category+"\"") {
t.Error("Missing Content for Label 'category'")
}
if !strings.Contains(labels, "severity=\""+completeResult.Severity+"\"") {
t.Error("Missing Content for Label 'severity'")
}
res := completeResult.Resources[0]
if !strings.Contains(labels, "kind=\""+res.Kind+"\"") {
t.Error("Missing Content for Label 'kind'")
}
if !strings.Contains(labels, "name=\""+res.Name+"\"") {
t.Error("Missing Content for Label 'name'")
}
if !strings.Contains(labels, "uid=\""+res.UID+"\"") {
t.Error("Missing Content for Label 'uid'")
}
if !strings.Contains(labels, "namespace=\""+res.Namespace+"\"") {
t.Error("Missing Content for Label 'namespace'")
}
}
loki := loki.NewClient("http://localhost:3100", "", testClient{callback, 200})
loki.Send(completeResult)
})
t.Run("Send Minimal Result", func(t *testing.T) {
callback := func(req *http.Request) {
if contentType := req.Header.Get("Content-Type"); contentType != "application/json" {
t.Errorf("Unexpected Content-Type: %s", contentType)
}
if agend := req.Header.Get("User-Agent"); agend != "Policy-API" {
t.Errorf("Unexpected Host: %s", agend)
}
if url := req.URL.String(); url != "http://localhost:3100/api/prom/push" {
t.Errorf("Unexpected Host: %s", url)
}
expectedLine := fmt.Sprintf("[%s] %s", strings.ToUpper(minimalResult.Priority.String()), minimalResult.Message)
labels, line := convertAndValidateBody(req, t)
if line != expectedLine {
t.Errorf("Unexpected LineContent: %s", line)
}
if !strings.Contains(labels, "policy=\""+minimalResult.Policy+"\"") {
t.Error("Missing Content for Label 'policy'")
}
if !strings.Contains(labels, "status=\""+minimalResult.Status+"\"") {
t.Error("Missing Content for Label 'status'")
}
if !strings.Contains(labels, "priority=\""+minimalResult.Priority.String()+"\"") {
t.Error("Missing Content for Label 'priority'")
}
if !strings.Contains(labels, "source=\"policy-reporter\"") {
t.Error("Missing Content for Label 'policy-reporter'")
}
if strings.Contains(labels, "rule") {
t.Error("Unexpected Label 'rule'")
}
if strings.Contains(labels, "category") {
t.Error("Unexpected Label 'category'")
}
if strings.Contains(labels, "severity") {
t.Error("Unexpected 'severity'")
}
if strings.Contains(labels, "kind") {
t.Error("Unexpected Label 'kind'")
}
if strings.Contains(labels, "name") {
t.Error("Unexpected 'name'")
}
if strings.Contains(labels, "uid") {
t.Error("Unexpected 'uid'")
}
if strings.Contains(labels, "namespace") {
t.Error("Unexpected 'namespace'")
}
}
loki := loki.NewClient("http://localhost:3100", "", testClient{callback, 200})
loki.Send(minimalResult)
})
}
func convertAndValidateBody(req *http.Request, t *testing.T) (string, string) {
payload := make(map[string]interface{})
err := json.NewDecoder(req.Body).Decode(&payload)
if err != nil {
t.Fatal(err)
}
streamsContent, ok := payload["streams"]
if !ok {
t.Errorf("Expected payload key 'streams' is missing")
}
streams := streamsContent.([]interface{})
if len(streams) != 1 {
t.Errorf("Expected one streams entry")
}
firstStream := streams[0].(map[string]interface{})
entriesContent, ok := firstStream["entries"]
if !ok {
t.Errorf("Expected stream key 'entries' is missing")
}
labels, ok := firstStream["labels"]
if !ok {
t.Errorf("Expected stream key 'labels' is missing")
}
entryContent := entriesContent.([]interface{})[0]
entry := entryContent.(map[string]interface{})
_, ok = entry["ts"]
if !ok {
t.Errorf("Expected entry key 'ts' is missing")
}
line, ok := entry["line"]
if !ok {
t.Errorf("Expected entry key 'line' is missing")
}
return labels.(string), line.(string)
}