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:
parent
90ac90b89f
commit
c649169a78
11 changed files with 313 additions and 11 deletions
|
@ -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
|
||||
}
|
||||
|
|
25
cmd/cli/kubectl-kyverno/commands/json/scan/command.go
Normal file
25
cmd/cli/kubectl-kyverno/commands/json/scan/command.go
Normal 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
|
||||
}
|
65
cmd/cli/kubectl-kyverno/commands/json/scan/command_test.go
Normal file
65
cmd/cli/kubectl-kyverno/commands/json/scan/command_test.go
Normal 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))
|
||||
}
|
10
cmd/cli/kubectl-kyverno/commands/json/scan/doc.go
Normal file
10
cmd/cli/kubectl-kyverno/commands/json/scan/doc.go
Normal file
|
@ -0,0 +1,10 @@
|
|||
package scan
|
||||
|
||||
// TODO
|
||||
var websiteUrl = ``
|
||||
|
||||
var description = []string{
|
||||
`Runs tests against any json compatible payloads/policies.`,
|
||||
}
|
||||
|
||||
var examples = [][]string{}
|
103
cmd/cli/kubectl-kyverno/commands/json/scan/options.go
Normal file
103
cmd/cli/kubectl-kyverno/commands/json/scan/options.go
Normal 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
|
||||
}
|
48
cmd/cli/kubectl-kyverno/commands/json/scan/output.go
Normal file
48
cmd/cli/kubectl-kyverno/commands/json/scan/output.go
Normal 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}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
47
docs/user/cli/commands/kyverno_json_scan.md
Normal file
47
docs/user/cli/commands/kyverno_json_scan.md
Normal 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
4
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
|
||||
|
|
8
go.sum
8
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=
|
||||
|
|
Loading…
Reference in a new issue