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
build
/test.yaml
**/test.db
sqlite-database*.db
*.db
values*.yaml
coverage.out*
heap*

View file

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

View file

@ -76,7 +76,7 @@ func newRunCMD(version string) *cobra.Command {
}
logger.Info("REST api enabled")
server.RegisterV1Handler(store)
server.RegisterV1Handler(store, resolver.ViolationsReporter())
}
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().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().StringP("template-dir", "t", "./templates", "template directory")
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")

View file

@ -11,6 +11,7 @@ import (
"go.uber.org/zap/zapcore"
v1 "github.com/kyverno/policy-reporter/pkg/api/v1"
"github.com/kyverno/policy-reporter/pkg/email/violations"
"github.com/kyverno/policy-reporter/pkg/target"
)
@ -25,7 +26,7 @@ type Server interface {
// RegisterMetricsHandler adds the optional metrics endpoint
RegisterMetricsHandler()
// RegisterV1Handler adds the optional v1 REST APIs
RegisterV1Handler(v1.PolicyReportFinder)
RegisterV1Handler(v1.PolicyReportFinder, *violations.Reporter)
// RegisterProfilingHandler adds the optional pprof profiling APIs
RegisterProfilingHandler()
}
@ -54,7 +55,7 @@ func (s *httpServer) RegisterLifecycleHandler() {
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)
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/results", s.middleware(handler.ClusterResourcesResultHandler()))
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() {

View file

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

View file

@ -14,6 +14,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/database"
"github.com/kyverno/policy-reporter/pkg/email/violations"
"github.com/kyverno/policy-reporter/pkg/target"
"github.com/kyverno/policy-reporter/pkg/target/loki"
)
@ -143,6 +144,7 @@ func Test_V1_API(t *testing.T) {
store.Add(ctx, creport)
handl := v1.NewHandler(store)
htmlHandl := v1.NewHTMLHandler(store, violations.NewReporter("../../../templates", "Cluster", "Report"))
t.Run("ClusterPolicyListHandler", func(t *testing.T) {
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.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) {

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,14 +243,13 @@ type EmailReport struct {
}
// EmailReport configuration
type EmailTemplates struct {
type Templates struct {
Dir string `mapstructure:"dir"`
}
// EmailReports configuration
type EmailReports struct {
SMTP SMTP `mapstructure:"smtp"`
Templates EmailTemplates `mapstructure:"templates"`
Summary EmailReport `mapstructure:"summary"`
Violations EmailReport `mapstructure:"violations"`
ClusterName string `mapstructure:"clusterName"`
@ -387,4 +386,5 @@ type Config struct {
Logging Logging `mapstructure:"logging"`
Database Database `mapstructure:"database"`
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 {
v.BindPFlag("emailReports.templates.dir", flag)
v.BindPFlag("templates.dir", flag)
}
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 {
t.Errorf("Unexpected Profiling Config: %v", c.Profiling.Enabled)
}
if c.EmailReports.Templates.Dir != "/app/templates" {
t.Errorf("Unexpected TemplateDir Config: %s", c.EmailReports.Templates.Dir)
if c.Templates.Dir != "/app/templates" {
t.Errorf("Unexpected TemplateDir Config: %s", c.Templates.Dir)
}
if c.DBFile != "sqlite-database.db" {
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/summary"
"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/secrets"
"github.com/kyverno/policy-reporter/pkg/leaderelection"
@ -384,9 +385,9 @@ func (r *Resolver) SummaryGenerator() (*summary.Generator, error) {
func (r *Resolver) SummaryReporter() *summary.Reporter {
return summary.NewReporter(
r.config.EmailReports.Templates.Dir,
r.config.Templates.Dir,
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 {
return violations.NewReporter(
r.config.EmailReports.Templates.Dir,
r.config.Templates.Dir,
r.config.EmailReports.ClusterName,
r.config.EmailReports.TitlePrefix,
)

View file

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

View file

@ -56,7 +56,7 @@
<tbody>
<tr>
<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%">
<tbody>
<tr>