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

API to render Violations Report (#429)

* API to render Violations Report

Signed-off-by: Frank Jogeleit <frank.jogeleit@lovoo.com>
This commit is contained in:
Frank Jogeleit 2024-05-04 10:04:27 +02:00 committed by GitHub
parent dd150ee3b6
commit 836d6fe436
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 194 additions and 24 deletions

3
.gitignore vendored
View file

@ -4,8 +4,7 @@
/config.yaml /config.yaml
build build
/test.yaml /test.yaml
**/test.db *.db
sqlite-database*.db
values*.yaml values*.yaml
coverage.out* coverage.out*
heap* heap*

View file

@ -67,6 +67,7 @@ spec:
- --rest-enabled={{ or .Values.rest.enabled .Values.ui.enabled }} - --rest-enabled={{ or .Values.rest.enabled .Values.ui.enabled }}
- --profile={{ .Values.profiling.enabled }} - --profile={{ .Values.profiling.enabled }}
- --lease-name={{ include "policyreporter.fullname" . }} - --lease-name={{ include "policyreporter.fullname" . }}
- --template-dir=/app/templates
ports: ports:
- name: {{ .Values.port.name }} - name: {{ .Values.port.name }}
containerPort: {{ .Values.port.number }} containerPort: {{ .Values.port.number }}

View file

@ -76,7 +76,7 @@ func newRunCMD(version string) *cobra.Command {
} }
logger.Info("REST api enabled") logger.Info("REST api enabled")
server.RegisterV1Handler(store) server.RegisterV1Handler(store, resolver.ViolationsReporter())
} }
if c.Metrics.Enabled { if c.Metrics.Enabled {
@ -175,6 +175,7 @@ func newRunCMD(version string) *cobra.Command {
cmd.PersistentFlags().Bool("profile", false, "Enable application profiling with pprof") cmd.PersistentFlags().Bool("profile", false, "Enable application profiling with pprof")
cmd.PersistentFlags().String("lease-name", "policy-reporter", "name of the LeaseLock") 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().String("pod-name", "policy-reporter", "name of the pod, used for leaderelection")
cmd.PersistentFlags().StringP("template-dir", "t", "./templates", "template directory")
cmd.PersistentFlags().Int("worker", 5, "amount of queue worker") cmd.PersistentFlags().Int("worker", 5, "amount of queue worker")
cmd.PersistentFlags().Float32("qps", 20, "K8s RESTClient QPS") cmd.PersistentFlags().Float32("qps", 20, "K8s RESTClient QPS")
cmd.PersistentFlags().Int("burst", 50, "K8s RESTClient burst") cmd.PersistentFlags().Int("burst", 50, "K8s RESTClient burst")

View file

@ -11,6 +11,7 @@ import (
"go.uber.org/zap/zapcore" "go.uber.org/zap/zapcore"
v1 "github.com/kyverno/policy-reporter/pkg/api/v1" v1 "github.com/kyverno/policy-reporter/pkg/api/v1"
"github.com/kyverno/policy-reporter/pkg/email/violations"
"github.com/kyverno/policy-reporter/pkg/target" "github.com/kyverno/policy-reporter/pkg/target"
) )
@ -25,7 +26,7 @@ type Server interface {
// RegisterMetricsHandler adds the optional metrics endpoint // RegisterMetricsHandler adds the optional metrics endpoint
RegisterMetricsHandler() RegisterMetricsHandler()
// RegisterV1Handler adds the optional v1 REST APIs // RegisterV1Handler adds the optional v1 REST APIs
RegisterV1Handler(v1.PolicyReportFinder) RegisterV1Handler(v1.PolicyReportFinder, *violations.Reporter)
// RegisterProfilingHandler adds the optional pprof profiling APIs // RegisterProfilingHandler adds the optional pprof profiling APIs
RegisterProfilingHandler() RegisterProfilingHandler()
} }
@ -54,7 +55,7 @@ func (s *httpServer) RegisterLifecycleHandler() {
s.mux.HandleFunc("/ready", ReadyHandler(s.synced)) s.mux.HandleFunc("/ready", ReadyHandler(s.synced))
} }
func (s *httpServer) RegisterV1Handler(finder v1.PolicyReportFinder) { func (s *httpServer) RegisterV1Handler(finder v1.PolicyReportFinder, reporter *violations.Reporter) {
handler := v1.NewHandler(finder) handler := v1.NewHandler(finder)
s.mux.HandleFunc("/v1/targets", s.middleware(handler.TargetsHandler(s.targets))) s.mux.HandleFunc("/v1/targets", s.middleware(handler.TargetsHandler(s.targets)))
@ -83,6 +84,10 @@ func (s *httpServer) RegisterV1Handler(finder v1.PolicyReportFinder) {
s.mux.HandleFunc("/v1/cluster-resources/status-counts", s.middleware(handler.ClusterResourcesStatusCountHandler())) s.mux.HandleFunc("/v1/cluster-resources/status-counts", s.middleware(handler.ClusterResourcesStatusCountHandler()))
s.mux.HandleFunc("/v1/cluster-resources/results", s.middleware(handler.ClusterResourcesResultHandler())) s.mux.HandleFunc("/v1/cluster-resources/results", s.middleware(handler.ClusterResourcesResultHandler()))
s.mux.HandleFunc("/v1/cluster-resources/categories", s.middleware(handler.ClusterCategoryListHandler())) s.mux.HandleFunc("/v1/cluster-resources/categories", s.middleware(handler.ClusterCategoryListHandler()))
htmlHandler := v1.NewHTMLHandler(finder, reporter)
s.mux.HandleFunc("/v1/html-report/violations", s.middleware(htmlHandler.HTMLReport()))
} }
func (s *httpServer) RegisterMetricsHandler() { func (s *httpServer) RegisterMetricsHandler() {

View file

@ -33,7 +33,7 @@ func Test_NewServer(t *testing.T) {
) )
server.RegisterMetricsHandler() server.RegisterMetricsHandler()
server.RegisterV1Handler(nil) server.RegisterV1Handler(nil, nil)
server.RegisterProfilingHandler() server.RegisterProfilingHandler()
serviceRunning := make(chan struct{}) serviceRunning := make(chan struct{})
@ -81,6 +81,6 @@ func Test_SetupServerWithAuth(t *testing.T) {
) )
server.RegisterMetricsHandler() server.RegisterMetricsHandler()
server.RegisterV1Handler(nil) server.RegisterV1Handler(nil, nil)
server.RegisterProfilingHandler() server.RegisterProfilingHandler()
} }

