1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2025-03-31 03:45:17 +00:00

refactor: split CLI jp command (#5566)

* 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>
This commit is contained in:
Charles-Edouard Brétéché 2022-12-21 07:06:13 +01:00 committed by GitHub
parent c6ba1c85b5
commit b28c16fe99
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 392 additions and 167 deletions

View file

@ -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()
}
}
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}