1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2024-12-14 11:57:48 +00:00

feat: add scan command for generic resources (#9651)

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>
This commit is contained in:
Charles-Edouard Brétéché 2024-02-05 16:49:01 +01:00 committed by GitHub
parent 90ac90b89f
commit c649169a78
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 313 additions and 11 deletions

View file

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

View file

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

View file

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

View file

@ -0,0 +1,10 @@
package scan
// TODO
var websiteUrl = ``
var description = []string{
`Runs tests against any json compatible payloads/policies.`,
}
var examples = [][]string{}

View file

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

View file

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

View file

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

View file

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

View file

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

4
go.mod
View file

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

8
go.sum
View file

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