View file

@ -14,6 +14,7 @@ import (
v1 "github.com/kyverno/policy-reporter/pkg/api/v1" 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/crd/api/policyreport/v1alpha2"
"github.com/kyverno/policy-reporter/pkg/database" "github.com/kyverno/policy-reporter/pkg/database"
"github.com/kyverno/policy-reporter/pkg/email/violations"
"github.com/kyverno/policy-reporter/pkg/target" "github.com/kyverno/policy-reporter/pkg/target"
"github.com/kyverno/policy-reporter/pkg/target/loki" "github.com/kyverno/policy-reporter/pkg/target/loki"
) )
@ -143,6 +144,7 @@ func Test_V1_API(t *testing.T) {
store.Add(ctx, creport) store.Add(ctx, creport)
handl := v1.NewHandler(store) handl := v1.NewHandler(store)
htmlHandl := v1.NewHTMLHandler(store, violations.NewReporter("../../../templates", "Cluster", "Report"))
t.Run("ClusterPolicyListHandler", func(t *testing.T) { t.Run("ClusterPolicyListHandler", func(t *testing.T) {
req, err := http.NewRequest("GET", "/v1/cluster-policies", nil) req, err := http.NewRequest("GET", "/v1/cluster-policies", nil)
@ -593,6 +595,21 @@ func Test_V1_API(t *testing.T) {
t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected) t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected)
} }
}) })
t.Run("HTMLReport", func(t *testing.T) {
req, err := http.NewRequest("GET", "/v1/html-report/violations", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
handler := htmlHandl.HTMLReport()
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)
}
})
} }
func Test_TargetsAPI(t *testing.T) { func Test_TargetsAPI(t *testing.T) {

146
pkg/api/v1/html_report.go Normal file
View file

@ -0,0 +1,146 @@
package v1
import (
"net/http"
"slices"
"github.com/kyverno/policy-reporter/pkg/email/violations"
"go.uber.org/zap"
)
type HTMLHandler struct {
reporter *violations.Reporter
finder PolicyReportFinder
}
func (h *HTMLHandler) HTMLReport() http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
sources := make([]violations.Source, 0)
namespaced, err := h.finder.FetchNamespacedSources(req.Context())
if err != nil {
zap.L().Error("failed to load data", zap.Error(err))
w.WriteHeader(http.StatusInternalServerError)
return
}
cluster, err := h.finder.FetchClusterSources(req.Context())
if err != nil {
zap.L().Error("failed to load data", zap.Error(err))
w.WriteHeader(http.StatusInternalServerError)
return
}
list := append(namespaced, cluster...)
slices.Sort(list)
list = slices.Compact(list)
for _, source := range list {
cPass, err := h.finder.CountClusterResults(req.Context(), Filter{
Sources: []string{source},
Status: []string{"pass"},
})
if err != nil {
continue
}
statusCounts, err := h.finder.FetchNamespacedStatusCounts(req.Context(), Filter{
Sources: []string{source},
Status: []string{"pass"},
})
if err != nil {
continue
}
nsPass := make(map[string]int, len(statusCounts))
for _, s := range statusCounts[0].Items {
nsPass[s.Namespace] = s.Count
}
clusterResults, err := h.finder.FetchClusterResults(req.Context(), Filter{
Sources: []string{source},
Status: []string{"warn", "fail", "error"},
}, Pagination{SortBy: defaultOrder})
if err != nil {
continue
}
cResults := make(map[string][]violations.Result)
for _, r := range clusterResults {
if _, ok := cResults[r.Status]; !ok {
cResults[r.Status] = make([]violations.Result, 0)
}
cResults[r.Status] = append(cResults[r.Status], violations.Result{
Kind: r.Kind,
Name: r.Name,
Policy: r.Policy,
Rule: r.Rule,
Status: r.Status,
})
}
namespaces, err := h.finder.FetchNamespaces(req.Context(), Filter{
Sources: []string{source},
})
if err != nil {
continue
}
nsResults := make(map[string]map[string][]violations.Result)
for _, ns := range namespaces {
results, err := h.finder.FetchNamespacedResults(req.Context(), Filter{
Sources: []string{source},
Status: []string{"warn", "fail", "error"},
Namespaces: []string{ns},
}, Pagination{SortBy: defaultOrder})
if err != nil {
continue
}
mapping := make(map[string][]violations.Result)
mapping["warn"] = make([]violations.Result, 0)
mapping["fail"] = make([]violations.Result, 0)
mapping["error"] = make([]violations.Result, 0)
for _, r := range results {
mapping[r.Status] = append(mapping[r.Status], violations.Result{
Kind: r.Kind,
Name: r.Name,
Policy: r.Policy,
Rule: r.Rule,
Status: r.Status,
})
}
nsResults[ns] = mapping
}
sources = append(sources, violations.Source{
Name: source,
ClusterReports: len(cluster) > 0,
ClusterPassed: cPass,
ClusterResults: cResults,
NamespacePassed: nsPass,
NamespaceResults: nsResults,
})
}
data, err := h.reporter.Report(sources, "HTML")
if err != nil {
zap.L().Error("failed to load data", zap.Error(err))
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write([]byte(data.Message))
}
}
func NewHTMLHandler(finder PolicyReportFinder, reporter *violations.Reporter) *HTMLHandler {
return &HTMLHandler{
finder: finder,
reporter: reporter,
}
}

View file

@ -243,18 +243,17 @@ type EmailReport struct {
} }
// EmailReport configuration // EmailReport configuration
type EmailTemplates struct { type Templates struct {
Dir string `mapstructure:"dir"` Dir string `mapstructure:"dir"`
} }
// EmailReports configuration // EmailReports configuration
type EmailReports struct { type EmailReports struct {
SMTP SMTP `mapstructure:"smtp"` SMTP SMTP `mapstructure:"smtp"`
Templates EmailTemplates `mapstructure:"templates"` Summary EmailReport `mapstructure:"summary"`
Summary EmailReport `mapstructure:"summary"` Violations EmailReport `mapstructure:"violations"`
Violations EmailReport `mapstructure:"violations"` ClusterName string `mapstructure:"clusterName"`
ClusterName string `mapstructure:"clusterName"` TitlePrefix string `mapstructure:"titlePrefix"`
TitlePrefix string `mapstructure:"titlePrefix"`
} }
// BasicAuth configuration // BasicAuth configuration
@ -387,4 +386,5 @@ type Config struct {
Logging Logging `mapstructure:"logging"` Logging Logging `mapstructure:"logging"`
Database Database `mapstructure:"database"` Database Database `mapstructure:"database"`
SourceConfig map[string]SourceConfig `mapstructure:"sourceConfig"` SourceConfig map[string]SourceConfig `mapstructure:"sourceConfig"`
Templates Templates `mapstructure:"templates"`
} }

