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:
parent
04c220d3e3
commit
1a1ab1786c
21 changed files with 1052 additions and 171 deletions
50
.github/workflows/ci.yaml
vendored
Normal file
50
.github/workflows/ci.yaml
vendored
Normal 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
|
8
Makefile
8
Makefile
|
@ -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 .
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
19
cmd/root.go
19
cmd/root.go
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package config
|
||||
|
||||
// Config of the PolicyReporter
|
||||
type Config struct {
|
||||
Loki struct {
|
||||
Host string `mapstructure:"host"`
|
||||
|
|
|
@ -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}
|
||||
}
|
||||
|
|
54
pkg/config/resolver_test.go
Normal file
54
pkg/config/resolver_test.go
Normal 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")
|
||||
}
|
||||
}
|
|
@ -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
166
pkg/kubernetes/mapper.go
Normal 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}
|
||||
}
|
292
pkg/kubernetes/mapper_test.go
Normal file
292
pkg/kubernetes/mapper_test.go
Normal 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)
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package metrics
|
||||
|
||||
// Metrics interface for Prometheus Metrics used for the Resolver
|
||||
type Metrics interface {
|
||||
// GenerateMetrics for Prometheus
|
||||
GenerateMetrics() error
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
170
pkg/report/model_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
215
pkg/target/loki/loki_test.go
Normal file
215
pkg/target/loki/loki_test.go
Normal 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)
|
||||
}
|
Loading…
Reference in a new issue