diff --git a/docs/user/commands/kyverno-json.md b/docs/user/commands/kyverno-json.md index 59769e26..a84a45e8 100644 --- a/docs/user/commands/kyverno-json.md +++ b/docs/user/commands/kyverno-json.md @@ -20,5 +20,6 @@ kyverno-json [flags] * [kyverno-json completion](kyverno-json_completion.md) - Generate the autocompletion script for the specified shell * [kyverno-json docs](kyverno-json_docs.md) - Generates reference documentation. +* [kyverno-json jp](kyverno-json_jp.md) - Provides a command-line interface to JMESPath, enhanced with custom functions. * [kyverno-json scan](kyverno-json_scan.md) - scan diff --git a/docs/user/commands/kyverno-json_jp.md b/docs/user/commands/kyverno-json_jp.md new file mode 100644 index 00000000..1e59567c --- /dev/null +++ b/docs/user/commands/kyverno-json_jp.md @@ -0,0 +1,24 @@ +## kyverno-json jp + +Provides a command-line interface to JMESPath, enhanced with custom functions. + +### Synopsis + +Provides a command-line interface to JMESPath, enhanced with custom functions. + +``` +kyverno-json jp [flags] +``` + +### Options + +``` + -h, --help help for jp +``` + +### SEE ALSO + +* [kyverno-json](kyverno-json.md) - kyverno-json +* [kyverno-json jp function](kyverno-json_jp_function.md) - Provides function informations. +* [kyverno-json jp parse](kyverno-json_jp_parse.md) - Parses jmespath expression and shows corresponding AST. + diff --git a/docs/user/commands/kyverno-json_jp_function.md b/docs/user/commands/kyverno-json_jp_function.md new file mode 100644 index 00000000..11992238 --- /dev/null +++ b/docs/user/commands/kyverno-json_jp_function.md @@ -0,0 +1,22 @@ +## kyverno-json jp function + +Provides function informations. + +### Synopsis + +Provides function informations. + +``` +kyverno-json jp function [function_name]... [flags] +``` + +### Options + +``` + -h, --help help for function +``` + +### SEE ALSO + +* [kyverno-json jp](kyverno-json_jp.md) - Provides a command-line interface to JMESPath, enhanced with custom functions. + diff --git a/docs/user/commands/kyverno-json_jp_parse.md b/docs/user/commands/kyverno-json_jp_parse.md new file mode 100644 index 00000000..f879ebd3 --- /dev/null +++ b/docs/user/commands/kyverno-json_jp_parse.md @@ -0,0 +1,23 @@ +## kyverno-json jp parse + +Parses jmespath expression and shows corresponding AST. + +### Synopsis + +Parses jmespath expression and shows corresponding AST. + +``` +kyverno-json jp parse [-f file|expression]... [flags] +``` + +### Options + +``` + -f, --file strings Read input from a JSON or YAML file instead of stdin + -h, --help help for parse +``` + +### SEE ALSO + +* [kyverno-json jp](kyverno-json_jp.md) - Provides a command-line interface to JMESPath, enhanced with custom functions. + diff --git a/pkg/commands/jp/command.go b/pkg/commands/jp/command.go index b7dd4447..bcaae6e9 100644 --- a/pkg/commands/jp/command.go +++ b/pkg/commands/jp/command.go @@ -3,6 +3,7 @@ package jp import ( "github.com/kyverno/kyverno-json/pkg/commands/jp/function" "github.com/kyverno/kyverno-json/pkg/commands/jp/parse" + "github.com/kyverno/kyverno-json/pkg/commands/jp/query" "github.com/spf13/cobra" ) @@ -20,7 +21,7 @@ func Command() *cobra.Command { cmd.AddCommand( function.Command(), parse.Command(), - // query.Command(), + query.Command(), ) return cmd } diff --git a/pkg/commands/jp/query/command.go b/pkg/commands/jp/query/command.go index 325c783c..d8f77857 100644 --- a/pkg/commands/jp/query/command.go +++ b/pkg/commands/jp/query/command.go @@ -1,183 +1,178 @@ package query -// import ( -// "encoding/json" -// "errors" -// "fmt" -// "io" -// "os" -// "path/filepath" +import ( + "encoding/json" + "errors" + "fmt" + "io" + "os" + "path/filepath" -// "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/command" -// "github.com/spf13/cobra" -// "sigs.k8s.io/yaml" -// ) + "github.com/jmespath-community/go-jmespath/pkg/parsing" + "github.com/kyverno/kyverno-json/pkg/engine/template" + "github.com/spf13/cobra" + "sigs.k8s.io/yaml" +) -// 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: command.FormatDescription(true, websiteUrl, false, description...), -// Long: command.FormatDescription(false, websiteUrl, false, description...), -// Example: command.FormatExamples(examples...), -// SilenceUsage: true, -// RunE: func(cmd *cobra.Command, args []string) error { -// queries, err := loadQueries(cmd, args, queries) -// if err != nil { -// return err -// } -// input, err := loadInput(cmd, 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(cmd, 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 Command() *cobra.Command { + var compact, unquoted bool + var input string + var queries []string + cmd := &cobra.Command{ + Use: "query [-i input] [-q query|query]...", + Short: "Provides a command-line interface to JMESPath, enhanced with Kyverno specific custom functions.", + Long: "Provides a command-line interface to JMESPath, enhanced with Kyverno specific custom functions.", + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + queries, err := loadQueries(cmd, args, queries) + if err != nil { + return err + } + input, err := loadInput(cmd, 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(cmd, 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 readFile(reader io.Reader) ([]byte, error) { + data, err := io.ReadAll(reader) + if err != nil { + return nil, err + } + return data, nil +} -// func loadFile(cmd *cobra.Command, 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.Fprintf(cmd.OutOrStdout(), "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 loadFile(cmd *cobra.Command, 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.Fprintf(cmd.OutOrStdout(), "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.Fprintln(cmd.OutOrStdout(), "Reading from terminal input.") -// fmt.Fprintln(cmd.OutOrStdout(), "Enter a jmespath expression and hit Ctrl+D.") -// data, err := readFile(cmd.InOrStdin()) -// if err != nil { -// return "", err -// } -// return string(data), nil -// } +func readQuery(cmd *cobra.Command) (string, error) { + fmt.Fprintln(cmd.OutOrStdout(), "Reading from terminal input.") + fmt.Fprintln(cmd.OutOrStdout(), "Enter a jmespath expression and hit Ctrl+D.") + data, err := readFile(cmd.InOrStdin()) + if err != nil { + return "", err + } + return string(data), nil +} -// func loadQueries(cmd *cobra.Command, args []string, files []string) ([]string, error) { -// var queries []string -// queries = append(queries, args...) -// for _, file := range files { -// query, err := loadFile(cmd, file) -// if err != nil { -// return nil, err -// } -// queries = append(queries, string(query)) -// } -// return queries, nil -// } +func loadQueries(cmd *cobra.Command, args []string, files []string) ([]string, error) { + var queries []string + queries = append(queries, args...) + for _, file := range files { + query, err := loadFile(cmd, file) + if err != nil { + return nil, err + } + queries = append(queries, string(query)) + } + return queries, nil +} -// func readInput(cmd *cobra.Command) (interface{}, error) { -// fmt.Fprintln(cmd.OutOrStdout(), "Reading from terminal input.") -// fmt.Fprintln(cmd.OutOrStdout(), "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 readInput(cmd *cobra.Command) (interface{}, error) { + fmt.Fprintln(cmd.OutOrStdout(), "Reading from terminal input.") + fmt.Fprintln(cmd.OutOrStdout(), "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(cmd *cobra.Command, file string) (interface{}, error) { -// if file == "" { -// return nil, nil -// } -// data, err := loadFile(cmd, 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 loadInput(cmd *cobra.Command, file string) (interface{}, error) { + if file == "" { + return nil, nil + } + data, err := loadFile(cmd, 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 := jmespath.New(config.NewDefaultConfiguration(false)) -// q, err := jp.Query(query) -// if err != nil { -// return nil, fmt.Errorf("failed to compile JMESPath: %s, error: %v", query, err) -// } -// result, err := q.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 evaluate(input interface{}, query string) (interface{}, error) { + result, err := template.Execute(query, input, nil) + if err != nil { + if syntaxError, ok := err.(parsing.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(cmd *cobra.Command, query string, result interface{}, unquoted bool, compact bool) error { -// converted, isString := result.(string) -// fmt.Fprintln(cmd.OutOrStdout(), "#", query) -// if unquoted && isString { -// fmt.Fprintln(cmd.OutOrStdout(), 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.Fprintln(cmd.OutOrStdout(), string(toJSON)) -// } -// return nil -// } +func printResult(cmd *cobra.Command, query string, result interface{}, unquoted bool, compact bool) error { + converted, isString := result.(string) + fmt.Fprintln(cmd.OutOrStdout(), "#", query) + if unquoted && isString { + fmt.Fprintln(cmd.OutOrStdout(), 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.Fprintln(cmd.OutOrStdout(), string(toJSON)) + } + return nil +} diff --git a/pkg/commands/jp/query/command_test.go b/pkg/commands/jp/query/command_test.go index 47c7fa97..a4cbd1d6 100644 --- a/pkg/commands/jp/query/command_test.go +++ b/pkg/commands/jp/query/command_test.go @@ -1,58 +1,58 @@ package query -// import ( -// "bytes" -// "io" -// "strings" -// "testing" +import ( + "bytes" + "io" + "strings" + "testing" -// "github.com/stretchr/testify/assert" -// ) + "github.com/stretchr/testify/assert" +) -// func TestCommand(t *testing.T) { -// cmd := Command() -// assert.NotNil(t, cmd) -// cmd.SetArgs([]string{"-i", "object.yaml", "-q", "query-file"}) -// err := cmd.Execute() -// assert.Error(t, err) -// } +func TestCommand(t *testing.T) { + cmd := Command() + assert.NotNil(t, cmd) + cmd.SetArgs([]string{"-i", "object.yaml", "-q", "query-file"}) + err := cmd.Execute() + assert.Error(t, err) +} -// func TestCommandWithInvalidArg(t *testing.T) { -// cmd := Command() -// assert.NotNil(t, cmd) -// b := bytes.NewBufferString("") -// cmd.SetErr(b) -// err := cmd.Execute() -// assert.Error(t, err) -// out, err := io.ReadAll(b) -// assert.NoError(t, err) -// expected := `Error: at least one query or input object is required` -// assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(out))) -// } +func TestCommandWithInvalidArg(t *testing.T) { + cmd := Command() + assert.NotNil(t, cmd) + b := bytes.NewBufferString("") + cmd.SetErr(b) + err := cmd.Execute() + assert.Error(t, err) + out, err := io.ReadAll(b) + assert.NoError(t, err) + expected := `Error: at least one query or input object is required` + assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(out))) +} -// func TestCommandWithInvalidFlag(t *testing.T) { -// cmd := Command() -// assert.NotNil(t, cmd) -// b := bytes.NewBufferString("") -// cmd.SetErr(b) -// cmd.SetArgs([]string{"--xxx"}) -// err := cmd.Execute() -// assert.Error(t, err) -// out, err := io.ReadAll(b) -// assert.NoError(t, err) -// expected := `Error: unknown flag: --xxx` -// assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(out))) -// } +func TestCommandWithInvalidFlag(t *testing.T) { + cmd := Command() + assert.NotNil(t, cmd) + b := bytes.NewBufferString("") + cmd.SetErr(b) + cmd.SetArgs([]string{"--xxx"}) + err := cmd.Execute() + assert.Error(t, err) + out, err := io.ReadAll(b) + assert.NoError(t, err) + expected := `Error: unknown flag: --xxx` + assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(out))) +} -// func TestCommandHelp(t *testing.T) { -// cmd := Command() -// assert.NotNil(t, cmd) -// b := bytes.NewBufferString("") -// cmd.SetOut(b) -// cmd.SetArgs([]string{"--help"}) -// err := cmd.Execute() -// assert.NoError(t, err) -// out, err := io.ReadAll(b) -// assert.NoError(t, err) -// assert.True(t, strings.HasPrefix(string(out), cmd.Long)) -// } +func TestCommandHelp(t *testing.T) { + cmd := Command() + assert.NotNil(t, cmd) + b := bytes.NewBufferString("") + cmd.SetOut(b) + cmd.SetArgs([]string{"--help"}) + err := cmd.Execute() + assert.NoError(t, err) + out, err := io.ReadAll(b) + assert.NoError(t, err) + assert.True(t, strings.HasPrefix(string(out), cmd.Long)) +}