View file

@ -74,7 +74,7 @@ func Load(cmd *cobra.Command) (*Config, error) {
} }
if flag := cmd.Flags().Lookup("template-dir"); flag != nil { if flag := cmd.Flags().Lookup("template-dir"); flag != nil {
v.BindPFlag("emailReports.templates.dir", flag) v.BindPFlag("templates.dir", flag)
} }
if flag := cmd.Flags().Lookup("lease-name"); flag != nil { if flag := cmd.Flags().Lookup("lease-name"); flag != nil {

View file

@ -58,8 +58,8 @@ func Test_Load(t *testing.T) {
if c.Profiling.Enabled != true { if c.Profiling.Enabled != true {
t.Errorf("Unexpected Profiling Config: %v", c.Profiling.Enabled) t.Errorf("Unexpected Profiling Config: %v", c.Profiling.Enabled)
} }
if c.EmailReports.Templates.Dir != "/app/templates" { if c.Templates.Dir != "/app/templates" {
t.Errorf("Unexpected TemplateDir Config: %s", c.EmailReports.Templates.Dir) t.Errorf("Unexpected TemplateDir Config: %s", c.Templates.Dir)
} }
if c.DBFile != "sqlite-database.db" { if c.DBFile != "sqlite-database.db" {
t.Errorf("Unexpected DBFile Config: %s", c.DBFile) t.Errorf("Unexpected DBFile Config: %s", c.DBFile)

View file

@ -28,6 +28,7 @@ import (
"github.com/kyverno/policy-reporter/pkg/email" "github.com/kyverno/policy-reporter/pkg/email"
"github.com/kyverno/policy-reporter/pkg/email/summary" "github.com/kyverno/policy-reporter/pkg/email/summary"
"github.com/kyverno/policy-reporter/pkg/email/violations" "github.com/kyverno/policy-reporter/pkg/email/violations"
"github.com/kyverno/policy-reporter/pkg/helper"
"github.com/kyverno/policy-reporter/pkg/kubernetes" "github.com/kyverno/policy-reporter/pkg/kubernetes"
"github.com/kyverno/policy-reporter/pkg/kubernetes/secrets" "github.com/kyverno/policy-reporter/pkg/kubernetes/secrets"
"github.com/kyverno/policy-reporter/pkg/leaderelection" "github.com/kyverno/policy-reporter/pkg/leaderelection"
@ -384,9 +385,9 @@ func (r *Resolver) SummaryGenerator() (*summary.Generator, error) {
func (r *Resolver) SummaryReporter() *summary.Reporter { func (r *Resolver) SummaryReporter() *summary.Reporter {
return summary.NewReporter( return summary.NewReporter(
r.config.EmailReports.Templates.Dir, r.config.Templates.Dir,
r.config.EmailReports.ClusterName, r.config.EmailReports.ClusterName,
r.config.EmailReports.TitlePrefix, helper.Defaults(r.config.EmailReports.TitlePrefix, "Report"),
) )
} }
@ -405,7 +406,7 @@ func (r *Resolver) ViolationsGenerator() (*violations.Generator, error) {
func (r *Resolver) ViolationsReporter() *violations.Reporter { func (r *Resolver) ViolationsReporter() *violations.Reporter {
return violations.NewReporter( return violations.NewReporter(
r.config.EmailReports.Templates.Dir, r.config.Templates.Dir,
r.config.EmailReports.ClusterName, r.config.EmailReports.ClusterName,
r.config.EmailReports.TitlePrefix, r.config.EmailReports.TitlePrefix,
) )

View file

@ -164,10 +164,10 @@ var testConfig = &config.Config{
Prefix: "prefix", Prefix: "prefix",
Channels: []*config.GCS{{}}, Channels: []*config.GCS{{}},
}, },
Templates: config.Templates{
Dir: "../../templates",
},
EmailReports: config.EmailReports{ EmailReports: config.EmailReports{
Templates: config.EmailTemplates{
Dir: "../../templates",
},
SMTP: config.SMTP{ SMTP: config.SMTP{
Host: "localhost", Host: "localhost",
Port: 465, Port: 465,

View file

@ -56,7 +56,7 @@
<tbody> <tbody>
<tr> <tr>
<td style="line-height: 24px; font-size: 16px; width: 100%; margin: 0; padding: 24px 16px 16px;" align="left" bgcolor="#ffffff"> <td style="line-height: 24px; font-size: 16px; width: 100%; margin: 0; padding: 24px 16px 16px;" align="left" bgcolor="#ffffff">
<h2 class="h2" style="padding-top: 0; padding-bottom: 0; font-weight: 500; vertical-align: baseline; font-size: 32px; line-height: 38.4px; margin: 0;" align="left">{{ $source.Name }}</h2> <h2 class="h2" style="padding-top: 0; padding-bottom: 0; font-weight: 500; vertical-align: baseline; font-size: 32px; line-height: 38.4px; margin: 0;" align="left">{{ title $source.Name }}</h2>
<table class="s-6 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%"> <table class="s-6 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody> <tbody>
<tr> <tr>