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:
parent
dd150ee3b6
commit
836d6fe436
13 changed files with 194 additions and 24 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -4,8 +4,7 @@
|
|||
/config.yaml
|
||||
build
|
||||
/test.yaml
|
||||
**/test.db
|
||||
sqlite-database*.db
|
||||
*.db
|
||||
values*.yaml
|
||||
coverage.out*
|
||||
heap*
|
||||
|
|
|
@ -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 }}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
146
pkg/api/v1/html_report.go
Normal 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,
|
||||
}
|
||||
}
|
|
@ -243,18 +243,17 @@ 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"`
|
||||
TitlePrefix string `mapstructure:"titlePrefix"`
|
||||
SMTP SMTP `mapstructure:"smtp"`
|
||||
Summary EmailReport `mapstructure:"summary"`
|
||||
Violations EmailReport `mapstructure:"violations"`
|
||||
ClusterName string `mapstructure:"clusterName"`
|
||||
TitlePrefix string `mapstructure:"titlePrefix"`
|
||||
}
|
||||
|
||||
// BasicAuth configuration
|
||||
|
@ -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"`
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -164,10 +164,10 @@ var testConfig = &config.Config{
|
|||
Prefix: "prefix",
|
||||
Channels: []*config.GCS{{}},
|
||||
},
|
||||
Templates: config.Templates{
|
||||
Dir: "../../templates",
|
||||
},
|
||||
EmailReports: config.EmailReports{
|
||||
Templates: config.EmailTemplates{
|
||||
Dir: "../../templates",
|
||||
},
|
||||
SMTP: config.SMTP{
|
||||
Host: "localhost",
|
||||
Port: 465,
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue