From b28c16fe995454c680ecf08da93d9f8056b480a5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Charles-Edouard=20Br=C3=A9t=C3=A9ch=C3=A9?=
 <charled.breteche@gmail.com>
Date: Wed, 21 Dec 2022 07:06:13 +0100
Subject: [PATCH] refactor: split CLI jp command (#5566)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* refactor: split CLI jp command

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

* fix sonatype comments

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

* fix

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>
Co-authored-by: shuting <shuting@nirmata.com>
Co-authored-by: Vyankatesh Kudtarkar <vyankateshkd@gmail.com>
---
 .../kubectl-kyverno/jp/function/function.go   |  54 +++++
 cmd/cli/kubectl-kyverno/jp/jp.go              |  29 +++
 cmd/cli/kubectl-kyverno/jp/jp_command.go      | 167 ---------------
 cmd/cli/kubectl-kyverno/jp/parse/parse.go     | 112 ++++++++++
 cmd/cli/kubectl-kyverno/jp/query/query.go     | 197 ++++++++++++++++++
 5 files changed, 392 insertions(+), 167 deletions(-)
 create mode 100644 cmd/cli/kubectl-kyverno/jp/function/function.go
 create mode 100644 cmd/cli/kubectl-kyverno/jp/jp.go
 delete mode 100644 cmd/cli/kubectl-kyverno/jp/jp_command.go
 create mode 100644 cmd/cli/kubectl-kyverno/jp/parse/parse.go
 create mode 100644 cmd/cli/kubectl-kyverno/jp/query/query.go

diff --git a/cmd/cli/kubectl-kyverno/jp/function/function.go b/cmd/cli/kubectl-kyverno/jp/function/function.go
new file mode 100644
index 0000000000..ecd148373f
--- /dev/null
+++ b/cmd/cli/kubectl-kyverno/jp/function/function.go
@@ -0,0 +1,54 @@
+package function
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/kyverno/kyverno/pkg/engine/jmespath"
+	"github.com/spf13/cobra"
+	"golang.org/x/exp/slices"
+	"k8s.io/apimachinery/pkg/util/sets"
+)
+
+var description = []string{
+	"Provides function informations",
+	"For more information visit: https://kyverno.io/docs/writing-policies/jmespath/ ",
+}
+
+var examples = []string{
+	"  # List functions    \n  kyverno jp function",
+	"  # Get function infos\n  kyverno jp function <function name>",
+}
+
+func Command() *cobra.Command {
+	return &cobra.Command{
+		Use:          "function [function_name]...",
+		Short:        strings.Join(description, "\n"),
+		Example:      strings.Join(examples, "\n\n"),
+		SilenceUsage: true,
+		Run: func(cmd *cobra.Command, args []string) {
+			printFunctions(args...)
+		},
+	}
+}
+
+func printFunctions(names ...string) {
+	functions := jmespath.GetFunctions()
+	slices.SortFunc(functions, func(a, b *jmespath.FunctionEntry) bool {
+		return a.String() < b.String()
+	})
+	namesSet := sets.NewString(names...)
+	for _, function := range functions {
+		if len(namesSet) == 0 || namesSet.Has(function.Entry.Name) {
+			function := *function
+			note := function.Note
+			function.Note = ""
+			fmt.Println("Name:", function.Entry.Name)
+			fmt.Println("  Signature:", function.String())
+			if note != "" {
+				fmt.Println("  Note:     ", note)
+			}
+			fmt.Println()
+		}
+	}
+}
diff --git a/cmd/cli/kubectl-kyverno/jp/jp.go b/cmd/cli/kubectl-kyverno/jp/jp.go
new file mode 100644
index 0000000000..227d2f8678
--- /dev/null
+++ b/cmd/cli/kubectl-kyverno/jp/jp.go
@@ -0,0 +1,29 @@
+package jp
+
+import (
+	"strings"
+
+	"github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/jp/function"
+	"github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/jp/parse"
+	"github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/jp/query"
+	"github.com/spf13/cobra"
+)
+
+var description = []string{
+	"Provides a command-line interface to JMESPath, enhanced with Kyverno specific custom functions",
+	"For more information visit: https://kyverno.io/docs/writing-policies/jmespath/ ",
+}
+
+func Command() *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "jp",
+		Short: strings.Join(description, "\n"),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return cmd.Help()
+		},
+	}
+	cmd.AddCommand(query.Command())
+	cmd.AddCommand(function.Command())
+	cmd.AddCommand(parse.Command())
+	return cmd
+}
diff --git a/cmd/cli/kubectl-kyverno/jp/jp_command.go b/cmd/cli/kubectl-kyverno/jp/jp_command.go
deleted file mode 100644
index 1cbd6ec020..0000000000
--- a/cmd/cli/kubectl-kyverno/jp/jp_command.go
+++ /dev/null
@@ -1,167 +0,0 @@
-package jp
-
-import (
-	"encoding/json"
-	"fmt"
-	"io"
-	"os"
-	"path/filepath"
-
-	gojmespath "github.com/jmespath/go-jmespath"
-	"github.com/kyverno/kyverno/pkg/engine/jmespath"
-	"github.com/spf13/cobra"
-	"golang.org/x/exp/slices"
-	"sigs.k8s.io/yaml"
-)
-
-var applyHelp = `
-For more information visit: https://kyverno.io/docs/writing-policies/jmespath/ 
-`
-
-// Command returns jp command
-func Command() *cobra.Command {
-	var compact, unquoted, ast, listFunctions bool
-	var filename, exprFile string
-	cmd := &cobra.Command{
-		Use:          "jp",
-		Short:        "Provides a command-line interface to JMESPath, enhanced with Kyverno specific custom functions",
-		SilenceUsage: true,
-		Example:      applyHelp,
-		RunE: func(cmd *cobra.Command, args []string) error {
-			if listFunctions {
-				printFunctionList()
-			} else {
-				expression, err := loadExpression(exprFile, args)
-				if err != nil {
-					return err
-				}
-				if ast {
-					return printAst(expression)
-				} else {
-					input, err := loadInput(filename)
-					if err != nil {
-						return err
-					}
-					result, err := evaluate(expression, input)
-					if err != nil {
-						return err
-					}
-					return printResult(result, unquoted, compact)
-				}
-			}
-			return nil
-		},
-	}
-	cmd.Flags().BoolVarP(&compact, "compact", "c", false, "Produce compact JSON output that omits non essential whitespace")
-	cmd.Flags().BoolVarP(&listFunctions, "list-functions", "l", false, "Output a list of custom JMESPath functions in Kyverno")
-	cmd.Flags().BoolVarP(&unquoted, "unquoted", "u", false, "If the final result is a string, it will be printed without quotes")
-	cmd.Flags().BoolVar(&ast, "ast", false, "Only print the AST of the parsed expression.  Do not rely on this output, only useful for debugging purposes")
-	cmd.Flags().StringVarP(&exprFile, "expr-file", "e", "", "Read JMESPath expression from the specified file")
-	cmd.Flags().StringVarP(&filename, "filename", "f", "", "Read input from a JSON or YAML file instead of stdin")
-	return cmd
-}
-
-func loadExpression(file string, args []string) (string, error) {
-	if file != "" {
-		data, err := os.ReadFile(filepath.Clean(file))
-		if err != nil {
-			return "", fmt.Errorf("error opening expression file: %w", err)
-		}
-		return string(data), nil
-	} else {
-		if len(args) == 0 {
-			return "", fmt.Errorf("must provide at least one argument")
-		}
-		return args[0], nil
-	}
-}
-
-func loadInput(file string) (interface{}, error) {
-	var data []byte
-	if file != "" {
-		f, err := os.ReadFile(filepath.Clean(file))
-		if err != nil {
-			return nil, fmt.Errorf("error opening input file: %w", err)
-		}
-		data = f
-	} else {
-		f, err := io.ReadAll(os.Stdin)
-		if err != nil {
-			return nil, fmt.Errorf("error opening input file: %w", err)
-		}
-		data = f
-	}
-	var input interface{}
-	if err := yaml.Unmarshal(data, &input); err != nil {
-		return nil, fmt.Errorf("error parsing input json: %w", err)
-	}
-	return input, nil
-}
-
-func evaluate(expression string, input interface{}) (interface{}, error) {
-	jp, err := jmespath.New(expression)
-	if err != nil {
-		return nil, fmt.Errorf("failed to compile JMESPath: %s, error: %v", expression, err)
-	}
-	result, err := jp.Search(input)
-	if err != nil {
-		if syntaxError, ok := err.(gojmespath.SyntaxError); ok {
-			return nil, fmt.Errorf("%s\n%s", syntaxError, syntaxError.HighlightLocation())
-		}
-		return nil, fmt.Errorf("error evaluating JMESPath expression: %w", err)
-	}
-	return result, nil
-}
-
-func printResult(result interface{}, unquoted bool, compact bool) error {
-	converted, isString := result.(string)
-	if unquoted && isString {
-		fmt.Println(converted)
-	} else {
-		var toJSON []byte
-		var err error
-		if compact {
-			toJSON, err = json.Marshal(result)
-		} else {
-			toJSON, err = json.MarshalIndent(result, "", "  ")
-		}
-		if err != nil {
-			return fmt.Errorf("error marshalling result to JSON: %w", err)
-		}
-		fmt.Println(string(toJSON))
-	}
-	return nil
-}
-
-func printFunctionList() {
-	functions := jmespath.GetFunctions()
-	slices.SortFunc(functions, func(a, b *jmespath.FunctionEntry) bool {
-		return a.String() < b.String()
-	})
-	for _, function := range functions {
-		function := *function
-		note := function.Note
-		function.Note = ""
-		fmt.Println("Name:", function.Entry.Name)
-		fmt.Println("  Signature:", function.String())
-		if note != "" {
-			fmt.Println("  Note:     ", note)
-		}
-		fmt.Println()
-	}
-}
-
-// The following function has been adapted from
-// https://github.com/jmespath/jp/blob/54882e03bd277fc4475a677fab1d35eaa478b839/jp.go
-func printAst(expression string) error {
-	parser := gojmespath.NewParser()
-	parsed, err := parser.Parse(expression)
-	if err != nil {
-		if syntaxError, ok := err.(gojmespath.SyntaxError); ok {
-			return fmt.Errorf("%w\n%s", syntaxError, syntaxError.HighlightLocation())
-		}
-		return err
-	}
-	fmt.Print(parsed)
-	return nil
-}
diff --git a/cmd/cli/kubectl-kyverno/jp/parse/parse.go b/cmd/cli/kubectl-kyverno/jp/parse/parse.go
new file mode 100644
index 0000000000..a31f0a22a9
--- /dev/null
+++ b/cmd/cli/kubectl-kyverno/jp/parse/parse.go
@@ -0,0 +1,112 @@
+package parse
+
+import (
+	"fmt"
+	"io"
+	"os"
+	"path/filepath"
+	"strings"
+
+	gojmespath "github.com/jmespath/go-jmespath"
+	"github.com/spf13/cobra"
+)
+
+var description = []string{
+	"Parses jmespath expression and shows corresponding AST",
+	"For more information visit: https://kyverno.io/docs/writing-policies/jmespath/ ",
+}
+
+var examples = []string{
+	"  # Parse expression            \n  kyverno jp parse 'request.object.metadata.name | truncate(@, `9`)'",
+	"  # Parse expression from a file\n  kyverno jp parse -f my-file",
+	"  # Parse expression from stdin \n  kyverno jp parse",
+	"  # Parse multiple expressionxs \n  kyverno jp parse -f my-file1 -f my-file-2 'request.object.metadata.name | truncate(@, `9`)'",
+	"  # Cat into                    \n  cat my-file | kyverno jp parse",
+}
+
+func Command() *cobra.Command {
+	var files []string
+	cmd := &cobra.Command{
+		Use:          "parse [-f file|expression]...",
+		Short:        strings.Join(description, "\n"),
+		Example:      strings.Join(examples, "\n\n"),
+		SilenceUsage: true,
+		RunE: func(cmd *cobra.Command, args []string) error {
+			expressions, err := loadExpressions(cmd, args, files)
+			if err != nil {
+				return err
+			}
+			for _, expression := range expressions {
+				if err := printAst(expression); err != nil {
+					return err
+				}
+			}
+			return nil
+		},
+	}
+	cmd.Flags().StringSliceVarP(&files, "file", "f", nil, "Read input from a JSON or YAML file instead of stdin")
+	return cmd
+}
+
+func readFile(reader io.Reader) (string, error) {
+	data, err := io.ReadAll(reader)
+	if err != nil {
+		return "", err
+	}
+	return string(data), nil
+}
+
+func loadFile(file string) (string, error) {
+	reader, err := os.Open(filepath.Clean(file))
+	if err != nil {
+		return "", fmt.Errorf("failed open file %s: %v", file, err)
+	}
+	defer func() {
+		if err := reader.Close(); err != nil {
+			fmt.Printf("Error closing file: %s\n", err)
+		}
+	}()
+	content, err := readFile(reader)
+	if err != nil {
+		return "", fmt.Errorf("failed read file %s: %v", file, err)
+	}
+	return content, nil
+}
+
+func loadExpressions(cmd *cobra.Command, args []string, files []string) ([]string, error) {
+	var expressions []string
+	expressions = append(expressions, args...)
+	for _, file := range files {
+		expression, err := loadFile(file)
+		if err != nil {
+			return nil, err
+		}
+		expressions = append(expressions, expression)
+	}
+	if len(expressions) == 0 {
+		fmt.Println("Reading from terminal input.")
+		fmt.Println("Enter a jmespatch expression and hit Ctrl+D.")
+		data, err := readFile(cmd.InOrStdin())
+		if err != nil {
+			return nil, fmt.Errorf("failed to read file STDIN: %v", err)
+		}
+		expressions = append(expressions, data)
+	}
+	return expressions, nil
+}
+
+// The following function has been adapted from
+// https://github.com/jmespath/jp/blob/54882e03bd277fc4475a677fab1d35eaa478b839/jp.go
+func printAst(expression string) error {
+	parser := gojmespath.NewParser()
+	parsed, err := parser.Parse(expression)
+	if err != nil {
+		if syntaxError, ok := err.(gojmespath.SyntaxError); ok {
+			return fmt.Errorf("%w\n%s", syntaxError, syntaxError.HighlightLocation())
+		}
+		return err
+	}
+	fmt.Println("#", expression)
+	fmt.Println(parsed)
+	return nil
+}
diff --git a/cmd/cli/kubectl-kyverno/jp/query/query.go b/cmd/cli/kubectl-kyverno/jp/query/query.go
new file mode 100644
index 0000000000..1140fb612b
--- /dev/null
+++ b/cmd/cli/kubectl-kyverno/jp/query/query.go
@@ -0,0 +1,197 @@
+package query
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io"
+	"os"
+	"path/filepath"
+	"strings"
+
+	gojmespath "github.com/jmespath/go-jmespath"
+	"github.com/kyverno/kyverno/pkg/engine/jmespath"
+	"github.com/spf13/cobra"
+	"sigs.k8s.io/yaml"
+)
+
+var description = []string{
+	"Provides a command-line interface to JMESPath, enhanced with Kyverno specific custom functions",
+	"For more information visit: https://kyverno.io/docs/writing-policies/jmespath/ ",
+}
+
+var examples = []string{
+	"  # Evaluate query             \n  kyverno jp query -i object.yaml 'request.object.metadata.name | truncate(@, `9`)'",
+	"  # Evaluate query             \n  kyverno jp query -i object.yaml -q query-file",
+	"  # Evaluate multiple queries  \n  kyverno jp query -i object.yaml -q query-file-1 -q query-file-2 'request.object.metadata.name | truncate(@, `9`)'",
+	"  # Cat query into             \n  cat query-file | kyverno jp query -i object.yaml",
+	"  # Cat object into            \n  cat object.yaml | kyverno jp query -q query-file",
+}
+
+// Command returns jp command
+func Command() *cobra.Command {
+	var compact, unquoted bool
+	var input string
+	var queries []string
+	cmd := &cobra.Command{
+		Use:          "query [-i input] [-q query|query]...",
+		Short:        strings.Join(description, "\n"),
+		SilenceUsage: true,
+		Example:      strings.Join(examples, "\n\n"),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			queries, err := loadQueries(args, queries)
+			if err != nil {
+				return err
+			}
+			input, err := loadInput(input)
+			if err != nil {
+				return err
+			}
+			if len(queries) == 0 && input == nil {
+				return errors.New("at least one query or input object is required")
+			}
+			if len(queries) == 0 {
+				query, err := readQuery(cmd)
+				if err != nil {
+					return err
+				}
+				queries = append(queries, query)
+			}
+			if input == nil {
+				i, err := readInput(cmd)
+				if err != nil {
+					return err
+				}
+				input = i
+			}
+			for _, query := range queries {
+				result, err := evaluate(input, query)
+				if err != nil {
+					return err
+				}
+				if err := printResult(query, result, unquoted, compact); err != nil {
+					return err
+				}
+			}
+			return nil
+		},
+	}
+	cmd.Flags().BoolVarP(&compact, "compact", "c", false, "Produce compact JSON output that omits non essential whitespace")
+	cmd.Flags().BoolVarP(&unquoted, "unquoted", "u", false, "If the final result is a string, it will be printed without quotes")
+	cmd.Flags().StringSliceVarP(&queries, "query", "q", nil, "Read JMESPath expression from the specified file")
+	cmd.Flags().StringVarP(&input, "input", "i", "", "Read input from a JSON or YAML file instead of stdin")
+	return cmd
+}
+
+func readFile(reader io.Reader) ([]byte, error) {
+	data, err := io.ReadAll(reader)
+	if err != nil {
+		return nil, err
+	}
+	return data, nil
+}
+
+func loadFile(file string) ([]byte, error) {
+	reader, err := os.Open(filepath.Clean(file))
+	if err != nil {
+		return nil, fmt.Errorf("failed open file %s: %v", file, err)
+	}
+	defer func() {
+		if err := reader.Close(); err != nil {
+			fmt.Printf("Error closing file: %s\n", err)
+		}
+	}()
+	content, err := readFile(reader)
+	if err != nil {
+		return nil, fmt.Errorf("failed to read file %s: %v", file, err)
+	}
+	return content, nil
+}
+
+func readQuery(cmd *cobra.Command) (string, error) {
+	fmt.Println("Reading from terminal input.")
+	fmt.Println("Enter a jmespatch expression and hit Ctrl+D.")
+	data, err := readFile(cmd.InOrStdin())
+	if err != nil {
+		return "", err
+	}
+	return string(data), nil
+}
+
+func loadQueries(args []string, files []string) ([]string, error) {
+	var queries []string
+	queries = append(queries, args...)
+	for _, file := range files {
+		query, err := loadFile(file)
+		if err != nil {
+			return nil, err
+		}
+		queries = append(queries, string(query))
+	}
+	return queries, nil
+}
+
+func readInput(cmd *cobra.Command) (interface{}, error) {
+	fmt.Println("Reading from terminal input.")
+	fmt.Println("Enter input object and hit Ctrl+D.")
+	data, err := readFile(cmd.InOrStdin())
+	if err != nil {
+		return nil, err
+	}
+	var input interface{}
+	if err := yaml.Unmarshal(data, &input); err != nil {
+		return nil, fmt.Errorf("error parsing input json: %w", err)
+	}
+	return input, nil
+}
+
+func loadInput(file string) (interface{}, error) {
+	if file == "" {
+		return nil, nil
+	}
+	data, err := loadFile(file)
+	if err != nil {
+		return nil, err
+	}
+	var input interface{}
+	if err := yaml.Unmarshal(data, &input); err != nil {
+		return nil, fmt.Errorf("error parsing input json: %w", err)
+	}
+	return input, nil
+}
+
+func evaluate(input interface{}, query string) (interface{}, error) {
+	jp, err := jmespath.New(query)
+	if err != nil {
+		return nil, fmt.Errorf("failed to compile JMESPath: %s, error: %v", query, err)
+	}
+	result, err := jp.Search(input)
+	if err != nil {
+		if syntaxError, ok := err.(gojmespath.SyntaxError); ok {
+			return nil, fmt.Errorf("%s\n%s", syntaxError, syntaxError.HighlightLocation())
+		}
+		return nil, fmt.Errorf("error evaluating JMESPath expression: %w", err)
+	}
+	return result, nil
+}
+
+func printResult(query string, result interface{}, unquoted bool, compact bool) error {
+	converted, isString := result.(string)
+	fmt.Println("#", query)
+	if unquoted && isString {
+		fmt.Println(converted)
+	} else {
+		var toJSON []byte
+		var err error
+		if compact {
+			toJSON, err = json.Marshal(result)
+		} else {
+			toJSON, err = json.MarshalIndent(result, "", "  ")
+		}
+		if err != nil {
+			return fmt.Errorf("error marshalling result to JSON: %w", err)
+		}
+		fmt.Println(string(toJSON))
+	}
+	return nil
+}