From 7ffb049b7fac3527df335a25bf633e09dfedd6a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Charles-Edouard=20Br=C3=A9t=C3=A9ch=C3=A9?= Date: Tue, 18 Apr 2023 14:08:17 +0200 Subject: [PATCH] refactor: restructure cli test command (#6942) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: restructure cli test command Signed-off-by: Charles-Edouard Brétéché * fix Signed-off-by: Charles-Edouard Brétéché --------- Signed-off-by: Charles-Edouard Brétéché --- cmd/cli/kubectl-kyverno/main.go | 54 +- cmd/cli/kubectl-kyverno/test/filter.go | 50 +- cmd/cli/kubectl-kyverno/test/load.go | 145 +++ cmd/cli/kubectl-kyverno/test/output.go | 36 +- cmd/cli/kubectl-kyverno/test/test.go | 600 ++++++++++++ cmd/cli/kubectl-kyverno/test/test_command.go | 908 +++--------------- .../kubectl-kyverno/utils/common/common.go | 40 +- cmd/cli/kubectl-kyverno/utils/common/fetch.go | 9 +- 8 files changed, 950 insertions(+), 892 deletions(-) create mode 100644 cmd/cli/kubectl-kyverno/test/load.go create mode 100644 cmd/cli/kubectl-kyverno/test/test.go diff --git a/cmd/cli/kubectl-kyverno/main.go b/cmd/cli/kubectl-kyverno/main.go index 5a067eb04a..edab122c0b 100644 --- a/cmd/cli/kubectl-kyverno/main.go +++ b/cmd/cli/kubectl-kyverno/main.go @@ -10,10 +10,8 @@ import ( "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/oci" "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/test" "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/version" + "github.com/kyverno/kyverno/pkg/logging" "github.com/spf13/cobra" - "k8s.io/klog/v2" - "k8s.io/klog/v2/klogr" - "sigs.k8s.io/controller-runtime/pkg/log" ) const EnableExperimentalEnv = "KYVERNO_EXPERIMENTAL" @@ -21,30 +19,27 @@ const EnableExperimentalEnv = "KYVERNO_EXPERIMENTAL" func main() { cli := &cobra.Command{ Use: "kyverno", - Long: `To enable experimental commands, KYVERNO_EXPERIMENTAL should be configured with true or 1.`, + Long: "To enable experimental commands, KYVERNO_EXPERIMENTAL should be configured with true or 1.", Short: "Kubernetes Native Policy Management", } - - configurelog(cli) - - commands := []*cobra.Command{ - version.Command(), - apply.Command(), - test.Command(), - jp.Command(), - } - - if enableExperimental() { - commands = append(commands, oci.Command()) - } - - cli.AddCommand(commands...) - + configureLogs(cli) + registerCommands(cli) if err := cli.Execute(); err != nil { os.Exit(1) } } +func configureLogs(cli *cobra.Command) { + logging.InitFlags(nil) + cli.PersistentFlags().AddGoFlagSet(flag.CommandLine) + _ = cli.PersistentFlags().MarkHidden("alsologtostderr") + _ = cli.PersistentFlags().MarkHidden("logtostderr") + _ = cli.PersistentFlags().MarkHidden("log_dir") + _ = cli.PersistentFlags().MarkHidden("log_backtrace_at") + _ = cli.PersistentFlags().MarkHidden("stderrthreshold") + _ = cli.PersistentFlags().MarkHidden("vmodule") +} + func enableExperimental() bool { if b, err := strconv.ParseBool(os.Getenv(EnableExperimentalEnv)); err == nil { return b @@ -52,20 +47,9 @@ func enableExperimental() bool { return false } -func configurelog(cli *cobra.Command) { - // clear flags initialized in static dependencies - if flag.CommandLine.Lookup("log_dir") != nil { - flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError) +func registerCommands(cli *cobra.Command) { + cli.AddCommand(version.Command(), apply.Command(), test.Command(), jp.Command()) + if enableExperimental() { + cli.AddCommand(oci.Command()) } - - klog.InitFlags(nil) - cli.PersistentFlags().AddGoFlagSet(flag.CommandLine) - log.SetLogger(klogr.New()) - - _ = cli.PersistentFlags().MarkHidden("alsologtostderr") - _ = cli.PersistentFlags().MarkHidden("logtostderr") - _ = cli.PersistentFlags().MarkHidden("log_dir") - _ = cli.PersistentFlags().MarkHidden("log_backtrace_at") - _ = cli.PersistentFlags().MarkHidden("stderrthreshold") - _ = cli.PersistentFlags().MarkHidden("vmodule") } diff --git a/cmd/cli/kubectl-kyverno/test/filter.go b/cmd/cli/kubectl-kyverno/test/filter.go index 2961ff9b07..63b5947296 100644 --- a/cmd/cli/kubectl-kyverno/test/filter.go +++ b/cmd/cli/kubectl-kyverno/test/filter.go @@ -15,30 +15,32 @@ func noFilter(api.TestResults) bool { func parseFilter(in string) filter { var filters []filter - for _, t := range strings.Split(in, ",") { - parts := strings.Split(t, "=") - if len(parts) != 2 { - fmt.Printf("\n Invalid test-case-selector argument. Selecting all test cases. \n") - return noFilter - } - key := strings.TrimSpace(parts[0]) - value := strings.TrimSpace(parts[1]) - switch key { - case "policy": - filters = append(filters, func(r api.TestResults) bool { - return r.Policy == "" || r.Policy == value - }) - case "rule": - filters = append(filters, func(r api.TestResults) bool { - return r.Rule == "" || r.Rule == value - }) - case "resource": - filters = append(filters, func(r api.TestResults) bool { - return r.Resource == "" || r.Resource == value - }) - default: - fmt.Printf("\n Invalid parameter. Parameter can only be policy, rule or resource. Selecting all test cases \n") - return noFilter + if in != "" { + for _, t := range strings.Split(in, ",") { + parts := strings.Split(t, "=") + if len(parts) != 2 { + fmt.Printf("\n Invalid test-case-selector argument (%s). Selecting all test cases. \n", t) + return noFilter + } + key := strings.TrimSpace(parts[0]) + value := strings.TrimSpace(parts[1]) + switch key { + case "policy": + filters = append(filters, func(r api.TestResults) bool { + return r.Policy == "" || r.Policy == value + }) + case "rule": + filters = append(filters, func(r api.TestResults) bool { + return r.Rule == "" || r.Rule == value + }) + case "resource": + filters = append(filters, func(r api.TestResults) bool { + return r.Resource == "" || r.Resource == value + }) + default: + fmt.Printf("\n Invalid parameter. Parameter can only be policy, rule or resource. Selecting all test cases \n") + return noFilter + } } } return func(r api.TestResults) bool { diff --git a/cmd/cli/kubectl-kyverno/test/load.go b/cmd/cli/kubectl-kyverno/test/load.go new file mode 100644 index 0000000000..052872a85a --- /dev/null +++ b/cmd/cli/kubectl-kyverno/test/load.go @@ -0,0 +1,145 @@ +package test + +import ( + "fmt" + "io" + "net/url" + "os" + "path" + "path/filepath" + "sort" + "strings" + + "github.com/go-git/go-billy/v5" + "github.com/go-git/go-billy/v5/memfs" + sanitizederror "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/utils/sanitizedError" + gitutils "github.com/kyverno/kyverno/pkg/utils/git" + "k8s.io/apimachinery/pkg/util/yaml" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +type policy struct { + bytes []byte + resourcePath string +} + +func loadTests( + dirPath []string, + fileName string, + gitBranch string, +) (billy.Filesystem, []policy, []error) { + var policies []policy + var errors []error + if strings.Contains(dirPath[0], "https://") { + fs := memfs.New() + if gitURL, err := url.Parse(dirPath[0]); err != nil { + errors = append(errors, sanitizederror.NewWithError("failed to parse URL", err)) + } else { + pathElems := strings.Split(gitURL.Path[1:], "/") + if len(pathElems) <= 1 { + err := fmt.Errorf("invalid URL path %s - expected https://github.com/:owner/:repository/:branch (without --git-branch flag) OR https://github.com/:owner/:repository/:directory (with --git-branch flag)", gitURL.Path) + fmt.Printf("Error: failed to parse URL \nCause: %s\n", err) + os.Exit(1) + } + gitURL.Path = strings.Join([]string{pathElems[0], pathElems[1]}, "/") + repoURL := gitURL.String() + var gitPathToYamls string + if gitBranch == "" { + gitPathToYamls = "/" + if string(dirPath[0][len(dirPath[0])-1]) == "/" { + gitBranch = strings.ReplaceAll(dirPath[0], repoURL+"/", "") + } else { + gitBranch = strings.ReplaceAll(dirPath[0], repoURL, "") + } + if gitBranch == "" { + gitBranch = "main" + } else if string(gitBranch[0]) == "/" { + gitBranch = gitBranch[1:] + } + } else { + if string(dirPath[0][len(dirPath[0])-1]) == "/" { + gitPathToYamls = strings.ReplaceAll(dirPath[0], repoURL+"/", "/") + } else { + gitPathToYamls = strings.ReplaceAll(dirPath[0], repoURL, "/") + } + } + _, cloneErr := gitutils.Clone(repoURL, fs, gitBranch) + 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) + os.Exit(1) + } + if policyYamls, err := gitutils.ListYamls(fs, gitPathToYamls); err != nil { + errors = append(errors, sanitizederror.NewWithError("failed to list YAMLs in repository", err)) + } else { + sort.Strings(policyYamls) + for _, yamlFilePath := range policyYamls { + file, err := fs.Open(yamlFilePath) + if err != nil { + errors = append(errors, sanitizederror.NewWithError("Error: failed to open file", err)) + continue + } + if path.Base(file.Name()) == fileName { + policyresoucePath := strings.Trim(yamlFilePath, fileName) + bytes, err := io.ReadAll(file) + if err != nil { + errors = append(errors, sanitizederror.NewWithError("Error: failed to read file", err)) + continue + } + policyBytes, err := yaml.ToJSON(bytes) + if err != nil { + errors = append(errors, sanitizederror.NewWithError("failed to convert to JSON", err)) + continue + } + policies = append(policies, policy{ + bytes: policyBytes, + resourcePath: policyresoucePath, + }) + } + } + } + } + return fs, policies, errors + } else { + path := filepath.Clean(dirPath[0]) + policies, errors = loadLocalTest(path, fileName) + return nil, policies, errors + } +} + +func loadLocalTest( + path string, + fileName string, +) ([]policy, []error) { + var policies []policy + var errors []error + files, err := os.ReadDir(path) + if err != nil { + errors = append(errors, fmt.Errorf("failed to read %v: %v", path, err.Error())) + } else { + for _, file := range files { + if file.IsDir() { + ps, errs := loadLocalTest(filepath.Join(path, file.Name()), fileName) + policies = append(policies, ps...) + errors = append(errors, errs...) + } else if file.Name() == fileName { + // We accept the risk of including files here as we read the test dir only. + yamlFile, err := os.ReadFile(filepath.Join(path, file.Name())) // #nosec G304 + if err != nil { + errors = append(errors, sanitizederror.NewWithError("unable to read yaml", err)) + continue + } + valuesBytes, err := yaml.ToJSON(yamlFile) + if err != nil { + errors = append(errors, sanitizederror.NewWithError("failed to convert json", err)) + continue + } + policies = append(policies, policy{ + bytes: valuesBytes, + resourcePath: path, + }) + } + } + } + return policies, errors +} diff --git a/cmd/cli/kubectl-kyverno/test/output.go b/cmd/cli/kubectl-kyverno/test/output.go index e8b429bf25..8d884f8d1f 100644 --- a/cmd/cli/kubectl-kyverno/test/output.go +++ b/cmd/cli/kubectl-kyverno/test/output.go @@ -9,32 +9,42 @@ import ( ) var ( - boldGreen = color.New(color.FgGreen).Add(color.Bold) - boldRed = color.New(color.FgRed).Add(color.Bold) - boldYellow = color.New(color.FgYellow).Add(color.Bold) - boldFgCyan = color.New(color.FgCyan).Add(color.Bold) + boldGreen *color.Color + boldRed *color.Color + boldYellow *color.Color + boldFgCyan *color.Color + headerBgColor int + headerFgColor int ) -func colorize(noColor bool, color *color.Color, format string, a ...interface{}) string { - if noColor { - return format +func initColors(noColor bool) { + toggleColor := func(c *color.Color) *color.Color { + if noColor { + c.DisableColor() + } + return c + } + boldGreen = toggleColor(color.New(color.FgGreen).Add(color.Bold)) + boldRed = toggleColor(color.New(color.FgRed).Add(color.Bold)) + boldYellow = toggleColor(color.New(color.FgYellow).Add(color.Bold)) + boldFgCyan = toggleColor(color.New(color.FgCyan).Add(color.Bold)) + if !noColor { + headerBgColor = tablewriter.BgBlackColor + headerFgColor = tablewriter.FgGreenColor } - return color.Sprintf(format, a...) } -func newTablePrinter(noColor bool) *tableprinter.Printer { +func newTablePrinter() *tableprinter.Printer { printer := tableprinter.New(os.Stdout) printer.BorderTop, printer.BorderBottom, printer.BorderLeft, printer.BorderRight = true, true, true, true printer.CenterSeparator = "│" printer.ColumnSeparator = "│" printer.RowSeparator = "─" printer.RowCharLimit = 300 + printer.HeaderBgColor = headerBgColor + printer.HeaderFgColor = headerFgColor printer.RowLengthTitle = func(rowsLength int) bool { return rowsLength > 10 } - if !noColor { - printer.HeaderBgColor = tablewriter.BgBlackColor - printer.HeaderFgColor = tablewriter.FgGreenColor - } return printer } diff --git a/cmd/cli/kubectl-kyverno/test/test.go b/cmd/cli/kubectl-kyverno/test/test.go new file mode 100644 index 0000000000..ac16edcaf9 --- /dev/null +++ b/cmd/cli/kubectl-kyverno/test/test.go @@ -0,0 +1,600 @@ +package test + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "regexp" + "strings" + + "github.com/go-git/go-billy/v5" + kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1" + "github.com/kyverno/kyverno/api/kyverno/v1beta1" + policyreportv1alpha2 "github.com/kyverno/kyverno/api/policyreport/v1alpha2" + "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/test/api" + "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/utils/common" + sanitizederror "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/utils/sanitizedError" + "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/utils/store" + "github.com/kyverno/kyverno/pkg/autogen" + "github.com/kyverno/kyverno/pkg/background/generate" + "github.com/kyverno/kyverno/pkg/clients/dclient" + engineapi "github.com/kyverno/kyverno/pkg/engine/api" + "github.com/kyverno/kyverno/pkg/openapi" + policy2 "github.com/kyverno/kyverno/pkg/policy" + "golang.org/x/exp/slices" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +func applyPoliciesFromPath( + fs billy.Filesystem, + policyBytes []byte, + isGit bool, + policyResourcePath string, + rc *resultCounts, + openApiManager openapi.Manager, + filter filter, + auditWarn bool, +) (map[string]policyreportv1alpha2.PolicyReportResult, []api.TestResults, error) { + engineResponses := make([]engineapi.EngineResponse, 0) + var dClient dclient.Interface + values := &api.Test{} + var variablesString string + var resultCounts common.ResultCounts + + store.SetMock(true) + if err := json.Unmarshal(policyBytes, values); err != nil { + return nil, nil, sanitizederror.NewWithError("failed to decode yaml", err) + } + + var filteredResults []api.TestResults + for _, res := range values.Results { + if filter(res) { + filteredResults = append(filteredResults, res) + } + } + values.Results = filteredResults + + if len(values.Results) == 0 { + return nil, nil, nil + } + + fmt.Printf("\nExecuting %s...\n", values.Name) + valuesFile := values.Variables + userInfoFile := values.UserInfo + + variables, globalValMap, valuesMap, namespaceSelectorMap, subresources, err := common.GetVariable(variablesString, values.Variables, fs, isGit, policyResourcePath) + if err != nil { + if !sanitizederror.IsErrorSanitized(err) { + return nil, nil, sanitizederror.NewWithError("failed to decode yaml", err) + } + return nil, nil, err + } + + // get the user info as request info from a different file + var userInfo v1beta1.RequestInfo + + if userInfoFile != "" { + userInfo, err = common.GetUserInfoFromPath(fs, userInfoFile, isGit, policyResourcePath) + if err != nil { + fmt.Printf("Error: failed to load request info\nCause: %s\n", err) + os.Exit(1) + } + } + + policyFullPath := getFullPath(values.Policies, policyResourcePath, isGit) + resourceFullPath := getFullPath(values.Resources, policyResourcePath, isGit) + + for i, result := range values.Results { + arrPatchedResource := []string{result.PatchedResource} + arrGeneratedResource := []string{result.GeneratedResource} + arrCloneSourceResource := []string{result.CloneSourceResource} + + patchedResourceFullPath := getFullPath(arrPatchedResource, policyResourcePath, isGit) + generatedResourceFullPath := getFullPath(arrGeneratedResource, policyResourcePath, isGit) + CloneSourceResourceFullPath := getFullPath(arrCloneSourceResource, policyResourcePath, isGit) + + values.Results[i].PatchedResource = patchedResourceFullPath[0] + values.Results[i].GeneratedResource = generatedResourceFullPath[0] + values.Results[i].CloneSourceResource = CloneSourceResourceFullPath[0] + } + + policies, err := common.GetPoliciesFromPaths(fs, policyFullPath, isGit, policyResourcePath) + if err != nil { + fmt.Printf("Error: failed to load policies\nCause: %s\n", err) + os.Exit(1) + } + + var filteredPolicies []kyvernov1.PolicyInterface + for _, p := range policies { + for _, res := range values.Results { + if p.GetName() == res.Policy { + filteredPolicies = append(filteredPolicies, p) + break + } + } + } + + ruleToCloneSourceResource := map[string]string{} + for _, p := range filteredPolicies { + var filteredRules []kyvernov1.Rule + + for _, rule := range autogen.ComputeRules(p) { + for _, res := range values.Results { + if rule.Name == res.Rule { + filteredRules = append(filteredRules, rule) + if rule.HasGenerate() { + ruleUnstr, err := generate.GetUnstrRule(rule.Generation.DeepCopy()) + if err != nil { + fmt.Printf("Error: failed to get unstructured rule\nCause: %s\n", err) + break + } + + genClone, _, err := unstructured.NestedMap(ruleUnstr.Object, "clone") + if err != nil { + fmt.Printf("Error: failed to read data\nCause: %s\n", err) + break + } + + if len(genClone) != 0 { + ruleToCloneSourceResource[rule.Name] = res.CloneSourceResource + } + } + break + } + } + } + p.GetSpec().SetRules(filteredRules) + } + policies = filteredPolicies + + err = common.PrintMutatedPolicy(policies) + if err != nil { + return nil, nil, sanitizederror.NewWithError("failed to print mutated policy", err) + } + + resources, err := common.GetResourceAccordingToResourcePath(fs, resourceFullPath, false, policies, dClient, "", false, isGit, policyResourcePath) + if err != nil { + fmt.Printf("Error: failed to load resources\nCause: %s\n", err) + os.Exit(1) + } + + checkableResources := selectResourcesForCheck(resources, values) + + msgPolicies := "1 policy" + if len(policies) > 1 { + msgPolicies = fmt.Sprintf("%d policies", len(policies)) + } + + msgResources := "1 resource" + if len(checkableResources) > 1 { + msgResources = fmt.Sprintf("%d resources", len(checkableResources)) + } + + if len(policies) > 0 && len(checkableResources) > 0 { + fmt.Printf("applying %s to %s... \n", msgPolicies, msgResources) + } + + for _, policy := range policies { + _, err := policy2.Validate(policy, nil, nil, true, openApiManager) + if err != nil { + log.Log.Error(err, "skipping invalid policy", "name", policy.GetName()) + continue + } + + matches := common.HasVariables(policy) + variable := common.RemoveDuplicateAndObjectVariables(matches) + + if len(variable) > 0 { + if len(variables) == 0 { + // check policy in variable file + if valuesFile == "" || valuesMap[policy.GetName()] == nil { + fmt.Printf("test skipped for policy %v (as required variables are not provided by the users) \n \n", policy.GetName()) + } + } + } + + kindOnwhichPolicyIsApplied := common.GetKindsFromPolicy(policy, subresources, dClient) + + for _, resource := range checkableResources { + thisPolicyResourceValues, err := common.CheckVariableForPolicy(valuesMap, globalValMap, policy.GetName(), resource.GetName(), resource.GetKind(), variables, kindOnwhichPolicyIsApplied, variable) + if err != nil { + return nil, nil, sanitizederror.NewWithError(fmt.Sprintf("policy `%s` have variables. pass the values for the variables for resource `%s` using set/values_file flag", policy.GetName(), resource.GetName()), err) + } + applyPolicyConfig := common.ApplyPolicyConfig{ + Policy: policy, + Resource: resource, + MutateLogPath: "", + Variables: thisPolicyResourceValues, + UserInfo: userInfo, + PolicyReport: true, + NamespaceSelectorMap: namespaceSelectorMap, + Rc: &resultCounts, + RuleToCloneSourceResource: ruleToCloneSourceResource, + Client: dClient, + Subresources: subresources, + } + ers, err := common.ApplyPolicyOnResource(applyPolicyConfig) + if err != nil { + return nil, nil, sanitizederror.NewWithError(fmt.Errorf("failed to apply policy %v on resource %v", policy.GetName(), resource.GetName()).Error(), err) + } + engineResponses = append(engineResponses, ers...) + } + } + resultsMap, testResults := buildPolicyResults(engineResponses, values.Results, policyResourcePath, fs, isGit, auditWarn) + return resultsMap, testResults, nil +} + +func getFullPath(paths []string, policyResourcePath string, isGit bool) []string { + var pols []string + var pol string + if !isGit { + for _, path := range paths { + pol = filepath.Join(policyResourcePath, path) + pols = append(pols, pol) + } + return pols + } + return paths +} + +func selectResourcesForCheck(resources []*unstructured.Unstructured, values *api.Test) []*unstructured.Unstructured { + res, _, _ := selectResourcesForCheckInternal(resources, values) + return res +} + +// selectResourcesForCheckInternal internal method to test duplicates and unused +func selectResourcesForCheckInternal(resources []*unstructured.Unstructured, values *api.Test) ([]*unstructured.Unstructured, int, int) { + var duplicates int + var unused int + uniqResources := make(map[string]*unstructured.Unstructured) + + for i := range resources { + r := resources[i] + key := fmt.Sprintf("%s/%s/%s", r.GetKind(), r.GetName(), r.GetNamespace()) + if _, ok := uniqResources[key]; ok { + fmt.Println("skipping duplicate resource, resource :", r) + duplicates++ + } else { + uniqResources[key] = r + } + } + + selectedResources := map[string]*unstructured.Unstructured{} + for key := range uniqResources { + r := uniqResources[key] + for _, res := range values.Results { + if res.Kind == r.GetKind() { + for _, testr := range res.Resources { + if r.GetName() == testr { + selectedResources[key] = r + } + } + if r.GetName() == res.Resource { + selectedResources[key] = r + } + } + } + } + + var checkableResources []*unstructured.Unstructured + + for key := range selectedResources { + checkableResources = append(checkableResources, selectedResources[key]) + delete(uniqResources, key) + } + for _, r := range uniqResources { + fmt.Println("skipping unused resource, resource :", r) + unused++ + } + return checkableResources, duplicates, unused +} + +func buildPolicyResults( + engineResponses []engineapi.EngineResponse, + testResults []api.TestResults, + policyResourcePath string, + fs billy.Filesystem, + isGit bool, + auditWarn bool, +) (map[string]policyreportv1alpha2.PolicyReportResult, []api.TestResults) { + results := map[string]policyreportv1alpha2.PolicyReportResult{} + + for _, resp := range engineResponses { + policyName := resp.Policy.GetName() + resourceName := resp.Resource.GetName() + resourceKind := resp.Resource.GetKind() + resourceNamespace := resp.Resource.GetNamespace() + policyNamespace := resp.Policy.GetNamespace() + + var rules []string + for _, rule := range resp.PolicyResponse.Rules { + rules = append(rules, rule.Name()) + } + + result := policyreportv1alpha2.PolicyReportResult{ + Policy: policyName, + Resources: []corev1.ObjectReference{ + { + Name: resourceName, + }, + }, + Message: buildMessage(resp), + } + + var patchedResourcePath []string + for i, test := range testResults { + var userDefinedPolicyNamespace string + var userDefinedPolicyName string + found, err := isNamespacedPolicy(test.Policy) + if err != nil { + log.Log.V(3).Info("error while checking the policy is namespaced or not", "policy: ", test.Policy, "error: ", err) + continue + } + + if found { + userDefinedPolicyNamespace, userDefinedPolicyName = getUserDefinedPolicyNameAndNamespace(test.Policy) + test.Policy = userDefinedPolicyName + } + + if test.Resources != nil { + if test.Policy == policyName { + // results[].namespace value implicit set same as metadata.namespace until and unless + // user provides explicit values for results[].namespace in test yaml file. + if test.Namespace == "" { + test.Namespace = resourceNamespace + testResults[i].Namespace = resourceNamespace + } + for _, resource := range test.Resources { + if resource == resourceName { + var resultsKey string + resultsKey = GetResultKeyAccordingToTestResults(userDefinedPolicyNamespace, test.Policy, test.Rule, test.Namespace, test.Kind, resource) + if !slices.Contains(rules, test.Rule) { + if !slices.Contains(rules, "autogen-"+test.Rule) { + if !slices.Contains(rules, "autogen-cronjob-"+test.Rule) { + result.Result = policyreportv1alpha2.StatusSkip + } else { + testResults[i].AutoGeneratedRule = "autogen-cronjob" + test.Rule = "autogen-cronjob-" + test.Rule + resultsKey = GetResultKeyAccordingToTestResults(userDefinedPolicyNamespace, test.Policy, test.Rule, test.Namespace, test.Kind, resource) + } + } else { + testResults[i].AutoGeneratedRule = "autogen" + test.Rule = "autogen-" + test.Rule + resultsKey = GetResultKeyAccordingToTestResults(userDefinedPolicyNamespace, test.Policy, test.Rule, test.Namespace, test.Kind, resource) + } + + if results[resultsKey].Result == "" { + result.Result = policyreportv1alpha2.StatusSkip + results[resultsKey] = result + } + } + + patchedResourcePath = append(patchedResourcePath, test.PatchedResource) + if _, ok := results[resultsKey]; !ok { + results[resultsKey] = result + } + } + } + } + } + if test.Resource != "" { + if test.Policy == policyName && test.Resource == resourceName { + var resultsKey string + resultsKey = GetResultKeyAccordingToTestResults(userDefinedPolicyNamespace, test.Policy, test.Rule, test.Namespace, test.Kind, test.Resource) + if !slices.Contains(rules, test.Rule) { + if !slices.Contains(rules, "autogen-"+test.Rule) { + if !slices.Contains(rules, "autogen-cronjob-"+test.Rule) { + result.Result = policyreportv1alpha2.StatusSkip + } else { + testResults[i].AutoGeneratedRule = "autogen-cronjob" + test.Rule = "autogen-cronjob-" + test.Rule + resultsKey = GetResultKeyAccordingToTestResults(userDefinedPolicyNamespace, test.Policy, test.Rule, test.Namespace, test.Kind, test.Resource) + } + } else { + testResults[i].AutoGeneratedRule = "autogen" + test.Rule = "autogen-" + test.Rule + resultsKey = GetResultKeyAccordingToTestResults(userDefinedPolicyNamespace, test.Policy, test.Rule, test.Namespace, test.Kind, test.Resource) + } + + if results[resultsKey].Result == "" { + result.Result = policyreportv1alpha2.StatusSkip + results[resultsKey] = result + } + } + + patchedResourcePath = append(patchedResourcePath, test.PatchedResource) + if _, ok := results[resultsKey]; !ok { + results[resultsKey] = result + } + } + } + + for _, rule := range resp.PolicyResponse.Rules { + if rule.RuleType() != engineapi.Generation || test.Rule != rule.Name() { + continue + } + + var resultsKey []string + var resultKey string + var result policyreportv1alpha2.PolicyReportResult + resultsKey = GetAllPossibleResultsKey(policyNamespace, policyName, rule.Name(), resourceNamespace, resourceKind, resourceName) + for _, key := range resultsKey { + if val, ok := results[key]; ok { + result = val + resultKey = key + } else { + continue + } + + if rule.Status() == engineapi.RuleStatusSkip { + result.Result = policyreportv1alpha2.StatusSkip + } else if rule.Status() == engineapi.RuleStatusError { + result.Result = policyreportv1alpha2.StatusError + } else { + var x string + result.Result = policyreportv1alpha2.StatusFail + x = getAndCompareResource(test.GeneratedResource, rule.GeneratedResource(), isGit, policyResourcePath, fs, true) + if x == "pass" { + result.Result = policyreportv1alpha2.StatusPass + } + } + results[resultKey] = result + } + } + + for _, rule := range resp.PolicyResponse.Rules { + if rule.RuleType() != engineapi.Mutation || test.Rule != rule.Name() { + continue + } + + var resultsKey []string + var resultKey string + var result policyreportv1alpha2.PolicyReportResult + resultsKey = GetAllPossibleResultsKey(policyNamespace, policyName, rule.Name(), resourceNamespace, resourceKind, resourceName) + for _, key := range resultsKey { + if val, ok := results[key]; ok { + result = val + resultKey = key + } else { + continue + } + + if rule.Status() == engineapi.RuleStatusSkip { + result.Result = policyreportv1alpha2.StatusSkip + } else if rule.Status() == engineapi.RuleStatusError { + result.Result = policyreportv1alpha2.StatusError + } else { + var x string + for _, path := range patchedResourcePath { + result.Result = policyreportv1alpha2.StatusFail + x = getAndCompareResource(path, resp.PatchedResource, isGit, policyResourcePath, fs, false) + if x == "pass" { + result.Result = policyreportv1alpha2.StatusPass + break + } + } + } + + results[resultKey] = result + } + } + + for _, rule := range resp.PolicyResponse.Rules { + if rule.RuleType() != engineapi.Validation && rule.RuleType() != engineapi.ImageVerify || test.Rule != rule.Name() { + continue + } + + var resultsKey []string + var resultKey string + var result policyreportv1alpha2.PolicyReportResult + resultsKey = GetAllPossibleResultsKey(policyNamespace, policyName, rule.Name(), resourceNamespace, resourceKind, resourceName) + for _, key := range resultsKey { + if val, ok := results[key]; ok { + result = val + resultKey = key + } else { + continue + } + + ann := resp.Policy.GetAnnotations() + if rule.Status() == engineapi.RuleStatusSkip { + result.Result = policyreportv1alpha2.StatusSkip + } else if rule.Status() == engineapi.RuleStatusError { + result.Result = policyreportv1alpha2.StatusError + } else if rule.Status() == engineapi.RuleStatusPass { + result.Result = policyreportv1alpha2.StatusPass + } else if rule.Status() == engineapi.RuleStatusFail { + if scored, ok := ann[kyvernov1.AnnotationPolicyScored]; ok && scored == "false" { + result.Result = policyreportv1alpha2.StatusWarn + } else if auditWarn && resp.GetValidationFailureAction().Audit() { + result.Result = policyreportv1alpha2.StatusWarn + } else { + result.Result = policyreportv1alpha2.StatusFail + } + } else { + fmt.Println(rule) + } + + results[resultKey] = result + } + } + } + } + return results, testResults +} + +func GetAllPossibleResultsKey(policyNamespace, policy, rule, resourceNamespace, kind, resource string) []string { + var resultsKey []string + resultKey1 := fmt.Sprintf("%s-%s-%s-%s", policy, rule, kind, resource) + resultKey2 := fmt.Sprintf("%s-%s-%s-%s-%s", policy, rule, resourceNamespace, kind, resource) + resultKey3 := fmt.Sprintf("%s-%s-%s-%s-%s", policyNamespace, policy, rule, kind, resource) + resultKey4 := fmt.Sprintf("%s-%s-%s-%s-%s-%s", policyNamespace, policy, rule, resourceNamespace, kind, resource) + resultsKey = append(resultsKey, resultKey1, resultKey2, resultKey3, resultKey4) + return resultsKey +} + +func GetResultKeyAccordingToTestResults(policyNs, policy, rule, resourceNs, kind, resource string) string { + var resultKey string + resultKey = fmt.Sprintf("%s-%s-%s-%s", policy, rule, kind, resource) + + if policyNs != "" && resourceNs != "" { + resultKey = fmt.Sprintf("%s-%s-%s-%s-%s-%s", policyNs, policy, rule, resourceNs, kind, resource) + } else if policyNs != "" { + resultKey = fmt.Sprintf("%s-%s-%s-%s-%s", policyNs, policy, rule, kind, resource) + } else if resourceNs != "" { + resultKey = fmt.Sprintf("%s-%s-%s-%s-%s", policy, rule, resourceNs, kind, resource) + } + return resultKey +} + +func isNamespacedPolicy(policyNames string) (bool, error) { + return regexp.MatchString("^[a-z]*/[a-z]*", policyNames) +} + +// getAndCompareResource --> Get the patchedResource or generatedResource from the path provided by user +// And compare this resource with engine generated resource. +func getAndCompareResource(path string, engineResource unstructured.Unstructured, isGit bool, policyResourcePath string, fs billy.Filesystem, isGenerate bool) string { + var status string + resourceType := "patchedResource" + if isGenerate { + resourceType = "generatedResource" + } + + userResource, err := common.GetResourceFromPath(fs, path, isGit, policyResourcePath, resourceType) + if err != nil { + fmt.Printf("Error: failed to load resources\nCause: %s\n", err) + return "" + } + matched, err := generate.ValidateResourceWithPattern(log.Log, engineResource.UnstructuredContent(), userResource.UnstructuredContent()) + if err != nil { + log.Log.V(3).Info(resourceType+" mismatch", "error", err.Error()) + status = "fail" + } else if matched == "" { + status = "pass" + } + return status +} + +func buildMessage(resp engineapi.EngineResponse) string { + var messages []string + for _, ruleResp := range resp.PolicyResponse.Rules { + message := strings.TrimSpace(ruleResp.Message()) + if message != "" { + messages = append(messages, message) + } + } + return strings.Join(messages, ",") +} + +func getUserDefinedPolicyNameAndNamespace(policyName string) (string, string) { + if strings.Contains(policyName, "/") { + parts := strings.Split(policyName, "/") + namespace := parts[0] + policy := parts[1] + return namespace, policy + } + return "", policyName +} diff --git a/cmd/cli/kubectl-kyverno/test/test_command.go b/cmd/cli/kubectl-kyverno/test/test_command.go index 85e12f8a2d..ccf1de752c 100644 --- a/cmd/cli/kubectl-kyverno/test/test_command.go +++ b/cmd/cli/kubectl-kyverno/test/test_command.go @@ -1,39 +1,16 @@ package test import ( - "encoding/json" "fmt" - "io" - "net/url" "os" - "path" - "path/filepath" - "regexp" - "sort" - "strings" - "github.com/go-git/go-billy/v5" - "github.com/go-git/go-billy/v5/memfs" - kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1" - "github.com/kyverno/kyverno/api/kyverno/v1beta1" policyreportv1alpha2 "github.com/kyverno/kyverno/api/policyreport/v1alpha2" "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/test/api" "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/test/manifest" - "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/utils/common" sanitizederror "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/utils/sanitizedError" "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/utils/store" - "github.com/kyverno/kyverno/pkg/autogen" - "github.com/kyverno/kyverno/pkg/background/generate" - "github.com/kyverno/kyverno/pkg/clients/dclient" - engineapi "github.com/kyverno/kyverno/pkg/engine/api" "github.com/kyverno/kyverno/pkg/openapi" - policy2 "github.com/kyverno/kyverno/pkg/policy" - gitutils "github.com/kyverno/kyverno/pkg/utils/git" "github.com/spf13/cobra" - "golang.org/x/exp/slices" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/util/yaml" "sigs.k8s.io/controller-runtime/pkg/log" ) @@ -42,7 +19,7 @@ func Command() *cobra.Command { var cmd *cobra.Command var testCase string var fileName, gitBranch string - var registryAccess, failOnly, removeColor, manifestValidate, manifestMutate bool + var registryAccess, failOnly, removeColor, manifestValidate, manifestMutate, compact bool cmd = &cobra.Command{ Use: "test [flags]\n kyverno test --git-branch \n kyverno test --manifest-mutate > kyverno-test.yaml\n kyverno test --manifest-validate > kyverno-test.yaml", // Args: cobra.ExactArgs(1), @@ -50,6 +27,7 @@ func Command() *cobra.Command { Long: longHelp, Example: exampleHelp, RunE: func(cmd *cobra.Command, dirPath []string) (err error) { + initColors(removeColor) defer func() { if err != nil { if !sanitizederror.IsErrorSanitized(err) { @@ -64,7 +42,7 @@ func Command() *cobra.Command { manifest.PrintValidate() } else { store.SetRegistryAccess(registryAccess) - _, err = testCommandExecute(dirPath, fileName, gitBranch, testCase, failOnly, removeColor, false) + _, err = testCommandExecute(dirPath, fileName, gitBranch, testCase, failOnly, false, compact) if err != nil { log.Log.V(3).Info("a directory is required") return err @@ -81,15 +59,49 @@ func Command() *cobra.Command { cmd.Flags().BoolVarP(®istryAccess, "registry", "", false, "If set to true, access the image registry using local docker credentials to populate external data") cmd.Flags().BoolVarP(&failOnly, "fail-only", "", false, "If set to true, display all the failing test only as output for the test command") cmd.Flags().BoolVarP(&removeColor, "remove-color", "", false, "Remove any color from output") + cmd.Flags().BoolVarP(&compact, "compact", "", true, "Does not show detailed results") return cmd } type Table struct { - ID int `header:"#"` - Policy string `header:"policy"` - Rule string `header:"rule"` - Resource string `header:"resource"` - Result string `header:"result"` + rows []Row +} + +func (t *Table) Rows(compact bool) interface{} { + if !compact { + return t.rows + } + var rows []CompactRow + for _, row := range t.rows { + rows = append(rows, row.CompactRow) + } + return rows +} + +func (t *Table) AddFailed(rows ...Row) { + for _, row := range rows { + if row.isFailure { + t.rows = append(t.rows, row) + } + } +} + +func (t *Table) Add(rows ...Row) { + t.rows = append(t.rows, rows...) +} + +type CompactRow struct { + isFailure bool + ID int `header:"id"` + Policy string `header:"policy"` + Rule string `header:"rule"` + Resource string `header:"resource"` + Result string `header:"result"` +} + +type Row struct { + CompactRow `header:"inline"` + Message string `header:"message"` } type resultCounts struct { @@ -98,16 +110,14 @@ type resultCounts struct { Fail int } -var ftable []Table - func testCommandExecute( dirPath []string, fileName string, gitBranch string, testCase string, failOnly bool, - removeColor bool, auditWarn bool, + compact bool, ) (rc *resultCounts, err error) { // check input dir if len(dirPath) == 0 { @@ -120,766 +130,70 @@ func testCommandExecute( if err != nil { return rc, fmt.Errorf("unable to create open api controller, %w", err) } - var errors []error - fs := memfs.New() + // load tests + fs, policies, errors := loadTests(dirPath, fileName, gitBranch) + if len(policies) == 0 { + fmt.Printf("\n No test yamls available \n") + } rc = &resultCounts{} - var testYamlCount int - if strings.Contains(dirPath[0], "https://") { - gitURL, err := url.Parse(dirPath[0]) - if err != nil { - return rc, sanitizederror.NewWithError("failed to parse URL", err) - } - - pathElems := strings.Split(gitURL.Path[1:], "/") - if len(pathElems) <= 1 { - err := fmt.Errorf("invalid URL path %s - expected https://github.com/:owner/:repository/:branch (without --git-branch flag) OR https://github.com/:owner/:repository/:directory (with --git-branch flag)", gitURL.Path) - fmt.Printf("Error: failed to parse URL \nCause: %s\n", err) - os.Exit(1) - } - - gitURL.Path = strings.Join([]string{pathElems[0], pathElems[1]}, "/") - repoURL := gitURL.String() - - var gitPathToYamls string - if gitBranch == "" { - gitPathToYamls = "/" - - if string(dirPath[0][len(dirPath[0])-1]) == "/" { - gitBranch = strings.ReplaceAll(dirPath[0], repoURL+"/", "") - } else { - gitBranch = strings.ReplaceAll(dirPath[0], repoURL, "") - } - - if gitBranch == "" { - gitBranch = "main" - } else if string(gitBranch[0]) == "/" { - gitBranch = gitBranch[1:] - } + var table Table + for _, p := range policies { + if reports, tests, err := applyPoliciesFromPath( + fs, + p.bytes, + fs != nil, + p.resourcePath, + rc, + openApiManager, + filter, + auditWarn, + ); err != nil { + return rc, sanitizederror.NewWithError("failed to apply test command", err) + } else if t, err := printTestResult(reports, tests, rc, failOnly, compact); err != nil { + return rc, sanitizederror.NewWithError("failed to print test result:", err) } else { - if string(dirPath[0][len(dirPath[0])-1]) == "/" { - gitPathToYamls = strings.ReplaceAll(dirPath[0], repoURL+"/", "/") - } else { - gitPathToYamls = strings.ReplaceAll(dirPath[0], repoURL, "/") - } - } - - _, cloneErr := gitutils.Clone(repoURL, fs, gitBranch) - 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) - os.Exit(1) - } - - policyYamls, err := gitutils.ListYamls(fs, gitPathToYamls) - if err != nil { - return rc, sanitizederror.NewWithError("failed to list YAMLs in repository", err) - } - sort.Strings(policyYamls) - - for _, yamlFilePath := range policyYamls { - file, err := fs.Open(yamlFilePath) - if err != nil { - errors = append(errors, sanitizederror.NewWithError("Error: failed to open file", err)) - continue - } - - if path.Base(file.Name()) == fileName { - testYamlCount++ - policyresoucePath := strings.Trim(yamlFilePath, fileName) - bytes, err := io.ReadAll(file) - if err != nil { - errors = append(errors, sanitizederror.NewWithError("Error: failed to read file", err)) - continue - } - - policyBytes, err := yaml.ToJSON(bytes) - if err != nil { - errors = append(errors, sanitizederror.NewWithError("failed to convert to JSON", err)) - continue - } - if err := applyPoliciesFromPath(fs, policyBytes, true, policyresoucePath, rc, openApiManager, filter, failOnly, removeColor, auditWarn); err != nil { - return rc, sanitizederror.NewWithError("failed to apply test command", err) - } - } - } - - if testYamlCount == 0 { - fmt.Printf("\n No test yamls available \n") - } - } else { - var testFiles int - path := filepath.Clean(dirPath[0]) - errors = getLocalDirTestFiles(fs, path, fileName, rc, &testFiles, openApiManager, filter, failOnly, removeColor, auditWarn) - - if testFiles == 0 { - fmt.Printf("\n No test files found. Please provide test YAML files named kyverno-test.yaml \n") + table.AddFailed(t.rows...) } } - if len(errors) > 0 && log.Log.V(1).Enabled() { - fmt.Printf("test errors: \n") + fmt.Println("test errors:") for _, e := range errors { fmt.Printf(" %v \n", e.Error()) } } - 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) } - fmt.Printf("\n") - + fmt.Println() if rc.Fail > 0 && !failOnly { - printFailedTestResult(removeColor) + printFailedTestResult(table, compact) os.Exit(1) } os.Exit(0) return rc, nil } -func getLocalDirTestFiles( - fs billy.Filesystem, - path string, - fileName string, - rc *resultCounts, - testFiles *int, - openApiManager openapi.Manager, - filter filter, - failOnly bool, - removeColor bool, - auditWarn bool, -) []error { - var errors []error - - files, err := os.ReadDir(path) - if err != nil { - return []error{fmt.Errorf("failed to read %v: %v", path, err.Error())} - } - for _, file := range files { - if file.IsDir() { - getLocalDirTestFiles(fs, filepath.Join(path, file.Name()), fileName, rc, testFiles, openApiManager, filter, failOnly, removeColor, auditWarn) - continue - } - if file.Name() == fileName { - *testFiles++ - // We accept the risk of including files here as we read the test dir only. - yamlFile, err := os.ReadFile(filepath.Join(path, file.Name())) // #nosec G304 - if err != nil { - errors = append(errors, sanitizederror.NewWithError("unable to read yaml", err)) - continue - } - valuesBytes, err := yaml.ToJSON(yamlFile) - if err != nil { - errors = append(errors, sanitizederror.NewWithError("failed to convert json", err)) - continue - } - if err := applyPoliciesFromPath(fs, valuesBytes, false, path, rc, openApiManager, filter, failOnly, removeColor, auditWarn); err != nil { - errors = append(errors, sanitizederror.NewWithError(fmt.Sprintf("failed to apply test command from file %s", file.Name()), err)) - continue - } - } - } - return errors -} - -func buildPolicyResults( - engineResponses []engineapi.EngineResponse, - testResults []api.TestResults, - policyResourcePath string, - fs billy.Filesystem, - isGit bool, - auditWarn bool, -) (map[string]policyreportv1alpha2.PolicyReportResult, []api.TestResults) { - results := map[string]policyreportv1alpha2.PolicyReportResult{} - - for _, resp := range engineResponses { - policyName := resp.Policy.GetName() - resourceName := resp.Resource.GetName() - resourceKind := resp.Resource.GetKind() - resourceNamespace := resp.Resource.GetNamespace() - policyNamespace := resp.Policy.GetNamespace() - - var rules []string - for _, rule := range resp.PolicyResponse.Rules { - rules = append(rules, rule.Name()) - } - - result := policyreportv1alpha2.PolicyReportResult{ - Policy: policyName, - Resources: []corev1.ObjectReference{ - { - Name: resourceName, - }, - }, - Message: buildMessage(resp), - } - - var patchedResourcePath []string - for i, test := range testResults { - var userDefinedPolicyNamespace string - var userDefinedPolicyName string - found, err := isNamespacedPolicy(test.Policy) - if err != nil { - log.Log.V(3).Info("error while checking the policy is namespaced or not", "policy: ", test.Policy, "error: ", err) - continue - } - - if found { - userDefinedPolicyNamespace, userDefinedPolicyName = getUserDefinedPolicyNameAndNamespace(test.Policy) - test.Policy = userDefinedPolicyName - } - - if test.Resources != nil { - if test.Policy == policyName { - // results[].namespace value implicit set same as metadata.namespace until and unless - // user provides explicit values for results[].namespace in test yaml file. - if test.Namespace == "" { - test.Namespace = resourceNamespace - testResults[i].Namespace = resourceNamespace - } - for _, resource := range test.Resources { - if resource == resourceName { - var resultsKey string - resultsKey = GetResultKeyAccordingToTestResults(userDefinedPolicyNamespace, test.Policy, test.Rule, test.Namespace, test.Kind, resource) - if !slices.Contains(rules, test.Rule) { - if !slices.Contains(rules, "autogen-"+test.Rule) { - if !slices.Contains(rules, "autogen-cronjob-"+test.Rule) { - result.Result = policyreportv1alpha2.StatusSkip - } else { - testResults[i].AutoGeneratedRule = "autogen-cronjob" - test.Rule = "autogen-cronjob-" + test.Rule - resultsKey = GetResultKeyAccordingToTestResults(userDefinedPolicyNamespace, test.Policy, test.Rule, test.Namespace, test.Kind, resource) - } - } else { - testResults[i].AutoGeneratedRule = "autogen" - test.Rule = "autogen-" + test.Rule - resultsKey = GetResultKeyAccordingToTestResults(userDefinedPolicyNamespace, test.Policy, test.Rule, test.Namespace, test.Kind, resource) - } - - if results[resultsKey].Result == "" { - result.Result = policyreportv1alpha2.StatusSkip - results[resultsKey] = result - } - } - - patchedResourcePath = append(patchedResourcePath, test.PatchedResource) - if _, ok := results[resultsKey]; !ok { - results[resultsKey] = result - } - } - } - } - } - if test.Resource != "" { - if test.Policy == policyName && test.Resource == resourceName { - var resultsKey string - resultsKey = GetResultKeyAccordingToTestResults(userDefinedPolicyNamespace, test.Policy, test.Rule, test.Namespace, test.Kind, test.Resource) - if !slices.Contains(rules, test.Rule) { - if !slices.Contains(rules, "autogen-"+test.Rule) { - if !slices.Contains(rules, "autogen-cronjob-"+test.Rule) { - result.Result = policyreportv1alpha2.StatusSkip - } else { - testResults[i].AutoGeneratedRule = "autogen-cronjob" - test.Rule = "autogen-cronjob-" + test.Rule - resultsKey = GetResultKeyAccordingToTestResults(userDefinedPolicyNamespace, test.Policy, test.Rule, test.Namespace, test.Kind, test.Resource) - } - } else { - testResults[i].AutoGeneratedRule = "autogen" - test.Rule = "autogen-" + test.Rule - resultsKey = GetResultKeyAccordingToTestResults(userDefinedPolicyNamespace, test.Policy, test.Rule, test.Namespace, test.Kind, test.Resource) - } - - if results[resultsKey].Result == "" { - result.Result = policyreportv1alpha2.StatusSkip - results[resultsKey] = result - } - } - - patchedResourcePath = append(patchedResourcePath, test.PatchedResource) - if _, ok := results[resultsKey]; !ok { - results[resultsKey] = result - } - } - } - - for _, rule := range resp.PolicyResponse.Rules { - if rule.RuleType() != engineapi.Generation || test.Rule != rule.Name() { - continue - } - - var resultsKey []string - var resultKey string - var result policyreportv1alpha2.PolicyReportResult - resultsKey = GetAllPossibleResultsKey(policyNamespace, policyName, rule.Name(), resourceNamespace, resourceKind, resourceName) - for _, key := range resultsKey { - if val, ok := results[key]; ok { - result = val - resultKey = key - } else { - continue - } - - if rule.Status() == engineapi.RuleStatusSkip { - result.Result = policyreportv1alpha2.StatusSkip - } else if rule.Status() == engineapi.RuleStatusError { - result.Result = policyreportv1alpha2.StatusError - } else { - var x string - result.Result = policyreportv1alpha2.StatusFail - x = getAndCompareResource(test.GeneratedResource, rule.GeneratedResource(), isGit, policyResourcePath, fs, true) - if x == "pass" { - result.Result = policyreportv1alpha2.StatusPass - } - } - results[resultKey] = result - } - } - - for _, rule := range resp.PolicyResponse.Rules { - if rule.RuleType() != engineapi.Mutation || test.Rule != rule.Name() { - continue - } - - var resultsKey []string - var resultKey string - var result policyreportv1alpha2.PolicyReportResult - resultsKey = GetAllPossibleResultsKey(policyNamespace, policyName, rule.Name(), resourceNamespace, resourceKind, resourceName) - for _, key := range resultsKey { - if val, ok := results[key]; ok { - result = val - resultKey = key - } else { - continue - } - - if rule.Status() == engineapi.RuleStatusSkip { - result.Result = policyreportv1alpha2.StatusSkip - } else if rule.Status() == engineapi.RuleStatusError { - result.Result = policyreportv1alpha2.StatusError - } else { - var x string - for _, path := range patchedResourcePath { - result.Result = policyreportv1alpha2.StatusFail - x = getAndCompareResource(path, resp.PatchedResource, isGit, policyResourcePath, fs, false) - if x == "pass" { - result.Result = policyreportv1alpha2.StatusPass - break - } - } - } - - results[resultKey] = result - } - } - - for _, rule := range resp.PolicyResponse.Rules { - if rule.RuleType() != engineapi.Validation && rule.RuleType() != engineapi.ImageVerify || test.Rule != rule.Name() { - continue - } - - var resultsKey []string - var resultKey string - var result policyreportv1alpha2.PolicyReportResult - resultsKey = GetAllPossibleResultsKey(policyNamespace, policyName, rule.Name(), resourceNamespace, resourceKind, resourceName) - for _, key := range resultsKey { - if val, ok := results[key]; ok { - result = val - resultKey = key - } else { - continue - } - - ann := resp.Policy.GetAnnotations() - if rule.Status() == engineapi.RuleStatusSkip { - result.Result = policyreportv1alpha2.StatusSkip - } else if rule.Status() == engineapi.RuleStatusError { - result.Result = policyreportv1alpha2.StatusError - } else if rule.Status() == engineapi.RuleStatusPass { - result.Result = policyreportv1alpha2.StatusPass - } else if rule.Status() == engineapi.RuleStatusFail { - if scored, ok := ann[kyvernov1.AnnotationPolicyScored]; ok && scored == "false" { - result.Result = policyreportv1alpha2.StatusWarn - } else if auditWarn && resp.GetValidationFailureAction().Audit() { - result.Result = policyreportv1alpha2.StatusWarn - } else { - result.Result = policyreportv1alpha2.StatusFail - } - } else { - fmt.Println(rule) - } - - results[resultKey] = result - } - } - } - } - return results, testResults -} - -func GetAllPossibleResultsKey(policyNamespace, policy, rule, resourceNamespace, kind, resource string) []string { - var resultsKey []string - resultKey1 := fmt.Sprintf("%s-%s-%s-%s", policy, rule, kind, resource) - resultKey2 := fmt.Sprintf("%s-%s-%s-%s-%s", policy, rule, resourceNamespace, kind, resource) - resultKey3 := fmt.Sprintf("%s-%s-%s-%s-%s", policyNamespace, policy, rule, kind, resource) - resultKey4 := fmt.Sprintf("%s-%s-%s-%s-%s-%s", policyNamespace, policy, rule, resourceNamespace, kind, resource) - resultsKey = append(resultsKey, resultKey1, resultKey2, resultKey3, resultKey4) - return resultsKey -} - -func GetResultKeyAccordingToTestResults(policyNs, policy, rule, resourceNs, kind, resource string) string { - var resultKey string - resultKey = fmt.Sprintf("%s-%s-%s-%s", policy, rule, kind, resource) - - if policyNs != "" && resourceNs != "" { - resultKey = fmt.Sprintf("%s-%s-%s-%s-%s-%s", policyNs, policy, rule, resourceNs, kind, resource) - } else if policyNs != "" { - resultKey = fmt.Sprintf("%s-%s-%s-%s-%s", policyNs, policy, rule, kind, resource) - } else if resourceNs != "" { - resultKey = fmt.Sprintf("%s-%s-%s-%s-%s", policy, rule, resourceNs, kind, resource) - } - return resultKey -} - -func isNamespacedPolicy(policyNames string) (bool, error) { - return regexp.MatchString("^[a-z]*/[a-z]*", policyNames) -} - -func getUserDefinedPolicyNameAndNamespace(policyName string) (string, string) { - if strings.Contains(policyName, "/") { - parts := strings.Split(policyName, "/") - namespace := parts[0] - policy := parts[1] - return namespace, policy - } - return "", policyName -} - -// getAndCompareResource --> Get the patchedResource or generatedResource from the path provided by user -// And compare this resource with engine generated resource. -func getAndCompareResource(path string, engineResource unstructured.Unstructured, isGit bool, policyResourcePath string, fs billy.Filesystem, isGenerate bool) string { - var status string - resourceType := "patchedResource" - if isGenerate { - resourceType = "generatedResource" - } - - userResource, err := common.GetResourceFromPath(fs, path, isGit, policyResourcePath, resourceType) - if err != nil { - fmt.Printf("Error: failed to load resources\nCause: %s\n", err) - return "" - } - matched, err := generate.ValidateResourceWithPattern(log.Log, engineResource.UnstructuredContent(), userResource.UnstructuredContent()) - if err != nil { - log.Log.V(3).Info(resourceType+" mismatch", "error", err.Error()) - status = "fail" - } else if matched == "" { - status = "pass" - } - return status -} - -func buildMessage(resp engineapi.EngineResponse) string { - var bldr strings.Builder - for _, ruleResp := range resp.PolicyResponse.Rules { - fmt.Fprintf(&bldr, " %s: %s \n", ruleResp.Name(), ruleResp.Status()) - fmt.Fprintf(&bldr, " %s \n", ruleResp.Message()) - } - - return bldr.String() -} - -func getFullPath(paths []string, policyResourcePath string, isGit bool) []string { - var pols []string - var pol string - if !isGit { - for _, path := range paths { - pol = filepath.Join(policyResourcePath, path) - pols = append(pols, pol) - } - return pols - } - return paths -} - -func applyPoliciesFromPath( - fs billy.Filesystem, - policyBytes []byte, - isGit bool, - policyResourcePath string, - rc *resultCounts, - openApiManager openapi.Manager, - filter filter, - failOnly bool, - removeColor bool, - auditWarn bool, -) (err error) { - engineResponses := make([]engineapi.EngineResponse, 0) - var dClient dclient.Interface - values := &api.Test{} - var variablesString string - var resultCounts common.ResultCounts - - store.SetMock(true) - if err := json.Unmarshal(policyBytes, values); err != nil { - return sanitizederror.NewWithError("failed to decode yaml", err) - } - - var filteredResults []api.TestResults - for _, res := range values.Results { - if filter(res) { - filteredResults = append(filteredResults, res) - } - } - values.Results = filteredResults - - if len(values.Results) == 0 { - return nil - } - - fmt.Printf("\nExecuting %s...\n", values.Name) - valuesFile := values.Variables - userInfoFile := values.UserInfo - - variables, globalValMap, valuesMap, namespaceSelectorMap, subresources, err := common.GetVariable(variablesString, values.Variables, fs, isGit, policyResourcePath) - if err != nil { - if !sanitizederror.IsErrorSanitized(err) { - return sanitizederror.NewWithError("failed to decode yaml", err) - } - return err - } - - // get the user info as request info from a different file - var userInfo v1beta1.RequestInfo - - if userInfoFile != "" { - userInfo, err = common.GetUserInfoFromPath(fs, userInfoFile, isGit, policyResourcePath) - if err != nil { - fmt.Printf("Error: failed to load request info\nCause: %s\n", err) - os.Exit(1) - } - } - - policyFullPath := getFullPath(values.Policies, policyResourcePath, isGit) - resourceFullPath := getFullPath(values.Resources, policyResourcePath, isGit) - - for i, result := range values.Results { - arrPatchedResource := []string{result.PatchedResource} - arrGeneratedResource := []string{result.GeneratedResource} - arrCloneSourceResource := []string{result.CloneSourceResource} - - patchedResourceFullPath := getFullPath(arrPatchedResource, policyResourcePath, isGit) - generatedResourceFullPath := getFullPath(arrGeneratedResource, policyResourcePath, isGit) - CloneSourceResourceFullPath := getFullPath(arrCloneSourceResource, policyResourcePath, isGit) - - values.Results[i].PatchedResource = patchedResourceFullPath[0] - values.Results[i].GeneratedResource = generatedResourceFullPath[0] - values.Results[i].CloneSourceResource = CloneSourceResourceFullPath[0] - } - - policies, err := common.GetPoliciesFromPaths(fs, policyFullPath, isGit, policyResourcePath) - if err != nil { - fmt.Printf("Error: failed to load policies\nCause: %s\n", err) - os.Exit(1) - } - - var filteredPolicies []kyvernov1.PolicyInterface - for _, p := range policies { - for _, res := range values.Results { - if p.GetName() == res.Policy { - filteredPolicies = append(filteredPolicies, p) - break - } - } - } - - ruleToCloneSourceResource := map[string]string{} - for _, p := range filteredPolicies { - var filteredRules []kyvernov1.Rule - - for _, rule := range autogen.ComputeRules(p) { - for _, res := range values.Results { - if rule.Name == res.Rule { - filteredRules = append(filteredRules, rule) - if rule.HasGenerate() { - ruleUnstr, err := generate.GetUnstrRule(rule.Generation.DeepCopy()) - if err != nil { - fmt.Printf("Error: failed to get unstructured rule\nCause: %s\n", err) - break - } - - genClone, _, err := unstructured.NestedMap(ruleUnstr.Object, "clone") - if err != nil { - fmt.Printf("Error: failed to read data\nCause: %s\n", err) - break - } - - if len(genClone) != 0 { - ruleToCloneSourceResource[rule.Name] = res.CloneSourceResource - } - } - break - } - } - } - p.GetSpec().SetRules(filteredRules) - } - policies = filteredPolicies - - err = common.PrintMutatedPolicy(policies) - if err != nil { - return sanitizederror.NewWithError("failed to print mutated policy", err) - } - - resources, err := common.GetResourceAccordingToResourcePath(fs, resourceFullPath, false, policies, dClient, "", false, isGit, policyResourcePath) - if err != nil { - fmt.Printf("Error: failed to load resources\nCause: %s\n", err) - os.Exit(1) - } - - checkableResources := selectResourcesForCheck(resources, values) - - msgPolicies := "1 policy" - if len(policies) > 1 { - msgPolicies = fmt.Sprintf("%d policies", len(policies)) - } - - msgResources := "1 resource" - if len(checkableResources) > 1 { - msgResources = fmt.Sprintf("%d resources", len(checkableResources)) - } - - if len(policies) > 0 && len(checkableResources) > 0 { - fmt.Printf("applying %s to %s... \n", msgPolicies, msgResources) - } - - for _, policy := range policies { - _, err := policy2.Validate(policy, nil, nil, true, openApiManager) - if err != nil { - log.Log.Error(err, "skipping invalid policy", "name", policy.GetName()) - continue - } - - matches := common.HasVariables(policy) - variable := common.RemoveDuplicateAndObjectVariables(matches) - - if len(variable) > 0 { - if len(variables) == 0 { - // check policy in variable file - if valuesFile == "" || valuesMap[policy.GetName()] == nil { - fmt.Printf("test skipped for policy %v (as required variables are not provided by the users) \n \n", policy.GetName()) - } - } - } - - kindOnwhichPolicyIsApplied := common.GetKindsFromPolicy(policy, subresources, dClient) - - for _, resource := range checkableResources { - thisPolicyResourceValues, err := common.CheckVariableForPolicy(valuesMap, globalValMap, policy.GetName(), resource.GetName(), resource.GetKind(), variables, kindOnwhichPolicyIsApplied, variable) - if err != nil { - return sanitizederror.NewWithError(fmt.Sprintf("policy `%s` have variables. pass the values for the variables for resource `%s` using set/values_file flag", policy.GetName(), resource.GetName()), err) - } - applyPolicyConfig := common.ApplyPolicyConfig{ - Policy: policy, - Resource: resource, - MutateLogPath: "", - Variables: thisPolicyResourceValues, - UserInfo: userInfo, - PolicyReport: true, - NamespaceSelectorMap: namespaceSelectorMap, - Rc: &resultCounts, - RuleToCloneSourceResource: ruleToCloneSourceResource, - Client: dClient, - Subresources: subresources, - } - ers, err := common.ApplyPolicyOnResource(applyPolicyConfig) - if err != nil { - return sanitizederror.NewWithError(fmt.Errorf("failed to apply policy %v on resource %v", policy.GetName(), resource.GetName()).Error(), err) - } - engineResponses = append(engineResponses, ers...) - } - } - resultsMap, testResults := buildPolicyResults(engineResponses, values.Results, policyResourcePath, fs, isGit, auditWarn) - resultErr := printTestResult(resultsMap, testResults, rc, failOnly, removeColor) - if resultErr != nil { - return sanitizederror.NewWithError("failed to print test result:", resultErr) - } - - return -} - -func selectResourcesForCheck(resources []*unstructured.Unstructured, values *api.Test) []*unstructured.Unstructured { - res, _, _ := selectResourcesForCheckInternal(resources, values) - return res -} - -// selectResourcesForCheckInternal internal method to test duplicates and unused -func selectResourcesForCheckInternal(resources []*unstructured.Unstructured, values *api.Test) ([]*unstructured.Unstructured, int, int) { - var duplicates int - var unused int - uniqResources := make(map[string]*unstructured.Unstructured) - - for i := range resources { - r := resources[i] - key := fmt.Sprintf("%s/%s/%s", r.GetKind(), r.GetName(), r.GetNamespace()) - if _, ok := uniqResources[key]; ok { - fmt.Println("skipping duplicate resource, resource :", r) - duplicates++ - } else { - uniqResources[key] = r - } - } - - selectedResources := map[string]*unstructured.Unstructured{} - for key := range uniqResources { - r := uniqResources[key] - for _, res := range values.Results { - if res.Kind == r.GetKind() { - for _, testr := range res.Resources { - if r.GetName() == testr { - selectedResources[key] = r - } - } - if r.GetName() == res.Resource { - selectedResources[key] = r - } - } - } - } - - var checkableResources []*unstructured.Unstructured - - for key := range selectedResources { - checkableResources = append(checkableResources, selectedResources[key]) - delete(uniqResources, key) - } - for _, r := range uniqResources { - fmt.Println("skipping unused resource, resource :", r) - unused++ - } - return checkableResources, duplicates, unused -} - -func printTestResult(resps map[string]policyreportv1alpha2.PolicyReportResult, testResults []api.TestResults, rc *resultCounts, failOnly, removeColor bool) error { - printer := newTablePrinter(removeColor) - var table []Table - +func printTestResult(resps map[string]policyreportv1alpha2.PolicyReportResult, testResults []api.TestResults, rc *resultCounts, failOnly bool, compact bool) (Table, error) { + printer := newTablePrinter() + var table Table var countDeprecatedResource int testCount := 1 for _, v := range testResults { - res := new(Table) - res.ID = testCount + var row Row + row.ID = testCount if v.Resources == nil { testCount++ } - res.Policy = colorize(removeColor, boldFgCyan, v.Policy) - res.Rule = colorize(removeColor, boldFgCyan, v.Rule) + row.Policy = boldFgCyan.Sprint(v.Policy) + row.Rule = boldFgCyan.Sprint(v.Rule) if v.Resources != nil { for _, resource := range v.Resources { - res.ID = testCount + row.ID = testCount testCount++ - res.Resource = colorize(removeColor, boldFgCyan, v.Namespace) + "/" + colorize(removeColor, boldFgCyan, v.Kind) + "/" + colorize(removeColor, boldFgCyan, resource) + row.Resource = boldFgCyan.Sprint(v.Namespace) + "/" + boldFgCyan.Sprint(v.Kind) + "/" + boldFgCyan.Sprint(resource) var ruleNameInResultKey string if v.AutoGeneratedRule != "" { ruleNameInResultKey = fmt.Sprintf("%s-%s", v.AutoGeneratedRule, v.Rule) @@ -895,10 +209,10 @@ func printTestResult(resps map[string]policyreportv1alpha2.PolicyReportResult, t resultKey = fmt.Sprintf("%s-%s-%s-%s-%s-%s", ns, v.Policy, ruleNameInResultKey, v.Namespace, v.Kind, resource) } else if found { resultKey = fmt.Sprintf("%s-%s-%s-%s-%s", ns, v.Policy, ruleNameInResultKey, v.Kind, resource) - res.Policy = colorize(removeColor, boldFgCyan, ns) + "/" + colorize(removeColor, boldFgCyan, v.Policy) - res.Resource = colorize(removeColor, boldFgCyan, v.Namespace) + "/" + colorize(removeColor, boldFgCyan, v.Kind) + "/" + colorize(removeColor, boldFgCyan, resource) + row.Policy = boldFgCyan.Sprint(ns) + "/" + boldFgCyan.Sprint(v.Policy) + row.Resource = boldFgCyan.Sprint(v.Namespace) + "/" + boldFgCyan.Sprint(v.Kind) + "/" + boldFgCyan.Sprint(resource) } else if v.Namespace != "" { - res.Resource = colorize(removeColor, boldFgCyan, v.Namespace) + "/" + colorize(removeColor, boldFgCyan, v.Kind) + "/" + colorize(removeColor, boldFgCyan, resource) + row.Resource = boldFgCyan.Sprint(v.Namespace) + "/" + boldFgCyan.Sprint(v.Kind) + "/" + boldFgCyan.Sprint(resource) resultKey = fmt.Sprintf("%s-%s-%s-%s-%s", v.Policy, ruleNameInResultKey, v.Namespace, v.Kind, resource) } @@ -907,19 +221,19 @@ func printTestResult(resps map[string]policyreportv1alpha2.PolicyReportResult, t testRes = val } else { log.Log.V(2).Info("result not found", "key", resultKey) - res.Result = colorize(removeColor, boldYellow, "Not found") + row.Result = boldYellow.Sprint("Not found") rc.Fail++ - table = append(table, *res) - ftable = append(ftable, *res) + row.isFailure = true + table.Add(row) continue } - + row.Message = testRes.Message if v.Result == "" && v.Status != "" { v.Result = v.Status } if testRes.Result == v.Result { - res.Result = colorize(removeColor, boldGreen, "Pass") + row.Result = boldGreen.Sprint("Pass") if testRes.Result == policyreportv1alpha2.StatusSkip { rc.Skip++ } else { @@ -927,22 +241,22 @@ func printTestResult(resps map[string]policyreportv1alpha2.PolicyReportResult, t } } else { log.Log.V(2).Info("result mismatch", "expected", v.Result, "received", testRes.Result, "key", resultKey) - res.Result = colorize(removeColor, boldRed, "Fail") + row.Result = boldRed.Sprint("Fail") rc.Fail++ - ftable = append(ftable, *res) + row.isFailure = true } if failOnly { - if res.Result == boldRed.Sprintf("Fail") || res.Result == "Fail" { - table = append(table, *res) + if row.Result == boldRed.Sprintf("Fail") || row.Result == "Fail" { + table.Add(row) } } else { - table = append(table, *res) + table.Add(row) } } } else if v.Resource != "" { countDeprecatedResource++ - res.Resource = colorize(removeColor, boldFgCyan, v.Namespace) + "/" + colorize(removeColor, boldFgCyan, v.Kind) + "/" + colorize(removeColor, boldFgCyan, v.Resource) + row.Resource = boldFgCyan.Sprint(v.Namespace) + "/" + boldFgCyan.Sprint(v.Kind) + "/" + boldFgCyan.Sprint(v.Resource) var ruleNameInResultKey string if v.AutoGeneratedRule != "" { ruleNameInResultKey = fmt.Sprintf("%s-%s", v.AutoGeneratedRule, v.Rule) @@ -958,10 +272,10 @@ func printTestResult(resps map[string]policyreportv1alpha2.PolicyReportResult, t resultKey = fmt.Sprintf("%s-%s-%s-%s-%s-%s", ns, v.Policy, ruleNameInResultKey, v.Namespace, v.Kind, v.Resource) } else if found { resultKey = fmt.Sprintf("%s-%s-%s-%s-%s", ns, v.Policy, ruleNameInResultKey, v.Kind, v.Resource) - res.Policy = colorize(removeColor, boldFgCyan, ns) + "/" + colorize(removeColor, boldFgCyan, v.Policy) - res.Resource = colorize(removeColor, boldFgCyan, v.Namespace) + "/" + colorize(removeColor, boldFgCyan, v.Kind) + "/" + colorize(removeColor, boldFgCyan, v.Resource) + row.Policy = boldFgCyan.Sprint(ns) + "/" + boldFgCyan.Sprint(v.Policy) + row.Resource = boldFgCyan.Sprint(v.Namespace) + "/" + boldFgCyan.Sprint(v.Kind) + "/" + boldFgCyan.Sprint(v.Resource) } else if v.Namespace != "" { - res.Resource = colorize(removeColor, boldFgCyan, v.Namespace) + "/" + colorize(removeColor, boldFgCyan, v.Kind) + "/" + colorize(removeColor, boldFgCyan, v.Resource) + row.Resource = boldFgCyan.Sprint(v.Namespace) + "/" + boldFgCyan.Sprint(v.Kind) + "/" + boldFgCyan.Sprint(v.Resource) resultKey = fmt.Sprintf("%s-%s-%s-%s-%s", v.Policy, ruleNameInResultKey, v.Namespace, v.Kind, v.Resource) } @@ -970,19 +284,21 @@ func printTestResult(resps map[string]policyreportv1alpha2.PolicyReportResult, t testRes = val } else { log.Log.V(2).Info("result not found", "key", resultKey) - res.Result = colorize(removeColor, boldYellow, "Not found") + row.Result = boldYellow.Sprint("Not found") rc.Fail++ - table = append(table, *res) - ftable = append(ftable, *res) + row.isFailure = true + table.Add(row) continue } + row.Message = testRes.Message + if v.Result == "" && v.Status != "" { v.Result = v.Status } if testRes.Result == v.Result { - res.Result = colorize(removeColor, boldGreen, "Pass") + row.Result = boldGreen.Sprint("Pass") if testRes.Result == policyreportv1alpha2.StatusSkip { rc.Skip++ } else { @@ -990,31 +306,31 @@ func printTestResult(resps map[string]policyreportv1alpha2.PolicyReportResult, t } } else { log.Log.V(2).Info("result mismatch", "expected", v.Result, "received", testRes.Result, "key", resultKey) - res.Result = colorize(removeColor, boldRed, "Fail") + row.Result = boldRed.Sprint("Fail") rc.Fail++ - ftable = append(ftable, *res) + row.isFailure = true } if failOnly { - if res.Result == boldRed.Sprintf("Fail") || res.Result == "Fail" { - table = append(table, *res) + if row.Result == boldRed.Sprintf("Fail") || row.Result == "Fail" { + table.Add(row) } } else { - table = append(table, *res) + table.Add(row) } } } fmt.Printf("\n") - printer.Print(table) - return nil + printer.Print(table.Rows(compact)) + return table, nil } -func printFailedTestResult(removeColor bool) { - printer := newTablePrinter(removeColor) - for i, v := range ftable { - v.ID = i + 1 +func printFailedTestResult(table Table, compact bool) { + printer := newTablePrinter() + for i := range table.rows { + table.rows[i].ID = i + 1 } fmt.Printf("Aggregated Failed Test Cases : ") - fmt.Printf("\n") - printer.Print(ftable) + fmt.Println() + printer.Print(table.Rows(compact)) } diff --git a/cmd/cli/kubectl-kyverno/utils/common/common.go b/cmd/cli/kubectl-kyverno/utils/common/common.go index 762a35477d..e31481c037 100644 --- a/cmd/cli/kubectl-kyverno/utils/common/common.go +++ b/cmd/cli/kubectl-kyverno/utils/common/common.go @@ -25,6 +25,7 @@ import ( engineContext "github.com/kyverno/kyverno/pkg/engine/context" "github.com/kyverno/kyverno/pkg/engine/jmespath" "github.com/kyverno/kyverno/pkg/engine/variables/regex" + "github.com/kyverno/kyverno/pkg/logging" "github.com/kyverno/kyverno/pkg/registryclient" datautils "github.com/kyverno/kyverno/pkg/utils/data" kubeutils "github.com/kyverno/kyverno/pkg/utils/kube" @@ -35,9 +36,10 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/yaml" - "sigs.k8s.io/controller-runtime/pkg/log" ) +var log = logging.WithName("kubectl-kyverno") + type ResultCounts struct { Pass int Fail int @@ -107,7 +109,7 @@ func HasVariables(policy kyvernov1.PolicyInterface) [][]string { // GetPolicies - Extracting the policies from multiple YAML func GetPolicies(paths []string) (policies []kyvernov1.PolicyInterface, errors []error) { for _, path := range paths { - log.Log.V(5).Info("reading policies", "path", path) + log.V(5).Info("reading policies", "path", path) var ( fileDesc os.FileInfo @@ -199,7 +201,7 @@ func GetPolicies(paths []string) (policies []kyvernov1.PolicyInterface, errors [ } } - log.Log.V(3).Info("read policies", "policies", len(policies), "errors", len(errors)) + log.V(3).Info("read policies", "policies", len(policies), "errors", len(errors)) return policies, errors } @@ -286,12 +288,12 @@ func GetVariable(variablesString, valuesFile string, fs billy.Filesystem, isGit if values.GlobalValues == nil { values.GlobalValues = make(map[string]string) values.GlobalValues["request.operation"] = "CREATE" - log.Log.V(3).Info("Defaulting request.operation to CREATE") + log.V(3).Info("Defaulting request.operation to CREATE") } else { if val, ok := values.GlobalValues["request.operation"]; ok { if val == "" { values.GlobalValues["request.operation"] = "CREATE" - log.Log.V(3).Info("Globally request.operation value provided by the user is empty, defaulting it to CREATE", "request.opearation: ", values.GlobalValues) + log.V(3).Info("Globally request.operation value provided by the user is empty, defaulting it to CREATE", "request.opearation: ", values.GlobalValues) } } } @@ -304,7 +306,7 @@ func GetVariable(variablesString, valuesFile string, fs billy.Filesystem, isGit if val, ok := r.Values["request.operation"]; ok { if val == "" { r.Values["request.operation"] = "CREATE" - log.Log.V(3).Info("No request.operation found, defaulting it to CREATE", "policy", p.Name) + log.V(3).Info("No request.operation found, defaulting it to CREATE", "policy", p.Name) } } for variableInFile := range r.Values { @@ -343,7 +345,7 @@ func GetVariable(variablesString, valuesFile string, fs billy.Filesystem, isGit if globalValMap != nil { if _, ok := globalValMap["request.operation"]; !ok { globalValMap["request.operation"] = "CREATE" - log.Log.V(3).Info("Defaulting request.operation to CREATE") + log.V(3).Info("Defaulting request.operation to CREATE") } } @@ -423,16 +425,16 @@ OuterLoop: } resPath := fmt.Sprintf("%s/%s/%s", c.Resource.GetNamespace(), c.Resource.GetKind(), c.Resource.GetName()) - log.Log.V(3).Info("applying policy on resource", "policy", c.Policy.GetName(), "resource", resPath) + log.V(3).Info("applying policy on resource", "policy", c.Policy.GetName(), "resource", resPath) resourceRaw, err := c.Resource.MarshalJSON() if err != nil { - log.Log.Error(err, "failed to marshal resource") + log.Error(err, "failed to marshal resource") } updatedResource, err := kubeutils.BytesToUnstructured(resourceRaw) if err != nil { - log.Log.Error(err, "unable to convert raw resource to unstructured") + log.Error(err, "unable to convert raw resource to unstructured") } ctx := engineContext.NewContext(jp) @@ -443,19 +445,19 @@ OuterLoop: } if err != nil { - log.Log.Error(err, "failed to load resource in context") + log.Error(err, "failed to load resource in context") } for key, value := range c.Variables { err = ctx.AddVariable(key, value) if err != nil { - log.Log.Error(err, "failed to add variable to context") + log.Error(err, "failed to add variable to context") } } cfg := config.NewDefaultConfiguration(false) if err := ctx.AddImageInfos(c.Resource, cfg); err != nil { - log.Log.Error(err, "failed to add image variables to context") + log.Error(err, "failed to add image variables to context") } gvk, subresource := updatedResource.GroupVersionKind(), "" @@ -541,7 +543,7 @@ OuterLoop: if !generateResponse.IsEmpty() { newRuleResponse, err := handleGeneratePolicy(&generateResponse, *policyContext, c.RuleToCloneSourceResource) if err != nil { - log.Log.Error(err, "failed to apply generate policy") + log.Error(err, "failed to apply generate policy") } else { generateResponse.PolicyResponse.Rules = newRuleResponse } @@ -627,7 +629,7 @@ func PrintMutatedOutput(mutateLogPath string, mutateLogPathIsDir bool, yaml stri if _, err := f.Write([]byte(yaml)); err != nil { closeErr := f.Close() if closeErr != nil { - log.Log.Error(closeErr, "failed to close file") + log.Error(closeErr, "failed to close file") } return err } @@ -687,7 +689,7 @@ func GetPoliciesFromPaths(fs billy.Filesystem, dirPath []string, isGit bool, pol } return nil, sanitizederror.New(fmt.Sprintf("no file found in paths %v", dirPath)) } - if len(errors) > 0 && log.Log.V(1).Enabled() { + if len(errors) > 0 && log.V(1).Enabled() { fmt.Printf("ignoring errors: \n") for _, e := range errors { fmt.Printf(" %v \n", e.Error()) @@ -893,7 +895,7 @@ func PrintMutatedPolicy(mutatedPolicies []kyvernov1.PolicyInterface) error { if err != nil { return sanitizederror.NewWithError("failed to marsal mutated policy", err) } - log.Log.V(5).Info("mutated Policy:", string(p)) + log.V(5).Info("mutated Policy:", string(p)) } return nil } @@ -1087,7 +1089,7 @@ func handleGeneratePolicy(generateResponse *engineapi.EngineResponse, policyCont var newRuleResponse []engineapi.RuleResponse for _, rule := range generateResponse.PolicyResponse.Rules { - genResource, err := c.ApplyGeneratePolicy(log.Log, &policyContext, gr, []string{rule.Name()}) + genResource, err := c.ApplyGeneratePolicy(log.V(2), &policyContext, gr, []string{rule.Name()}) if err != nil { return nil, err } @@ -1137,7 +1139,7 @@ func GetUserInfoFromPath(fs billy.Filesystem, path string, isGit bool, policyRes if err := json.Unmarshal(userInfoBytes, userInfo); err != nil { errors = append(errors, sanitizederror.NewWithError("failed to decode yaml", err)) } - if len(errors) > 0 && log.Log.V(1).Enabled() { + if len(errors) > 0 && log.V(1).Enabled() { fmt.Printf("ignoring errors: \n") for _, e := range errors { fmt.Printf(" %v \n", e.Error()) diff --git a/cmd/cli/kubectl-kyverno/utils/common/fetch.go b/cmd/cli/kubectl-kyverno/utils/common/fetch.go index 8d6090f876..2785b09997 100644 --- a/cmd/cli/kubectl-kyverno/utils/common/fetch.go +++ b/cmd/cli/kubectl-kyverno/utils/common/fetch.go @@ -20,7 +20,6 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/kubernetes/scheme" - "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/yaml" ) @@ -88,7 +87,7 @@ func whenClusterIsTrue(resourceTypes []schema.GroupVersionKind, subresourceMap m } if lenOfResource >= len(resources) { if policyReport { - log.Log.V(3).Info(fmt.Sprintf("%s not found in cluster", resourcePath)) + log.V(3).Info(fmt.Sprintf("%s not found in cluster", resourcePath)) } else { fmt.Printf("\n----------------------------------------------------------------------\nresource %s not found in cluster\n----------------------------------------------------------------------\n", resourcePath) } @@ -105,7 +104,7 @@ func whenClusterIsFalse(resourcePaths []string, policyReport bool) ([]*unstructu resourceBytes, err := getFileBytes(resourcePath) if err != nil { if policyReport { - log.Log.V(3).Info(fmt.Sprintf("failed to load resources: %s.", resourcePath), "error", err) + log.V(3).Info(fmt.Sprintf("failed to load resources: %s.", resourcePath), "error", err) } else { fmt.Printf("\n----------------------------------------------------------------------\nfailed to load resources: %s. \nerror: %s\n----------------------------------------------------------------------\n", resourcePath, err) } @@ -177,7 +176,7 @@ func GetResource(resourceBytes []byte) ([]*unstructured.Unstructured, error) { resource, err := convertResourceToUnstructured(resourceYaml) if err != nil { if strings.Contains(err.Error(), "Object 'Kind' is missing") { - log.Log.V(3).Info("skipping resource as kind not found") + log.V(3).Info("skipping resource as kind not found") continue } getErrString = getErrString + err.Error() + "\n" @@ -345,7 +344,7 @@ func addGVKToResourceTypesMap(kind string, resourceTypesMap map[schema.GroupVers group, version, kind, subresource := kubeutils.ParseKindSelector(kind) gvrss, err := client.Discovery().FindResources(group, version, kind, subresource) if err != nil { - log.Log.Info("failed to find resource", "kind", kind, "error", err) + log.Info("failed to find resource", "kind", kind, "error", err) return } for parent, child := range gvrss {