2021-02-07 20:26:56 -08:00
package test
2021-02-01 16:22:41 +05:30
import (
"encoding/json"
2021-02-07 20:26:56 -08:00
"fmt"
2021-02-01 16:22:41 +05:30
"io/ioutil"
2021-02-07 20:26:56 -08:00
"net/url"
2021-02-01 16:22:41 +05:30
"os"
"path/filepath"
"reflect"
2021-02-07 20:26:56 -08:00
"sort"
2021-02-01 16:22:41 +05:30
"strings"
2021-02-07 20:26:56 -08:00
"github.com/fatih/color"
"github.com/go-git/go-billy/v5"
"github.com/go-git/go-billy/v5/memfs"
"github.com/kataras/tablewriter"
2021-02-01 16:22:41 +05:30
v1 "github.com/kyverno/kyverno/pkg/api/kyverno/v1"
2021-02-07 20:26:56 -08:00
report "github.com/kyverno/kyverno/pkg/api/policyreport/v1alpha1"
client "github.com/kyverno/kyverno/pkg/dclient"
"github.com/kyverno/kyverno/pkg/engine/response"
"github.com/kyverno/kyverno/pkg/engine/utils"
2021-02-01 16:22:41 +05:30
"github.com/kyverno/kyverno/pkg/kyverno/common"
2021-02-07 20:26:56 -08:00
sanitizederror "github.com/kyverno/kyverno/pkg/kyverno/sanitizedError"
2021-02-01 16:22:41 +05:30
"github.com/kyverno/kyverno/pkg/openapi"
2021-02-07 20:26:56 -08:00
policy2 "github.com/kyverno/kyverno/pkg/policy"
2021-02-01 16:22:41 +05:30
"github.com/kyverno/kyverno/pkg/policyreport"
"github.com/lensesio/tableprinter"
2021-02-07 20:26:56 -08:00
"github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/yaml"
log "sigs.k8s.io/controller-runtime/pkg/log"
2021-02-01 16:22:41 +05:30
)
// Command returns version command
func Command ( ) * cobra . Command {
2021-02-18 01:00:41 +05:30
var cmd * cobra . Command
var valuesFile , fileName string
cmd = & cobra . Command {
2021-02-01 16:22:41 +05:30
Use : "test" ,
2021-02-18 01:00:41 +05:30
Short : "run tests from directory" ,
2021-02-01 23:34:15 +05:30
RunE : func ( cmd * cobra . Command , dirPath [ ] string ) ( err error ) {
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" )
}
}
} ( )
2021-03-05 05:39:18 +05:30
_ , err = testCommandExecute ( dirPath , valuesFile , fileName )
2021-02-01 16:22:41 +05:30
if err != nil {
2021-02-02 18:43:19 +05:30
log . Log . V ( 3 ) . Info ( "a directory is required" )
2021-02-01 16:22:41 +05:30
return err
}
return nil
} ,
}
2021-02-18 01:00:41 +05:30
cmd . Flags ( ) . StringVarP ( & fileName , "file-name" , "f" , "test.yaml" , "test filename" )
return cmd
2021-02-01 16:22:41 +05:30
}
type Test struct {
2021-02-07 20:26:56 -08:00
Name string ` json:"name" `
Policies [ ] string ` json:"policies" `
Resources [ ] string ` json:"resources" `
Variables string ` json:"variables" `
Results [ ] TestResults ` json:"results" `
2021-02-01 16:22:41 +05:30
}
type SkippedPolicy struct {
Name string ` json:"name" `
Rules [ ] v1 . Rule ` json:"rules" `
Variable string ` json:"variable" `
}
type TestResults struct {
2021-02-07 20:26:56 -08:00
Policy string ` json:"policy" `
Rule string ` json:"rule" `
Status string ` json:"status" `
Resource string ` json:"resource" `
2021-02-01 16:22:41 +05:30
}
type ReportResult struct {
2021-02-07 20:26:56 -08:00
TestResults
2021-02-01 16:22:41 +05:30
Resources [ ] * corev1 . ObjectReference ` json:"resources" `
}
type Resource struct {
2021-02-07 20:26:56 -08:00
Name string ` json:"name" `
2021-02-01 16:22:41 +05:30
Values map [ string ] string ` json:"values" `
}
type Table struct {
2021-02-07 20:26:56 -08:00
ID int ` header:"#" `
Resource string ` header:"test" `
Result string ` header:"result" `
2021-02-01 16:22:41 +05:30
}
type Policy struct {
Name string ` json:"name" `
Resources [ ] Resource ` json:"resources" `
}
type Values struct {
Policies [ ] Policy ` json:"policies" `
}
2021-03-05 05:39:18 +05:30
type resultCounts struct {
pass int
fail int
}
func testCommandExecute ( dirPath [ ] string , valuesFile string , fileName string ) ( rc * resultCounts , err error ) {
2021-02-01 16:22:41 +05:30
var errors [ ] error
fs := memfs . New ( )
2021-03-05 05:39:18 +05:30
rc = & resultCounts { }
2021-03-12 01:16:36 +05:30
var testYamlCount int
2021-02-01 23:34:15 +05:30
if len ( dirPath ) == 0 {
2021-03-05 05:39:18 +05:30
return rc , sanitizederror . NewWithError ( fmt . Sprintf ( "a directory is required" ) , err )
2021-02-07 20:26:56 -08:00
}
2021-02-02 18:43:19 +05:30
if strings . Contains ( string ( dirPath [ 0 ] ) , "https://" ) {
2021-03-05 05:39:18 +05:30
gitURL , err := url . Parse ( dirPath [ 0 ] )
2021-02-01 16:22:41 +05:30
if err != nil {
2021-03-05 05:39:18 +05:30
return rc , sanitizederror . NewWithError ( "failed to parse URL" , err )
2021-02-01 16:22:41 +05:30
}
2021-03-05 05:39:18 +05:30
pathElems := strings . Split ( gitURL . Path [ 1 : ] , "/" )
2021-03-12 01:16:36 +05:30
if len ( pathElems ) <= 2 {
2021-03-05 05:39:18 +05:30
err := fmt . Errorf ( "invalid URL path %s - expected https://github.com/:owner/:repository/:branch" , gitURL . Path )
fmt . Printf ( "Error: failed to parse URL \nCause: %s\n" , err )
os . Exit ( 1 )
2021-02-01 16:22:41 +05:30
}
2021-03-12 01:16:36 +05:30
gitURL . Path = strings . Join ( [ ] string { pathElems [ 0 ] , pathElems [ 1 ] } , "/" )
2021-03-05 05:39:18 +05:30
repoURL := gitURL . String ( )
2021-03-12 01:16:36 +05:30
branch := strings . ReplaceAll ( dirPath [ 0 ] , repoURL + "/" , "" )
_ , cloneErr := clone ( repoURL , fs , branch )
if cloneErr != nil {
fmt . Printf ( "Error: failed to clone repository \nCause: %s\n" , cloneErr )
log . Log . V ( 3 ) . Info ( fmt . Sprintf ( "failed to clone repository %v as it is not valid" , repoURL ) , "error" , cloneErr )
2021-03-05 05:39:18 +05:30
os . Exit ( 1 )
2021-02-01 16:22:41 +05:30
}
policyYamls , err := listYAMLs ( fs , "/" )
if err != nil {
2021-03-05 05:39:18 +05:30
return rc , sanitizederror . NewWithError ( "failed to list YAMLs in repository" , err )
2021-02-01 16:22:41 +05:30
}
sort . Strings ( policyYamls )
for _ , yamlFilePath := range policyYamls {
2021-02-07 20:26:56 -08:00
file , err := fs . Open ( yamlFilePath )
2021-05-03 08:20:22 -04:00
if err != nil {
errors = append ( errors , sanitizederror . NewWithError ( "Error: failed to open file" , err ) )
continue
}
2021-02-18 01:00:41 +05:30
if strings . Contains ( file . Name ( ) , fileName ) {
2021-03-12 01:16:36 +05:30
testYamlCount ++
2021-02-18 01:00:41 +05:30
policyresoucePath := strings . Trim ( yamlFilePath , fileName )
bytes , err := ioutil . ReadAll ( file )
if err != nil {
2021-05-03 08:20:22 -04:00
errors = append ( errors , sanitizederror . NewWithError ( "Error: failed to read file" , err ) )
2021-02-18 01:00:41 +05:30
continue
}
policyBytes , err := yaml . ToJSON ( bytes )
if err != nil {
2021-05-03 08:20:22 -04:00
errors = append ( errors , sanitizederror . NewWithError ( "failed to convert to JSON" , err ) )
2021-02-18 01:00:41 +05:30
continue
}
2021-03-05 05:39:18 +05:30
if err := applyPoliciesFromPath ( fs , policyBytes , valuesFile , true , policyresoucePath , rc ) ; err != nil {
return rc , sanitizederror . NewWithError ( "failed to apply test command" , err )
2021-02-18 01:00:41 +05:30
}
2021-02-01 16:22:41 +05:30
}
2021-05-03 08:20:22 -04:00
}
if testYamlCount == 0 {
fmt . Printf ( "\n No test yamls available \n" )
2021-02-01 16:22:41 +05:30
}
} else {
2021-02-01 23:34:15 +05:30
path := filepath . Clean ( dirPath [ 0 ] )
2021-05-03 08:20:22 -04:00
errors = getLocalDirTestFiles ( fs , path , fileName , valuesFile , rc )
}
if len ( errors ) > 0 && log . Log . V ( 1 ) . Enabled ( ) {
fmt . Printf ( "ignoring errors: \n" )
for _ , e := range errors {
fmt . Printf ( " %v \n" , e . Error ( ) )
2021-02-01 16:22:41 +05:30
}
}
2021-03-05 05:39:18 +05:30
if rc . fail > 0 {
os . Exit ( 1 )
}
os . Exit ( 0 )
return rc , nil
2021-02-01 16:22:41 +05:30
}
2021-02-01 23:34:15 +05:30
2021-05-03 08:20:22 -04:00
func getLocalDirTestFiles ( fs billy . Filesystem , path , fileName , valuesFile string , rc * resultCounts ) [ ] error {
var errors [ ] error
2021-02-18 01:00:41 +05:30
files , err := ioutil . ReadDir ( path )
if err != nil {
2021-05-03 08:20:22 -04:00
return [ ] error { fmt . Errorf ( "failed to read %v: %v" , path , err . Error ( ) ) }
2021-02-18 01:00:41 +05:30
}
for _ , file := range files {
if file . IsDir ( ) {
2021-05-03 08:20:22 -04:00
getLocalDirTestFiles ( fs , filepath . Join ( path , file . Name ( ) ) , fileName , valuesFile , rc )
2021-02-18 01:00:41 +05:30
continue
}
if strings . Contains ( file . Name ( ) , fileName ) {
yamlFile , err := ioutil . ReadFile ( filepath . Join ( path , file . Name ( ) ) )
if err != nil {
2021-05-03 08:20:22 -04:00
errors = append ( errors , sanitizederror . NewWithError ( "unable to read yaml" , err ) )
2021-02-18 01:00:41 +05:30
continue
}
valuesBytes , err := yaml . ToJSON ( yamlFile )
if err != nil {
2021-05-03 08:20:22 -04:00
errors = append ( errors , sanitizederror . NewWithError ( "failed to convert json" , err ) )
2021-02-18 01:00:41 +05:30
continue
}
2021-03-05 05:39:18 +05:30
if err := applyPoliciesFromPath ( fs , valuesBytes , valuesFile , false , path , rc ) ; err != nil {
2021-05-03 08:20:22 -04:00
errors = append ( errors , sanitizederror . NewWithError ( fmt . Sprintf ( "failed to apply test command from file %s" , file . Name ( ) ) , err ) )
2021-02-18 01:00:41 +05:30
continue
}
}
}
2021-05-03 08:20:22 -04:00
return errors
2021-02-18 01:00:41 +05:30
}
2021-02-01 16:22:41 +05:30
func buildPolicyResults ( resps [ ] * response . EngineResponse ) map [ string ] [ ] interface { } {
results := make ( map [ string ] [ ] interface { } )
infos := policyreport . GeneratePRsFromEngineResponse ( resps , log . Log )
for _ , info := range infos {
for _ , infoResult := range info . Results {
for _ , rule := range infoResult . Rules {
if rule . Type != utils . Validation . String ( ) {
continue
}
result := report . PolicyReportResult {
Policy : info . PolicyName ,
Resources : [ ] * corev1 . ObjectReference {
{
2021-02-07 20:26:56 -08:00
Name : infoResult . Resource . Name ,
2021-02-01 16:22:41 +05:30
} ,
2021-02-07 20:26:56 -08:00
} ,
2021-02-01 16:22:41 +05:30
}
result . Rule = rule . Name
result . Status = report . PolicyStatus ( rule . Check )
results [ rule . Name ] = append ( results [ rule . Name ] , result )
}
}
}
return results
}
2021-02-18 01:00:41 +05:30
func getPolicyResouceFullPath ( path [ ] string , policyresoucePath string , isGit bool ) [ ] string {
var pol [ ] string
if ! isGit {
for _ , p := range path {
pol = append ( pol , filepath . Join ( policyresoucePath , p ) )
}
return pol
}
return path
}
2021-03-05 05:39:18 +05:30
func applyPoliciesFromPath ( fs billy . Filesystem , policyBytes [ ] byte , valuesFile string , isGit bool , policyresoucePath string , rc * resultCounts ) ( err error ) {
2021-02-01 16:22:41 +05:30
openAPIController , err := openapi . NewOpenAPIController ( )
engineResponses := make ( [ ] * response . EngineResponse , 0 )
validateEngineResponses := make ( [ ] * response . EngineResponse , 0 )
skippedPolicies := make ( [ ] SkippedPolicy , 0 )
2021-02-02 18:43:19 +05:30
var dClient * client . Client
2021-02-01 16:22:41 +05:30
values := & Test { }
2021-02-02 18:43:19 +05:30
var variablesString string
2021-02-01 16:22:41 +05:30
if err := json . Unmarshal ( policyBytes , values ) ; err != nil {
return sanitizederror . NewWithError ( "failed to decode yaml" , err )
2021-02-07 20:26:56 -08:00
}
2021-02-18 01:00:41 +05:30
fmt . Printf ( "\nExecuting %s..." , values . Name )
2021-03-10 02:15:45 +05:30
_ , valuesMap , namespaceSelectorMap , err := common . GetVariable ( variablesString , values . Variables , fs , isGit , policyresoucePath )
2021-02-01 16:22:41 +05:30
if err != nil {
if ! sanitizederror . IsErrorSanitized ( err ) {
2021-02-07 20:26:56 -08:00
return sanitizederror . NewWithError ( "failed to decode yaml" , err )
2021-02-01 16:22:41 +05:30
}
2021-02-07 20:26:56 -08:00
return err
2021-02-01 16:22:41 +05:30
}
2021-02-18 01:00:41 +05:30
fullPolicyPath := getPolicyResouceFullPath ( values . Policies , policyresoucePath , isGit )
fullResourcePath := getPolicyResouceFullPath ( values . Resources , policyresoucePath , isGit )
policies , err := common . GetPoliciesFromPaths ( fs , fullPolicyPath , isGit , policyresoucePath )
2021-02-01 16:22:41 +05:30
if err != nil {
fmt . Printf ( "Error: failed to load policies\nCause: %s\n" , err )
os . Exit ( 1 )
}
2021-02-02 18:43:19 +05:30
mutatedPolicies , err := common . MutatePolices ( policies )
2021-02-07 20:26:56 -08:00
if err != nil {
2021-02-01 16:22:41 +05:30
if ! sanitizederror . IsErrorSanitized ( err ) {
2021-02-07 20:26:56 -08:00
return sanitizederror . NewWithError ( "failed to mutate policy" , err )
2021-02-01 16:22:41 +05:30
}
2021-02-07 20:26:56 -08:00
}
2021-02-18 01:00:41 +05:30
resources , err := common . GetResourceAccordingToResourcePath ( fs , fullResourcePath , false , mutatedPolicies , dClient , "" , false , isGit , policyresoucePath )
2021-02-07 20:26:56 -08:00
if err != nil {
fmt . Printf ( "Error: failed to load resources\nCause: %s\n" , err )
os . Exit ( 1 )
}
msgPolicies := "1 policy"
if len ( mutatedPolicies ) > 1 {
msgPolicies = fmt . Sprintf ( "%d policies" , len ( policies ) )
}
msgResources := "1 resource"
if len ( resources ) > 1 {
msgResources = fmt . Sprintf ( "%d resources" , len ( resources ) )
}
if len ( mutatedPolicies ) > 0 && len ( resources ) > 0 {
fmt . Printf ( "\napplying %s to %s... \n" , msgPolicies , msgResources )
}
for _ , policy := range mutatedPolicies {
err := policy2 . Validate ( policy , nil , true , openAPIController )
2021-02-01 16:22:41 +05:30
if err != nil {
2021-02-07 20:26:56 -08:00
log . Log . V ( 3 ) . Info ( fmt . Sprintf ( "skipping policy %v as it is not valid" , policy . Name ) , "error" , err )
continue
2021-02-01 16:22:41 +05:30
}
2021-02-07 20:26:56 -08:00
matches := common . PolicyHasVariables ( * policy )
variable := common . RemoveDuplicateVariables ( matches )
if len ( matches ) > 0 && variablesString == "" && values . Variables == "" {
skipPolicy := SkippedPolicy {
Name : policy . GetName ( ) ,
Rules : policy . Spec . Rules ,
Variable : variable ,
}
skippedPolicies = append ( skippedPolicies , skipPolicy )
log . Log . V ( 3 ) . Info ( fmt . Sprintf ( "skipping policy %s" , policy . Name ) , "error" , fmt . Sprintf ( "policy have variable - %s" , variable ) )
continue
2021-02-01 16:22:41 +05:30
}
2021-02-07 20:26:56 -08:00
for _ , resource := range resources {
thisPolicyResourceValues := make ( map [ string ] string )
if len ( valuesMap [ policy . GetName ( ) ] ) != 0 && ! reflect . DeepEqual ( valuesMap [ policy . GetName ( ) ] [ resource . GetName ( ) ] , Resource { } ) {
thisPolicyResourceValues = valuesMap [ policy . GetName ( ) ] [ resource . GetName ( ) ] . Values
2021-02-01 16:22:41 +05:30
}
2021-02-07 20:26:56 -08:00
if len ( common . PolicyHasVariables ( * policy ) ) > 0 && len ( thisPolicyResourceValues ) == 0 {
return sanitizederror . NewWithError ( fmt . Sprintf ( "policy %s have variables. pass the values for the variables using set/values_file flag" , policy . Name ) , err )
2021-02-01 16:22:41 +05:30
}
2021-02-07 20:26:56 -08:00
2021-03-26 23:33:45 +05:30
ers , validateErs , _ , _ , err := common . ApplyPolicyOnResource ( policy , resource , "" , false , thisPolicyResourceValues , true , namespaceSelectorMap , false )
2021-02-07 20:26:56 -08:00
if err != nil {
return sanitizederror . NewWithError ( fmt . Errorf ( "failed to apply policy %v on resource %v" , policy . Name , resource . GetName ( ) ) . Error ( ) , err )
2021-02-01 16:22:41 +05:30
}
2021-02-07 20:26:56 -08:00
engineResponses = append ( engineResponses , ers ... )
validateEngineResponses = append ( validateEngineResponses , validateErs )
2021-02-01 16:22:41 +05:30
}
2021-02-07 20:26:56 -08:00
}
resultsMap := buildPolicyResults ( validateEngineResponses )
2021-03-05 05:39:18 +05:30
resultErr := printTestResult ( resultsMap , values . Results , rc )
2021-02-07 20:26:56 -08:00
if resultErr != nil {
return sanitizederror . NewWithError ( "Unable to genrate result. Error:" , resultErr )
}
2021-02-01 16:22:41 +05:30
return
}
2021-03-05 05:39:18 +05:30
func printTestResult ( resps map [ string ] [ ] interface { } , testResults [ ] TestResults , rc * resultCounts ) error {
2021-02-01 16:22:41 +05:30
printer := tableprinter . New ( os . Stdout )
table := [ ] * Table { }
boldRed := color . New ( color . FgRed ) . Add ( color . Bold )
2021-05-03 08:55:04 -04:00
boldYellow := color . New ( color . FgYellow ) . Add ( color . Bold )
2021-02-01 16:22:41 +05:30
boldFgCyan := color . New ( color . FgCyan ) . Add ( color . Bold )
for i , v := range testResults {
res := new ( Table )
2021-02-07 20:26:56 -08:00
res . ID = i + 1
res . Resource = boldFgCyan . Sprintf ( v . Resource ) + " with " + boldFgCyan . Sprintf ( v . Policy ) + "/" + boldFgCyan . Sprintf ( v . Rule )
2021-02-01 16:22:41 +05:30
n := resps [ v . Rule ]
data , _ := json . Marshal ( n )
valuesBytes , err := yaml . ToJSON ( data )
if err != nil {
2021-02-07 20:26:56 -08:00
return sanitizederror . NewWithError ( "failed to convert json" , err )
2021-02-01 16:22:41 +05:30
}
2021-02-03 18:24:50 +05:30
var r [ ] ReportResult
json . Unmarshal ( valuesBytes , & r )
2021-05-03 08:55:04 -04:00
res . Result = boldYellow . Sprintf ( "Not found" )
2021-02-03 18:24:50 +05:30
if len ( r ) != 0 {
var resource TestResults
for _ , testRes := range r {
if testRes . Resources [ 0 ] . Name == v . Resource {
2021-02-07 20:26:56 -08:00
resource . Policy = testRes . Policy
resource . Rule = testRes . Rule
resource . Status = testRes . Status
resource . Resource = testRes . Resources [ 0 ] . Name
2021-02-03 18:24:50 +05:30
if v == resource {
2021-02-07 20:26:56 -08:00
res . Result = "Pass"
2021-03-05 05:39:18 +05:30
rc . pass ++
} else {
2021-05-03 08:55:04 -04:00
res . Result = boldRed . Sprintf ( "Fail" )
2021-03-05 05:39:18 +05:30
rc . fail ++
2021-02-01 16:22:41 +05:30
}
}
}
}
2021-02-03 18:24:50 +05:30
table = append ( table , res )
2021-02-07 20:26:56 -08:00
}
2021-02-01 16:22:41 +05:30
printer . BorderTop , printer . BorderBottom , printer . BorderLeft , printer . BorderRight = true , true , true , true
printer . CenterSeparator = "│"
printer . ColumnSeparator = "│"
printer . RowSeparator = "─"
printer . RowCharLimit = 300
2021-02-07 20:26:56 -08:00
printer . RowLengthTitle = func ( rowsLength int ) bool {
2021-02-01 16:22:41 +05:30
return rowsLength > 10
}
printer . HeaderBgColor = tablewriter . BgBlackColor
printer . HeaderFgColor = tablewriter . FgGreenColor
printer . Print ( table )
return nil
2021-02-07 20:26:56 -08:00
}