From f65eae40cbf7add5eaaf8ba57f31ae6be159cb5f Mon Sep 17 00:00:00 2001 From: Georgi Baltiev <82998942+georgibaltiev@users.noreply.github.com> Date: Thu, 9 Jan 2025 11:43:02 +0200 Subject: [PATCH] Add a minimal status option to the report generating command (#418) * Add a function that discards all check results that fall below the minStatus * Add a --min-status flag to the Cobra generate comand * Add tests to validate the filtering function * Typo * Add suggested changes * Move method * Correct a typo and add sorted explanation --- cmd/diki/app/app.go | 13 +++ pkg/report/report.go | 20 +++++ pkg/report/report_test.go | 175 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 208 insertions(+) create mode 100644 pkg/report/report_test.go diff --git a/cmd/diki/app/app.go b/cmd/diki/app/app.go index a6546d0d..90db8d40 100644 --- a/cmd/diki/app/app.go +++ b/cmd/diki/app/app.go @@ -13,6 +13,7 @@ import ( "log/slog" "os" "path/filepath" + "slices" "github.com/spf13/cobra" "gopkg.in/yaml.v3" @@ -22,6 +23,7 @@ import ( "github.com/gardener/diki/pkg/config" "github.com/gardener/diki/pkg/provider" "github.com/gardener/diki/pkg/report" + "github.com/gardener/diki/pkg/rule" "github.com/gardener/diki/pkg/ruleset" ) @@ -144,6 +146,7 @@ func addRunFlags(cmd *cobra.Command, opts *runOptions) { func addReportGenerateFlags(cmd *cobra.Command, opts *generateOptions) { cmd.PersistentFlags().Var(cliflag.NewMapStringString(&opts.distinctBy), "distinct-by", "If set generates a merged report. The keys are the IDs for the providers which the merged report will include and the values are distinct metadata attributes to be used as IDs for the different reports.") cmd.PersistentFlags().StringVar(&opts.format, "format", "html", "Format for the output report. Format can be one of 'html' or 'json'.") + cmd.PersistentFlags().StringVar(&opts.minStatus, "min-status", "Passed", "If set specifies the minimal status that will be included in the generated report. Ordered from lowest to highest priority, Status can be one of 'Passed', 'Skipped', 'Accepted', 'Warning', 'Failed', 'Errored' or 'NotImplemented'") } func addReportDiffFlags(cmd *cobra.Command, opts *diffOptions) { @@ -263,6 +266,14 @@ func generateCmd(args []string, rootOpts reportOptions, opts generateOptions, lo return errors.New("generate command requires a single filepath argument when the distinct-by flag is not set") } + minStatus := rule.Passed + if len(opts.minStatus) != 0 { + minStatus = rule.Status(opts.minStatus) + if !slices.Contains(rule.Statuses(), minStatus) { + return fmt.Errorf("not defined status: %s", minStatus) + } + } + var reports []*report.Report for _, arg := range args { fileData, err := os.ReadFile(filepath.Clean(arg)) @@ -276,6 +287,7 @@ func generateCmd(args []string, rootOpts reportOptions, opts generateOptions, lo return fmt.Errorf("failed to unmarshal data: %w", err) } + rep.SetMinStatus(minStatus) reports = append(reports, rep) } @@ -455,6 +467,7 @@ type runOptions struct { type generateOptions struct { distinctBy map[string]string format string + minStatus string } type generateDiffOptions struct { diff --git a/pkg/report/report.go b/pkg/report/report.go index 16a8673a..e9199beb 100644 --- a/pkg/report/report.go +++ b/pkg/report/report.go @@ -125,6 +125,26 @@ func (r *Report) WriteToFile(filePath string) error { return os.WriteFile(filePath, data, 0600) } +// SetMinStatus sets minStatus of the report. It also removes all checks with status less than the specified one. +// If the new minStatus is less than the original one, nothing is changed. +func (r *Report) SetMinStatus(minStatus rule.Status) { + if minStatus.Less(r.MinStatus) { + return + } + + r.MinStatus = minStatus + for _, provider := range r.Providers { + for _, ruleset := range provider.Rulesets { + for ruleIdx := range ruleset.Rules { + filteredChecks := slices.DeleteFunc(ruleset.Rules[ruleIdx].Checks, func(check Check) bool { + return check.Status.Less(minStatus) + }) + ruleset.Rules[ruleIdx].Checks = filteredChecks + } + } + } +} + // rulesetSummaryText returns a summary string with the number of rules with results per status. func rulesetSummaryText(ruleset *Ruleset) string { statuses := rule.Statuses() diff --git a/pkg/report/report_test.go b/pkg/report/report_test.go new file mode 100644 index 00000000..32819d6d --- /dev/null +++ b/pkg/report/report_test.go @@ -0,0 +1,175 @@ +// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and Gardener contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package report_test + +import ( + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/gardener/diki/pkg/report" + "github.com/gardener/diki/pkg/rule" +) + +var _ = Describe("report", func() { + Describe("Report", func() { + var ( + reportTime time.Time + providerID = "provider-foo" + providerName = "Provider Foo" + rulesetID = "ruleset-foo" + rulesetName = "Ruleset Foo" + rulesetVersion = "v1" + simpleReport report.Report + ) + BeforeEach(func() { + reportTime = time.Date(2000, time.January, 1, 0, 0, 0, 0, time.Local) + simpleReport = report.Report{ + Time: reportTime, + DikiVersion: "1", + MinStatus: rule.Accepted, + Providers: []report.Provider{ + { + ID: providerID, + Name: providerName, + Rulesets: []report.Ruleset{ + { + ID: rulesetID, + Name: rulesetName, + Version: rulesetVersion, + Rules: []report.Rule{ + { + ID: "1", + Name: "1", + Severity: rule.SeverityHigh, + Checks: []report.Check{ + { + Status: "Accepted", + Message: "foo", + Targets: []rule.Target{}, + }, + { + Status: "Failed", + Message: "bar", + Targets: []rule.Target{}, + }, + { + Status: "Skipped", + Message: "baz", + Targets: []rule.Target{}, + }, + }, + }, + { + ID: "2", + Name: "2", + Severity: rule.SeverityLow, + Checks: []report.Check{ + { + Status: "Accepted", + Message: "foo", + Targets: []rule.Target{}, + }, + { + Status: "Accepted", + Message: "bar", + Targets: []rule.Target{}, + }, + }, + }, + { + ID: "3", + Name: "3", + Severity: rule.SeverityLow, + Checks: []report.Check{ + { + Status: "Failed", + Message: "foo", + Targets: []rule.Target{}, + }, + { + Status: "Failed", + Message: "bar", + Targets: []rule.Target{}, + }, + }, + }, + }, + }, + }, + }, + }, + } + }) + + It("should correctly remove ruleset checks that are below the minStatus", func() { + expectedReportResult := report.Report{ + Time: reportTime, + DikiVersion: "1", + MinStatus: rule.Failed, + Providers: []report.Provider{ + { + ID: providerID, + Name: providerName, + Rulesets: []report.Ruleset{ + { + ID: rulesetID, + Name: rulesetName, + Version: rulesetVersion, + Rules: []report.Rule{ + { + ID: "1", + Name: "1", + Severity: rule.SeverityHigh, + Checks: []report.Check{ + { + Status: "Failed", + Message: "bar", + Targets: []rule.Target{}, + }, + }, + }, + { + ID: "2", + Name: "2", + Severity: rule.SeverityLow, + Checks: []report.Check{}, + }, + { + ID: "3", + Name: "3", + Severity: rule.SeverityLow, + Checks: []report.Check{ + { + Status: "Failed", + Message: "foo", + Targets: []rule.Target{}, + }, + { + Status: "Failed", + Message: "bar", + Targets: []rule.Target{}, + }, + }, + }, + }, + }, + }, + }, + }, + } + simpleReport.SetMinStatus(rule.Failed) + Expect(simpleReport).To(Equal(expectedReportResult)) + }) + + It("should not alter the report when the passed minStatus is not lower the report's minStatus", func() { + expectedReport := simpleReport + simpleReport.SetMinStatus(rule.Passed) + Expect(simpleReport).To(Equal(expectedReport)) + }) + }) + +})