diff --git a/cmd/cli/kubectl-kyverno/apis/v1alpha1/test.go b/cmd/cli/kubectl-kyverno/apis/v1alpha1/test.go index 19f9e1e059..4595a01431 100644 --- a/cmd/cli/kubectl-kyverno/apis/v1alpha1/test.go +++ b/cmd/cli/kubectl-kyverno/apis/v1alpha1/test.go @@ -1,6 +1,7 @@ package v1alpha1 import ( + "github.com/kyverno/kyverno-json/pkg/apis/v1alpha1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -32,6 +33,31 @@ type Test struct { // Results are the results to be checked in the test Results []TestResult `json:"results,omitempty"` + // Checks are the verifications to be checked in the test + Checks []CheckResult `json:"checks,omitempty"` + // Values are the values to be used in the test Values *ValuesSpec `json:"values,omitempty"` } + +type CheckResult struct { + // Match tells how to match relevant rule responses + Match CheckMatch `json:"match,omitempty"` + + // Assert contains assertion to be performed on the relevant rule responses + Assert v1alpha1.Any `json:"assert"` + + // Error contains negative assertion to be performed on the relevant rule responses + Error v1alpha1.Any `json:"error"` +} + +type CheckMatch struct { + // Resource filters engine responses + Resource *v1alpha1.Any `json:"resource,omitempty"` + + // Policy filters engine responses + Policy *v1alpha1.Any `json:"policy,omitempty"` + + // Rule filters rule responses + Rule *v1alpha1.Any `json:"rule,omitempty"` +} diff --git a/cmd/cli/kubectl-kyverno/commands/test/command.go b/cmd/cli/kubectl-kyverno/commands/test/command.go index 7a6f139138..1a45960539 100644 --- a/cmd/cli/kubectl-kyverno/commands/test/command.go +++ b/cmd/cli/kubectl-kyverno/commands/test/command.go @@ -101,7 +101,7 @@ func testCommandExecute( } } rc := &resultCounts{} - var table table.Table + var fullTable table.Table for _, test := range tests { if test.Err == nil { deprecations.CheckTest(out, test.Path, test.Test) @@ -121,11 +121,24 @@ func testCommandExecute( return fmt.Errorf("failed to run test (%w)", err) } fmt.Fprintln(out, " Checking results ...") - t, err := printTestResult(out, filteredResults, responses, rc, failOnly, detailedResults, test.Fs, resourcePath) - if err != nil { - return fmt.Errorf("failed to print test result (%w)", err) + var resultsTable table.Table + { + err := printTestResult(out, filteredResults, responses, rc, &resultsTable, test.Fs, resourcePath) + if err != nil { + return fmt.Errorf("failed to print test result (%w)", err) + } } - table.AddFailed(t.RawRows...) + { + err := printCheckResult(out, test.Test.Checks, responses, rc, &resultsTable) + if err != nil { + return fmt.Errorf("failed to print test result (%w)", err) + } + } + fullTable.AddFailed(resultsTable.RawRows...) + printer := table.NewTablePrinter(out) + fmt.Fprintln(out) + printer.Print(resultsTable.Rows(detailedResults)) + fmt.Fprintln(out) } } if !failOnly { @@ -136,7 +149,7 @@ func testCommandExecute( fmt.Fprintln(out) if rc.Fail > 0 { if !failOnly { - printFailedTestResult(out, table, detailedResults) + printFailedTestResult(out, fullTable, detailedResults) } return fmt.Errorf("%d tests failed", rc.Fail) } diff --git a/cmd/cli/kubectl-kyverno/commands/test/output.go b/cmd/cli/kubectl-kyverno/commands/test/output.go index a885112725..a19898f808 100644 --- a/cmd/cli/kubectl-kyverno/commands/test/output.go +++ b/cmd/cli/kubectl-kyverno/commands/test/output.go @@ -1,30 +1,174 @@ package test import ( + "context" "fmt" "io" "github.com/go-git/go-billy/v5" + "github.com/kyverno/kyverno-json/pkg/engine/assert" policyreportv1alpha2 "github.com/kyverno/kyverno/api/policyreport/v1alpha2" "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/apis/v1alpha1" "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/output/color" "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/output/table" engineapi "github.com/kyverno/kyverno/pkg/engine/api" + "k8s.io/apimachinery/pkg/runtime" ) +func printCheckResult( + out io.Writer, + checks []v1alpha1.CheckResult, + responses []engineapi.EngineResponse, + rc *resultCounts, + resultsTable *table.Table, +) error { + testCount := 1 + for _, check := range checks { + // filter engine responses + matchingEngineResponses := responses + // 1. by resource + if check.Match.Resource != nil { + var filtered []engineapi.EngineResponse + for _, response := range matchingEngineResponses { + errs, err := assert.Validate(context.Background(), check.Match.Resource.Value, response.Resource.UnstructuredContent(), nil) + if err != nil { + return err + } + if len(errs) == 0 { + filtered = append(filtered, response) + } + } + matchingEngineResponses = filtered + } + // 2. by policy + if check.Match.Policy != nil { + var filtered []engineapi.EngineResponse + for _, response := range matchingEngineResponses { + data, err := runtime.DefaultUnstructuredConverter.ToUnstructured(response.Policy().MetaObject()) + if err != nil { + return err + } + errs, err := assert.Validate(context.Background(), check.Match.Policy.Value, data, nil) + if err != nil { + return err + } + if len(errs) == 0 { + filtered = append(filtered, response) + } + } + matchingEngineResponses = filtered + } + for _, response := range matchingEngineResponses { + // filter rule responses + matchingRuleResponses := response.PolicyResponse.Rules + if check.Match.Rule != nil { + var filtered []engineapi.RuleResponse + for _, response := range matchingRuleResponses { + data := map[string]any{ + "name": response.Name(), + } + errs, err := assert.Validate(context.Background(), check.Match.Rule.Value, data, nil) + if err != nil { + return err + } + if len(errs) == 0 { + filtered = append(filtered, response) + } + } + matchingRuleResponses = filtered + } + for _, rule := range matchingRuleResponses { + // perform check + data := map[string]any{ + "name": rule.Name(), + "ruleType": rule.RuleType(), + "message": rule.Message(), + "status": string(rule.Status()), + // generatedResource unstructured.Unstructured + // patchedTarget *unstructured.Unstructured + // patchedTargetParentResourceGVR metav1.GroupVersionResource + // patchedTargetSubresourceName string + // podSecurityChecks contains pod security checks (only if this is a pod security rule) + "podSecurityChecks": rule.PodSecurityChecks(), + "exception ": rule.Exception(), + } + if check.Assert.Value != nil { + errs, err := assert.Validate(context.Background(), check.Assert.Value, data, nil) + if err != nil { + return err + } + row := table.Row{ + RowCompact: table.RowCompact{ + ID: testCount, + Policy: color.Policy("", response.Policy().GetName()), + Rule: color.Rule(rule.Name()), + Resource: color.Resource(response.Resource.GetKind(), response.Resource.GetNamespace(), response.Resource.GetName()), + IsFailure: len(errs) != 0, + }, + Message: rule.Message(), + } + if len(errs) == 0 { + row.Result = color.ResultPass() + row.Reason = "Ok" + if rule.Status() == engineapi.RuleStatusSkip { + rc.Skip++ + } else { + rc.Pass++ + } + } else { + row.Result = color.ResultFail() + row.Reason = errs.ToAggregate().Error() + rc.Fail++ + } + resultsTable.Add(row) + testCount++ + } + if check.Error.Value != nil { + errs, err := assert.Validate(context.Background(), check.Error.Value, data, nil) + if err != nil { + return err + } + row := table.Row{ + RowCompact: table.RowCompact{ + ID: testCount, + Policy: color.Policy("", response.Policy().GetName()), + Rule: color.Rule(rule.Name()), + Resource: color.Resource(response.Resource.GetKind(), response.Resource.GetNamespace(), response.Resource.GetName()), + IsFailure: len(errs) != 0, + }, + Message: rule.Message(), + } + if len(errs) != 0 { + row.Result = color.ResultPass() + row.Reason = errs.ToAggregate().Error() + if rule.Status() == engineapi.RuleStatusSkip { + rc.Skip++ + } else { + rc.Pass++ + } + } else { + row.Result = color.ResultFail() + row.Reason = "The assertion succeeded but was expected to fail" + rc.Fail++ + } + resultsTable.Add(row) + testCount++ + } + } + } + } + return nil +} + func printTestResult( out io.Writer, tests []v1alpha1.TestResult, responses []engineapi.EngineResponse, rc *resultCounts, - failOnly bool, - detailedResults bool, + resultsTable *table.Table, fs billy.Filesystem, resoucePath string, -) (table.Table, error) { - printer := table.NewTablePrinter(out) - var resultsTable table.Table - var countDeprecatedResource int +) error { testCount := 1 for _, test := range tests { // lookup matching engine responses (without the resource name) @@ -36,7 +180,6 @@ func printTestResult( if test.Resources != nil { resources = append(resources, test.Resources...) } else if test.Resource != "" { - countDeprecatedResource++ resources = append(resources, test.Resource) } for _, resource := range resources { @@ -116,10 +259,7 @@ func printTestResult( } } } - fmt.Fprintln(out) - printer.Print(resultsTable.Rows(detailedResults)) - fmt.Fprintln(out) - return resultsTable, nil + return nil } func printFailedTestResult(out io.Writer, resultsTable table.Table, detailedResults bool) { diff --git a/go.mod b/go.mod index 2922cdf05f..9476b36a4d 100644 --- a/go.mod +++ b/go.mod @@ -30,6 +30,7 @@ require ( github.com/julienschmidt/httprouter v1.3.0 github.com/kataras/tablewriter v0.0.0-20180708051242-e063d29b7c23 github.com/kyverno/go-jmespath v0.4.1-0.20231124160150-95e59c162877 + github.com/kyverno/kyverno-json v0.0.1 github.com/lensesio/tableprinter v0.0.0-20201125135848-89e81fc956e7 github.com/notaryproject/notation-core-go v1.0.2 github.com/notaryproject/notation-go v1.0.1 @@ -92,6 +93,11 @@ require ( require github.com/open-policy-agent/gatekeeper/v3 v3.14.0 // indirect +require ( + github.com/jmespath-community/go-jmespath v1.1.2-0.20231004164315-78945398586a // indirect + github.com/smarty/assertions v1.15.1 // indirect +) + require ( cloud.google.com/go/compute v1.23.3 // indirect cloud.google.com/go/compute/metadata v0.2.3 // indirect diff --git a/go.sum b/go.sum index f38f35e81f..889384f3ec 100644 --- a/go.sum +++ b/go.sum @@ -547,6 +547,8 @@ github.com/jellydator/ttlcache/v3 v3.1.1 h1:RCgYJqo3jgvhl+fEWvjNW8thxGWsgxi+TPhR github.com/jellydator/ttlcache/v3 v3.1.1/go.mod h1:hi7MGFdMAwZna5n2tuvh63DvFLzVKySzCVW6+0gA2n4= github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= +github.com/jmespath-community/go-jmespath v1.1.2-0.20231004164315-78945398586a h1:8W5d74FhEWTJPnFwpDDxbwUK3pPLUbY4RlfN2uzTTSE= +github.com/jmespath-community/go-jmespath v1.1.2-0.20231004164315-78945398586a/go.mod h1:4gOyFJsR/Gk+05RgTKYrifT7tBPWD8Lubtb5jRrfy9I= 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= @@ -588,6 +590,8 @@ github.com/kyverno/go-jmespath v0.4.1-0.20231124160150-95e59c162877 h1:XOLJNGX/q github.com/kyverno/go-jmespath v0.4.1-0.20231124160150-95e59c162877/go.mod h1:yzDHaKovQy16rjN4kFnjF+IdNoN4p1ndw+va6+B8zUU= github.com/kyverno/go-jmespath/internal/testify v1.5.2-0.20230630133209-945021c749d9 h1:lL311dF3a2aeNibJj8v+uhFU3XkvRHZmCtAdSPOrQYY= github.com/kyverno/go-jmespath/internal/testify v1.5.2-0.20230630133209-945021c749d9/go.mod h1:XRxUGHIiCy1WYma1CdfdO1WOhIe8dLPTENaZr5D1ex4= +github.com/kyverno/kyverno-json v0.0.1 h1:2d3k1M0YCWRz9r5fdHkIMesChPbmtSYqR6qk+2s05b0= +github.com/kyverno/kyverno-json v0.0.1/go.mod h1:7lNc9nnrNYC1Pbn/Qd5acyoRXa6sqBrZulc6Rg64q7w= github.com/lensesio/tableprinter v0.0.0-20201125135848-89e81fc956e7 h1:k/1ku0yehLCPqERCHkIHMDqDg1R02AcCScRuHbamU3s= github.com/lensesio/tableprinter v0.0.0-20201125135848-89e81fc956e7/go.mod h1:YR/zYthNdWfO8+0IOyHDcIDBBBS2JMnYUIwSsnwmRqU= github.com/letsencrypt/boulder v0.0.0-20240122173420-ce5632b480f0 h1:Ixlk3bGcKTpJeRujQ/YsO7aKq4CIHvBLodPcAzp/QZg= @@ -781,11 +785,13 @@ github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EE github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 h1:unQFBIznI+VYD1/1fApl1A+9VcBk+9dcqGfnePY87LY= github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262/go.mod h1:MyOHs9Po2fbM1LHej6sBUT8ozbxmMOFG+E+rx/GSGuc= +github.com/smarty/assertions v1.15.1 h1:812oFiXI+G55vxsFf+8bIZ1ux30qtkdqzKbEFwyX3Tk= +github.com/smarty/assertions v1.15.1/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/assertions v1.1.0 h1:MkTeG1DMwsrdH7QtLXy5W+fUxWq+vmb6cLmyJ7aRtF0= github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= -github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY= +github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60= github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=