diff --git a/cmd/cli/kubectl-kyverno/commands/json/command.go b/cmd/cli/kubectl-kyverno/commands/json/command.go index 828f265e11..bc47ace9f5 100644 --- a/cmd/cli/kubectl-kyverno/commands/json/command.go +++ b/cmd/cli/kubectl-kyverno/commands/json/command.go @@ -2,6 +2,7 @@ package json import ( "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/command" + "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/commands/json/scan" "github.com/spf13/cobra" ) @@ -17,5 +18,6 @@ func Command() *cobra.Command { return cmd.Help() }, } + cmd.AddCommand(scan.Command()) return cmd } diff --git a/cmd/cli/kubectl-kyverno/commands/json/scan/command.go b/cmd/cli/kubectl-kyverno/commands/json/scan/command.go new file mode 100644 index 0000000000..cfc0b556fb --- /dev/null +++ b/cmd/cli/kubectl-kyverno/commands/json/scan/command.go @@ -0,0 +1,25 @@ +package scan + +import ( + "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/command" + "github.com/spf13/cobra" +) + +func Command() *cobra.Command { + var options options + cmd := &cobra.Command{ + Use: "scan", + Short: command.FormatDescription(true, websiteUrl, true, description...), + Long: command.FormatDescription(false, websiteUrl, true, description...), + Example: command.FormatExamples(examples...), + Args: cobra.NoArgs, + SilenceUsage: true, + RunE: options.run, + } + cmd.Flags().StringVar(&options.payload, "payload", "", "Path to payload (json or yaml file)") + cmd.Flags().StringSliceVar(&options.preprocessors, "pre-process", nil, "JMESPath expression used to pre process payload") + cmd.Flags().StringSliceVar(&options.policies, "policy", nil, "Path to kyverno-json policies") + cmd.Flags().StringSliceVar(&options.selectors, "labels", nil, "Labels selectors for policies") + cmd.Flags().StringVar(&options.output, "output", "text", "Output format (text or json)") + return cmd +} diff --git a/cmd/cli/kubectl-kyverno/commands/json/scan/command_test.go b/cmd/cli/kubectl-kyverno/commands/json/scan/command_test.go new file mode 100644 index 0000000000..6d013ed0fe --- /dev/null +++ b/cmd/cli/kubectl-kyverno/commands/json/scan/command_test.go @@ -0,0 +1,65 @@ +package scan + +import ( + "bytes" + "io" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCommand(t *testing.T) { + cmd := Command() + assert.NotNil(t, cmd) + err := cmd.Execute() + assert.Error(t, err) +} + +func TestCommandInvalidFileName(t *testing.T) { + cmd := Command() + assert.NotNil(t, cmd) + cmd.SetArgs([]string{"foo", "-f", ""}) + 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: requires at least 1 arg(s), only received 0` +// 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)) +} diff --git a/cmd/cli/kubectl-kyverno/commands/json/scan/doc.go b/cmd/cli/kubectl-kyverno/commands/json/scan/doc.go new file mode 100644 index 0000000000..9a2f71a861 --- /dev/null +++ b/cmd/cli/kubectl-kyverno/commands/json/scan/doc.go @@ -0,0 +1,10 @@ +package scan + +// TODO +var websiteUrl = `` + +var description = []string{ + `Runs tests against any json compatible payloads/policies.`, +} + +var examples = [][]string{} diff --git a/cmd/cli/kubectl-kyverno/commands/json/scan/options.go b/cmd/cli/kubectl-kyverno/commands/json/scan/options.go new file mode 100644 index 0000000000..715c3d2864 --- /dev/null +++ b/cmd/cli/kubectl-kyverno/commands/json/scan/options.go @@ -0,0 +1,103 @@ +package scan + +import ( + "context" + "errors" + "fmt" + "strings" + + "github.com/kyverno/kyverno-json/pkg/apis/v1alpha1" + "github.com/kyverno/kyverno-json/pkg/engine/template" + jsonengine "github.com/kyverno/kyverno-json/pkg/json-engine" + "github.com/kyverno/kyverno-json/pkg/payload" + "github.com/kyverno/kyverno-json/pkg/policy" + "github.com/kyverno/kyverno/ext/output/pluralize" + "github.com/spf13/cobra" + "go.uber.org/multierr" + "k8s.io/apimachinery/pkg/labels" +) + +type options struct { + payload string + preprocessors []string + policies []string + selectors []string + output string +} + +func (c *options) run(cmd *cobra.Command, _ []string) error { + out := newOutput(cmd.OutOrStdout(), c.output) + out.println("Loading policies ...") + policies, err := policy.Load(c.policies...) + if err != nil { + return err + } + selector := labels.Everything() + if len(c.selectors) != 0 { + parsed, err := labels.Parse(strings.Join(c.selectors, ",")) + if err != nil { + return err + } + selector = parsed + } + { + var filteredPolicies []*v1alpha1.ValidatingPolicy + for _, policy := range policies { + if selector.Matches(labels.Set(policy.Labels)) { + filteredPolicies = append(filteredPolicies, policy) + } + } + policies = filteredPolicies + } + out.println("Loading payload ...") + payload, err := payload.Load(c.payload) + if err != nil { + return err + } + if payload == nil { + return errors.New("payload is `null`") + } + out.println("Pre processing ...") + for _, preprocessor := range c.preprocessors { + result, err := template.Execute(context.Background(), preprocessor, payload, nil) + if err != nil { + return err + } + if result == nil { + return fmt.Errorf("prepocessor resulted in `null` payload (%s)", preprocessor) + } + payload = result + } + var resources []any + if slice, ok := payload.([]any); ok { + resources = slice + } else { + resources = append(resources, payload) + } + out.println("Running", "(", "evaluating", len(resources), pluralize.Pluralize(len(resources), "resource", "resources"), "against", len(policies), pluralize.Pluralize(len(policies), "policy", "policies"), ")", "...") + e := jsonengine.New() + var responses []jsonengine.Response + for _, resource := range resources { + responses = append(responses, e.Run(context.Background(), jsonengine.Request{ + Resource: resource, + Policies: policies, + })) + } + for _, response := range responses { + for _, policy := range response.Policies { + for _, rule := range policy.Rules { + if rule.Error != nil { + out.println("-", policy.Policy.Name, "/", rule.Rule.Name, "/", rule.Identifier, "ERROR:", rule.Error.Error()) + } else if len(rule.Violations) != 0 { + out.println("-", policy.Policy.Name, "/", rule.Rule.Name, "/", rule.Identifier, "FAILED:", multierr.Combine(rule.Violations...).Error()) + } else { + // TODO: handle skip, warn + out.println("-", policy.Policy.Name, "/", rule.Rule.Name, "/", rule.Identifier, "PASSED") + } + } + } + } + out.responses(responses...) + out.println("Done") + return nil +} diff --git a/cmd/cli/kubectl-kyverno/commands/json/scan/output.go b/cmd/cli/kubectl-kyverno/commands/json/scan/output.go new file mode 100644 index 0000000000..f6a1962d04 --- /dev/null +++ b/cmd/cli/kubectl-kyverno/commands/json/scan/output.go @@ -0,0 +1,48 @@ +package scan + +import ( + "encoding/json" + "fmt" + "io" + + jsonengine "github.com/kyverno/kyverno-json/pkg/json-engine" +) + +type output interface { + println(args ...any) + responses(responses ...jsonengine.Response) +} + +type textOutput struct { + out io.Writer +} + +func (t *textOutput) println(args ...any) { + fmt.Fprintln(t.out, args...) +} + +func (t *textOutput) responses(responses ...jsonengine.Response) { +} + +type jsonOutput struct { + out io.Writer +} + +func (t *jsonOutput) println(args ...any) { +} + +func (t *jsonOutput) responses(responses ...jsonengine.Response) { + payload, err := json.MarshalIndent(responses, "", " ") + if err != nil { + fmt.Fprintln(t.out, err) + } else { + fmt.Fprintln(t.out, string(payload)) + } +} + +func newOutput(out io.Writer, format string) output { + if format == "json" { + return &jsonOutput{out: out} + } + return &textOutput{out: out} +} diff --git a/cmd/cli/kubectl-kyverno/commands/test/output.go b/cmd/cli/kubectl-kyverno/commands/test/output.go index a19898f808..e392cb89cb 100644 --- a/cmd/cli/kubectl-kyverno/commands/test/output.go +++ b/cmd/cli/kubectl-kyverno/commands/test/output.go @@ -22,6 +22,7 @@ func printCheckResult( rc *resultCounts, resultsTable *table.Table, ) error { + ctx := context.Background() testCount := 1 for _, check := range checks { // filter engine responses @@ -30,7 +31,7 @@ func printCheckResult( if check.Match.Resource != nil { var filtered []engineapi.EngineResponse for _, response := range matchingEngineResponses { - errs, err := assert.Validate(context.Background(), check.Match.Resource.Value, response.Resource.UnstructuredContent(), nil) + errs, err := assert.Assert(ctx, nil, assert.Parse(ctx, check.Match.Resource.Value), response.Resource.UnstructuredContent(), nil) if err != nil { return err } @@ -48,7 +49,7 @@ func printCheckResult( if err != nil { return err } - errs, err := assert.Validate(context.Background(), check.Match.Policy.Value, data, nil) + errs, err := assert.Assert(ctx, nil, assert.Parse(ctx, check.Match.Policy.Value), data, nil) if err != nil { return err } @@ -67,7 +68,7 @@ func printCheckResult( data := map[string]any{ "name": response.Name(), } - errs, err := assert.Validate(context.Background(), check.Match.Rule.Value, data, nil) + errs, err := assert.Assert(ctx, nil, assert.Parse(ctx, check.Match.Rule.Value), data, nil) if err != nil { return err } @@ -93,7 +94,7 @@ func printCheckResult( "exception ": rule.Exception(), } if check.Assert.Value != nil { - errs, err := assert.Validate(context.Background(), check.Assert.Value, data, nil) + errs, err := assert.Assert(ctx, nil, assert.Parse(ctx, check.Assert.Value), data, nil) if err != nil { return err } @@ -124,7 +125,7 @@ func printCheckResult( testCount++ } if check.Error.Value != nil { - errs, err := assert.Validate(context.Background(), check.Error.Value, data, nil) + errs, err := assert.Assert(ctx, nil, assert.Parse(ctx, check.Error.Value), data, nil) if err != nil { return err } diff --git a/docs/user/cli/commands/kyverno_json.md b/docs/user/cli/commands/kyverno_json.md index b245ac70f9..3b1b90e642 100644 --- a/docs/user/cli/commands/kyverno_json.md +++ b/docs/user/cli/commands/kyverno_json.md @@ -39,4 +39,5 @@ kyverno json [flags] ### SEE ALSO * [kyverno](kyverno.md) - Kubernetes Native Policy Management. +* [kyverno json scan](kyverno_json_scan.md) - Runs tests against any json compatible payloads/policies. diff --git a/docs/user/cli/commands/kyverno_json_scan.md b/docs/user/cli/commands/kyverno_json_scan.md new file mode 100644 index 0000000000..4f9b38538f --- /dev/null +++ b/docs/user/cli/commands/kyverno_json_scan.md @@ -0,0 +1,47 @@ +## kyverno json scan + +Runs tests against any json compatible payloads/policies. + +### Synopsis + +Runs tests against any json compatible payloads/policies. + + NOTE: This is an experimental command, use `KYVERNO_EXPERIMENTAL=true` to enable it. + +``` +kyverno json scan [flags] +``` + +### Options + +``` + -h, --help help for scan + --labels strings Labels selectors for policies + --output string Output format (text or json) (default "text") + --payload string Path to payload (json or yaml file) + --policy strings Path to kyverno-json policies + --pre-process strings JMESPath expression used to pre process payload +``` + +### Options inherited from parent commands + +``` + --add_dir_header If true, adds the file directory to the header of the log messages + --alsologtostderr log to standard error as well as files (no effect when -logtostderr=true) + --log_backtrace_at traceLocation when logging hits line file:N, emit a stack trace (default :0) + --log_dir string If non-empty, write log files in this directory (no effect when -logtostderr=true) + --log_file string If non-empty, use this log file (no effect when -logtostderr=true) + --log_file_max_size uint Defines the maximum size a log file can grow to (no effect when -logtostderr=true). Unit is megabytes. If the value is 0, the maximum file size is unlimited. (default 1800) + --logtostderr log to standard error instead of files (default true) + --one_output If true, only write logs to their native severity level (vs also writing to each lower severity level; no effect when -logtostderr=true) + --skip_headers If true, avoid header prefixes in the log messages + --skip_log_headers If true, avoid headers when opening log files (no effect when -logtostderr=true) + --stderrthreshold severity logs at or above this threshold go to stderr when writing to files and stderr (no effect when -logtostderr=true or -alsologtostderr=true) (default 2) + -v, --v Level number for the log level verbosity + --vmodule moduleSpec comma-separated list of pattern=N settings for file-filtered logging +``` + +### SEE ALSO + +* [kyverno json](kyverno_json.md) - Runs tests against any json compatible payloads/policies. + diff --git a/go.mod b/go.mod index ecbb409193..d0fbbdabd3 100644 --- a/go.mod +++ b/go.mod @@ -31,7 +31,7 @@ require ( github.com/julienschmidt/httprouter v1.3.0 github.com/kataras/tablewriter v0.0.0-20180708051242-e063d29b7c23 github.com/kyverno/go-jmespath v0.4.1-0.20231124160150-95e59c162877 - github.com/kyverno/kyverno-json v0.0.1 + github.com/kyverno/kyverno-json v0.0.3-0.20240202090521-82d03384ea37 github.com/lensesio/tableprinter v0.0.0-20201125135848-89e81fc956e7 github.com/notaryproject/notation-core-go v1.0.2 github.com/notaryproject/notation-go v1.1.0 @@ -253,7 +253,7 @@ require ( github.com/jedisct1/go-minisign v0.0.0-20230811132847-661be99b8267 // indirect github.com/jellydator/ttlcache/v3 v3.1.1 // indirect github.com/jinzhu/copier v0.4.0 // indirect - github.com/jmespath-community/go-jmespath v1.1.2-0.20231004164315-78945398586a // indirect + github.com/jmespath-community/go-jmespath v1.1.2-0.20240117150817-e430401a2172 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 diff --git a/go.sum b/go.sum index cccaff7ac5..30adf764f2 100644 --- a/go.sum +++ b/go.sum @@ -547,8 +547,8 @@ github.com/jellydator/ttlcache/v3 v3.1.1 h1:RCgYJqo3jgvhl+fEWvjNW8thxGWsgxi+TPhR github.com/jellydator/ttlcache/v3 v3.1.1/go.mod h1:hi7MGFdMAwZna5n2tuvh63DvFLzVKySzCVW6+0gA2n4= github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= -github.com/jmespath-community/go-jmespath v1.1.2-0.20231004164315-78945398586a h1:8W5d74FhEWTJPnFwpDDxbwUK3pPLUbY4RlfN2uzTTSE= -github.com/jmespath-community/go-jmespath v1.1.2-0.20231004164315-78945398586a/go.mod h1:4gOyFJsR/Gk+05RgTKYrifT7tBPWD8Lubtb5jRrfy9I= +github.com/jmespath-community/go-jmespath v1.1.2-0.20240117150817-e430401a2172 h1:XQYEhx+bEiWn6eiHFivu4wEHm91FoZ/gCvoLZK6Ze5Y= +github.com/jmespath-community/go-jmespath v1.1.2-0.20240117150817-e430401a2172/go.mod h1:j4OeykGPBbhX3rw4AOPGXSmX2/zuWXktm704A4MtHFs= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= @@ -590,8 +590,8 @@ github.com/kyverno/go-jmespath v0.4.1-0.20231124160150-95e59c162877 h1:XOLJNGX/q github.com/kyverno/go-jmespath v0.4.1-0.20231124160150-95e59c162877/go.mod h1:yzDHaKovQy16rjN4kFnjF+IdNoN4p1ndw+va6+B8zUU= github.com/kyverno/go-jmespath/internal/testify v1.5.2-0.20230630133209-945021c749d9 h1:lL311dF3a2aeNibJj8v+uhFU3XkvRHZmCtAdSPOrQYY= github.com/kyverno/go-jmespath/internal/testify v1.5.2-0.20230630133209-945021c749d9/go.mod h1:XRxUGHIiCy1WYma1CdfdO1WOhIe8dLPTENaZr5D1ex4= -github.com/kyverno/kyverno-json v0.0.1 h1:2d3k1M0YCWRz9r5fdHkIMesChPbmtSYqR6qk+2s05b0= -github.com/kyverno/kyverno-json v0.0.1/go.mod h1:7lNc9nnrNYC1Pbn/Qd5acyoRXa6sqBrZulc6Rg64q7w= +github.com/kyverno/kyverno-json v0.0.3-0.20240202090521-82d03384ea37 h1:h3USymehtGfFbqzk24SYZjaM3BonWutMG+0Hscj3CZQ= +github.com/kyverno/kyverno-json v0.0.3-0.20240202090521-82d03384ea37/go.mod h1:6He8M7B7FO51qojD5tZVU/OuVWFFn/urXbQXO5DuEVk= github.com/lensesio/tableprinter v0.0.0-20201125135848-89e81fc956e7 h1:k/1ku0yehLCPqERCHkIHMDqDg1R02AcCScRuHbamU3s= github.com/lensesio/tableprinter v0.0.0-20201125135848-89e81fc956e7/go.mod h1:YR/zYthNdWfO8+0IOyHDcIDBBBS2JMnYUIwSsnwmRqU= github.com/letsencrypt/boulder v0.0.0-20240127020530-97a19b18d21e h1:7QjzPboPE+0pVMsZP1sz1mN26m6vew78YmcIZz1FMrg=