From a7b408961391d834ec70647a3c87acf1f30ae687 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Charles-Edouard=20Br=C3=A9t=C3=A9ch=C3=A9?= Date: Sat, 3 Dec 2022 19:18:01 +0100 Subject: [PATCH] refactor: cli jp command (#5552) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Charles-Edouard Brétéché Signed-off-by: Charles-Edouard Brétéché --- cmd/cli/kubectl-kyverno/jp/jp_command.go | 197 ++++++++++++++--------- 1 file changed, 117 insertions(+), 80 deletions(-) diff --git a/cmd/cli/kubectl-kyverno/jp/jp_command.go b/cmd/cli/kubectl-kyverno/jp/jp_command.go index 216ef39683..1cbd6ec020 100644 --- a/cmd/cli/kubectl-kyverno/jp/jp_command.go +++ b/cmd/cli/kubectl-kyverno/jp/jp_command.go @@ -6,12 +6,11 @@ import ( "io" "os" "path/filepath" - "sort" - "strings" gojmespath "github.com/jmespath/go-jmespath" - jmespath "github.com/kyverno/kyverno/pkg/engine/jmespath" + "github.com/kyverno/kyverno/pkg/engine/jmespath" "github.com/spf13/cobra" + "golang.org/x/exp/slices" "sigs.k8s.io/yaml" ) @@ -31,87 +30,29 @@ func Command() *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { if listFunctions { printFunctionList() - return nil - } - // The following function has been adapted from - // https://github.com/jmespath/jp/blob/54882e03bd277fc4475a677fab1d35eaa478b839/jp.go - var expression string - if exprFile != "" { - byteExpr, err := os.ReadFile(filepath.Clean(exprFile)) - if err != nil { - return fmt.Errorf("error opening expression file: %w", err) - } - expression = string(byteExpr) } else { - if len(args) == 0 { - return fmt.Errorf("must provide at least one argument") - } - expression = args[0] - } - if ast { - parser := gojmespath.NewParser() - parsed, err := parser.Parse(expression) + expression, err := loadExpression(exprFile, args) if err != nil { - if syntaxError, ok := err.(gojmespath.SyntaxError); ok { - return fmt.Errorf("%w\n%s", - syntaxError, - syntaxError.HighlightLocation()) - } return err } - fmt.Printf("%s", parsed) - return nil - } - var input interface{} - if filename != "" { - f, err := os.ReadFile(filepath.Clean(filename)) - if err != nil { - return fmt.Errorf("error opening input file: %w", err) - } - if err := yaml.Unmarshal(f, &input); err != nil { - return fmt.Errorf("error parsing input json: %w", err) - } - } else { - f, err := io.ReadAll(os.Stdin) - if err != nil { - return fmt.Errorf("error opening input file: %w", err) - } - if err := yaml.Unmarshal(f, &input); err != nil { - return fmt.Errorf("error parsing input json: %w", err) - } - } - jp, err := jmespath.New(expression) - if err != nil { - return 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 fmt.Errorf("%s\n%s", - syntaxError, - syntaxError.HighlightLocation()) - } - return fmt.Errorf("error evaluating JMESPath expression: %w", err) - } - converted, isString := result.(string) - if unquoted && isString { - fmt.Println(converted) - } else { - var toJSON []byte - if compact { - toJSON, err = json.Marshal(result) + if ast { + return printAst(expression) } else { - toJSON, err = json.MarshalIndent(result, "", " ") + input, err := loadInput(filename) + if err != nil { + return err + } + result, err := evaluate(expression, input) + if err != nil { + return err + } + return printResult(result, unquoted, compact) } - if err != nil { - return fmt.Errorf("error marshalling result to JSON: %w", err) - } - fmt.Println(string(toJSON)) } return nil }, } - cmd.Flags().BoolVarP(&compact, "compact", "c", false, "Produce compact JSON output that omits nonessential whitespace") + 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") @@ -120,11 +61,107 @@ func Command() *cobra.Command { return cmd } -func printFunctionList() { - functions := []string{} - for _, function := range jmespath.GetFunctions() { - functions = append(functions, function.String()) +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 } - sort.Strings(functions) - fmt.Println(strings.Join(functions, "\n")) +} + +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 }