2021-02-07 20:26:56 -08:00
package test
2021-02-01 16:22:41 +05:30
import (
2021-02-07 20:26:56 -08:00
"fmt"
2021-02-01 16:22:41 +05:30
"os"
2023-09-04 13:36:34 +02:00
"path/filepath"
2021-02-07 20:26:56 -08:00
2023-09-04 11:34:27 +02:00
"github.com/go-git/go-billy/v5"
2022-05-17 13:12:43 +02:00
policyreportv1alpha2 "github.com/kyverno/kyverno/api/policyreport/v1alpha2"
2023-09-05 10:55:01 +02:00
"github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/output/color"
"github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/output/table"
"github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/test/api"
"github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/test/filter"
2023-09-04 21:56:23 +02:00
cobrautils "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/utils/cobra"
2023-09-04 11:34:27 +02:00
reportutils "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/utils/report"
2022-04-14 17:50:18 +05:30
sanitizederror "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/utils/sanitizedError"
"github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/utils/store"
2023-09-04 11:34:27 +02:00
engineapi "github.com/kyverno/kyverno/pkg/engine/api"
2021-02-01 16:22:41 +05:30
"github.com/kyverno/kyverno/pkg/openapi"
2021-02-07 20:26:56 -08:00
"github.com/spf13/cobra"
2022-12-09 22:15:23 +05:30
"sigs.k8s.io/controller-runtime/pkg/log"
2021-02-01 16:22:41 +05:30
)
func Command ( ) * cobra . Command {
2021-02-18 01:00:41 +05:30
var cmd * cobra . Command
2022-03-09 13:10:53 +05:30
var testCase string
var fileName , gitBranch string
2023-08-29 21:28:26 +02:00
var registryAccess , failOnly , removeColor , detailedResults bool
2021-02-18 01:00:41 +05:30
cmd = & cobra . Command {
2023-09-04 21:56:23 +02:00
Use : "test [local folder or git repository]..." ,
Args : cobra . MinimumNArgs ( 1 ) ,
2023-09-05 04:14:28 +02:00
Short : cobrautils . FormatDescription ( true , websiteUrl , false , description ... ) ,
Long : cobrautils . FormatDescription ( false , websiteUrl , false , description ... ) ,
2023-09-04 21:56:23 +02:00
Example : cobrautils . FormatExamples ( examples ... ) ,
2021-02-01 23:34:15 +05:30
RunE : func ( cmd * cobra . Command , dirPath [ ] string ) ( err error ) {
2023-07-06 13:48:19 +02:00
color . InitColors ( removeColor )
2021-02-01 16:22:41 +05:30
defer func ( ) {
if err != nil {
if ! sanitizederror . IsErrorSanitized ( err ) {
log . Log . Error ( err , "failed to sanitize" )
err = fmt . Errorf ( "internal error" )
}
}
} ( )
2023-08-29 21:28:26 +02:00
store . SetRegistryAccess ( registryAccess )
2023-09-05 13:09:45 +02:00
return testCommandExecute ( dirPath , fileName , gitBranch , testCase , failOnly , detailedResults )
2021-02-01 16:22:41 +05:30
} ,
}
2023-09-04 21:56:23 +02:00
cmd . Flags ( ) . StringVarP ( & fileName , "file-name" , "f" , "kyverno-test.yaml" , "Test filename" )
cmd . Flags ( ) . StringVarP ( & gitBranch , "git-branch" , "b" , "" , "Test github repository branch" )
cmd . Flags ( ) . StringVarP ( & testCase , "test-case-selector" , "t" , "policy=*,rule=*,resource=*" , "Filter test cases to run" )
2023-07-03 12:04:07 +02:00
cmd . Flags ( ) . BoolVar ( & registryAccess , "registry" , false , "If set to true, access the image registry using local docker credentials to populate external data" )
cmd . Flags ( ) . BoolVar ( & failOnly , "fail-only" , false , "If set to true, display all the failing test only as output for the test command" )
cmd . Flags ( ) . BoolVar ( & removeColor , "remove-color" , false , "Remove any color from output" )
2023-07-31 17:15:47 +03:00
cmd . Flags ( ) . BoolVar ( & detailedResults , "detailed-results" , false , "If set to true, display detailed results" )
2021-02-18 01:00:41 +05:30
return cmd
2021-02-01 16:22:41 +05:30
}
2021-03-05 05:39:18 +05:30
type resultCounts struct {
2021-09-02 23:11:35 +05:30
Skip int
Pass int
Fail int
2021-03-05 05:39:18 +05:30
}
2023-04-12 14:51:03 +02:00
func testCommandExecute (
dirPath [ ] string ,
fileName string ,
gitBranch string ,
testCase string ,
failOnly bool ,
2023-07-31 17:15:47 +03:00
detailedResults bool ,
2023-09-05 13:09:45 +02:00
) ( err error ) {
2023-04-12 17:17:51 +02:00
// check input dir
2021-02-01 23:34:15 +05:30
if len ( dirPath ) == 0 {
2023-09-05 13:09:45 +02:00
return sanitizederror . NewWithError ( "a directory is required" , err )
2021-02-07 20:26:56 -08:00
}
2023-04-12 17:17:51 +02:00
// parse filter
2023-09-05 10:55:01 +02:00
filter , errors := filter . ParseFilter ( testCase )
2023-08-30 12:24:43 +02:00
if len ( errors ) > 0 {
fmt . Println ( )
fmt . Println ( "Filter errors:" )
for _ , e := range errors {
2023-09-05 13:09:45 +02:00
fmt . Println ( " Error:" , e )
2023-08-30 12:24:43 +02:00
}
}
2023-04-12 17:17:51 +02:00
// init openapi manager
2023-02-09 16:15:51 +01:00
openApiManager , err := openapi . NewManager ( log . Log )
2022-02-24 15:34:12 +00:00
if err != nil {
2023-09-05 13:09:45 +02:00
return fmt . Errorf ( "unable to create open api controller, %w" , err )
2022-02-24 15:34:12 +00:00
}
2023-04-18 14:08:17 +02:00
// load tests
2023-09-04 22:27:20 +02:00
tests , err := loadTests ( dirPath , fileName , gitBranch )
2023-09-04 13:36:34 +02:00
if err != nil {
fmt . Println ( )
2023-09-05 13:09:45 +02:00
fmt . Println ( "Error loading tests:" , err )
return err
2023-09-04 13:36:34 +02:00
}
if len ( tests ) == 0 {
2023-08-29 00:04:00 +02:00
fmt . Println ( )
fmt . Println ( "No test yamls available" )
}
2023-09-04 13:36:34 +02:00
if errs := tests . Errors ( ) ; len ( errs ) > 0 {
2023-08-29 00:04:00 +02:00
fmt . Println ( )
fmt . Println ( "Test errors:" )
2023-09-04 13:36:34 +02:00
for _ , e := range errs {
2023-09-05 13:09:45 +02:00
fmt . Println ( " Path:" , e . Path )
fmt . Println ( " Error:" , e . Err )
2023-08-29 00:04:00 +02:00
}
}
2023-09-04 13:36:34 +02:00
if len ( tests ) == 0 {
2023-08-29 00:04:00 +02:00
if len ( errors ) == 0 {
os . Exit ( 0 )
} else {
os . Exit ( 1 )
}
2023-04-18 14:08:17 +02:00
}
2023-09-05 13:09:45 +02:00
rc := & resultCounts { }
2023-07-06 13:48:19 +02:00
var table table . Table
2023-09-05 13:09:45 +02:00
for _ , test := range tests {
if test . Err == nil {
resourcePath := filepath . Dir ( test . Path )
if tests , responses , err := applyPoliciesFromPath (
test ,
resourcePath ,
rc ,
openApiManager ,
filter ,
false ,
) ; err != nil {
return sanitizederror . NewWithError ( "failed to apply test command" , err )
} else if t , err := printTestResult ( tests , responses , rc , failOnly , detailedResults , test . Fs , resourcePath ) ; err != nil {
return sanitizederror . NewWithError ( "failed to print test result:" , err )
} else {
table . AddFailed ( t . RawRows ... )
}
2022-01-14 14:15:59 +05:30
}
2021-05-03 08:20:22 -04:00
}
2022-07-22 20:02:12 +05:30
if ! failOnly {
fmt . Printf ( "\nTest Summary: %d tests passed and %d tests failed\n" , rc . Pass + rc . Skip , rc . Fail )
} else {
fmt . Printf ( "\nTest Summary: %d out of %d tests failed\n" , rc . Fail , rc . Pass + rc . Skip + rc . Fail )
}
2023-04-18 14:08:17 +02:00
fmt . Println ( )
2023-06-30 07:39:04 -05:00
if rc . Fail > 0 {
if ! failOnly {
2023-07-31 17:15:47 +03:00
printFailedTestResult ( table , detailedResults )
2023-06-30 07:39:04 -05:00
}
2021-03-05 05:39:18 +05:30
os . Exit ( 1 )
}
2023-09-05 13:09:45 +02:00
return nil
2021-02-01 16:22:41 +05:30
}
2021-02-01 23:34:15 +05:30
2023-09-04 11:34:27 +02:00
func checkResult ( test api . TestResults , fs billy . Filesystem , resoucePath string , response engineapi . EngineResponse , rule engineapi . RuleResponse ) ( bool , string , string ) {
expected := test . Result
// fallback to the deprecated field
if expected == "" {
expected = test . Status
}
// fallback on deprecated field
if test . PatchedResource != "" {
2023-09-05 01:25:06 +02:00
equals , err := getAndCompareResource ( response . PatchedResource , fs , filepath . Join ( resoucePath , test . PatchedResource ) )
2023-09-04 11:34:27 +02:00
if err != nil {
return false , err . Error ( ) , "Resource error"
}
if ! equals {
return false , "Patched resource didn't match the patched resource in the test result" , "Resource diff"
}
}
if test . GeneratedResource != "" {
2023-09-05 01:25:06 +02:00
equals , err := getAndCompareResource ( rule . GeneratedResource ( ) , fs , filepath . Join ( resoucePath , test . GeneratedResource ) )
2023-09-04 11:34:27 +02:00
if err != nil {
return false , err . Error ( ) , "Resource error"
}
if ! equals {
return false , "Generated resource didn't match the generated resource in the test result" , "Resource diff"
}
}
result := reportutils . ComputePolicyReportResult ( false , response , rule )
if result . Result != expected {
return false , result . Message , fmt . Sprintf ( "Want %s, got %s" , expected , result . Result )
}
return true , result . Message , "Ok"
}
func lookupEngineResponses ( test api . TestResults , resourceName string , responses ... engineapi . EngineResponse ) [ ] engineapi . EngineResponse {
var matches [ ] engineapi . EngineResponse
for _ , response := range responses {
policy := response . Policy ( )
resource := response . Resource
if policy . GetName ( ) != test . Policy {
continue
}
if test . Kind != resource . GetKind ( ) {
continue
}
if resourceName != "" && resourceName != resource . GetName ( ) {
continue
}
if test . Namespace != "" && test . Namespace != resource . GetNamespace ( ) {
continue
}
matches = append ( matches , response )
}
return matches
}
func lookupRuleResponses ( test api . TestResults , responses ... engineapi . RuleResponse ) [ ] engineapi . RuleResponse {
var matches [ ] engineapi . RuleResponse
2023-09-04 19:32:23 +03:00
// Since there are no rules in case of validating admission policies, responses are returned without checking rule names.
if test . IsValidatingAdmissionPolicy {
matches = responses
} else {
for _ , response := range responses {
rule := response . Name ( )
if rule != test . Rule && rule != "autogen-" + test . Rule && rule != "autogen-cronjob-" + test . Rule {
continue
}
matches = append ( matches , response )
2023-09-04 11:34:27 +02:00
}
}
return matches
}
func printTestResult (
tests [ ] api . TestResults ,
responses [ ] engineapi . EngineResponse ,
rc * resultCounts ,
failOnly bool ,
detailedResults bool ,
fs billy . Filesystem ,
resoucePath string ,
) ( table . Table , error ) {
2023-07-06 13:48:19 +02:00
printer := table . NewTablePrinter ( )
var resultsTable table . Table
2022-08-24 15:08:24 +02:00
var countDeprecatedResource int
2022-10-11 18:00:11 +05:30
testCount := 1
2023-09-04 11:34:27 +02:00
for _ , test := range tests {
// lookup matching engine responses (without the resource name)
// to reduce the search scope
responses := lookupEngineResponses ( test , "" , responses ... )
// TODO fix deprecated fields
// identify the resources to be looked up
var resources [ ] string
if test . Resources != nil {
resources = append ( resources , test . Resources ... )
} else if test . Resource != "" {
countDeprecatedResource ++
resources = append ( resources , test . Resource )
2022-10-11 18:00:11 +05:30
}
2023-09-04 11:34:27 +02:00
for _ , resource := range resources {
var rows [ ] table . Row
// lookup matching engine responses (with the resource name this time)
for _ , response := range lookupEngineResponses ( test , resource , responses ... ) {
// lookup matching rule responses
for _ , rule := range lookupRuleResponses ( test , response . PolicyResponse . Rules ... ) {
// perform test checks
ok , message , reason := checkResult ( test , fs , resoucePath , response , rule )
// if checks failed but we were expecting a fail it's considered a success
success := ok || ( ! ok && test . Result == policyreportv1alpha2 . StatusFail )
row := table . Row {
CompactRow : table . CompactRow {
ID : testCount ,
Policy : color . Policy ( "" , test . Policy ) ,
Rule : color . Rule ( test . Rule ) ,
Resource : color . Resource ( test . Kind , test . Namespace , resource ) ,
Reason : reason ,
IsFailure : ! success ,
} ,
Message : message ,
2023-05-10 11:12:53 +03:00
}
2023-09-04 11:34:27 +02:00
if success {
row . Result = color . ResultPass ( )
if test . Result == policyreportv1alpha2 . StatusSkip {
rc . Skip ++
} else {
rc . Pass ++
}
2023-05-10 11:12:53 +03:00
} else {
2023-09-04 11:34:27 +02:00
row . Result = color . ResultFail ( )
rc . Fail ++
2023-05-10 11:12:53 +03:00
}
2023-09-04 11:34:27 +02:00
testCount ++
rows = append ( rows , row )
2022-07-22 20:02:12 +05:30
}
2022-06-20 12:08:13 +05:30
}
2023-09-04 11:34:27 +02:00
// if not found
if len ( rows ) == 0 {
row := table . Row {
CompactRow : table . CompactRow {
ID : testCount ,
Policy : color . Policy ( "" , test . Policy ) ,
Rule : color . Rule ( test . Rule ) ,
Resource : color . Resource ( test . Kind , test . Namespace , resource ) ,
IsFailure : true ,
Result : color . ResultFail ( ) ,
Reason : color . NotFound ( ) ,
} ,
Message : color . NotFound ( ) ,
2023-05-10 11:12:53 +03:00
}
2023-09-04 11:34:27 +02:00
testCount ++
2023-07-06 13:48:19 +02:00
resultsTable . Add ( row )
2022-06-20 12:08:13 +05:30
rc . Fail ++
2022-07-22 20:02:12 +05:30
} else {
2023-09-04 11:34:27 +02:00
resultsTable . Add ( rows ... )
2022-07-22 20:02:12 +05:30
}
2022-06-20 12:08:13 +05:30
}
2021-02-07 20:26:56 -08:00
}
2021-10-01 14:16:33 +05:30
fmt . Printf ( "\n" )
2023-07-31 17:15:47 +03:00
printer . Print ( resultsTable . Rows ( detailedResults ) )
2023-07-06 13:48:19 +02:00
return resultsTable , nil
2021-02-07 20:26:56 -08:00
}
2022-08-19 19:11:19 +05:30
2023-07-31 17:15:47 +03:00
func printFailedTestResult ( resultsTable table . Table , detailedResults bool ) {
2023-07-06 13:48:19 +02:00
printer := table . NewTablePrinter ( )
for i := range resultsTable . RawRows {
resultsTable . RawRows [ i ] . ID = i + 1
2022-06-02 23:01:46 +05:30
}
fmt . Printf ( "Aggregated Failed Test Cases : " )
2023-04-18 14:08:17 +02:00
fmt . Println ( )
2023-07-31 17:15:47 +03:00
printer . Print ( resultsTable . Rows ( detailedResults ) )
2022-06-02 23:01:46 +05:30
}