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

External SQL DB support (#304)

* Support external Databases

Signed-off-by: Frank Jogeleit <frank.jogeleit@web.de>
This commit is contained in:
Frank Jogeleit 2023-05-02 11:00:14 +02:00 committed by GitHub
parent 49cc1a50cc
commit 72abc63ce0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
48 changed files with 2537 additions and 2208 deletions

View file

@ -1,5 +1,18 @@
# Changelog
# 2.19.0
* Policy Reporter
* New AWS SecurityHub push target - See [values.yaml](https://github.com/kyverno/policy-reporter/blob/main/charts/policy-reporter/values.yaml#L550) for available configurations
* External DB support (PostgreSQL, MySQL, MariaDB) - See [values.yaml](https://github.com/kyverno/policy-reporter/blob/main/charts/policy-reporter/values.yaml#L172) for available configurations
* HA Mode support - only leader write into the DB
* Versioned Schema, autoupdated when another version is detected
* Configurable over values and secrets
* Cache improvements to reduce duplicated pushes
* Split Category API into namespaced scoped and cluster scoped API
* Support search for contained words in the results API
* Policy Reporter UI
* Update API requests
# 2.18.3
* Policy Reporter
* new value to add `priorityClassName` to pods [[#283](https://github.com/kyverno/policy-reporter/pull/283) by [boniek83](https://github.com/boniek83)]

View file

@ -1,8 +1,8 @@
GO ?= go
BUILD ?= build
REPO ?= ghcr.io/kyverno/policy-reporter
IMAGE_TAG ?= 2.8.1
LD_FLAGS='-s -w -linkmode external -extldflags "-static"'
IMAGE_TAG ?= 2.15.0
LD_FLAGS=-s -w -linkmode external -extldflags "-static"
PLATFORMS ?= linux/arm64,linux/amd64,linux/s390x
all: build
@ -29,16 +29,16 @@ build: prepare
.PHONY: docker-build
docker-build:
@docker buildx build --progress plane --platform $(PLATFORMS) --tag $(REPO):$(IMAGE_TAG) . --build-arg LD_FLAGS=$(LD_FLAGS)
@docker buildx build --progress plane --platform $(PLATFORMS) --tag $(REPO):$(IMAGE_TAG) . --build-arg LD_FLAGS='$(LD_FLAGS) -X main.Version=$(IMAGE_TAG)'
.PHONY: docker-push
docker-push:
@docker buildx build --progress plane --platform $(PLATFORMS) --tag $(REPO):$(IMAGE_TAG) . --build-arg LD_FLAGS=$(LD_FLAGS) --push
@docker buildx build --progress plane --platform $(PLATFORMS) --tag $(REPO):latest . --build-arg LD_FLAGS=$(LD_FLAGS) --push
@docker buildx build --progress plane --platform $(PLATFORMS) --tag $(REPO):$(IMAGE_TAG) . --build-arg LD_FLAGS='$(LD_FLAGS) -X main.Version=$(IMAGE_TAG)' --push
@docker buildx build --progress plane --platform $(PLATFORMS) --tag $(REPO):latest . --build-arg LD_FLAGS='$(LD_FLAGS) -X main.Version=$(IMAGE_TAG)' --push
.PHONY: docker-push-dev
docker-push-dev:
@docker buildx build --progress plane --platform $(PLATFORMS) --tag $(REPO):dev . --build-arg LD_FLAGS=$(LD_FLAGS) --push
@docker buildx build --progress plane --platform $(PLATFORMS) --tag $(REPO):dev . --build-arg LD_FLAGS='$(LD_FLAGS) -X main.Version=$(IMAGE_TAG)-dev' --push
.PHONY: fmt
fmt:

View file

@ -4,9 +4,9 @@ dependencies:
version: 2.7.1
- name: ui
repository: ""
version: 2.9.3
version: 2.9.4
- name: kyvernoPlugin
repository: ""
version: 1.5.4
digest: sha256:274143bd81946a2e65f7349e88222d31b2c4c2f9153671a502f5ea254d24954c
generated: "2023-04-05T14:57:22.183437589+02:00"
digest: sha256:8c60bb2f27f40a297021c1ed8ac5d52d0053e36f65f1d71ad296a7beff95dd70
generated: "2023-05-02T09:19:51.799892+02:00"

View file

@ -5,8 +5,8 @@ description: |
It creates Prometheus Metrics and can send rule validation events to different targets like Loki, Elasticsearch, Slack or Discord
type: application
version: 2.18.3
appVersion: 2.14.2
version: 2.19.0
appVersion: 2.15.0
icon: https://github.com/kyverno/kyverno/raw/main/img/logo.png
home: https://kyverno.github.io/policy-reporter
@ -21,7 +21,7 @@ dependencies:
version: "2.7.1"
- name: ui
condition: ui.enabled
version: "2.9.3"
version: "2.9.4"
- name: kyvernoPlugin
condition: kyvernoPlugin.enabled
version: "1.5.4"

View file

@ -3,5 +3,5 @@ name: ui
description: Policy Reporter UI
type: application
version: 2.9.3
appVersion: 1.8.3
version: 2.9.4
appVersion: 1.8.4

View file

@ -9,7 +9,7 @@ image:
registry: ghcr.io
repository: kyverno/policy-reporter-ui
pullPolicy: IfNotPresent
tag: 1.8.3
tag: 1.8.4
# possible default displayModes: light/dark
displayMode: ""

View file

@ -314,3 +314,14 @@ logging:
api:
logging: {{ .Values.api.logging }}
database:
type: {{ .Values.database.type }}
database: {{ .Values.database.database }}
username: {{ .Values.database.username }}
password: {{ .Values.database.password }}
host: {{ .Values.database.host }}
enableSSL: {{ .Values.database.enableSSL }}
dsn: {{ .Values.database.dsn }}
secretRef: {{ .Values.database.secretRef }}
mountedSecret: {{ .Values.database.mountedSecret }}

View file

@ -5,7 +5,7 @@ image:
registry: ghcr.io
repository: kyverno/policy-reporter
pullPolicy: IfNotPresent
tag: 2.14.2
tag: 2.15.0
imagePullSecrets: []
@ -169,6 +169,25 @@ kyvernoPlugin:
monitoring:
enabled: false
database:
# Database Type, supported: mysql, postgres, mariadb
type: ""
database: "" # Database Name
username: ""
password: ""
host: ""
enableSSL: false
# instead of configure the individual values you can also provide an DSN string
# example postgres: postgres://postgres:password@localhost:5432/postgres?sslmode=disable
# example mysql: root:password@tcp(localhost:3306)/test?tls=false
dsn: ""
# configure an existing secret as source for your values
# supported fields: username, password, host, dsn, database
secretRef: ""
# use an mounted secret as source for your values, required the information in JSON format
# supported fields: username, password, host, dsn, database
mountedSecret: ""
global:
# available plugins
plugins:

View file

@ -5,7 +5,7 @@ import (
)
// NewCLI creates a new instance of the root CLI
func NewCLI() *cobra.Command {
func NewCLI(version string) *cobra.Command {
rootCmd := &cobra.Command{
Use: "policyreporter",
Short: "Generates PolicyReport Metrics and Send Results to different targets",
@ -13,7 +13,8 @@ func NewCLI() *cobra.Command {
Sends notifications to different targets like Grafana's Loki.`,
}
rootCmd.AddCommand(newRunCMD())
rootCmd.AddCommand(newVersionCMD(version))
rootCmd.AddCommand(newRunCMD(version))
rootCmd.AddCommand(newSendCMD())
return rootCmd

View file

@ -2,6 +2,7 @@ package cmd
import (
"context"
"errors"
"flag"
"github.com/spf13/cobra"
@ -11,10 +12,11 @@ import (
"k8s.io/client-go/tools/clientcmd"
"github.com/kyverno/policy-reporter/pkg/config"
"github.com/kyverno/policy-reporter/pkg/database"
"github.com/kyverno/policy-reporter/pkg/listener"
)
func newRunCMD() *cobra.Command {
func newRunCMD(version string) *cobra.Command {
cmd := &cobra.Command{
Use: "run",
Short: "Run PolicyReporter Watcher & HTTP Metrics Server",
@ -23,6 +25,7 @@ func newRunCMD() *cobra.Command {
if err != nil {
return err
}
c.Version = version
var k8sConfig *rest.Config
if c.K8sClient.Kubeconfig != "" {
@ -37,6 +40,7 @@ func newRunCMD() *cobra.Command {
k8sConfig.QPS = c.K8sClient.QPS
k8sConfig.Burst = c.K8sClient.Burst
readinessProbe := config.NewReadinessProbe(c)
resolver := config.NewResolver(c, k8sConfig)
logger, err := resolver.Logger()
if err != nil {
@ -52,20 +56,26 @@ func newRunCMD() *cobra.Command {
g := &errgroup.Group{}
var store *database.Store
if c.REST.Enabled {
db, err := resolver.Database()
if err != nil {
return err
db := resolver.Database()
if db == nil {
return errors.New("unable to create database connection")
}
defer db.Close()
store, err := resolver.PolicyReportStore(db)
store, err = resolver.PolicyReportStore(db)
if err != nil {
return err
}
if !c.LeaderElection.Enabled || store.IsSQLite() {
store.PrepareDatabase(cmd.Context())
resolver.RegisterStoreListener(cmd.Context(), store)
}
logger.Info("REST api enabled")
resolver.RegisterStoreListener(store)
server.RegisterV1Handler(store)
}
@ -80,41 +90,75 @@ func newRunCMD() *cobra.Command {
server.RegisterProfilingHandler()
}
if resolver.HasTargets() && c.LeaderElection.Enabled {
if !resolver.ResultCache().Shared() {
logger.Debug("register new result listener")
resolver.RegisterNewResultsListener()
}
if resolver.EnableLeaderElection() {
elector, err := resolver.LeaderElectionClient()
if err != nil {
return err
}
elector.RegisterOnStart(func(c context.Context) {
elector.RegisterOnStart(func(ctx context.Context) {
logger.Info("started leadership")
if c.REST.Enabled && !store.IsSQLite() {
store.PrepareDatabase(cmd.Context())
logger.Debug("register database persistence")
resolver.RegisterStoreListener(ctx, store)
if readinessProbe.Running() {
logger.Debug("trigger informer restart")
client.Stop()
}
}
resolver.RegisterSendResultListener()
readinessProbe.Ready()
}).RegisterOnNew(func(currentID, lockID string) {
if currentID != lockID {
logger.Info("leadership", zap.String("leader", currentID))
readinessProbe.Ready()
return
}
}).RegisterOnStop(func() {
logger.Info("stopped leadership")
resolver.EventPublisher().UnregisterListener(listener.NewResults)
if !store.IsSQLite() {
resolver.EventPublisher().UnregisterListener(listener.Store)
}
if resolver.HasTargets() {
resolver.UnregisterSendResultListener()
}
})
g.Go(func() error {
return elector.Run(cmd.Context())
})
} else if resolver.HasTargets() {
} else {
resolver.RegisterSendResultListener()
}
g.Go(server.Start)
g.Go(func() error {
stop := make(chan struct{})
defer close(stop)
readinessProbe.Wait()
logger.Info("start client", zap.Int("worker", c.WorkerCount))
return client.Run(c.WorkerCount, stop)
for {
stop := make(chan struct{})
if err := client.Run(c.WorkerCount, stop); err != nil {
zap.L().Error("informer client error", zap.Error(err))
}
zap.L().Debug("informer restarts")
}
})
return g.Wait()
@ -130,6 +174,7 @@ func newRunCMD() *cobra.Command {
cmd.PersistentFlags().BoolP("rest-enabled", "r", false, "Enable Policy Reporter's REST API")
cmd.PersistentFlags().Bool("profile", false, "Enable application profiling with pprof")
cmd.PersistentFlags().String("lease-name", "policy-reporter", "name of the LeaseLock")
cmd.PersistentFlags().String("pod-name", "policy-reporter", "name of the pod, used for leaderelection")
cmd.PersistentFlags().Int("worker", 5, "amount of queue worker")
cmd.PersistentFlags().Float32("qps", 20, "K8s RESTClient QPS")
cmd.PersistentFlags().Int("burst", 50, "K8s RESTClient burst")

17
cmd/version.go Normal file
View file

@ -0,0 +1,17 @@
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
func newVersionCMD(version string) *cobra.Command {
return &cobra.Command{
Use: "version",
Short: "Policy Reporter AppVersion",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("AppVersion: " + version)
},
}
}

21
go.mod
View file

@ -6,6 +6,7 @@ require (
cloud.google.com/go/storage v1.30.1
github.com/aws/aws-sdk-go v1.44.229
github.com/go-redis/redis/v8 v8.11.5
github.com/go-sql-driver/mysql v1.7.1
github.com/kyverno/go-wildcard v1.0.5
github.com/mattn/go-sqlite3 v1.14.16
github.com/patrickmn/go-cache v2.1.0+incompatible
@ -14,6 +15,11 @@ require (
github.com/segmentio/fasthash v1.0.3
github.com/spf13/cobra v1.6.1
github.com/spf13/viper v1.15.0
github.com/uptrace/bun v1.1.12
github.com/uptrace/bun/dialect/mysqldialect v1.1.12
github.com/uptrace/bun/dialect/pgdialect v1.1.12
github.com/uptrace/bun/dialect/sqlitedialect v1.1.12
github.com/uptrace/bun/driver/pgdriver v1.1.12
github.com/xhit/go-simple-mail/v2 v2.13.0
go.uber.org/zap v1.24.0
golang.org/x/sync v0.1.0
@ -47,6 +53,7 @@ require (
github.com/googleapis/gax-go/v2 v2.8.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
@ -65,12 +72,17 @@ require (
github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 // indirect
github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
go.opencensus.io v0.24.0 // indirect
go.uber.org/atomic v1.10.0 // indirect
go.uber.org/multierr v1.10.0 // indirect
golang.org/x/sys v0.6.0 // indirect
golang.org/x/text v0.8.0 // indirect
golang.org/x/crypto v0.8.0 // indirect
golang.org/x/mod v0.10.0 // indirect
golang.org/x/sys v0.7.0 // indirect
golang.org/x/text v0.9.0 // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20230323212658-478b75c54725 // indirect
@ -81,6 +93,7 @@ require (
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/klog/v2 v2.90.1 // indirect
k8s.io/utils v0.0.0-20230313181309-38a27ef9d749 // indirect
mellium.im/sasl v0.3.1 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
)
@ -92,9 +105,9 @@ require (
github.com/imdario/mergo v0.3.15 // indirect
github.com/prometheus/common v0.42.0 // indirect
github.com/subosito/gotenv v1.4.2 // indirect
golang.org/x/net v0.8.0 // indirect
golang.org/x/net v0.9.0 // indirect
golang.org/x/oauth2 v0.6.0
golang.org/x/term v0.6.0 // indirect
golang.org/x/term v0.7.0 // indirect
golang.org/x/time v0.3.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
k8s.io/api v0.26.3

43
go.sum
View file

@ -107,6 +107,8 @@ github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
@ -200,6 +202,8 @@ github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+h
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
@ -291,6 +295,7 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
@ -298,8 +303,24 @@ github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKs
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs=
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 h1:PM5hJF7HVfNWmCjMdEfbuOBNXSVF2cMFGgQTPdKCbwM=
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns=
github.com/uptrace/bun v1.1.12 h1:sOjDVHxNTuM6dNGaba0wUuz7KvDE1BmNu9Gqs2gJSXQ=
github.com/uptrace/bun v1.1.12/go.mod h1:NPG6JGULBeQ9IU6yHp7YGELRa5Agmd7ATZdz4tGZ6z0=
github.com/uptrace/bun/dialect/mysqldialect v1.1.12 h1:Rpp0N7E9wmpWm8oTXuQ7tG9Ekdp5hLO/lSQCbiAQvYY=
github.com/uptrace/bun/dialect/mysqldialect v1.1.12/go.mod h1:Zz+fRspfRjkRYUQLGFfkq5s5ilEsPW5KFmORgy64dy8=
github.com/uptrace/bun/dialect/pgdialect v1.1.12 h1:m/CM1UfOkoBTglGO5CUTKnIKKOApOYxkcP2qn0F9tJk=
github.com/uptrace/bun/dialect/pgdialect v1.1.12/go.mod h1:Ij6WIxQILxLlL2frUBxUBOZJtLElD2QQNDcu/PWDHTc=
github.com/uptrace/bun/dialect/sqlitedialect v1.1.12 h1:Ud31nqZmebcQpl151nb108+vtcpxJ7kfXmbPYbALBiI=
github.com/uptrace/bun/dialect/sqlitedialect v1.1.12/go.mod h1:Pwg7s31BdF3PMBlWTnYkEn2I9ASsvatt1Ln/AERCTV4=
github.com/uptrace/bun/driver/pgdriver v1.1.12 h1:3rRWB1GK0psTJrHwxzNfEij2MLibggiLdTqjTtfHc1w=
github.com/uptrace/bun/driver/pgdriver v1.1.12/go.mod h1:ssYUP+qwSEgeDDS1xm2XBip9el1y9Mi5mTAvLoiADLM=
github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU=
github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
@ -334,6 +355,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@ -368,6 +391,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -405,8 +430,8 @@ golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -472,13 +497,13 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.7.0 h1:BEvjmm5fURWqcfbSKTdpkDXYBrUS1c0m8agp14W48vQ=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -489,8 +514,8 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@ -696,6 +721,8 @@ k8s.io/kube-openapi v0.0.0-20230308215209-15aac26d736a h1:gmovKNur38vgoWfGtP5QOG
k8s.io/kube-openapi v0.0.0-20230308215209-15aac26d736a/go.mod h1:y5VtZWM9sHHc2ZodIH/6SHzXj+TPU5USoA8lcIeKEKY=
k8s.io/utils v0.0.0-20230313181309-38a27ef9d749 h1:xMMXJlJbsU8w3V5N2FLDQ8YgU8s1EoULdbQBcAeNJkY=
k8s.io/utils v0.0.0-20230313181309-38a27ef9d749/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
mellium.im/sasl v0.3.1 h1:wE0LW6g7U83vhvxjC1IY8DnXM+EU095yeo8XClvCdfo=
mellium.im/sasl v0.3.1/go.mod h1:xm59PUYpZHhgQ9ZqoJ5QaCqzWMi8IeS49dhp6plPCzw=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

View file

@ -7,8 +7,10 @@ import (
"github.com/kyverno/policy-reporter/cmd"
)
var Version = "development"
func main() {
if err := cmd.NewCLI().Execute(); err != nil {
if err := cmd.NewCLI(Version).Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}

View file

@ -47,13 +47,13 @@ func (s *httpServer) RegisterV1Handler(finder v1.PolicyReportFinder) {
handler := v1.NewHandler(finder)
s.mux.HandleFunc("/v1/targets", Gzip(handler.TargetsHandler(s.targets)))
s.mux.HandleFunc("/v1/categories", Gzip(handler.CategoryListHandler()))
s.mux.HandleFunc("/v1/namespaces", Gzip(handler.NamespaceListHandler()))
s.mux.HandleFunc("/v1/rule-status-count", Gzip(handler.RuleStatusCountHandler()))
s.mux.HandleFunc("/v1/policy-reports", Gzip(handler.PolicyReportListHandler()))
s.mux.HandleFunc("/v1/cluster-policy-reports", Gzip(handler.ClusterPolicyReportListHandler()))
s.mux.HandleFunc("/v1/namespaced-resources/categories", Gzip(handler.NamespacedCategoryListHandler()))
s.mux.HandleFunc("/v1/namespaced-resources/policies", Gzip(handler.NamespacedResourcesPolicyListHandler()))
s.mux.HandleFunc("/v1/namespaced-resources/rules", Gzip(handler.NamespacedResourcesRuleListHandler()))
s.mux.HandleFunc("/v1/namespaced-resources/kinds", Gzip(handler.NamespacedResourcesKindListHandler()))
@ -71,6 +71,7 @@ func (s *httpServer) RegisterV1Handler(finder v1.PolicyReportFinder) {
s.mux.HandleFunc("/v1/cluster-resources/report-labels", Gzip(handler.ClusterReportLabelListHandler()))
s.mux.HandleFunc("/v1/cluster-resources/status-counts", Gzip(handler.ClusterResourcesStatusCountHandler()))
s.mux.HandleFunc("/v1/cluster-resources/results", Gzip(handler.ClusterResourcesResultHandler()))
s.mux.HandleFunc("/v1/cluster-resources/categories", Gzip(handler.ClusterCategoryListHandler()))
}
func (s *httpServer) RegisterMetricsHandler() {

View file

@ -1,11 +1,7 @@
package v1
import (
"strconv"
"github.com/segmentio/fasthash/fnv1a"
"github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2"
"context"
)
type Filter struct {
@ -29,115 +25,57 @@ type Pagination struct {
Direction string
}
type ResultFilterValues struct {
ReportID string
Namespace string
Source string
Kind string
Category string
Policy string
Severity string
Result string
Count int
}
func (r ResultFilterValues) Hash() string {
h1 := fnv1a.Init64
h1 = fnv1a.AddString64(h1, r.ReportID)
h1 = fnv1a.AddString64(h1, r.Namespace)
h1 = fnv1a.AddString64(h1, r.Source)
h1 = fnv1a.AddString64(h1, r.Kind)
h1 = fnv1a.AddString64(h1, r.Category)
h1 = fnv1a.AddString64(h1, r.Policy)
h1 = fnv1a.AddString64(h1, r.Severity)
h1 = fnv1a.AddString64(h1, r.Result)
return strconv.FormatUint(h1, 10)
}
func ExtractFilterValues(polr v1alpha2.ReportInterface) []*ResultFilterValues {
mapping := make(map[string]*ResultFilterValues)
for _, res := range polr.GetResults() {
kind := res.GetKind()
if kind == "" && polr.GetScope() != nil {
kind = polr.GetScope().Namespace
}
value := &ResultFilterValues{
ReportID: polr.GetID(),
Namespace: polr.GetNamespace(),
Source: res.Source,
Kind: kind,
Category: res.Category,
Policy: res.Policy,
Severity: string(res.Severity),
Result: string(res.Result),
Count: 1,
}
if item, ok := mapping[value.Hash()]; ok {
item.Count = item.Count + 1
} else {
mapping[value.Hash()] = value
}
}
list := make([]*ResultFilterValues, 0, len(mapping))
for _, v := range mapping {
list = append(list, v)
}
return list
}
type PolicyReportFinder interface {
// FetchClusterPolicyReports by filter and pagination
FetchClusterPolicyReports(Filter, Pagination) ([]*PolicyReport, error)
FetchClusterPolicyReports(context.Context, Filter, Pagination) ([]*PolicyReport, error)
// FetchPolicyReports by filter and pagination
FetchPolicyReports(Filter, Pagination) ([]*PolicyReport, error)
FetchPolicyReports(context.Context, Filter, Pagination) ([]*PolicyReport, error)
// CountClusterPolicyReports by filter
CountClusterPolicyReports(Filter) (int, error)
CountClusterPolicyReports(context.Context, Filter) (int, error)
// CountPolicyReports by filter
CountPolicyReports(Filter) (int, error)
CountPolicyReports(context.Context, Filter) (int, error)
// FetchClusterPolicies from current PolicyReportResults
FetchClusterPolicies(Filter) ([]string, error)
FetchClusterPolicies(context.Context, Filter) ([]string, error)
// FetchClusterRules from current PolicyReportResults
FetchClusterRules(Filter) ([]string, error)
FetchClusterRules(context.Context, Filter) ([]string, error)
// FetchNamespacedPolicies from current PolicyReportResults with a Namespace
FetchNamespacedPolicies(Filter) ([]string, error)
FetchNamespacedPolicies(context.Context, Filter) ([]string, error)
// FetchNamespacedRules from current PolicyReportResults with a Namespace
FetchNamespacedRules(Filter) ([]string, error)
// FetchCategories from current PolicyReportResults
FetchCategories(Filter) ([]string, error)
FetchNamespacedRules(context.Context, Filter) ([]string, error)
// FetchClusterCategories from current PolicyReportResults
FetchClusterCategories(context.Context, Filter) ([]string, error)
// FetchNamespacedCategories from current PolicyReportResults
FetchNamespacedCategories(context.Context, Filter) ([]string, error)
// FetchClusterSources from current PolicyReportResults
FetchClusterSources() ([]string, error)
FetchClusterSources(context.Context) ([]string, error)
// FetchNamespacedSources from current PolicyReportResults with a Namespace
FetchNamespacedSources() ([]string, error)
FetchNamespacedSources(context.Context) ([]string, error)
// FetchNamespacedKinds from current PolicyReportResults with a Namespace
FetchNamespacedKinds(Filter) ([]string, error)
FetchNamespacedKinds(context.Context, Filter) ([]string, error)
// FetchNamespacedResources from current PolicyReportResults with a Namespace
FetchNamespacedResources(Filter) ([]*Resource, error)
FetchNamespacedResources(context.Context, Filter) ([]*Resource, error)
// FetchClusterResources from current PolicyReportResults
FetchClusterResources(Filter) ([]*Resource, error)
FetchClusterResources(context.Context, Filter) ([]*Resource, error)
// FetchClusterKinds from current PolicyReportResults
FetchClusterKinds(Filter) ([]string, error)
FetchClusterKinds(context.Context, Filter) ([]string, error)
// FetchNamespaces from current PolicyReports
FetchNamespaces(Filter) ([]string, error)
FetchNamespaces(context.Context, Filter) ([]string, error)
// FetchNamespacedStatusCounts from current PolicyReportResults with a Namespace
FetchNamespacedStatusCounts(Filter) ([]NamespacedStatusCount, error)
// FetchStatusCounts from current PolicyReportResults
FetchStatusCounts(Filter) ([]StatusCount, error)
FetchNamespacedStatusCounts(context.Context, Filter) ([]NamespacedStatusCount, error)
// FetchClusterStatusCounts from current PolicyReportResults
FetchClusterStatusCounts(context.Context, Filter) ([]StatusCount, error)
// FetchNamespacedResults from current PolicyReportResults with a Namespace
FetchNamespacedResults(Filter, Pagination) ([]*ListResult, error)
FetchNamespacedResults(context.Context, Filter, Pagination) ([]*ListResult, error)
// FetchClusterResults from current PolicyReportResults
FetchClusterResults(Filter, Pagination) ([]*ListResult, error)
FetchClusterResults(context.Context, Filter, Pagination) ([]*ListResult, error)
// CountNamespacedResults from current PolicyReportResults with a Namespace
CountNamespacedResults(Filter) (int, error)
CountNamespacedResults(context.Context, Filter) (int, error)
// CountClusterResults from current PolicyReportResults
CountClusterResults(Filter) (int, error)
CountClusterResults(context.Context, Filter) (int, error)
// FetchRuleStatusCounts from current PolicyReportResults
FetchRuleStatusCounts(policy, rule string) ([]StatusCount, error)
FetchRuleStatusCounts(context.Context, string, string) ([]StatusCount, error)
// FetchClusterReportLabels from ClusterPolicyReports
FetchClusterReportLabels(Filter) (map[string][]string, error)
FetchClusterReportLabels(context.Context, Filter) (map[string][]string, error)
// FetchNamespacedReportLabels from PolicyReports
FetchNamespacedReportLabels(Filter) (map[string][]string, error)
FetchNamespacedReportLabels(context.Context, Filter) (map[string][]string, error)
}

View file

@ -39,8 +39,8 @@ func (h *Handler) TargetsHandler(targets []target.Client) http.HandlerFunc {
func (h *Handler) PolicyReportListHandler() http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
filter := buildFilter(req)
count, _ := h.finder.CountPolicyReports(filter)
list, err := h.finder.FetchPolicyReports(filter, buildPaginatiomn(req, []string{"namespace", "name"}))
count, _ := h.finder.CountPolicyReports(req.Context(), filter)
list, err := h.finder.FetchPolicyReports(req.Context(), filter, buildPaginatiomn(req, []string{"namespace", "name"}))
h.logError(err)
helper.SendJSONResponse(w, PolicyReportList{Items: list, Count: count}, err)
}
@ -50,8 +50,8 @@ func (h *Handler) PolicyReportListHandler() http.HandlerFunc {
func (h *Handler) ClusterPolicyReportListHandler() http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
filter := buildFilter(req)
count, _ := h.finder.CountClusterPolicyReports(filter)
list, err := h.finder.FetchClusterPolicyReports(filter, buildPaginatiomn(req, []string{"namespace", "name"}))
count, _ := h.finder.CountClusterPolicyReports(req.Context(), filter)
list, err := h.finder.FetchClusterPolicyReports(req.Context(), filter, buildPaginatiomn(req, []string{"namespace", "name"}))
h.logError(err)
helper.SendJSONResponse(w, PolicyReportList{Items: list, Count: count}, err)
}
@ -60,7 +60,7 @@ func (h *Handler) ClusterPolicyReportListHandler() http.HandlerFunc {
// ClusterResourcesPolicyListHandler REST API
func (h *Handler) ClusterResourcesPolicyListHandler() http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
list, err := h.finder.FetchClusterPolicies(buildFilter(req))
list, err := h.finder.FetchClusterPolicies(req.Context(), buildFilter(req))
h.logError(err)
helper.SendJSONResponse(w, list, err)
}
@ -69,7 +69,7 @@ func (h *Handler) ClusterResourcesPolicyListHandler() http.HandlerFunc {
// ClusterResourcesRuleListHandler REST API
func (h *Handler) ClusterResourcesRuleListHandler() http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
list, err := h.finder.FetchClusterRules(buildFilter(req))
list, err := h.finder.FetchClusterRules(req.Context(), buildFilter(req))
h.logError(err)
helper.SendJSONResponse(w, list, err)
}
@ -78,7 +78,7 @@ func (h *Handler) ClusterResourcesRuleListHandler() http.HandlerFunc {
// NamespacedResourcesPolicyListHandler REST API
func (h *Handler) NamespacedResourcesPolicyListHandler() http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
list, err := h.finder.FetchNamespacedPolicies(buildFilter(req))
list, err := h.finder.FetchNamespacedPolicies(req.Context(), buildFilter(req))
h.logError(err)
helper.SendJSONResponse(w, list, err)
}
@ -87,16 +87,25 @@ func (h *Handler) NamespacedResourcesPolicyListHandler() http.HandlerFunc {
// NamespacedResourcesRuleListHandler REST API
func (h *Handler) NamespacedResourcesRuleListHandler() http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
list, err := h.finder.FetchNamespacedRules(buildFilter(req))
list, err := h.finder.FetchNamespacedRules(req.Context(), buildFilter(req))
h.logError(err)
helper.SendJSONResponse(w, list, err)
}
}
// CategoryListHandler REST API
func (h *Handler) CategoryListHandler() http.HandlerFunc {
func (h *Handler) ClusterCategoryListHandler() http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
list, err := h.finder.FetchCategories(buildFilter(req))
list, err := h.finder.FetchClusterCategories(req.Context(), buildFilter(req))
h.logError(err)
helper.SendJSONResponse(w, list, err)
}
}
// CategoryListHandler REST API
func (h *Handler) NamespacedCategoryListHandler() http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
list, err := h.finder.FetchNamespacedCategories(req.Context(), buildFilter(req))
h.logError(err)
helper.SendJSONResponse(w, list, err)
}
@ -105,7 +114,7 @@ func (h *Handler) CategoryListHandler() http.HandlerFunc {
// ClusterResourcesKindListHandler REST API
func (h *Handler) ClusterResourcesKindListHandler() http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
list, err := h.finder.FetchClusterKinds(buildFilter(req))
list, err := h.finder.FetchClusterKinds(req.Context(), buildFilter(req))
h.logError(err)
helper.SendJSONResponse(w, list, err)
}
@ -114,7 +123,7 @@ func (h *Handler) ClusterResourcesKindListHandler() http.HandlerFunc {
// NamespacedResourcesKindListHandler REST API
func (h *Handler) NamespacedResourcesKindListHandler() http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
list, err := h.finder.FetchNamespacedKinds(buildFilter(req))
list, err := h.finder.FetchNamespacedKinds(req.Context(), buildFilter(req))
h.logError(err)
helper.SendJSONResponse(w, list, err)
}
@ -123,7 +132,7 @@ func (h *Handler) NamespacedResourcesKindListHandler() http.HandlerFunc {
// ClusterResourcesListHandler REST API
func (h *Handler) ClusterResourcesListHandler() http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
list, err := h.finder.FetchClusterResources(buildFilter(req))
list, err := h.finder.FetchClusterResources(req.Context(), buildFilter(req))
h.logError(err)
helper.SendJSONResponse(w, list, err)
}
@ -132,7 +141,7 @@ func (h *Handler) ClusterResourcesListHandler() http.HandlerFunc {
// NamespacedResourcesListHandler REST API
func (h *Handler) NamespacedResourcesListHandler() http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
list, err := h.finder.FetchNamespacedResources(buildFilter(req))
list, err := h.finder.FetchNamespacedResources(req.Context(), buildFilter(req))
h.logError(err)
helper.SendJSONResponse(w, list, err)
}
@ -141,7 +150,7 @@ func (h *Handler) NamespacedResourcesListHandler() http.HandlerFunc {
// ClusterResourcesSourceListHandler REST API
func (h *Handler) ClusterResourcesSourceListHandler() http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
list, err := h.finder.FetchClusterSources()
list, err := h.finder.FetchClusterSources(req.Context())
h.logError(err)
helper.SendJSONResponse(w, list, err)
}
@ -150,7 +159,7 @@ func (h *Handler) ClusterResourcesSourceListHandler() http.HandlerFunc {
// NamespacedSourceListHandler REST API
func (h *Handler) NamespacedSourceListHandler() http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
list, err := h.finder.FetchNamespacedSources()
list, err := h.finder.FetchNamespacedSources(req.Context())
h.logError(err)
helper.SendJSONResponse(w, list, err)
}
@ -159,7 +168,7 @@ func (h *Handler) NamespacedSourceListHandler() http.HandlerFunc {
// NamespacedReportLabelListHandler REST API
func (h *Handler) NamespacedReportLabelListHandler() http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
list, err := h.finder.FetchNamespacedReportLabels(buildFilter(req))
list, err := h.finder.FetchNamespacedReportLabels(req.Context(), buildFilter(req))
h.logError(err)
helper.SendJSONResponse(w, list, err)
}
@ -168,7 +177,7 @@ func (h *Handler) NamespacedReportLabelListHandler() http.HandlerFunc {
// ClusterReportLabelListHandler REST API
func (h *Handler) ClusterReportLabelListHandler() http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
list, err := h.finder.FetchClusterReportLabels(buildFilter(req))
list, err := h.finder.FetchClusterReportLabels(req.Context(), buildFilter(req))
h.logError(err)
helper.SendJSONResponse(w, list, err)
}
@ -177,7 +186,7 @@ func (h *Handler) ClusterReportLabelListHandler() http.HandlerFunc {
// ClusterResourcesStatusCountHandler REST API
func (h *Handler) ClusterResourcesStatusCountHandler() http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
list, err := h.finder.FetchStatusCounts(buildFilter(req))
list, err := h.finder.FetchClusterStatusCounts(req.Context(), buildFilter(req))
h.logError(err)
helper.SendJSONResponse(w, list, err)
}
@ -186,7 +195,7 @@ func (h *Handler) ClusterResourcesStatusCountHandler() http.HandlerFunc {
// NamespacedResourcesStatusCountsHandler REST API
func (h *Handler) NamespacedResourcesStatusCountsHandler() http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
list, err := h.finder.FetchNamespacedStatusCounts(buildFilter(req))
list, err := h.finder.FetchNamespacedStatusCounts(req.Context(), buildFilter(req))
h.logError(err)
helper.SendJSONResponse(w, list, err)
}
@ -196,6 +205,7 @@ func (h *Handler) NamespacedResourcesStatusCountsHandler() http.HandlerFunc {
func (h *Handler) RuleStatusCountHandler() http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
list, err := h.finder.FetchRuleStatusCounts(
req.Context(),
req.URL.Query().Get("policy"),
req.URL.Query().Get("rule"),
)
@ -208,8 +218,8 @@ func (h *Handler) RuleStatusCountHandler() http.HandlerFunc {
func (h *Handler) NamespacedResourcesResultHandler() http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
filter := buildFilter(req)
count, _ := h.finder.CountNamespacedResults(filter)
list, err := h.finder.FetchNamespacedResults(filter, buildPaginatiomn(req, defaultOrder))
count, _ := h.finder.CountNamespacedResults(req.Context(), filter)
list, err := h.finder.FetchNamespacedResults(req.Context(), filter, buildPaginatiomn(req, defaultOrder))
h.logError(err)
helper.SendJSONResponse(w, ResultList{Items: list, Count: count}, err)
}
@ -219,8 +229,8 @@ func (h *Handler) NamespacedResourcesResultHandler() http.HandlerFunc {
func (h *Handler) ClusterResourcesResultHandler() http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
filter := buildFilter(req)
count, _ := h.finder.CountClusterResults(filter)
list, err := h.finder.FetchClusterResults(filter, buildPaginatiomn(req, defaultOrder))
count, _ := h.finder.CountClusterResults(req.Context(), filter)
list, err := h.finder.FetchClusterResults(req.Context(), filter, buildPaginatiomn(req, defaultOrder))
h.logError(err)
helper.SendJSONResponse(w, ResultList{Items: list, Count: count}, err)
}
@ -229,7 +239,7 @@ func (h *Handler) ClusterResourcesResultHandler() http.HandlerFunc {
// NamespaceListHandler REST API
func (h *Handler) NamespaceListHandler() http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
list, err := h.finder.FetchNamespaces(Filter{
list, err := h.finder.FetchNamespaces(req.Context(), Filter{
Sources: req.URL.Query()["sources"],
Categories: req.URL.Query()["categories"],
Policies: req.URL.Query()["policies"],

View file

@ -1,6 +1,7 @@
package v1_test
import (
"context"
"net/http"
"net/http/httptest"
"strings"
@ -12,7 +13,7 @@ import (
v1 "github.com/kyverno/policy-reporter/pkg/api/v1"
"github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2"
"github.com/kyverno/policy-reporter/pkg/sqlite3"
"github.com/kyverno/policy-reporter/pkg/database"
"github.com/kyverno/policy-reporter/pkg/target"
"github.com/kyverno/policy-reporter/pkg/target/loki"
)
@ -120,8 +121,10 @@ var creport = &v1alpha2.ClusterPolicyReport{
Summary: v1alpha2.PolicyReportSummary{},
}
var ctx = context.Background()
func Test_V1_API(t *testing.T) {
db, err := sqlite3.NewDatabase("test.db")
db, err := database.NewSQLiteDB("test.db")
if err != nil {
t.Error(err)
}
@ -129,14 +132,15 @@ func Test_V1_API(t *testing.T) {
if err != nil {
t.Fatal(err)
}
store, err := sqlite3.NewPolicyReportStore(db)
store, err := database.NewStore(db, "test")
if err != nil {
t.Fatal(err)
}
defer store.CleanUp()
store.PrepareDatabase(ctx)
defer store.CleanUp(ctx)
store.Add(preport)
store.Add(creport)
store.Add(ctx, preport)
store.Add(ctx, creport)
handl := v1.NewHandler(store)
@ -220,21 +224,41 @@ func Test_V1_API(t *testing.T) {
}
})
t.Run("CategoryListHandler", func(t *testing.T) {
t.Run("NamespacedCategoryListHandler", func(t *testing.T) {
req, err := http.NewRequest("GET", "/v1/categories", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
handler := handl.CategoryListHandler()
handler := handl.NamespacedCategoryListHandler()
handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK)
}
expected := `["Best Practices","Convention"]`
expected := `["Best Practices"]`
if !strings.Contains(rr.Body.String(), expected) {
t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected)
}
})
t.Run("ClusterCategoryListHandler", func(t *testing.T) {
req, err := http.NewRequest("GET", "/v1/categories", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
handler := handl.ClusterCategoryListHandler()
handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK)
}
expected := `["Convention"]`
if !strings.Contains(rr.Body.String(), expected) {
t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected)
}
@ -491,7 +515,7 @@ func Test_V1_API(t *testing.T) {
})
t.Run("ClusterReportLabelListHandler", func(t *testing.T) {
req, err := http.NewRequest("GET", "/v1/cluster-resources/report-labels?sources=kyverno", nil)
req, err := http.NewRequest("GET", "/v1/cluster-resources/report-labels?sources=Kyverno", nil)
if err != nil {
t.Fatal(err)
}
@ -511,7 +535,7 @@ func Test_V1_API(t *testing.T) {
})
t.Run("ClusterReportLabelListHandler", func(t *testing.T) {
req, err := http.NewRequest("GET", "/v1/namespaced-resources/report-labels?sources=kyverno", nil)
req, err := http.NewRequest("GET", "/v1/namespaced-resources/report-labels?sources=Kyverno", nil)
if err != nil {
t.Fatal(err)
}

View file

@ -16,6 +16,8 @@ type PolicyReport struct {
Warn int `json:"warn"`
Error int `json:"error"`
Fail int `json:"fail"`
Type string `json:"-"`
Created int64 `json:"-"`
}
type PolicyReportList struct {
@ -41,6 +43,7 @@ type NamespacedStatusCount struct {
type NamespaceCount struct {
Namespace string `json:"namespace"`
Count int `json:"count"`
Status string `json:"-"`
}
type Resource struct {
@ -60,7 +63,7 @@ type ListResult struct {
Rule string `json:"rule"`
Status string `json:"status"`
Severity string `json:"severity,omitempty"`
Timestamp int `json:"timestamp,omitempty"`
Timestamp int64 `json:"timestamp,omitempty"`
Properties map[string]string `json:"properties,omitempty"`
}

2
pkg/cache/cache.go vendored
View file

@ -6,4 +6,6 @@ type Cache interface {
RemoveReport(id string)
AddReport(report v1alpha2.ReportInterface)
GetResults(id string) []string
Shared() bool
Clear()
}

74
pkg/cache/memory.go vendored
View file

@ -9,32 +9,86 @@ import (
)
type inMemoryCache struct {
cache *gocache.Cache
caches *gocache.Cache
}
func (c *inMemoryCache) AddReport(report v1alpha2.ReportInterface) {
c.cache.Set(report.GetID(), reportResultsIds(report), gocache.NoExpiration)
cache, ok := c.getCache(report.GetID())
if !ok {
cache = gocache.New(gocache.NoExpiration, 5*time.Minute)
c.caches.Set(report.GetID(), cache, gocache.NoExpiration)
}
next := make(map[string]bool)
for _, result := range report.GetResults() {
cache.Set(result.GetID(), nil, gocache.NoExpiration)
next[result.GetID()] = true
}
for id, item := range cache.Items() {
if !next[id] && item.Expiration == 0 {
cache.Set(id, nil, 6*time.Hour)
}
}
c.caches.Set(report.GetID(), cache, gocache.NoExpiration)
}
func (c *inMemoryCache) RemoveReport(id string) {
val, ok := c.cache.Get(id)
if ok {
// don't remove it directly to prevent sending results from instantly recreated reports
c.cache.Set(id, val, 5*time.Minute)
cache, ok := c.getCache(id)
if !ok {
return
}
c.caches.Set(id, cache, 10*time.Minute)
}
func (c *inMemoryCache) getCache(id string) (*gocache.Cache, bool) {
cache, ok := c.caches.Get(id)
if !ok {
return nil, false
}
return cache.(*gocache.Cache), ok
}
func (c *inMemoryCache) GetResults(id string) []string {
list, ok := c.cache.Get(id)
list := make([]string, 0)
cache, ok := c.getCache(id)
if !ok {
return make([]string, 0)
return list
}
return list.([]string)
for id := range cache.Items() {
list = append(list, id)
}
return list
}
func (c *inMemoryCache) Clear() {
for _, cache := range c.caches.Items() {
cache.Object.(*gocache.Cache).Flush()
}
c.caches.Flush()
}
func (c *inMemoryCache) Shared() bool {
return false
}
func NewInMermoryCache() Cache {
cache := gocache.New(gocache.NoExpiration, 5*time.Minute)
cache.OnEvicted(func(s string, i interface{}) {
if c, ok := i.(*gocache.Cache); ok {
c.Flush()
}
})
return &inMemoryCache{
cache: gocache.New(gocache.NoExpiration, 5*time.Minute),
caches: gocache.New(gocache.NoExpiration, 5*time.Minute),
}
}

64
pkg/cache/redis.go vendored
View file

@ -2,11 +2,12 @@ package cache
import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
goredis "github.com/go-redis/redis/v8"
"go.uber.org/zap"
"github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2"
)
@ -18,28 +19,73 @@ type redisCache struct {
}
func (r *redisCache) AddReport(report v1alpha2.ReportInterface) {
list := reportResultsIds(report)
next := make(map[string]bool)
value, _ := json.Marshal(list)
for _, result := range report.GetResults() {
r.rdb.Set(context.Background(), r.generateKey(report.GetID(), result.GetID()), nil, 0)
next[result.GetID()] = true
}
r.rdb.Set(context.Background(), r.generateKey(report.GetID()), string(value), 0)
for _, id := range r.GetResults(report.GetID()) {
if !next[id] {
r.rdb.Set(context.Background(), r.generateKey(report.GetID(), id), nil, 6*time.Hour)
}
}
}
func (r *redisCache) RemoveReport(id string) {
r.rdb.Del(context.Background(), r.generateKey(id))
keys, err := r.rdb.Keys(context.Background(), r.generateKeyPattern(id)).Result()
if err != nil {
zap.L().Error("failed to load report keys", zap.Error(err))
return
}
for _, key := range keys {
r.rdb.Expire(context.Background(), key, 10*time.Minute)
}
}
func (r *redisCache) GetResults(id string) []string {
list, _ := r.rdb.Get(context.Background(), r.generateKey(id)).Result()
results := make([]string, 0)
pattern := r.generateKeyPattern(id)
json.Unmarshal([]byte(list), &results)
keys, err := r.rdb.Keys(context.Background(), pattern).Result()
if err != nil {
zap.L().Error("failed to load report keys", zap.Error(err))
return results
}
prefix := strings.TrimSuffix(pattern, "*")
for _, key := range keys {
results = append(results, strings.TrimPrefix(key, prefix))
}
return results
}
func (r *redisCache) generateKey(id string) string {
return fmt.Sprintf("%s:%s", r.prefix, id)
func (r *redisCache) Shared() bool {
return true
}
func (r *redisCache) Clear() {
results := r.rdb.Keys(context.Background(), r.prefix+":*")
keys, err := results.Result()
if err != nil {
zap.L().Error("failed to find cache keys in redis", zap.Error(err))
}
for _, key := range keys {
r.rdb.Del(context.Background(), key)
}
}
func (r *redisCache) generateKey(report, id string) string {
return fmt.Sprintf("%s:%s:%s", r.prefix, report, id)
}
func (r *redisCache) generateKeyPattern(report string) string {
return fmt.Sprintf("%s:%s:*", r.prefix, report)
}
func NewRedisCache(prefix string, rdb *goredis.Client, ttl time.Duration) Cache {

View file

@ -256,8 +256,21 @@ type Logging struct {
Development bool `mapstructure:"development"`
}
type Database struct {
Type string `mapstructure:"type"`
DSN string `mapstructure:"dsn"`
Username string `mapstructure:"username"`
Password string `mapstructure:"password"`
Database string `mapstructure:"database"`
Host string `mapstructure:"host"`
EnableSSL bool `mapstructure:"enableSSL"`
SecretRef string `mapstructure:"secretRef"`
MountedSecret string `mapstructure:"mountedSecret"`
}
// Config of the PolicyReporter
type Config struct {
Version string
Namespace string `mapstructure:"namespace"`
Loki Loki `mapstructure:"loki"`
Elasticsearch Elasticsearch `mapstructure:"elasticsearch"`
@ -283,4 +296,5 @@ type Config struct {
LeaderElection LeaderElection `mapstructure:"leaderElection"`
K8sClient K8sClient `mapstructure:"k8sClient"`
Logging Logging `mapstructure:"logging"`
Database Database `mapstructure:"database"`
}

View file

@ -0,0 +1,142 @@
package config
import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"os"
"time"
_ "github.com/go-sql-driver/mysql"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect/mysqldialect"
"github.com/uptrace/bun/dialect/pgdialect"
"github.com/uptrace/bun/driver/pgdriver"
"go.uber.org/zap"
"github.com/kyverno/policy-reporter/pkg/database"
"github.com/kyverno/policy-reporter/pkg/kubernetes/secrets"
)
var ErrNoConfig = errors.New("no configuration for the provider found")
// DatabaseFactory manages database connection and creation
type DatabaseFactory struct {
secretClient secrets.Client
}
func (f *DatabaseFactory) NewPostgres(config Database) *bun.DB {
if (config.SecretRef != "" && f.secretClient != nil) || config.MountedSecret != "" {
f.mapSecretValues(&config, config.SecretRef, config.MountedSecret)
}
if config.Host == "" && config.DSN == "" {
return nil
}
dsn := config.DSN
if config.DSN == "" {
sslMode := "disable"
if config.EnableSSL {
sslMode = "verify-full"
}
dsn = fmt.Sprintf("postgres://%s:%s@%s/%s?sslmode=%s", config.Username, config.Password, config.Host, config.Database, sslMode)
}
sqldb := sql.OpenDB(pgdriver.NewConnector(pgdriver.WithDSN(dsn)))
sqldb.SetMaxOpenConns(25)
sqldb.SetMaxIdleConns(25)
sqldb.SetConnMaxLifetime(15 * time.Minute)
return bun.NewDB(sqldb, pgdialect.New())
}
func (f *DatabaseFactory) NewMySQL(config Database) *bun.DB {
if (config.SecretRef != "" && f.secretClient != nil) || config.MountedSecret != "" {
f.mapSecretValues(&config, config.SecretRef, config.MountedSecret)
}
if config.Host == "" && config.DSN == "" {
return nil
}
dsn := config.DSN
if config.DSN == "" {
dsn = fmt.Sprintf("%s:%s@tcp(%s)/%s?tls=%v", config.Username, config.Password, config.Host, config.Database, config.EnableSSL)
}
sqldb, err := sql.Open("mysql", dsn)
if err != nil {
zap.L().Error("failed to create mysql connection", zap.Error(err))
return nil
}
sqldb.SetMaxOpenConns(25)
sqldb.SetMaxIdleConns(25)
sqldb.SetConnMaxLifetime(15 * time.Minute)
return bun.NewDB(sqldb, mysqldialect.New())
}
func (f *DatabaseFactory) NewSQLite(file string) *bun.DB {
sqldb, err := database.NewSQLiteDB(file)
if err != nil {
zap.L().Error("failed to create sqlite connection", zap.Error(err))
return nil
}
return sqldb
}
func (f *DatabaseFactory) mapSecretValues(config any, ref, mountedSecret string) {
values := secrets.Values{}
if ref != "" {
secretValues, err := f.secretClient.Get(context.Background(), ref)
values = secretValues
if err != nil {
zap.L().Warn("failed to get secret reference", zap.Error(err))
return
}
}
if mountedSecret != "" {
file, err := os.ReadFile(mountedSecret)
if err != nil {
zap.L().Warn("failed to get mounted secret", zap.Error(err))
return
}
err = json.Unmarshal(file, &values)
if err != nil {
zap.L().Warn("failed to unmarshal mounted secret", zap.Error(err))
return
}
}
if c, ok := config.(*Database); ok {
if values.Host != "" {
c.Host = values.Host
}
if values.Username != "" {
c.Username = values.Username
}
if values.Password != "" {
c.Password = values.Password
}
if values.Database != "" {
c.Database = values.Database
}
if values.DSN != "" {
c.DSN = values.DSN
}
}
}
func NewDatabaseFactory(client secrets.Client) *DatabaseFactory {
return &DatabaseFactory{
secretClient: client,
}
}

View file

@ -0,0 +1,70 @@
package config_test
import (
"testing"
"github.com/kyverno/policy-reporter/pkg/config"
"github.com/kyverno/policy-reporter/pkg/kubernetes/secrets"
"github.com/uptrace/bun/dialect"
)
func Test_ResolveDatabase(t *testing.T) {
factory := config.NewDatabaseFactory(nil)
t.Run("SQLite Fallback", func(t *testing.T) {
db := factory.NewSQLite("test.db")
if db == nil || db.Dialect().Name() != dialect.SQLite {
t.Error("Expected SQLite database as fallback")
}
})
t.Run("MySQL", func(t *testing.T) {
db := factory.NewMySQL(config.Database{
Username: "admin",
Password: "password",
Host: "localhost:3306",
EnableSSL: true,
})
if db == nil || db.Dialect().Name() != dialect.MySQL {
t.Error("Expected MySQL DB")
}
})
t.Run("PostgreSQL", func(t *testing.T) {
db := factory.NewPostgres(config.Database{
Username: "admin",
Password: "password",
Host: "localhost:5432",
EnableSSL: true,
})
if db == nil || db.Dialect().Name() != dialect.PG {
t.Error("Expected PostgreSQL")
}
})
}
func Test_DatabaseValuesFromSecret(t *testing.T) {
factory := config.NewDatabaseFactory(secrets.NewClient(newFakeClient()))
mountSecret()
t.Run("Values from SecretRef", func(t *testing.T) {
db := factory.NewPostgres(config.Database{SecretRef: secretName, EnableSSL: false})
if db == nil {
t.Error("Expected PostgreSQL connection created")
}
})
t.Run("Values from MountedSecret", func(t *testing.T) {
db := factory.NewMySQL(config.Database{MountedSecret: mountedSecret, EnableSSL: false})
if db == nil {
t.Error("Expected MySQL connection created")
}
})
t.Run("Get none existing mounted secret skips target", func(t *testing.T) {
db := factory.NewPostgres(config.Database{MountedSecret: "no-exists"})
if db != nil {
t.Error("Expected no connection created without host or DSN config")
}
})
}

View file

@ -81,6 +81,10 @@ func Load(cmd *cobra.Command) (*Config, error) {
v.BindPFlag("leaderElection.lockName", flag)
}
if flag := cmd.Flags().Lookup("pod-name"); flag != nil {
v.BindPFlag("leaderElection.podName", flag)
}
if err := v.BindEnv("leaderElection.podName", "POD_NAME"); err != nil {
log.Printf("[WARNING] failed to bind env POD_NAME")
}

View file

@ -19,6 +19,8 @@ func createCMD() *cobra.Command {
cmd.Flags().BoolP("rest-enabled", "r", false, "Enable Policy Reporter's REST API")
cmd.Flags().Bool("profile", false, "Enable application profiling with pprof")
cmd.Flags().StringP("template-dir", "t", "./templates", "template directory for email reports")
cmd.Flags().String("lease-name", "policy-reporter", "name of the LeaseLock")
cmd.Flags().String("pod-name", "policy-reporter", "name of the pod, used for leaderelection")
return cmd
}
@ -33,6 +35,8 @@ func Test_Load(t *testing.T) {
_ = cmd.Flags().Set("profile", "1")
_ = cmd.Flags().Set("template-dir", "/app/templates")
_ = cmd.Flags().Set("dbfile", "")
_ = cmd.Flags().Set("pod-name", "policy-reporter")
_ = cmd.Flags().Set("lease-name", "policy-reporter")
c, err := config.Load(cmd)
if err != nil {

View file

@ -0,0 +1,50 @@
package config
import (
"go.uber.org/zap"
)
type ReadinessProbe struct {
config *Config
ready chan bool
running bool
}
func (r *ReadinessProbe) required() bool {
if !r.config.REST.Enabled {
return false
}
return r.config.LeaderElection.Enabled
}
func (r *ReadinessProbe) Ready() {
if r.required() && !r.running {
go func() {
zap.L().Debug("readiness probe ready")
r.ready <- true
}()
}
}
func (r *ReadinessProbe) Wait() {
if r.required() && !r.running {
r.running = <-r.ready
close(r.ready)
zap.L().Debug("readiness probe finished")
return
}
}
func (r *ReadinessProbe) Running() bool {
return r.running
}
func NewReadinessProbe(config *Config) *ReadinessProbe {
return &ReadinessProbe{
config: config,
ready: make(chan bool),
running: false,
}
}

View file

@ -0,0 +1,53 @@
package config_test
import (
"testing"
"github.com/kyverno/policy-reporter/pkg/config"
)
func Test_ReadinessProbe(t *testing.T) {
t.Run("immediate return without REST enabled", func(t *testing.T) {
rdy := config.NewReadinessProbe(
&config.Config{
REST: config.REST{Enabled: false},
LeaderElection: config.LeaderElection{Enabled: false},
},
)
rdy.Wait()
})
t.Run("immediate return without LeaderElection enabled", func(t *testing.T) {
rdy := config.NewReadinessProbe(
&config.Config{
REST: config.REST{Enabled: true},
LeaderElection: config.LeaderElection{Enabled: false},
},
)
rdy.Wait()
})
t.Run("wait for ready state", func(t *testing.T) {
rdy := config.NewReadinessProbe(
&config.Config{
REST: config.REST{Enabled: true},
LeaderElection: config.LeaderElection{Enabled: true},
},
)
if rdy.Running() {
t.Error("should not be running until ready was called")
}
go func() {
rdy.Wait()
if !rdy.Running() {
t.Error("should be running after ready was called")
}
}()
rdy.Ready()
})
}

View file

@ -1,11 +1,13 @@
package config
import (
"database/sql"
"context"
"time"
goredis "github.com/go-redis/redis/v8"
_ "github.com/mattn/go-sqlite3"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect"
mail "github.com/xhit/go-simple-mail/v2"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
@ -18,6 +20,7 @@ import (
"github.com/kyverno/policy-reporter/pkg/cache"
"github.com/kyverno/policy-reporter/pkg/crd/client/clientset/versioned"
wgpolicyk8sv1alpha2 "github.com/kyverno/policy-reporter/pkg/crd/client/clientset/versioned/typed/policyreport/v1alpha2"
"github.com/kyverno/policy-reporter/pkg/database"
"github.com/kyverno/policy-reporter/pkg/email"
"github.com/kyverno/policy-reporter/pkg/email/summary"
"github.com/kyverno/policy-reporter/pkg/email/violations"
@ -27,7 +30,6 @@ import (
"github.com/kyverno/policy-reporter/pkg/listener"
"github.com/kyverno/policy-reporter/pkg/listener/metrics"
"github.com/kyverno/policy-reporter/pkg/report"
"github.com/kyverno/policy-reporter/pkg/sqlite3"
"github.com/kyverno/policy-reporter/pkg/target"
"github.com/kyverno/policy-reporter/pkg/validate"
)
@ -38,13 +40,15 @@ type Resolver struct {
k8sConfig *rest.Config
mapper report.Mapper
publisher report.EventPublisher
policyStore sqlite3.PolicyReportStore
policyStore *database.Store
database *bun.DB
policyReportClient report.PolicyReportClient
leaderElector *leaderelection.Client
targetClients []target.Client
resultCache cache.Cache
targetsCreated bool
logger *zap.Logger
resultListener *listener.ResultListener
}
// APIServer resolver method
@ -63,17 +67,43 @@ func (r *Resolver) APIServer(synced func() bool) api.Server {
}
// Database resolver method
func (r *Resolver) Database() (*sql.DB, error) {
return sqlite3.NewDatabase(r.config.DBFile)
func (r *Resolver) Database() *bun.DB {
if r.database != nil {
return r.database
}
factory := r.DatabaseFactory()
switch r.config.Database.Type {
case database.MySQL:
if r.database = factory.NewMySQL(r.config.Database); r.database != nil {
zap.L().Info("mysql connection created")
return r.database
}
case database.MariaDB:
if r.database = factory.NewMySQL(r.config.Database); r.database != nil {
zap.L().Info("mariadb connection created")
return r.database
}
case database.PostgreSQL:
if r.database = factory.NewPostgres(r.config.Database); r.database != nil {
zap.L().Info("postgres connection created")
return r.database
}
}
zap.L().Info("sqlite connection created")
r.database = factory.NewSQLite(r.config.DBFile)
return r.database
}
// PolicyReportStore resolver method
func (r *Resolver) PolicyReportStore(db *sql.DB) (sqlite3.PolicyReportStore, error) {
func (r *Resolver) PolicyReportStore(db *bun.DB) (*database.Store, error) {
if r.policyStore != nil {
return r.policyStore, nil
}
s, err := sqlite3.NewPolicyReportStore(db)
s, err := database.NewStore(db, r.config.Version)
r.policyStore = s
return r.policyStore, err
@ -130,20 +160,49 @@ func (r *Resolver) Queue() (*kubernetes.Queue, error) {
), nil
}
// RegisterSendResultListener resolver method
func (r *Resolver) RegisterSendResultListener() {
// RegisterNewResultsListener resolver method
func (r *Resolver) RegisterNewResultsListener() {
targets := r.TargetClients()
if len(targets) > 0 {
newResultListener := listener.NewResultListener(r.SkipExistingOnStartup(), r.ResultCache(), time.Now())
newResultListener.RegisterListener(listener.NewSendResultListener(targets, r.Mapper()))
r.EventPublisher().RegisterListener(listener.NewResults, newResultListener.Listen)
if len(targets) == 0 {
return
}
newResultListener := listener.NewResultListener(r.SkipExistingOnStartup(), r.ResultCache(), time.Now())
r.resultListener = newResultListener
r.EventPublisher().RegisterListener(listener.NewResults, newResultListener.Listen)
}
// RegisterSendResultListener resolver method
func (r *Resolver) RegisterStoreListener(store report.PolicyReportStore) {
r.EventPublisher().RegisterListener(listener.Store, listener.NewStoreListener(store))
func (r *Resolver) RegisterSendResultListener() {
targets := r.TargetClients()
if len(targets) == 0 {
return
}
if r.resultListener == nil {
r.RegisterNewResultsListener()
}
r.resultListener.RegisterListener(listener.NewSendResultListener(targets, r.Mapper()))
}
// RegisterSendResultListener resolver method
func (r *Resolver) UnregisterSendResultListener() {
if r.ResultCache().Shared() {
r.EventPublisher().UnregisterListener(listener.NewResults)
}
if r.resultListener == nil {
return
}
r.resultListener.UnregisterListener()
}
// RegisterSendResultListener resolver method
func (r *Resolver) RegisterStoreListener(ctx context.Context, store report.PolicyReportStore) {
r.EventPublisher().RegisterListener(listener.Store, listener.NewStoreListener(ctx, store))
}
// RegisterMetricsListener resolver method
@ -190,7 +249,12 @@ func (r *Resolver) SecretClient() secrets.Client {
func (r *Resolver) TargetFactory() *TargetFactory {
return &TargetFactory{
namespace: r.config.Namespace,
secretClient: r.SecretClient(),
}
}
func (r *Resolver) DatabaseFactory() *DatabaseFactory {
return &DatabaseFactory{
secretClient: r.SecretClient(),
}
}
@ -230,6 +294,18 @@ func (r *Resolver) HasTargets() bool {
return len(r.TargetClients()) > 0
}
func (r *Resolver) EnableLeaderElection() bool {
if !r.config.LeaderElection.Enabled {
return false
}
if !r.HasTargets() && r.Database().Dialect().Name() == dialect.SQLite {
return false
}
return true
}
// SkipExistingOnStartup config method
func (r *Resolver) SkipExistingOnStartup() bool {
for _, client := range r.TargetClients() {

View file

@ -1,11 +1,13 @@
package config_test
import (
"context"
"testing"
"k8s.io/client-go/rest"
"github.com/kyverno/policy-reporter/pkg/config"
"github.com/kyverno/policy-reporter/pkg/database"
"github.com/kyverno/policy-reporter/pkg/report"
)
@ -254,7 +256,7 @@ func Test_ResolveLeaderElectionClient(t *testing.T) {
func Test_ResolvePolicyStore(t *testing.T) {
resolver := config.NewResolver(&config.Config{DBFile: "test.db"}, &rest.Config{})
db, _ := resolver.Database()
db := resolver.Database()
defer db.Close()
store1, err := resolver.PolicyReportStore(db)
@ -401,7 +403,7 @@ func Test_ResolveSecretCClientWithInvalidK8sConfig(t *testing.T) {
func Test_RegisterStoreListener(t *testing.T) {
t.Run("Register StoreListener", func(t *testing.T) {
resolver := config.NewResolver(testConfig, &rest.Config{})
resolver.RegisterStoreListener(report.NewPolicyReportStore())
resolver.RegisterStoreListener(context.Background(), report.NewPolicyReportStore())
if len(resolver.EventPublisher().GetListener()) != 1 {
t.Error("Expected one Listener to be registered")
@ -532,3 +534,42 @@ func Test_ResolveLogger(t *testing.T) {
t.Error("A second call resolver.Mapper() should return the cached first cache")
}
}
func Test_ResolveEnableLeaderElection(t *testing.T) {
t.Run("general disabled", func(t *testing.T) {
resolver := config.NewResolver(&config.Config{
LeaderElection: config.LeaderElection{Enabled: false},
Loki: config.Loki{Host: "localhost:3100"},
Database: config.Database{Type: database.MySQL},
}, &rest.Config{})
if resolver.EnableLeaderElection() {
t.Error("leaderelection should be not enabled if its general disabled")
}
})
t.Run("no pushes and SQLite Database", func(t *testing.T) {
resolver := config.NewResolver(&config.Config{
LeaderElection: config.LeaderElection{Enabled: true},
Database: config.Database{Type: database.SQLite},
DBFile: "test.db",
}, &rest.Config{})
if resolver.EnableLeaderElection() {
t.Error("leaderelection should be not enabled if no pushes configured and SQLite is used")
}
})
t.Run("enabled if pushes defined", func(t *testing.T) {
resolver := config.NewResolver(&config.Config{
LeaderElection: config.LeaderElection{Enabled: true},
Database: config.Database{Type: database.SQLite},
Loki: config.Loki{Host: "localhost:3100"},
DBFile: "test.db",
}, &rest.Config{})
if !resolver.EnableLeaderElection() {
t.Error("leaderelection should be enabled if general enabled and targets configured")
}
})
}

View file

@ -30,7 +30,6 @@ import (
// TargetFactory manages target creation
type TargetFactory struct {
secretClient secrets.Client
namespace string
}
// LokiClients resolver method
@ -990,6 +989,6 @@ func createReportFilter(filter TargetFilter) *report.ReportFilter {
)
}
func NewTargetFactory(namespace string, secretClient secrets.Client) *TargetFactory {
return &TargetFactory{namespace: namespace, secretClient: secretClient}
func NewTargetFactory(secretClient secrets.Client) *TargetFactory {
return &TargetFactory{secretClient: secretClient}
}

View file

@ -37,6 +37,8 @@ func newFakeClient() v1.SecretInterface {
"kmsKeyId": []byte("kmsKeyId"),
"token": []byte("token"),
"credentials": []byte(`{"token": "token", "type": "authorized_user"}`),
"database": []byte("database"),
"dsn": []byte(""),
},
}).CoreV1().Secrets("default")
}
@ -52,6 +54,8 @@ func mountSecret() {
KmsKeyID: "kmsKeyId",
Token: "token",
Credentials: `{"token": "token", "type": "authorized_user"}`,
Database: "database",
DSN: "",
}
file, _ := json.MarshalIndent(secretValues, "", " ")
_ = os.WriteFile(mountedSecret, file, 0o644)
@ -60,7 +64,7 @@ func mountSecret() {
var logger = zap.NewNop()
func Test_ResolveTarget(t *testing.T) {
factory := config.NewTargetFactory("", nil)
factory := config.NewTargetFactory(nil)
t.Run("Loki", func(t *testing.T) {
clients := factory.LokiClients(testConfig.Loki)
@ -125,7 +129,7 @@ func Test_ResolveTarget(t *testing.T) {
}
func Test_ResolveTargetWithoutHost(t *testing.T) {
factory := config.NewTargetFactory("", nil)
factory := config.NewTargetFactory(nil)
t.Run("Loki", func(t *testing.T) {
if len(factory.LokiClients(config.Loki{})) != 0 {
@ -260,7 +264,7 @@ func Test_ResolveTargetWithoutHost(t *testing.T) {
}
func Test_GetValuesFromSecret(t *testing.T) {
factory := config.NewTargetFactory("default", secrets.NewClient(newFakeClient()))
factory := config.NewTargetFactory(secrets.NewClient(newFakeClient()))
t.Run("Get Loki values from Secret", func(t *testing.T) {
clients := factory.LokiClients(config.Loki{TargetBaseOptions: config.TargetBaseOptions{SecretRef: secretName}})
@ -512,7 +516,7 @@ func Test_GetValuesFromSecret(t *testing.T) {
}
func Test_GetValuesFromMountedSecret(t *testing.T) {
factory := config.NewTargetFactory("", nil)
factory := config.NewTargetFactory(nil)
mountSecret()
defer os.Remove(mountedSecret)

996
pkg/database/bun.go Normal file
View file

@ -0,0 +1,996 @@
package database
import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"os"
"strings"
"time"
_ "github.com/mattn/go-sqlite3"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect"
"github.com/uptrace/bun/dialect/sqlitedialect"
"go.uber.org/zap"
corev1 "k8s.io/api/core/v1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
api "github.com/kyverno/policy-reporter/pkg/api/v1"
"github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2"
"github.com/kyverno/policy-reporter/pkg/report"
)
type Type = string
const (
MySQL Type = "mysql"
MariaDB Type = "mariadb"
PostgreSQL Type = "postgres"
SQLite Type = "sqlite"
)
type Store struct {
db *bun.DB
version string
jsonExtractLayout string
}
func (s *Store) CreateSchemas(ctx context.Context) error {
if s.db.Dialect().Name() == dialect.SQLite {
if _, err := s.db.Exec("PRAGMA foreign_keys = ON"); err != nil {
return err
}
}
_, err := s.db.
NewCreateTable().
IfNotExists().
Model((*Config)(nil)).
Exec(ctx)
logOnError("create policy_report table", err)
_, err = s.db.
NewCreateTable().
IfNotExists().
Model((*PolicyReport)(nil)).
Exec(ctx)
logOnError("create policy_report table", err)
_, err = s.db.
NewCreateTable().
IfNotExists().
Model((*PolicyReportResult)(nil)).
ForeignKey(`(policy_report_id) REFERENCES policy_report(id) ON DELETE CASCADE`).
Exec(ctx)
logOnError("create policy_report_result table", err)
_, err = s.db.
NewCreateTable().
IfNotExists().
Model((*PolicyReportFilter)(nil)).
ForeignKey(`(policy_report_id) REFERENCES policy_report(id) ON DELETE CASCADE`).
Exec(ctx)
logOnError("create policy_report_filter table", err)
return err
}
func (s *Store) DropSchema(ctx context.Context) error {
_, err := s.db.NewDropTable().
IfExists().
Model((*Config)(nil)).
Exec(ctx)
logOnError("drop policy_report_config table", err)
_, err = s.db.NewDropTable().
IfExists().
Model((*PolicyReportFilter)(nil)).
Exec(ctx)
logOnError("drop policy_report_filter table", err)
_, err = s.db.NewDropTable().
IfExists().
Model((*PolicyReportResult)(nil)).
Exec(ctx)
logOnError("drop policy_report_result table", err)
_, err = s.db.NewDropTable().
IfExists().
Model((*PolicyReport)(nil)).
Exec(ctx)
logOnError("drop policy_report table", err)
return err
}
func (s *Store) Add(ctx context.Context, report v1alpha2.ReportInterface) error {
_, err := s.db.NewInsert().Model(MapPolicyReport(report)).Exec(ctx)
if err != nil {
zap.L().Error("failed to persist policy report", zap.Error(err))
}
filters := chunkSlice(MapPolicyReportFilter(report), 50)
for _, list := range filters {
_, err = s.db.NewInsert().Ignore().Model(&list).Exec(ctx)
if err != nil {
zap.L().Error("failed to bulk import policy report filter", zap.Error(err))
return err
}
}
results := chunkSlice(MapPolicyReportResults(report), 50)
for _, list := range results {
_, err = s.db.NewInsert().Ignore().Model(&list).Exec(ctx)
if err != nil {
zap.L().Error("failed to bulk import policy report results", zap.Error(err))
return err
}
}
return err
}
func (s *Store) Update(ctx context.Context, report v1alpha2.ReportInterface) error {
err := s.Remove(ctx, report.GetID())
if err != nil {
return err
}
s.Add(ctx, report)
return err
}
func (s *Store) Remove(ctx context.Context, id string) error {
_, err := s.db.NewDelete().Model((*PolicyReport)(nil)).Where("id = ?", id).Exec(ctx)
if err != nil {
zap.L().Error("failed to remove previews policy report", zap.Error(err))
}
return err
}
func (s *Store) CleanUp(ctx context.Context) error {
_, err := s.db.NewDelete().Model((*PolicyReport)(nil)).Where("id is not null").Exec(ctx)
if err != nil {
zap.L().Error("failed to remove policy reports", zap.Error(err))
}
return err
}
func (s *Store) FetchPolicyReports(ctx context.Context, filter api.Filter, pagination api.Pagination) ([]*api.PolicyReport, error) {
list := []*api.PolicyReport{}
query := s.db.NewSelect().Model((*PolicyReport)(nil))
s.addFilter(query, filter)
addPolicyReportFilter(query, filter)
query.Where(`pr.type = ?`, report.PolicyReportType)
addPagination(query, pagination)
err := query.Scan(ctx, &list)
if err != nil {
zap.L().Error("failed to select policy report results", zap.Error(err), zap.Any("filter", filter), zap.Any("pagination", pagination))
}
return list, err
}
func (s *Store) CountPolicyReports(ctx context.Context, filter api.Filter) (int, error) {
query := s.db.NewSelect().Model((*PolicyReport)(nil))
s.addFilter(query, filter)
addPolicyReportFilter(query, filter)
query.Where(`pr.type = ?`, report.PolicyReportType)
count, err := query.Count(ctx)
if err != nil {
zap.L().Error("failed to select policy report results", zap.Error(err), zap.Any("filter", filter))
}
return count, err
}
func (s *Store) FetchNamespacedReportLabels(ctx context.Context, filter api.Filter) (map[string][]string, error) {
results := []string{}
list := make(map[string][]string)
query := s.db.NewSelect().
TableExpr("policy_report as pr").
Distinct().
Where(`pr.type = ?`, report.PolicyReportType)
if s.db.Dialect().Name() == dialect.PG {
query.ColumnExpr("labels::text")
} else {
query.Column("labels")
}
addPolicyReportFilter(query, filter)
err := query.Scan(ctx, &results)
if err != nil {
return list, err
}
for _, labels := range results {
for key, value := range convertJSONToMap(labels) {
_, ok := list[key]
contained := contains(value, list[key])
if ok && !contained {
list[key] = append(list[key], value)
continue
} else if ok && contained {
continue
}
list[key] = []string{value}
}
}
return list, nil
}
func (s *Store) FetchClusterPolicyReports(ctx context.Context, filter api.Filter, pagination api.Pagination) ([]*api.PolicyReport, error) {
list := []*api.PolicyReport{}
query := s.db.NewSelect().Model((*PolicyReport)(nil))
s.addFilter(query, filter)
addPolicyReportFilter(query, filter)
query.Where(`pr.type = ?`, report.ClusterPolicyReportType)
addPagination(query, pagination)
err := query.Scan(ctx, &list)
if err != nil {
zap.L().Error("failed to select policy report results", zap.Error(err), zap.Any("filter", filter), zap.Any("pagination", pagination))
}
return list, err
}
func (s *Store) CountClusterPolicyReports(ctx context.Context, filter api.Filter) (int, error) {
query := s.db.NewSelect().Model((*PolicyReport)(nil))
s.addFilter(query, filter)
addPolicyReportFilter(query, filter)
query.Where(`pr.type = ?`, report.ClusterPolicyReportType)
count, err := query.Count(ctx)
if err != nil {
zap.L().Error("failed to select policy report results", zap.Error(err), zap.Any("filter", filter))
}
return count, err
}
func (s *Store) FetchClusterReportLabels(ctx context.Context, filter api.Filter) (map[string][]string, error) {
results := []string{}
list := make(map[string][]string)
query := s.db.NewSelect().
TableExpr("policy_report as pr").
Distinct().
Where(`pr.type = ?`, report.ClusterPolicyReportType)
if s.db.Dialect().Name() == dialect.PG {
query.ColumnExpr("labels::text")
} else {
query.Column("labels")
}
addPolicyReportFilter(query, filter)
err := query.Scan(ctx, &results)
if err != nil {
return list, err
}
for _, labels := range results {
for key, value := range convertJSONToMap(labels) {
_, ok := list[key]
contained := contains(value, list[key])
if ok && !contained {
list[key] = append(list[key], value)
continue
} else if ok && contained {
continue
}
list[key] = []string{value}
}
}
return list, nil
}
func (s *Store) FetchClusterRules(ctx context.Context, filter api.Filter) ([]string, error) {
list := make([]string, 0)
query := s.db.
NewSelect().
TableExpr("policy_report_result as r").
Column("rule").
Distinct().
Order("rule ASC").
Where(`r.resource_namespace = ''`)
if len(filter.ReportLabel) > 0 {
query.Join("JOIN policy_report AS pr ON pr.id = r.policy_report_id")
}
s.addFilter(query, filter)
addPolicyReportResultFilter(query, filter)
query.Scan(ctx, &list)
return list, nil
}
func (s *Store) FetchClusterResources(ctx context.Context, filter api.Filter) ([]*api.Resource, error) {
list := make([]*api.Resource, 0)
query := s.db.
NewSelect().
TableExpr("policy_report_result as r").
ColumnExpr("resource_name as name, resource_kind as kind").
Distinct().
Order("kind ASC", "name ASC").
Where(`r.resource_namespace = ''`)
if len(filter.ReportLabel) > 0 {
query.Join("JOIN policy_report AS pr ON pr.id = r.policy_report_id")
}
s.addFilter(query, filter)
addPolicyReportResultFilter(query, filter)
query.Scan(ctx, &list)
return list, nil
}
func (s *Store) FetchClusterPolicies(ctx context.Context, filter api.Filter) ([]string, error) {
return s.fetchFilterOptions(ctx, "policy", filter, false)
}
func (s *Store) FetchClusterKinds(ctx context.Context, filter api.Filter) ([]string, error) {
return s.fetchFilterOptions(ctx, "kind", filter, false)
}
func (s *Store) FetchClusterCategories(ctx context.Context, filter api.Filter) ([]string, error) {
return s.fetchFilterOptions(ctx, "category", filter, false)
}
func (s *Store) FetchClusterSources(ctx context.Context) ([]string, error) {
return s.fetchFilterOptions(ctx, "source", api.Filter{}, false)
}
func (s *Store) FetchClusterStatusCounts(ctx context.Context, filter api.Filter) ([]api.StatusCount, error) {
var list map[string]api.StatusCount
if len(filter.Status) == 0 {
list = map[string]api.StatusCount{
v1alpha2.StatusPass: {Status: v1alpha2.StatusPass},
v1alpha2.StatusFail: {Status: v1alpha2.StatusFail},
v1alpha2.StatusWarn: {Status: v1alpha2.StatusWarn},
v1alpha2.StatusError: {Status: v1alpha2.StatusError},
v1alpha2.StatusSkip: {Status: v1alpha2.StatusSkip},
}
} else {
list = map[string]api.StatusCount{}
for _, status := range filter.Status {
list[status] = api.StatusCount{Status: status}
}
}
counts := make([]api.StatusCount, 0, len(list))
results := make([]api.StatusCount, 0)
query := s.db.
NewSelect().
TableExpr("policy_report_filter as f").
ColumnExpr("SUM(f.count) as count, f.result as status").
Where(`f.namespace = ''`).
Group("status")
if len(filter.ReportLabel) > 0 {
query.Join("JOIN policy_report AS pr ON pr.id = f.policy_report_id")
}
s.addFilter(query, filter)
addPolicyReportFilterFilter(query, filter)
err := query.Scan(ctx, &results)
if err != nil {
zap.L().Error("failed to load cluster status counts", zap.Error(err))
return nil, err
}
for _, count := range results {
list[count.Status] = count
}
for _, count := range list {
counts = append(counts, count)
}
return counts, nil
}
func (s *Store) FetchClusterResults(ctx context.Context, filter api.Filter, pagination api.Pagination) ([]*api.ListResult, error) {
results := make([]*PolicyReportResult, 0)
query := s.db.
NewSelect().
Model(&results).
Where(`r.resource_namespace = ''`)
if len(filter.ReportLabel) > 0 {
query.Join("JOIN policy_report AS pr ON pr.id = r.policy_report_id")
}
s.addFilter(query, filter)
addPolicyReportResultFilter(query, filter)
addPagination(query, pagination)
err := query.Scan(ctx)
if err != nil {
return nil, err
}
return MapListResult(results), nil
}
func (s *Store) CountClusterResults(ctx context.Context, filter api.Filter) (int, error) {
query := s.db.
NewSelect().
Model((*PolicyReportResult)(nil)).
Where(`r.resource_namespace = ''`)
if len(filter.ReportLabel) > 0 {
query.Join("JOIN policy_report AS pr ON pr.id = r.policy_report_id")
}
s.addFilter(query, filter)
addPolicyReportResultFilter(query, filter)
return query.Count(ctx)
}
func (s *Store) FetchNamespacedRules(ctx context.Context, filter api.Filter) ([]string, error) {
list := make([]string, 0)
query := s.db.
NewSelect().
TableExpr("policy_report_result as r").
Column("rule").
Distinct().
Order("rule ASC").
Where(`r.resource_namespace != ''`)
if len(filter.ReportLabel) > 0 {
query.Join("JOIN policy_report AS pr ON pr.id = r.policy_report_id")
}
s.addFilter(query, filter)
addPolicyReportResultFilter(query, filter)
query.Scan(ctx, &list)
return list, nil
}
func (s *Store) FetchNamespacedResources(ctx context.Context, filter api.Filter) ([]*api.Resource, error) {
list := make([]*api.Resource, 0)
query := s.db.
NewSelect().
TableExpr("policy_report_result as r").
ColumnExpr("resource_name as name, resource_kind as kind").
Distinct().
Order("kind ASC", "name ASC").
Where(`r.resource_namespace != ''`)
if len(filter.ReportLabel) > 0 {
query.Join("JOIN policy_report AS pr ON pr.id = r.policy_report_id")
}
s.addFilter(query, filter)
addPolicyReportResultFilter(query, filter)
query.Scan(ctx, &list)
return list, nil
}
func (s *Store) FetchNamespacedPolicies(ctx context.Context, filter api.Filter) ([]string, error) {
return s.fetchFilterOptions(ctx, "policy", filter, true)
}
func (s *Store) FetchNamespacedKinds(ctx context.Context, filter api.Filter) ([]string, error) {
return s.fetchFilterOptions(ctx, "kind", filter, true)
}
func (s *Store) FetchNamespacedCategories(ctx context.Context, filter api.Filter) ([]string, error) {
return s.fetchFilterOptions(ctx, "category", filter, true)
}
func (s *Store) FetchNamespacedSources(ctx context.Context) ([]string, error) {
return s.fetchFilterOptions(ctx, "source", api.Filter{}, true)
}
func (s *Store) FetchNamespaces(ctx context.Context, filter api.Filter) ([]string, error) {
return s.fetchFilterOptions(ctx, "f.namespace", filter, true)
}
func (s *Store) FetchNamespacedStatusCounts(ctx context.Context, filter api.Filter) ([]api.NamespacedStatusCount, error) {
var list map[string][]api.NamespaceCount
if len(filter.Status) == 0 {
list = map[string][]api.NamespaceCount{
v1alpha2.StatusPass: make([]api.NamespaceCount, 0),
v1alpha2.StatusFail: make([]api.NamespaceCount, 0),
v1alpha2.StatusWarn: make([]api.NamespaceCount, 0),
v1alpha2.StatusError: make([]api.NamespaceCount, 0),
v1alpha2.StatusSkip: make([]api.NamespaceCount, 0),
}
} else {
list = map[string][]api.NamespaceCount{}
for _, status := range filter.Status {
list[status] = make([]api.NamespaceCount, 0)
}
}
statusCounts := make([]api.NamespacedStatusCount, 0, 5)
counts := make([]api.NamespaceCount, 0)
query := s.db.
NewSelect().
TableExpr("policy_report_filter as f").
ColumnExpr("SUM(f.count) as count, f.namespace, f.result as status").
Where(`f.namespace != ''`).
Group("f.namespace", "status").
Order("f.namespace ASC")
if len(filter.ReportLabel) > 0 {
query.Join("JOIN policy_report AS pr ON pr.id = f.policy_report_id")
}
s.addFilter(query, filter)
addPolicyReportFilterFilter(query, filter)
err := query.Scan(ctx, &counts)
if err != nil {
zap.L().Error("failed to load namespaced status counts", zap.Error(err))
return nil, err
}
for _, count := range counts {
list[count.Status] = append(list[count.Status], count)
}
for status, items := range list {
statusCounts = append(statusCounts, api.NamespacedStatusCount{
Status: status,
Items: items,
})
}
return statusCounts, nil
}
func (s *Store) FetchRuleStatusCounts(ctx context.Context, policy, rule string) ([]api.StatusCount, error) {
list := map[string]api.StatusCount{
v1alpha2.StatusPass: {Status: v1alpha2.StatusPass},
v1alpha2.StatusFail: {Status: v1alpha2.StatusFail},
v1alpha2.StatusWarn: {Status: v1alpha2.StatusWarn},
v1alpha2.StatusError: {Status: v1alpha2.StatusError},
v1alpha2.StatusSkip: {Status: v1alpha2.StatusSkip},
}
statusCounts := make([]api.StatusCount, 0, len(list))
counts := make([]api.StatusCount, 0)
err := s.db.NewSelect().
Table("policy_report_result").
ColumnExpr("COUNT(id) as count, result as status").
Where("rule = ?", rule).
Where("policy = ?", policy).
Group("status").
Scan(ctx, &counts)
if err != nil {
return statusCounts, err
}
for _, count := range counts {
list[count.Status] = count
}
for _, count := range list {
statusCounts = append(statusCounts, count)
}
return statusCounts, nil
}
func (s *Store) FetchNamespacedResults(ctx context.Context, filter api.Filter, pagination api.Pagination) ([]*api.ListResult, error) {
results := make([]*PolicyReportResult, 0)
query := s.db.
NewSelect().
Model(&results).
Where(`r.resource_namespace != ''`)
if len(filter.ReportLabel) > 0 {
query.Join("JOIN policy_report AS pr ON pr.id = r.policy_report_id")
}
s.addFilter(query, filter)
addPolicyReportResultFilter(query, filter)
addPagination(query, pagination)
err := query.Scan(ctx)
if err != nil {
return nil, err
}
return MapListResult(results), nil
}
func (s *Store) CountNamespacedResults(ctx context.Context, filter api.Filter) (int, error) {
query := s.db.
NewSelect().
Model((*PolicyReportResult)(nil)).
Where(`r.resource_namespace != ''`)
if len(filter.ReportLabel) > 0 {
query.Join("JOIN policy_report AS pr ON pr.id = r.policy_report_id")
}
s.addFilter(query, filter)
addPolicyReportResultFilter(query, filter)
return query.Count(ctx)
}
func (s *Store) Get(ctx context.Context, id string) (v1alpha2.ReportInterface, error) {
polr := &PolicyReport{}
err := s.db.NewSelect().Model(polr).Where("id = ?", id).Scan(ctx)
if err == sql.ErrNoRows {
return nil, err
} else if err != nil {
zap.L().Error("failed to load policy report", zap.Error(err))
return nil, err
}
results, err := s.fetchResults(ctx, id)
if err != nil {
zap.L().Error("failed to load policy report results", zap.Error(err))
return nil, err
}
return &v1alpha2.PolicyReport{
ObjectMeta: v1.ObjectMeta{
Name: polr.Name,
Namespace: polr.Namespace,
CreationTimestamp: v1.NewTime(time.Unix(polr.Created, 0)),
Labels: polr.Labels,
},
Summary: v1alpha2.PolicyReportSummary{
Skip: polr.Skip,
Pass: polr.Pass,
Warn: polr.Warn,
Fail: polr.Fail,
Error: polr.Error,
},
Results: results,
}, nil
}
func (s *Store) SQLDialect() dialect.Name {
return s.db.Dialect().Name()
}
func (s *Store) IsSQLite() bool {
return s.db.Dialect().Name() == dialect.SQLite
}
func (s *Store) fetchResults(ctx context.Context, id string) ([]v1alpha2.PolicyReportResult, error) {
polr := []*PolicyReportResult{}
err := s.db.NewSelect().Model(&polr).Where("policy_report_id = ?", id).Scan(ctx)
if err != nil {
zap.L().Error("failed to load policy report results", zap.Error(err))
return nil, err
}
list := make([]v1alpha2.PolicyReportResult, 0, len(polr))
for _, result := range polr {
list = append(list, v1alpha2.PolicyReportResult{
ID: result.ID,
Result: v1alpha2.PolicyResult(result.Result),
Severity: v1alpha2.PolicySeverity(result.Severity),
Policy: result.Policy,
Rule: result.Rule,
Message: result.Message,
Source: result.Source,
Resources: []corev1.ObjectReference{
{
APIVersion: result.Resource.APIVersion,
Kind: result.Resource.Kind,
Namespace: result.Resource.Namespace,
Name: result.Resource.Name,
UID: types.UID(result.Resource.UID),
},
},
Scored: result.Scored,
Properties: result.Properties,
Category: result.Category,
Timestamp: v1.Timestamp{
Seconds: result.Created,
},
})
}
return list, nil
}
func (s *Store) fetchFilterOptions(ctx context.Context, option string, filter api.Filter, namespaced bool) ([]string, error) {
list := make([]string, 0)
query := s.db.
NewSelect().
TableExpr("policy_report_filter as f").
Column(option).
Distinct().
Order(option+" ASC").
Where(`? != ''`, bun.Ident(option))
if namespaced {
query.Where(`f.namespace != ''`)
} else {
query.Where(`f.namespace = ''`)
}
if len(filter.ReportLabel) > 0 {
query.Join("JOIN policy_report AS pr ON pr.id = f.policy_report_id")
}
s.addFilter(query, filter)
addPolicyReportFilterFilter(query, filter)
err := query.Scan(ctx, &list)
return list, err
}
func (s *Store) Configure() {
if s.db.Dialect().Name() == dialect.PG {
s.jsonExtractLayout = "(pr.labels->>'%s') = ?"
return
}
s.jsonExtractLayout = "json_extract(pr.labels, '$.\"%s\"') = ?"
}
func (s *Store) RequireSchemaUpgrade(ctx context.Context) bool {
config := Config{}
err := s.db.NewSelect().Model(&config).Where("id = ?", 1).Scan(ctx)
if err != nil {
zap.L().Debug("failed to load config", zap.Error(err))
return true
}
return config.Version != s.version
}
func (s *Store) PersistSchemaVersion(ctx context.Context) error {
config := Config{
Version: s.version,
}
_, err := s.db.NewInsert().Model(&config).Exec(ctx)
if err != nil {
zap.L().Error("failed to persist database version", zap.Error(err))
return err
}
return nil
}
func (s *Store) PrepareDatabase(ctx context.Context) error {
zap.L().Debug("preparing database")
if s.RequireSchemaUpgrade(ctx) {
zap.L().Debug("database schema upgrade started")
if err := s.DropSchema(ctx); err != nil {
return err
}
if err := s.CreateSchemas(ctx); err != nil {
return err
}
if err := s.PersistSchemaVersion(ctx); err != nil {
return err
}
}
return s.CleanUp(ctx)
}
func NewStore(db *bun.DB, version string) (*Store, error) {
if db == nil {
return nil, errors.New("missing database connection")
}
s := &Store{
db: db,
version: version,
}
s.Configure()
return s, nil
}
func NewSQLiteDB(dbFile string) (*bun.DB, error) {
sqldb, err := createSQLiteDB(dbFile)
if err != nil {
return nil, err
}
return bun.NewDB(sqldb, sqlitedialect.New()), nil
}
func createSQLiteDB(dbFile string) (*sql.DB, error) {
os.Remove(dbFile)
file, err := os.Create(dbFile)
if err != nil {
return nil, err
}
file.Close()
db, err := sql.Open("sqlite3", dbFile+"?cache=shared")
if err != nil {
return nil, err
}
db.SetMaxOpenConns(1)
return db, nil
}
func addPolicyReportFilterFilter(query *bun.SelectQuery, filter api.Filter) {
if len(filter.Namespaces) > 0 {
query.Where("f.namespace IN (?)", bun.In(filter.Namespaces))
}
if len(filter.Kinds) > 0 {
query.Where("f.kind IN (?)", bun.In(filter.Kinds))
}
if len(filter.Sources) > 0 {
query.Where("f.source IN (?)", bun.In(filter.Sources))
}
}
func addPolicyReportResultFilter(query *bun.SelectQuery, filter api.Filter) {
if len(filter.Namespaces) > 0 {
query.Where("r.resource_namespace IN (?)", bun.In(filter.Namespaces))
}
if len(filter.Rules) > 0 {
query.Where("r.rule IN (?)", bun.In(filter.Rules))
}
if len(filter.Kinds) > 0 {
query.Where("r.resource_kind IN (?)", bun.In(filter.Kinds))
}
if len(filter.Resources) > 0 {
query.Where("r.resource_name IN (?)", bun.In(filter.Resources))
}
if len(filter.Sources) > 0 {
query.Where("r.source IN (?)", bun.In(filter.Sources))
}
if filter.Search != "" {
query.Where(`(resource_namespace LIKE ?0 OR resource_name LIKE ?0 OR policy LIKE ?0 OR rule LIKE ?0 OR severity = ?1 OR result = ?1 OR LOWER(resource_kind) = LOWER(?1))`, "%"+filter.Search+"%", filter.Search)
}
}
func addPolicyReportFilter(query *bun.SelectQuery, filter api.Filter) {
if len(filter.Namespaces) > 0 {
query.Where("pr.namespace IN (?)", bun.In(filter.Namespaces))
}
if len(filter.Sources) > 0 {
query.Where("pr.source IN (?)", bun.In(filter.Sources))
}
}
func (s *Store) addFilter(query *bun.SelectQuery, filter api.Filter) {
if len(filter.Policies) > 0 {
query.Where("policy IN (?)", bun.In(filter.Policies))
}
if len(filter.Categories) > 0 {
query.Where("category IN (?)", bun.In(filter.Categories))
}
if len(filter.Severities) > 0 {
query.Where("severity IN (?)", bun.In(filter.Severities))
}
if len(filter.Status) > 0 {
query.Where("result IN (?)", bun.In(filter.Status))
}
if len(filter.ReportLabel) > 0 {
for key, value := range filter.ReportLabel {
query.Where(fmt.Sprintf(s.jsonExtractLayout, key), value)
}
}
}
func addPagination(query *bun.SelectQuery, pagination api.Pagination) {
query.OrderExpr(fmt.Sprintf(
"%s %s",
strings.Join(pagination.SortBy, ","),
pagination.Direction,
))
if pagination.Page == 0 || pagination.Offset == 0 {
return
}
query.Limit(pagination.Offset)
query.Offset((pagination.Page - 1) * pagination.Offset)
}
func convertJSONToMap(s string) map[string]string {
m := make(map[string]string)
if s == "" {
return m
}
_ = json.Unmarshal([]byte(s), &m)
return m
}
func contains(source string, sources []string) bool {
for _, s := range sources {
if strings.EqualFold(s, source) {
return true
}
}
return false
}
func chunkSlice[K interface{}](slice []K, chunkSize int) [][]K {
var chunks [][]K
for i := 0; i < len(slice); i += chunkSize {
end := i + chunkSize
if end > len(slice) {
end = len(slice)
}
chunks = append(chunks, slice[i:end])
}
return chunks
}
func logOnError(operation string, err error) {
if err == nil {
return
}
zap.L().Error("failed to execute db operatopn", zap.String("operatopm", operation), zap.Error(err))
}

View file

@ -1,6 +1,8 @@
package sqlite3_test
package database_test
import (
"context"
"database/sql"
"testing"
corev1 "k8s.io/api/core/v1"
@ -8,8 +10,8 @@ import (
v1 "github.com/kyverno/policy-reporter/pkg/api/v1"
"github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2"
"github.com/kyverno/policy-reporter/pkg/database"
"github.com/kyverno/policy-reporter/pkg/fixtures"
"github.com/kyverno/policy-reporter/pkg/sqlite3"
)
var pagination = v1.Pagination{Page: 1, Offset: 20, Direction: "ASC", SortBy: []string{"resource_name"}}
@ -77,35 +79,43 @@ var scopeReport = &v1alpha2.PolicyReport{
}
func Test_PolicyReportStore(t *testing.T) {
db, _ := sqlite3.NewDatabase("test.db")
db, err := database.NewSQLiteDB("test.db")
if err != nil {
t.Fatal(err)
}
defer db.Close()
store, _ := sqlite3.NewPolicyReportStore(db)
ctx := context.Background()
store, _ := database.NewStore(db, "develop")
store.PrepareDatabase(ctx)
t.Run("Add/Get/Update PolicyReport", func(t *testing.T) {
_, ok := store.Get(preport.GetID())
if ok == true {
_, err := store.Get(ctx, preport.GetID())
if err != sql.ErrNoRows {
t.Fatalf("Should not be found in empty Store")
}
err := store.Add(preport)
err = store.Add(ctx, preport)
if err != nil {
t.Fatalf("Unexpected error: %s", err)
}
r1, ok := store.Get(preport.GetID())
if ok == false {
t.Errorf("Should be found in Store after adding report to the store")
}
if r1.GetSummary().Pass != 0 {
t.Errorf("Expected 0 Passed Results in GetSummary()")
polr, err := store.Get(ctx, preport.GetID())
if err != nil {
t.Fatalf("Should found policy reporter after adding: %v", err)
}
if r1.GetLabels()["app"] != "policy-reporter" {
t.Errorf("Expected Labels are persisted")
if len(polr.GetResults()) == 0 {
t.Fatalf("Failed to load PolicyReportResults: %v", err)
}
store.Update(ureport)
r2, _ := store.Get(preport.GetID())
err = store.Update(ctx, ureport)
if err != nil {
t.Fatalf("Failed to update policy report: %v", err)
}
r2, _ := store.Get(ctx, ureport.GetID())
if r2.GetSummary().Pass != 1 {
t.Errorf("Expected 1 Passed Results in GetSummary() after Update")
}
@ -114,31 +124,20 @@ func Test_PolicyReportStore(t *testing.T) {
t.Errorf("Expected Labels are updated")
}
})
t.Run("Add/Get ClusterPolicyReport", func(t *testing.T) {
_, ok := store.Get(creport.GetID())
if ok == true {
t.Fatalf("Should not be found in empty Store")
}
store.Add(creport)
_, ok = store.Get(creport.GetID())
if ok == false {
t.Errorf("Should be found in Store after adding report to the store")
}
})
t.Run("Add/Get PolicyReport with ScopeResource", func(t *testing.T) {
_, ok := store.Get(scopeReport.GetID())
if ok == true {
_, err := store.Get(ctx, scopeReport.GetID())
if err != sql.ErrNoRows {
t.Fatalf("Should not be found in empty Store")
}
err := store.Add(scopeReport)
err = store.Add(ctx, scopeReport)
if err != nil {
t.Fatalf("Unexpected add error: %s", err)
}
rep, ok := store.Get(scopeReport.GetID())
if ok == false {
rep, err := store.Get(ctx, scopeReport.GetID())
if err != nil {
t.Error("Should be found in Store after adding report to the store")
}
if len(rep.GetResults()) == 0 {
@ -149,56 +148,178 @@ func Test_PolicyReportStore(t *testing.T) {
t.Error("Expected scope resource set as result resource")
}
store.Remove(rep.GetID())
store.Remove(ctx, rep.GetID())
})
t.Run("FetchNamespacedKinds", func(t *testing.T) {
items, err := store.FetchNamespacedKinds(v1.Filter{Sources: []string{"kyverno"}, ReportLabel: map[string]string{"app": "policy-reporter"}})
t.Run("Add/Get ClusterPolicyReport", func(t *testing.T) {
_, err := store.Get(ctx, creport.GetID())
if err != sql.ErrNoRows {
t.Fatalf("Should not be found in empty Store")
}
err = store.Add(ctx, creport)
if err != nil {
t.Fatalf("Failed to persist ClusterPolicyReport: %v", err)
}
_, err = store.Get(ctx, creport.GetID())
if err != nil {
t.Fatalf("Should be found in Store after adding report to the store")
}
})
t.Run("FetchPolicyReports", func(t *testing.T) {
items, err := store.FetchPolicyReports(ctx, v1.Filter{Namespaces: []string{"test"}, ReportLabel: map[string]string{"scope": "namespaced"}}, polrPagination)
if err != nil {
t.Fatalf("Unexpected Error: %s", err)
}
if len(items) != 2 {
t.Fatalf("Should Find 2 Kinds with Namespace Scope")
}
if items[0] != "Deployment" {
t.Errorf("Should return 'Deployment' as first result")
}
if items[1] != "Pod" {
t.Errorf("Should return 'Pod' as second result")
if len(items) != 1 {
t.Fatalf("Should return one policy report, got %d", len(items))
}
})
t.Run("FetchClusterKinds", func(t *testing.T) {
items, err := store.FetchClusterKinds(v1.Filter{Sources: []string{"kyverno"}, ReportLabel: map[string]string{"app": "policy-reporter"}})
t.Run("CountPolicyReports", func(t *testing.T) {
count, err := store.CountPolicyReports(ctx, v1.Filter{Namespaces: []string{"test"}, ReportLabel: map[string]string{"scope": "namespaced"}})
if err != nil {
t.Fatalf("Unexpected Error: %s", err)
}
if count != 1 {
t.Fatalf("Should return one policy report, got %d", count)
}
})
t.Run("NamespacedGetLabels()", func(t *testing.T) {
items, err := store.FetchNamespacedReportLabels(ctx, v1.Filter{Sources: []string{"Kyverno"}, Namespaces: []string{"test"}})
if err != nil {
t.Fatalf("Unexpected Error: %s", err)
}
if len(items) != 3 {
t.Fatalf("Should return 3 GetLabels() results")
}
if len(items["scope"]) != 1 && items["scope"][0] != "namespaced" {
t.Fatalf("Should return cluster as scope value")
}
if len(items["app"]) != 1 && items["app"][0] != "policy-reporter" {
t.Fatalf("Should return policy-reporter as app value")
}
if len(items["owner"]) != 1 && items["owner"][0] != "team-a" {
t.Fatalf("Should return policy-reporter as app value")
}
})
t.Run("FetchClusterReports", func(t *testing.T) {
items, err := store.FetchClusterPolicyReports(ctx, v1.Filter{ReportLabel: map[string]string{"scope": "cluster"}}, polrPagination)
if err != nil {
t.Fatalf("Unexpected Error: %s", err)
}
if len(items) != 1 {
t.Fatalf("Should return one policy report, got %d", len(items))
}
})
t.Run("CountClusterReports", func(t *testing.T) {
items, err := store.CountClusterPolicyReports(ctx, v1.Filter{ReportLabel: map[string]string{"scope": "cluster"}})
if err != nil {
t.Fatalf("Unexpected Error: %s", err)
}
if items != 1 {
t.Fatalf("Should return one policy report, got %d", items)
}
})
t.Run("ClusterGetLabels()", func(t *testing.T) {
items, err := store.FetchClusterReportLabels(ctx, v1.Filter{Sources: []string{"Kyverno"}})
if err != nil {
t.Fatalf("Unexpected Error: %s", err)
}
if len(items) != 2 {
t.Fatalf("Should return 2 GetLabels() results")
}
if len(items["scope"]) != 1 && items["scope"][0] != "cluster" {
t.Fatalf("Should return cluster as scope value")
}
if len(items["app"]) != 1 && items["app"][0] != "policy-reporter" {
t.Fatalf("Should return policy-reporter as app value")
}
})
t.Run("FetchClusterPolicies", func(t *testing.T) {
items, err := store.FetchClusterPolicies(ctx, v1.Filter{Sources: []string{"Kyverno"}, ReportLabel: map[string]string{"app": "policy-reporter"}})
if err != nil {
t.Fatalf("Unexpected Error: %s", err)
}
if len(items) != 1 {
t.Fatalf("Should find 1 kind with cluster scope")
t.Fatalf("Should Find 1 cluster scoped policy, found %d", len(items))
}
if items[0] != "Namespace" {
t.Errorf("Should return 'Namespace' as first result")
if items[0] != "require-ns-GetLabels()" {
t.Fatalf("Should return 'require-ns-GetLabels()' policy")
}
})
t.Run("FetchClusterRules", func(t *testing.T) {
items, err := store.FetchClusterRules(ctx, v1.Filter{Sources: []string{"Kyverno"}, ReportLabel: map[string]string{"app": "policy-reporter"}})
if err != nil {
t.Fatalf("Unexpected Error: %s", err)
}
if len(items) != 1 {
t.Fatalf("Should Find 1 cluster scoped rule, found %d", len(items))
}
if items[0] != "check-for-GetLabels()-on-namespace" {
t.Fatalf("Should return 'check-for-GetLabels()-on-namespace' rule")
}
})
t.Run("FetchNamespacedPolicies", func(t *testing.T) {
items, err := store.FetchNamespacedPolicies(ctx, v1.Filter{Sources: []string{"Kyverno"}, ReportLabel: map[string]string{"app": "policy-reporter"}})
if err != nil {
t.Fatalf("Unexpected Error: %s", err)
}
if len(items) != 1 {
t.Errorf("Should find 1 namespace scoped policy")
}
if items[0] != "require-requests-and-limits-required" {
t.Errorf("Should return 'require-requests-and-limits-required' policy")
}
})
t.Run("FetchNamespacedRules", func(t *testing.T) {
items, err := store.FetchNamespacedRules(ctx, v1.Filter{Sources: []string{"Kyverno"}, ReportLabel: map[string]string{"app": "policy-reporter"}})
if err != nil {
t.Fatalf("Unexpected Error: %s", err)
}
if len(items) != 1 {
t.Fatalf("Should find 1 namespace scoped policy, found %d", len(items))
}
if items[0] != "autogen-check-for-requests-and-limits" {
t.Fatalf("Should return 'require-requests-and-limits-required' policy")
}
})
t.Run("FetchNamespacedResources", func(t *testing.T) {
items, err := store.FetchNamespacedResources(v1.Filter{Sources: []string{"kyverno"}, Kinds: []string{"pod"}, ReportLabel: map[string]string{"app": "policy-reporter"}})
items, err := store.FetchNamespacedResources(ctx, v1.Filter{Sources: []string{"Kyverno"}, Kinds: []string{"Pod"}, ReportLabel: map[string]string{"app": "policy-reporter"}})
if err != nil {
t.Fatalf("Unexpected Error: %s", err)
}
if len(items) != 2 {
t.Fatalf("Should Find 2 Resources with Namespace Scope")
if len(items) != 1 {
t.Fatalf("Should find 1 distinct resource with namespace Scope, got %d", len(items))
}
if items[0].Name != "nginx" {
t.Errorf("Should return 'nginx' as first result, got %s", items[0].Name)
}
if items[1].Name != "nginx" {
t.Errorf("Should return 'nginx' as second result get %s", items[1].Name)
}
})
t.Run("FetchClusterResources", func(t *testing.T) {
items, err := store.FetchClusterResources(v1.Filter{Sources: []string{"kyverno"}, Kinds: []string{"namespace"}, ReportLabel: map[string]string{"app": "policy-reporter"}})
items, err := store.FetchClusterResources(ctx, v1.Filter{Sources: []string{"Kyverno"}, Kinds: []string{"Namespace"}, ReportLabel: map[string]string{"app": "policy-reporter"}})
if err != nil {
t.Fatalf("Unexpected Error: %s", err)
}
@ -213,8 +334,47 @@ func Test_PolicyReportStore(t *testing.T) {
}
})
t.Run("FetchClusterSources", func(t *testing.T) {
items, err := store.FetchClusterSources(ctx)
if err != nil {
t.Fatalf("Unexpected Error: %s", err)
}
if len(items) != 1 {
t.Errorf("Should find 1 Source")
}
if items[0] != "Kyverno" {
t.Errorf("Should return Kyverno")
}
})
t.Run("FetchNamespacedSources", func(t *testing.T) {
items, err := store.FetchNamespacedSources(ctx)
if err != nil {
t.Fatalf("Unexpected Error: %s", err)
}
if len(items) != 1 {
t.Errorf("Should find 1 Source")
}
if items[0] != "Kyverno" {
t.Errorf("Should return Kyverno")
}
})
t.Run("FetchNamespaces", func(t *testing.T) {
items, err := store.FetchNamespaces(ctx, v1.Filter{Sources: []string{"Kyverno"}, ReportLabel: map[string]string{"app": "policy-reporter"}})
if err != nil {
t.Fatalf("Unexpected Error: %s", err)
}
if len(items) != 1 {
t.Fatal("Should find 1 Namespace")
}
if items[0] != "test" {
t.Errorf("Should return test namespace")
}
})
t.Run("FetchNamespacedStatusCounts", func(t *testing.T) {
items, err := store.FetchNamespacedStatusCounts(v1.Filter{ReportLabel: map[string]string{"app": "policy-reporter"}})
items, err := store.FetchNamespacedStatusCounts(ctx, v1.Filter{ReportLabel: map[string]string{"app": "policy-reporter"}})
if err != nil {
t.Fatalf("Unexpected Error: %s", err)
}
@ -249,7 +409,7 @@ func Test_PolicyReportStore(t *testing.T) {
})
t.Run("FetchNamespacedStatusCounts with StatusFilter", func(t *testing.T) {
items, err := store.FetchNamespacedStatusCounts(v1.Filter{Status: []string{v1alpha2.StatusPass}, ReportLabel: map[string]string{"app": "policy-reporter"}})
items, err := store.FetchNamespacedStatusCounts(ctx, v1.Filter{Status: []string{v1alpha2.StatusPass}, ReportLabel: map[string]string{"app": "policy-reporter"}})
if err != nil {
t.Fatalf("Unexpected Error: %s", err)
}
@ -264,33 +424,8 @@ func Test_PolicyReportStore(t *testing.T) {
}
})
t.Run("FetchRuleStatusCounts", func(t *testing.T) {
items, err := store.FetchRuleStatusCounts("require-requests-and-limits-required", "autogen-check-for-requests-and-limits")
var passed v1.StatusCount
var failed v1.StatusCount
for _, item := range items {
if item.Status == v1alpha2.StatusPass {
passed = item
}
if item.Status == v1alpha2.StatusFail {
failed = item
}
}
if err != nil {
t.Fatalf("Unexpected Error: %s", err)
}
if passed.Count != 1 {
t.Errorf("Expected count to be one for pass")
}
if failed.Count != 1 {
t.Errorf("Expected count to be one for fail")
}
})
t.Run("FetchStatusCounts", func(t *testing.T) {
items, err := store.FetchStatusCounts(v1.Filter{ReportLabel: map[string]string{"app": "policy-reporter"}})
t.Run("FetchClusterStatusCounts", func(t *testing.T) {
items, err := store.FetchClusterStatusCounts(ctx, v1.Filter{ReportLabel: map[string]string{"app": "policy-reporter"}})
if err != nil {
t.Fatalf("Unexpected Error: %s", err)
}
@ -315,8 +450,8 @@ func Test_PolicyReportStore(t *testing.T) {
}
})
t.Run("FetchStatusCounts with StatusFilter", func(t *testing.T) {
items, err := store.FetchStatusCounts(v1.Filter{Status: []string{v1alpha2.StatusPass}})
t.Run("FetchClusterStatusCounts with StatusFilter", func(t *testing.T) {
items, err := store.FetchClusterStatusCounts(ctx, v1.Filter{Status: []string{v1alpha2.StatusPass}})
if err != nil {
t.Fatalf("Unexpected Error: %s", err)
}
@ -331,8 +466,33 @@ func Test_PolicyReportStore(t *testing.T) {
}
})
t.Run("FetchRuleStatusCounts", func(t *testing.T) {
items, err := store.FetchRuleStatusCounts(ctx, "require-requests-and-limits-required", "autogen-check-for-requests-and-limits")
var passed v1.StatusCount
var failed v1.StatusCount
for _, item := range items {
if item.Status == v1alpha2.StatusPass {
passed = item
}
if item.Status == v1alpha2.StatusFail {
failed = item
}
}
if err != nil {
t.Fatalf("Unexpected Error: %s", err)
}
if passed.Count != 1 {
t.Errorf("Expected count to be one for pass")
}
if failed.Count != 1 {
t.Errorf("Expected count to be one for fail")
}
})
t.Run("FetchNamespacedResults", func(t *testing.T) {
items, err := store.FetchNamespacedResults(v1.Filter{Namespaces: []string{"test"}}, pagination)
items, err := store.FetchNamespacedResults(ctx, v1.Filter{Namespaces: []string{"test"}}, pagination)
if err != nil {
t.Fatalf("Unexpected Error: %s", err)
}
@ -343,7 +503,7 @@ func Test_PolicyReportStore(t *testing.T) {
})
t.Run("FetchNamespacedResults with SeverityFilter", func(t *testing.T) {
items, err := store.FetchNamespacedResults(v1.Filter{Severities: []string{v1alpha2.SeverityHigh}}, pagination)
items, err := store.FetchNamespacedResults(ctx, v1.Filter{Severities: []string{v1alpha2.SeverityHigh}}, pagination)
if err != nil {
t.Fatalf("Unexpected Error: %s", err)
}
@ -357,7 +517,7 @@ func Test_PolicyReportStore(t *testing.T) {
})
t.Run("CountNamespacedResults", func(t *testing.T) {
count, err := store.CountNamespacedResults(v1.Filter{ReportLabel: map[string]string{"app": "policy-reporter"}})
count, err := store.CountNamespacedResults(ctx, v1.Filter{ReportLabel: map[string]string{"app": "policy-reporter"}})
if err != nil {
t.Fatalf("Unexpected Error: %s", err)
}
@ -368,7 +528,7 @@ func Test_PolicyReportStore(t *testing.T) {
})
t.Run("CountNamespacedResults with SeverityFilter", func(t *testing.T) {
count, err := store.CountNamespacedResults(v1.Filter{Severities: []string{v1alpha2.SeverityHigh}})
count, err := store.CountNamespacedResults(ctx, v1.Filter{Severities: []string{v1alpha2.SeverityHigh}})
if err != nil {
t.Fatalf("Unexpected Error: %s", err)
}
@ -379,7 +539,7 @@ func Test_PolicyReportStore(t *testing.T) {
})
t.Run("FetchNamespacedResults with SearchFilter::Severity", func(t *testing.T) {
items, err := store.FetchNamespacedResults(v1.Filter{Search: v1alpha2.SeverityHigh}, pagination)
items, err := store.FetchNamespacedResults(ctx, v1.Filter{Search: v1alpha2.SeverityHigh}, pagination)
if err != nil {
t.Fatalf("Unexpected Error: %s", err)
}
@ -393,7 +553,7 @@ func Test_PolicyReportStore(t *testing.T) {
})
t.Run("FetchNamespacedResults with SearchFilter::Kind", func(t *testing.T) {
items, err := store.FetchNamespacedResults(v1.Filter{Search: "deployment"}, pagination)
items, err := store.FetchNamespacedResults(ctx, v1.Filter{Search: "deployment"}, pagination)
if err != nil {
t.Fatalf("Unexpected Error: %s", err)
}
@ -407,7 +567,7 @@ func Test_PolicyReportStore(t *testing.T) {
})
t.Run("FetchClusterResults", func(t *testing.T) {
items, err := store.FetchClusterResults(v1.Filter{Status: []string{v1alpha2.StatusPass, v1alpha2.StatusFail}, ReportLabel: map[string]string{"app": "policy-reporter"}}, pagination)
items, err := store.FetchClusterResults(ctx, v1.Filter{Status: []string{v1alpha2.StatusPass, v1alpha2.StatusFail}, ReportLabel: map[string]string{"app": "policy-reporter"}}, pagination)
if err != nil {
t.Fatalf("Unexpected Error: %s", err)
}
@ -418,7 +578,7 @@ func Test_PolicyReportStore(t *testing.T) {
})
t.Run("CountClusterResults", func(t *testing.T) {
count, err := store.CountClusterResults(v1.Filter{Status: []string{v1alpha2.StatusPass, v1alpha2.StatusFail}, ReportLabel: map[string]string{"app": "policy-reporter"}})
count, err := store.CountClusterResults(ctx, v1.Filter{Status: []string{v1alpha2.StatusPass, v1alpha2.StatusFail}, ReportLabel: map[string]string{"app": "policy-reporter"}})
if err != nil {
t.Fatalf("Unexpected Error: %s", err)
}
@ -429,7 +589,7 @@ func Test_PolicyReportStore(t *testing.T) {
})
t.Run("FetchClusterResults with SeverityFilter", func(t *testing.T) {
items, err := store.FetchClusterResults(v1.Filter{Severities: []string{v1alpha2.SeverityHigh}}, pagination)
items, err := store.FetchClusterResults(ctx, v1.Filter{Severities: []string{v1alpha2.SeverityHigh}}, pagination)
if err != nil {
t.Fatalf("Unexpected Error: %s", err)
}
@ -443,7 +603,7 @@ func Test_PolicyReportStore(t *testing.T) {
})
t.Run("FetchClusterResults with SearchFilter", func(t *testing.T) {
items, err := store.FetchClusterResults(v1.Filter{Search: v1alpha2.SeverityHigh}, pagination)
items, err := store.FetchClusterResults(ctx, v1.Filter{Search: v1alpha2.SeverityHigh}, pagination)
if err != nil {
t.Fatalf("Unexpected Error: %s", err)
}
@ -456,294 +616,63 @@ func Test_PolicyReportStore(t *testing.T) {
}
})
t.Run("FetchStatusCounts with StatusFilter", func(t *testing.T) {
items, err := store.FetchStatusCounts(v1.Filter{Status: []string{v1alpha2.StatusPass}})
t.Run("FetchNamespacedKinds", func(t *testing.T) {
items, err := store.FetchNamespacedKinds(ctx, v1.Filter{Sources: []string{"Kyverno"}, ReportLabel: map[string]string{"app": "policy-reporter"}})
if err != nil {
t.Fatalf("Unexpected Error: %s", err)
}
if len(items) != 2 {
t.Fatalf("Should Find 2 Kinds with Namespace Scope")
}
if items[0] != "Deployment" {
t.Errorf("Should return 'Deployment' as first result")
}
if items[1] != "Pod" {
t.Errorf("Should return 'Pod' as second result")
}
})
t.Run("FetchClusterKinds", func(t *testing.T) {
items, err := store.FetchClusterKinds(ctx, v1.Filter{Sources: []string{"Kyverno"}, ReportLabel: map[string]string{"app": "policy-reporter"}})
if err != nil {
t.Fatalf("Unexpected Error: %s", err)
}
if len(items) != 1 {
t.Fatalf("Should have only 1 item for pass counts")
t.Fatalf("Should find 1 kind with cluster scope")
}
if items[0].Status != v1alpha2.StatusPass {
t.Errorf("Expected Pass Counts")
}
if items[0].Count != 1 {
t.Errorf("Expected count to be one for pass")
if items[0] != "Namespace" {
t.Errorf("Should return 'Namespace' as first result")
}
})
t.Run("FetchNamespaces", func(t *testing.T) {
items, err := store.FetchNamespaces(v1.Filter{Sources: []string{"kyverno"}, ReportLabel: map[string]string{"app": "policy-reporter"}})
t.Run("FetchNamespacedCategories", func(t *testing.T) {
items, err := store.FetchNamespacedCategories(ctx, v1.Filter{Sources: []string{"Kyverno"}, ReportLabel: map[string]string{"app": "policy-reporter"}})
if err != nil {
t.Fatalf("Unexpected Error: %s", err)
}
if len(items) != 1 {
t.Errorf("Should find 1 Namespace")
}
if items[0] != "test" {
t.Errorf("Should return test namespace")
}
})
t.Run("FetchCategories", func(t *testing.T) {
items, err := store.FetchCategories(v1.Filter{Sources: []string{"kyverno"}, ReportLabel: map[string]string{"app": "policy-reporter"}})
if err != nil {
t.Fatalf("Unexpected Error: %s", err)
}
if len(items) != 3 {
t.Errorf("Should Find 2 Categories")
if len(items) != 2 {
t.Errorf("Should find 2 categories, got %d", len(items))
}
if items[0] != "Best Practices" {
t.Errorf("Should return 'Best Practices' as first category")
}
})
t.Run("FetchClusterPolicies", func(t *testing.T) {
items, err := store.FetchClusterPolicies(v1.Filter{Sources: []string{"kyverno"}, ReportLabel: map[string]string{"app": "policy-reporter"}})
t.Run("FetchClusterCategories", func(t *testing.T) {
items, err := store.FetchClusterCategories(ctx, v1.Filter{Sources: []string{"Kyverno"}, ReportLabel: map[string]string{"app": "policy-reporter"}})
if err != nil {
t.Fatalf("Unexpected Error: %s", err)
}
if len(items) != 1 {
t.Errorf("Should Find 1 cluster scoped Policy")
t.Errorf("Should find 1 category, got %d", len(items))
}
if items[0] != "require-ns-GetLabels()" {
t.Errorf("Should return 'require-ns-GetLabels()' policy")
if items[0] != "namespaces" {
t.Errorf("Should return 'Best Practices' as first category, get '%s'", items[0])
}
})
t.Run("FetchClusterRules", func(t *testing.T) {
items, err := store.FetchClusterRules(v1.Filter{Sources: []string{"kyverno"}, ReportLabel: map[string]string{"app": "policy-reporter"}})
if err != nil {
t.Fatalf("Unexpected Error: %s", err)
}
if len(items) != 1 {
t.Errorf("Should Find 1 cluster scoped Policy")
}
if items[0] != "check-for-GetLabels()-on-namespace" {
t.Errorf("Should return 'check-for-GetLabels()-on-namespace' rule")
}
})
t.Run("FetchNamespacedPolicies", func(t *testing.T) {
items, err := store.FetchNamespacedPolicies(v1.Filter{Sources: []string{"kyverno"}, ReportLabel: map[string]string{"app": "policy-reporter"}})
if err != nil {
t.Fatalf("Unexpected Error: %s", err)
}
if len(items) != 1 {
t.Errorf("Should find 1 namespace scoped policy")
}
if items[0] != "require-requests-and-limits-required" {
t.Errorf("Should return 'require-requests-and-limits-required' policy")
}
})
t.Run("FetchNamespacedRules", func(t *testing.T) {
items, err := store.FetchNamespacedRules(v1.Filter{Sources: []string{"kyverno"}, ReportLabel: map[string]string{"app": "policy-reporter"}})
if err != nil {
t.Fatalf("Unexpected Error: %s", err)
}
if len(items) != 1 {
t.Errorf("Should find 1 namespace scoped policy")
}
if items[0] != "autogen-check-for-requests-and-limits" {
t.Errorf("Should return 'require-requests-and-limits-required' policy")
}
})
t.Run("FetchClusterSources", func(t *testing.T) {
items, err := store.FetchClusterSources()
if err != nil {
t.Fatalf("Unexpected Error: %s", err)
}
if len(items) != 1 {
t.Errorf("Should find 1 Source")
}
if items[0] != "Kyverno" {
t.Errorf("Should return Kyverno")
}
})
t.Run("FetchNamespacedSources", func(t *testing.T) {
items, err := store.FetchNamespacedSources()
if err != nil {
t.Fatalf("Unexpected Error: %s", err)
}
if len(items) != 1 {
t.Errorf("Should find 1 Source")
}
if items[0] != "Kyverno" {
t.Errorf("Should return Kyverno")
}
})
t.Run("NamespacedResults: ReportLabel Filter", func(t *testing.T) {
items, err := store.FetchNamespacedResults(v1.Filter{ReportLabel: map[string]string{"app": "policy-reporter"}}, pagination)
if err != nil {
t.Fatalf("Unexpected Error: %s", err)
}
if len(items) != 2 {
t.Fatalf("Should return 2 namespaced results")
}
items, err = store.FetchNamespacedResults(v1.Filter{ReportLabel: map[string]string{"app": "not-exist"}}, pagination)
if err != nil {
t.Fatalf("Unexpected Error: %s", err)
}
if len(items) != 0 {
t.Fatalf("Should return 0 namespaced results")
}
})
t.Run("ClusterResults: ReportLabel Filter", func(t *testing.T) {
items, err := store.FetchClusterResults(v1.Filter{ReportLabel: map[string]string{"app": "policy-reporter"}}, pagination)
if err != nil {
t.Fatalf("Unexpected Error: %s", err)
}
if len(items) != 2 {
t.Fatalf("Should return 2 namespaced results")
}
items, err = store.FetchClusterResults(v1.Filter{ReportLabel: map[string]string{"app": "not-exist"}}, pagination)
if err != nil {
t.Fatalf("Unexpected Error: %s", err)
}
if len(items) != 0 {
t.Fatalf("Should return 0 namespaced results")
}
})
t.Run("NamespacedGetLabels()", func(t *testing.T) {
items, err := store.FetchNamespacedReportLabels(v1.Filter{Sources: []string{"Kyverno"}, Namespaces: []string{"test"}})
if err != nil {
t.Fatalf("Unexpected Error: %s", err)
}
if len(items) != 3 {
t.Fatalf("Should return 3 GetLabels() results")
}
if len(items["scope"]) != 1 && items["scope"][0] != "namespaced" {
t.Fatalf("Should return cluster as scope value")
}
if len(items["app"]) != 1 && items["app"][0] != "policy-reporter" {
t.Fatalf("Should return policy-reporter as app value")
}
if len(items["owner"]) != 1 && items["owner"][0] != "team-a" {
t.Fatalf("Should return policy-reporter as app value")
}
})
t.Run("CountPolicyReports", func(t *testing.T) {
items, err := store.CountPolicyReports(v1.Filter{Namespaces: []string{"test"}, ReportLabel: map[string]string{"scope": "namespaced"}})
if err != nil {
t.Fatalf("Unexpected Error: %s", err)
}
if items != 1 {
t.Fatalf("Should return one policy report, got %d", items)
}
})
t.Run("FetchPolicyReports", func(t *testing.T) {
items, err := store.FetchPolicyReports(v1.Filter{Namespaces: []string{"test"}, ReportLabel: map[string]string{"scope": "namespaced"}}, polrPagination)
if err != nil {
t.Fatalf("Unexpected Error: %s", err)
}
if len(items) != 1 {
t.Fatalf("Should return one policy report, got %d", len(items))
}
})
t.Run("CountClusterReports", func(t *testing.T) {
items, err := store.CountClusterPolicyReports(v1.Filter{ReportLabel: map[string]string{"scope": "cluster"}})
if err != nil {
t.Fatalf("Unexpected Error: %s", err)
}
if items != 1 {
t.Fatalf("Should return one policy report, got %d", items)
}
})
t.Run("FetchClusterReports", func(t *testing.T) {
items, err := store.FetchClusterPolicyReports(v1.Filter{ReportLabel: map[string]string{"scope": "cluster"}}, polrPagination)
if err != nil {
t.Fatalf("Unexpected Error: %s", err)
}
if len(items) != 1 {
t.Fatalf("Should return one policy report, got %d", len(items))
}
})
t.Run("ClusterGetLabels()", func(t *testing.T) {
items, err := store.FetchClusterReportLabels(v1.Filter{Sources: []string{"Kyverno"}})
if err != nil {
t.Fatalf("Unexpected Error: %s", err)
}
if len(items) != 2 {
t.Fatalf("Should return 2 GetLabels() results")
}
if len(items["scope"]) != 1 && items["scope"][0] != "cluster" {
t.Fatalf("Should return cluster as scope value")
}
if len(items["app"]) != 1 && items["app"][0] != "policy-reporter" {
t.Fatalf("Should return policy-reporter as app value")
}
})
t.Run("Delete/Get", func(t *testing.T) {
_, ok := store.Get(preport.GetID())
if ok == false {
t.Errorf("Should be found in Store after adding report to the store")
}
store.Remove(preport.GetID())
_, ok = store.Get(preport.GetID())
if ok == true {
t.Fatalf("Should not be found after Remove report from Store")
}
})
t.Run("CleanUp", func(t *testing.T) {
store.Add(preport)
store.CleanUp()
list, _ := store.FetchNamespacedResults(v1.Filter{}, v1.Pagination{Page: 0})
if len(list) == 1 {
t.Fatalf("Should have no results after CleanUp")
}
})
t.Run("Insert duplicates", func(t *testing.T) {
err := store.Add(dreport)
if err != nil {
t.Errorf("Should ignore duplicated ID: %s", err)
}
polr, ok := store.Get(preport.GetID())
if ok == false {
t.Errorf("Should be found in Store after adding report to the store")
}
if len(polr.GetResults()) != 2 {
t.Errorf("Should ignore duplicated result")
}
store.Remove(dreport.GetID())
_, ok = store.Get(dreport.GetID())
if ok == true {
t.Fatalf("Should not be found after Remove report from Store")
}
})
err = store.CleanUp(ctx)
if err != nil {
t.Fatalf("Failed to cleanup policy reports: %v", err)
}
}

206
pkg/database/model.go Normal file
View file

@ -0,0 +1,206 @@
package database
import (
"strconv"
"github.com/segmentio/fasthash/fnv1a"
"github.com/uptrace/bun"
corev1 "k8s.io/api/core/v1"
api "github.com/kyverno/policy-reporter/pkg/api/v1"
"github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2"
"github.com/kyverno/policy-reporter/pkg/report"
)
type Config struct {
bun.BaseModel `bun:"table:policy_report_config,alias:c"`
ID int `bun:"id,pk,autoincrement" json:"id"`
Version string
}
type PolicyReport struct {
bun.BaseModel `bun:"table:policy_report,alias:pr" json:"-"`
ID string `bun:",pk" json:"id"`
Type string `json:"type"`
Name string `json:"name"`
Namespace string `json:"namespace,omitempty"`
Source string `json:"source"`
Labels map[string]string `bun:",type:json" json:"labels"`
Skip int `json:"skip"`
Pass int `json:"pass"`
Warn int `json:"warn"`
Fail int `json:"fail"`
Error int `json:"error"`
Created int64 `json:"created"`
}
type Resource struct {
APIVersion string `bun:"api_version"`
Kind string
Name string
Namespace string
UID string
}
type PolicyReportResult struct {
bun.BaseModel `bun:"table:policy_report_result,alias:r" json:"-"`
ID string `bun:",pk" json:"id"`
PolicyReportID string `bund:"policy_report_id" json:"-"`
Resource Resource `bun:"embed:resource_"`
Policy string
Rule string
Message string
Scored bool
Result string
Severity string
Category string
Source string
Properties map[string]string `bun:",type:json"`
Created int64
}
type PolicyReportFilter struct {
bun.BaseModel `bun:"table:policy_report_filter,alias:f"`
PolicyReportID string `bund:"policy_report_id"`
Namespace string
Policy string
Kind string
Result string
Severity string
Category string
Source string
Count int
}
func (r *PolicyReportFilter) Hash() string {
h1 := fnv1a.Init64
h1 = fnv1a.AddString64(h1, r.PolicyReportID)
h1 = fnv1a.AddString64(h1, r.Namespace)
h1 = fnv1a.AddString64(h1, r.Source)
h1 = fnv1a.AddString64(h1, r.Kind)
h1 = fnv1a.AddString64(h1, r.Category)
h1 = fnv1a.AddString64(h1, r.Policy)
h1 = fnv1a.AddString64(h1, r.Severity)
h1 = fnv1a.AddString64(h1, r.Result)
return strconv.FormatUint(h1, 10)
}
func MapPolicyReport(r v1alpha2.ReportInterface) *PolicyReport {
return &PolicyReport{
ID: r.GetID(),
Type: report.GetType(r),
Name: r.GetName(),
Namespace: r.GetNamespace(),
Source: r.GetSource(),
Labels: r.GetLabels(),
Skip: r.GetSummary().Skip,
Pass: r.GetSummary().Pass,
Warn: r.GetSummary().Warn,
Fail: r.GetSummary().Fail,
Error: r.GetSummary().Error,
Created: r.GetCreationTimestamp().Unix(),
}
}
func MapPolicyReportResults(polr v1alpha2.ReportInterface) []*PolicyReportResult {
list := make([]*PolicyReportResult, 0, len(polr.GetResults()))
for _, result := range polr.GetResults() {
res := result.GetResource()
if res == nil && polr.GetScope() != nil {
res = polr.GetScope()
} else if res == nil {
res = &corev1.ObjectReference{}
}
ns := res.Namespace
if ns == "" {
ns = polr.GetNamespace()
}
list = append(list, &PolicyReportResult{
ID: result.GetID(),
PolicyReportID: polr.GetID(),
Resource: Resource{
APIVersion: res.APIVersion,
Kind: res.Kind,
Name: res.Name,
Namespace: ns,
UID: string(res.UID),
},
Policy: result.Policy,
Rule: result.Rule,
Source: result.Source,
Scored: result.Scored,
Message: result.Message,
Result: string(result.Result),
Severity: string(result.Severity),
Category: result.Category,
Properties: result.Properties,
Created: result.Timestamp.Seconds,
})
}
return list
}
func MapPolicyReportFilter(polr v1alpha2.ReportInterface) []*PolicyReportFilter {
mapping := make(map[string]*PolicyReportFilter)
for _, res := range polr.GetResults() {
kind := res.GetKind()
if kind == "" && polr.GetScope() != nil {
kind = polr.GetScope().Namespace
}
value := &PolicyReportFilter{
PolicyReportID: polr.GetID(),
Namespace: polr.GetNamespace(),
Source: res.Source,
Kind: kind,
Category: res.Category,
Policy: res.Policy,
Severity: string(res.Severity),
Result: string(res.Result),
Count: 1,
}
if item, ok := mapping[value.Hash()]; ok {
item.Count = item.Count + 1
} else {
mapping[value.Hash()] = value
}
}
list := make([]*PolicyReportFilter, 0, len(mapping))
for _, v := range mapping {
list = append(list, v)
}
return list
}
func MapListResult(results []*PolicyReportResult) []*api.ListResult {
list := make([]*api.ListResult, 0, len(results))
for _, res := range results {
list = append(list, &api.ListResult{
ID: res.ID,
Namespace: res.Resource.Namespace,
Kind: res.Resource.Kind,
APIVersion: res.Resource.APIVersion,
Name: res.Resource.Name,
Message: res.Message,
Category: res.Category,
Policy: res.Policy,
Rule: res.Rule,
Status: res.Result,
Severity: res.Severity,
Timestamp: res.Created,
Properties: res.Properties,
})
}
return list
}

View file

@ -7,7 +7,6 @@ import (
"go.uber.org/zap"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/informers"
"k8s.io/client-go/metadata"
"k8s.io/client-go/metadata/metadatainformer"
"k8s.io/client-go/tools/cache"
@ -16,31 +15,40 @@ import (
"github.com/kyverno/policy-reporter/pkg/report"
)
var (
polrResource = pr.SchemeGroupVersion.WithResource("policyreports")
cpolrResource = pr.SchemeGroupVersion.WithResource("clusterpolicyreports")
)
type k8sPolicyReportClient struct {
queue *Queue
fatcory metadatainformer.SharedInformerFactory
polr informers.GenericInformer
cpolr informers.GenericInformer
metaClient metadata.Interface
synced bool
mx *sync.Mutex
reportFilter *report.Filter
stopChan chan struct{}
}
func (k *k8sPolicyReportClient) HasSynced() bool {
return k.synced
}
func (k *k8sPolicyReportClient) Stop() {
close(k.stopChan)
}
func (k *k8sPolicyReportClient) Sync(stopper chan struct{}) error {
factory := metadatainformer.NewSharedInformerFactory(k.metaClient, 15*time.Minute)
var cpolrInformer cache.SharedIndexInformer
polrInformer := k.configureInformer(k.polr.Informer())
polrInformer := k.configureInformer(factory.ForResource(polrResource).Informer())
if !k.reportFilter.DisableClusterReports() {
cpolrInformer = k.configureInformer(k.cpolr.Informer())
cpolrInformer = k.configureInformer(factory.ForResource(cpolrResource).Informer())
}
k.fatcory.Start(stopper)
factory.Start(stopper)
if !cache.WaitForCacheSync(stopper, polrInformer.HasSynced) {
return fmt.Errorf("failed to sync policy reports")
@ -58,6 +66,8 @@ func (k *k8sPolicyReportClient) Sync(stopper chan struct{}) error {
}
func (k *k8sPolicyReportClient) Run(worker int, stopper chan struct{}) error {
k.stopChan = stopper
if err := k.Sync(stopper); err != nil {
return err
}
@ -101,14 +111,8 @@ func (k *k8sPolicyReportClient) configureInformer(informer cache.SharedIndexInfo
// NewPolicyReportClient new Client for Policy Report Kubernetes API
func NewPolicyReportClient(metaClient metadata.Interface, reportFilter *report.Filter, queue *Queue) report.PolicyReportClient {
fatcory := metadatainformer.NewSharedInformerFactory(metaClient, 15*time.Minute)
polr := fatcory.ForResource(pr.SchemeGroupVersion.WithResource("policyreports"))
cpolr := fatcory.ForResource(pr.SchemeGroupVersion.WithResource("clusterpolicyreports"))
return &k8sPolicyReportClient{
fatcory: fatcory,
polr: polr,
cpolr: cpolr,
metaClient: metaClient,
mx: &sync.Mutex{},
queue: queue,
reportFilter: reportFilter,

View file

@ -41,8 +41,6 @@ func (q *Queue) Add(obj *v1.PartialObjectMetadata) error {
func (q *Queue) Run(workers int, stopCh chan struct{}) {
defer runtime.HandleCrash()
defer q.queue.ShutDown()
for i := 0; i < workers; i++ {
go wait.Until(q.runWorker, time.Second, stopCh)
}

View file

@ -19,6 +19,8 @@ type Values struct {
KmsKeyID string `json:"kmsKeyId,omitempty"`
Token string `json:"token,omitempty"`
Credentials string `json:"credentials,omitempty"`
Database string `json:"database,omitempty"`
DSN string `json:"dsn,omitempty"`
}
type Client interface {
@ -56,6 +58,14 @@ func (c *k8sClient) Get(ctx context.Context, name string) (Values, error) {
values.Password = string(password)
}
if database, ok := secret.Data["database"]; ok {
values.Database = string(database)
}
if dsn, ok := secret.Data["dsn"]; ok {
values.DSN = string(dsn)
}
if accessKeyID, ok := secret.Data["accessKeyID"]; ok {
values.AccessKeyID = string(accessKeyID)
}

View file

@ -31,6 +31,8 @@ func newFakeClient() v1.SecretInterface {
"kmsKeyId": []byte("kmsKeyId"),
"token": []byte("token"),
"accountID": []byte("accountID"),
"database": []byte("database"),
"dsn": []byte("dsn"),
},
}).CoreV1().Secrets("default")
}
@ -79,6 +81,14 @@ func Test_Client(t *testing.T) {
if values.AccountID != "accountID" {
t.Errorf("Unexpected AccountID: %s", values.AccountID)
}
if values.Database != "database" {
t.Errorf("Unexpected Database: %s", values.Database)
}
if values.DSN != "dsn" {
t.Errorf("Unexpected DSN: %s", values.DSN)
}
})
t.Run("Get values from not existing secret", func(t *testing.T) {

View file

@ -23,13 +23,24 @@ func (l *ResultListener) RegisterListener(listener report.PolicyReportResultList
l.listener = append(l.listener, listener)
}
func (l *ResultListener) UnregisterListener() {
l.listener = make([]report.PolicyReportResultListener, 0)
}
func (l *ResultListener) Listen(event report.LifecycleEvent) {
if event.Type != report.Added && event.Type != report.Updated {
l.cache.RemoveReport(event.PolicyReport.GetID())
return
}
if len(event.PolicyReport.GetResults()) == 0 {
resultCount := len(event.PolicyReport.GetResults())
if resultCount == 0 {
return
}
listenerCount := len(l.listener)
if listenerCount == 0 {
l.cache.AddReport(event.PolicyReport)
return
}
@ -44,28 +55,42 @@ func (l *ResultListener) Listen(event report.LifecycleEvent) {
}
}
wg := sync.WaitGroup{}
existing := l.cache.GetResults(event.PolicyReport.GetID())
for _, r := range event.PolicyReport.GetResults() {
if helper.Contains(r.GetID(), existing) {
continue
}
grp := sync.WaitGroup{}
grp.Add(resultCount)
for _, res := range event.PolicyReport.GetResults() {
go func(r v1alpha2.PolicyReportResult) {
defer grp.Done()
wg.Add(len(l.listener))
if helper.Contains(r.GetID(), existing) {
return
}
for _, cb := range l.listener {
go func(callback report.PolicyReportResultListener, result v1alpha2.PolicyReportResult) {
callback(event.PolicyReport, result, preExisted)
wg.Done()
}(cb, r)
}
if r.Timestamp.Seconds > 0 {
created := time.Unix(r.Timestamp.Seconds, int64(r.Timestamp.Nanos))
if l.skipExisting && created.Local().Before(l.startUp) {
return
}
}
wg := sync.WaitGroup{}
wg.Add(listenerCount)
for _, cb := range l.listener {
go func(callback report.PolicyReportResultListener, result v1alpha2.PolicyReportResult) {
callback(event.PolicyReport, result, preExisted)
wg.Done()
}(cb, r)
}
wg.Wait()
}(res)
}
l.cache.AddReport(event.PolicyReport)
grp.Wait()
wg.Wait()
l.cache.AddReport(event.PolicyReport)
}
func NewResultListener(skipExisting bool, rcache cache.Cache, startUp time.Time) *ResultListener {

View file

@ -1,6 +1,8 @@
package listener
import (
"context"
"go.uber.org/zap"
"github.com/kyverno/policy-reporter/pkg/report"
@ -8,19 +10,19 @@ import (
const Store = "store_listener"
func NewStoreListener(store report.PolicyReportStore) report.PolicyReportListener {
func NewStoreListener(ctx context.Context, store report.PolicyReportStore) report.PolicyReportListener {
return func(event report.LifecycleEvent) {
if event.Type == report.Deleted {
logOnError("remove", event.PolicyReport.GetName(), store.Remove(event.PolicyReport.GetID()))
logOnError("remove", event.PolicyReport.GetName(), store.Remove(ctx, event.PolicyReport.GetID()))
return
}
if event.Type == report.Updated {
logOnError("update", event.PolicyReport.GetName(), store.Update(event.PolicyReport))
logOnError("update", event.PolicyReport.GetName(), store.Update(ctx, event.PolicyReport))
return
}
logOnError("add", event.PolicyReport.GetName(), store.Add(event.PolicyReport))
logOnError("add", event.PolicyReport.GetName(), store.Update(ctx, event.PolicyReport))
}
}

View file

@ -1,36 +1,39 @@
package listener_test
import (
"context"
"testing"
"github.com/kyverno/policy-reporter/pkg/listener"
"github.com/kyverno/policy-reporter/pkg/report"
)
var ctx = context.Background()
func Test_StoreListener(t *testing.T) {
store := report.NewPolicyReportStore()
t.Run("Save New Report", func(t *testing.T) {
slistener := listener.NewStoreListener(store)
slistener := listener.NewStoreListener(ctx, store)
slistener(report.LifecycleEvent{Type: report.Added, PolicyReport: preport1})
if _, ok := store.Get(preport1.GetID()); !ok {
if _, err := store.Get(ctx, preport1.GetID()); err != nil {
t.Error("Expected Report to be stored")
}
})
t.Run("Update Modified Report", func(t *testing.T) {
slistener := listener.NewStoreListener(store)
slistener := listener.NewStoreListener(ctx, store)
slistener(report.LifecycleEvent{Type: report.Updated, PolicyReport: preport2})
if preport, ok := store.Get(preport2.GetID()); !ok && len(preport.GetResults()) == 2 {
if preport, err := store.Get(ctx, preport2.GetID()); err != nil && len(preport.GetResults()) == 2 {
t.Error("Expected Report to be updated")
}
})
t.Run("Remove Deleted Report", func(t *testing.T) {
slistener := listener.NewStoreListener(store)
slistener := listener.NewStoreListener(ctx, store)
slistener(report.LifecycleEvent{Type: report.Deleted, PolicyReport: preport2})
if _, ok := store.Get(preport2.GetID()); ok {
if _, err := store.Get(ctx, preport2.GetID()); err == nil {
t.Error("Expected Report to be removed")
}
})

View file

@ -1,6 +1,8 @@
package report
import "github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2"
import (
"github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2"
)
// PolicyReportListener is called whenever a new PolicyReport comes in
type PolicyReportListener = func(LifecycleEvent)
@ -16,4 +18,6 @@ type PolicyReportClient interface {
Sync(stopper chan struct{}) error
// HasSynced the configured PolicyReport
HasSynced() bool
// Stop the client
Stop()
}

View file

@ -1,6 +1,8 @@
package report
import (
"context"
"errors"
"sync"
"github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2"
@ -8,17 +10,17 @@ import (
type PolicyReportStore interface {
// CreateSchemas for PolicyReports and PolicyReportResults
CreateSchemas() error
CreateSchemas(context.Context) error
// Get an PolicyReport by Type and ID
Get(id string) (v1alpha2.ReportInterface, bool)
Get(ctx context.Context, id string) (v1alpha2.ReportInterface, error)
// Add a PolicyReport to the Store
Add(r v1alpha2.ReportInterface) error
Add(ctx context.Context, r v1alpha2.ReportInterface) error
// Update a PolicyReport to the Store
Update(r v1alpha2.ReportInterface) error
Update(ctx context.Context, r v1alpha2.ReportInterface) error
// Remove a PolicyReport with the given Type and ID from the Store
Remove(id string) error
Remove(ctx context.Context, id string) error
// CleanUp removes all items in the store
CleanUp() error
CleanUp(ctx context.Context) error
}
// PolicyReportStore caches the latest version of an PolicyReport
@ -27,26 +29,29 @@ type policyReportStore struct {
rwm *sync.RWMutex
}
func (s *policyReportStore) CreateSchemas() error {
func (s *policyReportStore) CreateSchemas(_ context.Context) error {
return nil
}
func (s *policyReportStore) Get(id string) (v1alpha2.ReportInterface, bool) {
func (s *policyReportStore) Get(_ context.Context, id string) (v1alpha2.ReportInterface, error) {
s.rwm.RLock()
r, ok := s.store[PolicyReportType][id]
s.rwm.RUnlock()
if ok {
return r, ok
return r, nil
}
s.rwm.RLock()
r, ok = s.store[ClusterPolicyReportType][id]
s.rwm.RUnlock()
if ok {
return r, nil
}
return r, ok
return nil, errors.New("report not found")
}
func (s *policyReportStore) Add(r v1alpha2.ReportInterface) error {
func (s *policyReportStore) Add(_ context.Context, r v1alpha2.ReportInterface) error {
s.rwm.Lock()
defer s.rwm.Unlock()
s.store[GetType(r)][r.GetID()] = r
@ -54,7 +59,7 @@ func (s *policyReportStore) Add(r v1alpha2.ReportInterface) error {
return nil
}
func (s *policyReportStore) Update(r v1alpha2.ReportInterface) error {
func (s *policyReportStore) Update(_ context.Context, r v1alpha2.ReportInterface) error {
s.rwm.Lock()
defer s.rwm.Unlock()
s.store[GetType(r)][r.GetID()] = r
@ -62,8 +67,8 @@ func (s *policyReportStore) Update(r v1alpha2.ReportInterface) error {
return nil
}
func (s *policyReportStore) Remove(id string) error {
if r, ok := s.Get(id); ok {
func (s *policyReportStore) Remove(ctx context.Context, id string) error {
if r, err := s.Get(ctx, id); err == nil {
s.rwm.Lock()
defer s.rwm.Unlock()
delete(s.store[GetType(r)], id)
@ -72,7 +77,7 @@ func (s *policyReportStore) Remove(id string) error {
return nil
}
func (s *policyReportStore) CleanUp() error {
func (s *policyReportStore) CleanUp(_ context.Context) error {
s.rwm.Lock()
defer s.rwm.Unlock()
s.store = map[ResourceType]map[string]v1alpha2.ReportInterface{

View file

@ -1,6 +1,7 @@
package report_test
import (
"context"
"testing"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -9,19 +10,21 @@ import (
"github.com/kyverno/policy-reporter/pkg/report"
)
var ctx = context.Background()
func Test_PolicyReportStore(t *testing.T) {
store := report.NewPolicyReportStore()
store.CreateSchemas()
store.CreateSchemas(ctx)
t.Run("Add/Get", func(t *testing.T) {
_, ok := store.Get(preport.GetID())
if ok == true {
_, err := store.Get(ctx, preport.GetID())
if err == nil {
t.Fatalf("Should not be found in empty Store")
}
store.Add(preport)
_, ok = store.Get(preport.GetID())
if ok == false {
store.Add(ctx, preport)
_, err = store.Get(ctx, preport.GetID())
if err != nil {
t.Errorf("Should be found in Store after adding report to the store")
}
})
@ -37,38 +40,38 @@ func Test_PolicyReportStore(t *testing.T) {
Summary: v1alpha2.PolicyReportSummary{Skip: 1},
}
store.Add(preport)
r, _ := store.Get(preport.GetID())
store.Add(ctx, preport)
r, _ := store.Get(ctx, preport.GetID())
if r.GetSummary().Skip != 0 {
t.Errorf("Expected Summary.Skip to be 0")
}
store.Update(ureport)
r2, _ := store.Get(preport.GetID())
store.Update(ctx, ureport)
r2, _ := store.Get(ctx, preport.GetID())
if r2.GetSummary().Skip != 1 {
t.Errorf("Expected Summary.Skip to be 1 after update")
}
})
t.Run("Delete/Get", func(t *testing.T) {
_, ok := store.Get(preport.GetID())
if ok == false {
_, err := store.Get(ctx, preport.GetID())
if err != nil {
t.Errorf("Should be found in Store after adding report to the store")
}
store.Remove(preport.GetID())
_, ok = store.Get(preport.GetID())
if ok == true {
store.Remove(ctx, preport.GetID())
_, err = store.Get(ctx, preport.GetID())
if err == nil {
t.Fatalf("Should not be found after Remove report from Store")
}
})
t.Run("CleanUp", func(t *testing.T) {
store.Add(preport)
store.Add(ctx, preport)
store.CleanUp()
_, ok := store.Get(preport.GetID())
if ok == true {
store.CleanUp(ctx)
_, err := store.Get(ctx, preport.GetID())
if err == nil {
t.Fatalf("Should have no results after CleanUp")
}
})

File diff suppressed because it is too large Load diff

View file

@ -4,9 +4,9 @@ import (
"fmt"
"time"
hub "github.com/aws/aws-sdk-go/service/securityhub"
"go.uber.org/zap"
hub "github.com/aws/aws-sdk-go/service/securityhub"
"github.com/kyverno/policy-reporter/pkg/crd/api/policyreport/v1alpha2"
"github.com/kyverno/policy-reporter/pkg/target"
)