From d24b0848a63263338541dd9e7006682fa6096c60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Charles-Edouard=20Br=C3=A9t=C3=A9ch=C3=A9?= Date: Tue, 12 Sep 2023 23:47:03 +0200 Subject: [PATCH] chore: add cli commands unit tests (#8366) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: add cli unit tests Signed-off-by: Charles-Edouard Brétéché * chore: add cli commands unit tests Signed-off-by: Charles-Edouard Brétéché --------- Signed-off-by: Charles-Edouard Brétéché --- cmd/cli/kubectl-kyverno/commands/command.go | 3 +- .../commands/create/command.go | 11 +- .../commands/create/command_test.go | 22 ++++ .../commands/create/exception/command.go | 12 +- .../commands/create/metrics-config/command.go | 15 ++- .../create/metrics-config/command_test.go | 22 ++++ .../commands/create/test/command.go | 15 ++- .../commands/create/test/command_test.go | 22 ++++ .../commands/create/user-info/command.go | 15 ++- .../commands/create/user-info/command_test.go | 22 ++++ .../commands/create/values/command.go | 15 ++- .../commands/create/values/command_test.go | 22 ++++ .../kubectl-kyverno/commands/docs/command.go | 64 ++-------- .../commands/docs/command_test.go | 23 ++++ .../kubectl-kyverno/commands/docs/options.go | 41 +++++++ .../kubectl-kyverno/commands/docs/utils.go | 35 ++++++ .../kubectl-kyverno/commands/fix/command.go | 11 +- .../commands/fix/command_test.go | 22 ++++ .../commands/fix/test/command.go | 7 +- .../commands/fix/test/command_test.go | 22 ++++ .../commands/fix/test/options.go | 52 ++++---- .../kubectl-kyverno/commands/jp/command.go | 21 ++-- .../commands/jp/command_test.go | 22 ++++ .../commands/jp/function/command.go | 24 ++-- .../commands/jp/function/command_test.go | 30 +++++ .../commands/jp/parse/command.go | 29 ++--- .../commands/jp/parse/command_test.go | 15 +++ .../commands/jp/query/command.go | 45 +++---- .../commands/jp/query/command_test.go | 15 +++ .../kubectl-kyverno/commands/oci/command.go | 13 +- .../commands/oci/command_test.go | 22 ++++ .../commands/oci/pull/command.go | 115 +++--------------- .../commands/oci/pull/command_test.go | 33 +++++ .../kubectl-kyverno/commands/oci/pull/doc.go | 2 +- .../commands/oci/pull/options.go | 108 ++++++++++++++++ .../commands/oci/push/command.go | 83 ++----------- .../commands/oci/push/command_test.go | 33 +++++ .../kubectl-kyverno/commands/oci/push/doc.go | 4 +- .../commands/oci/push/options.go | 84 +++++++++++++ .../commands/version/command.go | 4 +- .../commands/version/command_test.go | 2 + docs/user/cli/kyverno_fix_test.md | 2 +- docs/user/cli/kyverno_oci_pull.md | 9 +- docs/user/cli/kyverno_oci_push.md | 9 +- .../kyverno-test.yaml | 28 +++-- 45 files changed, 847 insertions(+), 378 deletions(-) create mode 100644 cmd/cli/kubectl-kyverno/commands/create/command_test.go create mode 100644 cmd/cli/kubectl-kyverno/commands/create/metrics-config/command_test.go create mode 100644 cmd/cli/kubectl-kyverno/commands/create/test/command_test.go create mode 100644 cmd/cli/kubectl-kyverno/commands/create/user-info/command_test.go create mode 100644 cmd/cli/kubectl-kyverno/commands/create/values/command_test.go create mode 100644 cmd/cli/kubectl-kyverno/commands/docs/command_test.go create mode 100644 cmd/cli/kubectl-kyverno/commands/docs/options.go create mode 100644 cmd/cli/kubectl-kyverno/commands/docs/utils.go create mode 100644 cmd/cli/kubectl-kyverno/commands/fix/command_test.go create mode 100644 cmd/cli/kubectl-kyverno/commands/fix/test/command_test.go create mode 100644 cmd/cli/kubectl-kyverno/commands/jp/command_test.go create mode 100644 cmd/cli/kubectl-kyverno/commands/jp/function/command_test.go create mode 100644 cmd/cli/kubectl-kyverno/commands/jp/parse/command_test.go create mode 100644 cmd/cli/kubectl-kyverno/commands/jp/query/command_test.go create mode 100644 cmd/cli/kubectl-kyverno/commands/oci/command_test.go create mode 100644 cmd/cli/kubectl-kyverno/commands/oci/pull/command_test.go create mode 100644 cmd/cli/kubectl-kyverno/commands/oci/pull/options.go create mode 100644 cmd/cli/kubectl-kyverno/commands/oci/push/command_test.go create mode 100644 cmd/cli/kubectl-kyverno/commands/oci/push/options.go diff --git a/cmd/cli/kubectl-kyverno/commands/command.go b/cmd/cli/kubectl-kyverno/commands/command.go index 44192957b9..4b433ee3f1 100644 --- a/cmd/cli/kubectl-kyverno/commands/command.go +++ b/cmd/cli/kubectl-kyverno/commands/command.go @@ -18,9 +18,10 @@ func RootCommand(experimental bool) *cobra.Command { Use: "kyverno", Short: command.FormatDescription(true, websiteUrl, false, description...), Long: command.FormatDescription(false, websiteUrl, false, description...), + Args: cobra.NoArgs, SilenceErrors: true, SilenceUsage: true, - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, _ []string) error { return cmd.Help() }, } diff --git a/cmd/cli/kubectl-kyverno/commands/create/command.go b/cmd/cli/kubectl-kyverno/commands/create/command.go index a698a9f793..5de86353ee 100644 --- a/cmd/cli/kubectl-kyverno/commands/create/command.go +++ b/cmd/cli/kubectl-kyverno/commands/create/command.go @@ -12,10 +12,13 @@ import ( func Command() *cobra.Command { cmd := &cobra.Command{ - Use: "create", - Short: command.FormatDescription(true, websiteUrl, false, description...), - Long: command.FormatDescription(false, websiteUrl, false, description...), - Example: command.FormatExamples(examples...), + Use: "create", + Short: command.FormatDescription(true, websiteUrl, false, description...), + Long: command.FormatDescription(false, websiteUrl, false, description...), + Example: command.FormatExamples(examples...), + Args: cobra.NoArgs, + SilenceErrors: true, + SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) error { return cmd.Help() }, diff --git a/cmd/cli/kubectl-kyverno/commands/create/command_test.go b/cmd/cli/kubectl-kyverno/commands/create/command_test.go new file mode 100644 index 0000000000..d58fe4bdd5 --- /dev/null +++ b/cmd/cli/kubectl-kyverno/commands/create/command_test.go @@ -0,0 +1,22 @@ +package create + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCommand(t *testing.T) { + cmd := Command() + assert.NotNil(t, cmd) + err := cmd.Execute() + assert.NoError(t, err) +} + +func TestCommandWithArgs(t *testing.T) { + cmd := Command() + assert.NotNil(t, cmd) + cmd.SetArgs([]string{"foo"}) + err := cmd.Execute() + assert.Error(t, err) +} diff --git a/cmd/cli/kubectl-kyverno/commands/create/exception/command.go b/cmd/cli/kubectl-kyverno/commands/create/exception/command.go index 8f96080146..c110601867 100644 --- a/cmd/cli/kubectl-kyverno/commands/create/exception/command.go +++ b/cmd/cli/kubectl-kyverno/commands/create/exception/command.go @@ -27,11 +27,13 @@ func Command() *cobra.Command { var rules, any, all []string var options options cmd := &cobra.Command{ - Use: "exception [name]", - Short: command.FormatDescription(true, websiteUrl, false, description...), - Long: command.FormatDescription(false, websiteUrl, false, description...), - Example: command.FormatExamples(examples...), - Args: cobra.ExactArgs(1), + Use: "exception [name]", + Short: command.FormatDescription(true, websiteUrl, false, description...), + Long: command.FormatDescription(false, websiteUrl, false, description...), + Example: command.FormatExamples(examples...), + Args: cobra.ExactArgs(1), + SilenceErrors: true, + SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) error { tmpl, err := template.New("exception").Parse(templates.ExceptionTemplate) if err != nil { diff --git a/cmd/cli/kubectl-kyverno/commands/create/metrics-config/command.go b/cmd/cli/kubectl-kyverno/commands/create/metrics-config/command.go index e8cb07a956..c93e96e7dd 100644 --- a/cmd/cli/kubectl-kyverno/commands/create/metrics-config/command.go +++ b/cmd/cli/kubectl-kyverno/commands/create/metrics-config/command.go @@ -25,16 +25,19 @@ func Command() *cobra.Command { var path string var options options cmd := &cobra.Command{ - Use: "metrics-config", - Short: command.FormatDescription(true, websiteUrl, false, description...), - Long: command.FormatDescription(false, websiteUrl, false, description...), - Example: command.FormatExamples(examples...), - RunE: func(cmd *cobra.Command, args []string) error { + Use: "metrics-config", + Short: command.FormatDescription(true, websiteUrl, false, description...), + Long: command.FormatDescription(false, websiteUrl, false, description...), + Example: command.FormatExamples(examples...), + Args: cobra.NoArgs, + SilenceErrors: true, + SilenceUsage: true, + RunE: func(cmd *cobra.Command, _ []string) error { tmpl, err := template.New("metricsconfig").Funcs(sprig.HermeticTxtFuncMap()).Parse(templates.MetricsConfigTemplate) if err != nil { return err } - output := os.Stdout + output := cmd.OutOrStdout() if path != "" { file, err := os.Create(path) if err != nil { diff --git a/cmd/cli/kubectl-kyverno/commands/create/metrics-config/command_test.go b/cmd/cli/kubectl-kyverno/commands/create/metrics-config/command_test.go new file mode 100644 index 0000000000..3a7645d679 --- /dev/null +++ b/cmd/cli/kubectl-kyverno/commands/create/metrics-config/command_test.go @@ -0,0 +1,22 @@ +package metricsconfig + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCommand(t *testing.T) { + cmd := Command() + assert.NotNil(t, cmd) + err := cmd.Execute() + assert.NoError(t, err) +} + +func TestCommandWithArgs(t *testing.T) { + cmd := Command() + assert.NotNil(t, cmd) + cmd.SetArgs([]string{"foo"}) + err := cmd.Execute() + assert.Error(t, err) +} diff --git a/cmd/cli/kubectl-kyverno/commands/create/test/command.go b/cmd/cli/kubectl-kyverno/commands/create/test/command.go index 424ca0d7e5..e49b6466e9 100644 --- a/cmd/cli/kubectl-kyverno/commands/create/test/command.go +++ b/cmd/cli/kubectl-kyverno/commands/create/test/command.go @@ -33,11 +33,14 @@ func Command() *cobra.Command { var options options var pass, fail, skip []string cmd := &cobra.Command{ - Use: "test", - Short: command.FormatDescription(true, websiteUrl, false, description...), - Long: command.FormatDescription(false, websiteUrl, false, description...), - Example: command.FormatExamples(examples...), - RunE: func(cmd *cobra.Command, args []string) error { + Use: "test", + Short: command.FormatDescription(true, websiteUrl, false, description...), + Long: command.FormatDescription(false, websiteUrl, false, description...), + Example: command.FormatExamples(examples...), + Args: cobra.NoArgs, + SilenceErrors: true, + SilenceUsage: true, + RunE: func(cmd *cobra.Command, _ []string) error { tmpl, err := template.New("test").Parse(templates.TestTemplate) if err != nil { return err @@ -60,7 +63,7 @@ func Command() *cobra.Command { options.Results = append(options.Results, result) } } - output := os.Stdout + output := cmd.OutOrStdout() if path != "" { file, err := os.Create(path) if err != nil { diff --git a/cmd/cli/kubectl-kyverno/commands/create/test/command_test.go b/cmd/cli/kubectl-kyverno/commands/create/test/command_test.go new file mode 100644 index 0000000000..ff87af915f --- /dev/null +++ b/cmd/cli/kubectl-kyverno/commands/create/test/command_test.go @@ -0,0 +1,22 @@ +package test + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCommand(t *testing.T) { + cmd := Command() + assert.NotNil(t, cmd) + err := cmd.Execute() + assert.NoError(t, err) +} + +func TestCommandWithArgs(t *testing.T) { + cmd := Command() + assert.NotNil(t, cmd) + cmd.SetArgs([]string{"foo"}) + err := cmd.Execute() + assert.Error(t, err) +} diff --git a/cmd/cli/kubectl-kyverno/commands/create/user-info/command.go b/cmd/cli/kubectl-kyverno/commands/create/user-info/command.go index 4db3d2de6c..0fcc11fbe6 100644 --- a/cmd/cli/kubectl-kyverno/commands/create/user-info/command.go +++ b/cmd/cli/kubectl-kyverno/commands/create/user-info/command.go @@ -16,16 +16,19 @@ func Command() *cobra.Command { var username string var roles, clusterRoles, groups []string cmd := &cobra.Command{ - Use: "user-info", - Short: command.FormatDescription(true, websiteUrl, false, description...), - Long: command.FormatDescription(false, websiteUrl, false, description...), - Example: command.FormatExamples(examples...), - RunE: func(cmd *cobra.Command, args []string) error { + Use: "user-info", + Short: command.FormatDescription(true, websiteUrl, false, description...), + Long: command.FormatDescription(false, websiteUrl, false, description...), + Example: command.FormatExamples(examples...), + Args: cobra.NoArgs, + SilenceErrors: true, + SilenceUsage: true, + RunE: func(cmd *cobra.Command, _ []string) error { tmpl, err := template.New("userinfo").Parse(templates.UserInfoTemplate) if err != nil { return err } - output := os.Stdout + output := cmd.OutOrStdout() if path != "" { file, err := os.Create(path) if err != nil { diff --git a/cmd/cli/kubectl-kyverno/commands/create/user-info/command_test.go b/cmd/cli/kubectl-kyverno/commands/create/user-info/command_test.go new file mode 100644 index 0000000000..4167790899 --- /dev/null +++ b/cmd/cli/kubectl-kyverno/commands/create/user-info/command_test.go @@ -0,0 +1,22 @@ +package userinfo + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCommand(t *testing.T) { + cmd := Command() + assert.NotNil(t, cmd) + err := cmd.Execute() + assert.NoError(t, err) +} + +func TestCommandWithArgs(t *testing.T) { + cmd := Command() + assert.NotNil(t, cmd) + cmd.SetArgs([]string{"foo"}) + err := cmd.Execute() + assert.Error(t, err) +} diff --git a/cmd/cli/kubectl-kyverno/commands/create/values/command.go b/cmd/cli/kubectl-kyverno/commands/create/values/command.go index 8dcf02d2bf..d769b00184 100644 --- a/cmd/cli/kubectl-kyverno/commands/create/values/command.go +++ b/cmd/cli/kubectl-kyverno/commands/create/values/command.go @@ -15,16 +15,19 @@ func Command() *cobra.Command { var path string var globalValues, namespaceSelector, rules, resources []string cmd := &cobra.Command{ - Use: "values", - Short: command.FormatDescription(true, websiteUrl, false, description...), - Long: command.FormatDescription(false, websiteUrl, false, description...), - Example: command.FormatExamples(examples...), - RunE: func(cmd *cobra.Command, args []string) error { + Use: "values", + Short: command.FormatDescription(true, websiteUrl, false, description...), + Long: command.FormatDescription(false, websiteUrl, false, description...), + Example: command.FormatExamples(examples...), + Args: cobra.NoArgs, + SilenceErrors: true, + SilenceUsage: true, + RunE: func(cmd *cobra.Command, _ []string) error { tmpl, err := template.New("values").Parse(templates.ValuesTemplate) if err != nil { return err } - output := os.Stdout + output := cmd.OutOrStdout() if path != "" { file, err := os.Create(path) if err != nil { diff --git a/cmd/cli/kubectl-kyverno/commands/create/values/command_test.go b/cmd/cli/kubectl-kyverno/commands/create/values/command_test.go new file mode 100644 index 0000000000..8cfd463f74 --- /dev/null +++ b/cmd/cli/kubectl-kyverno/commands/create/values/command_test.go @@ -0,0 +1,22 @@ +package values + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCommand(t *testing.T) { + cmd := Command() + assert.NotNil(t, cmd) + err := cmd.Execute() + assert.NoError(t, err) +} + +func TestCommandWithArgs(t *testing.T) { + cmd := Command() + assert.NotNil(t, cmd) + cmd.SetArgs([]string{"foo"}) + err := cmd.Execute() + assert.Error(t, err) +} diff --git a/cmd/cli/kubectl-kyverno/commands/docs/command.go b/cmd/cli/kubectl-kyverno/commands/docs/command.go index 83a68cf72b..d51c21c2eb 100644 --- a/cmd/cli/kubectl-kyverno/commands/docs/command.go +++ b/cmd/cli/kubectl-kyverno/commands/docs/command.go @@ -1,74 +1,32 @@ package docs import ( - "errors" - "fmt" "log" - "os" - "path" - "path/filepath" - "strings" - "time" "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/command" "github.com/spf13/cobra" - "github.com/spf13/cobra/doc" ) -const fmTemplate = `--- -date: %s -title: "%s" -weight: 35 ---- -` - -func websitePrepender(filename string) string { - now := time.Now().Format(time.RFC3339) - name := filepath.Base(filename) - base := strings.TrimSuffix(name, path.Ext(name)) - return fmt.Sprintf(fmTemplate, now, strings.Replace(base, "_", " ", -1)) -} - -func websiteLinkHandler(filename string) string { - return "../" + strings.TrimSuffix(filename, filepath.Ext(filename)) -} - -func identity(s string) string { - return s -} - -func empty(s string) string { - return "" -} - func Command(root *cobra.Command) *cobra.Command { - var path string - var website bool - var autogenTag bool + var options options cmd := &cobra.Command{ Use: "docs", Short: command.FormatDescription(true, websiteUrl, false, description...), Long: command.FormatDescription(false, websiteUrl, false, description...), Example: command.FormatExamples(examples...), - RunE: func(_ *cobra.Command, args []string) error { - prepender := empty - linkHandler := identity - if website { - prepender = websitePrepender - linkHandler = websiteLinkHandler + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, _ []string) error { + if err := options.validate(root); err != nil { + return err } - if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) { - if err := os.MkdirAll(path, os.ModeDir|os.ModePerm); err != nil { - return err - } - } - root.DisableAutoGenTag = !autogenTag - return doc.GenMarkdownTreeCustom(root, path, prepender, linkHandler) + cmd.SilenceUsage = true + cmd.SilenceErrors = true + return options.execute(root) }, } - cmd.Flags().StringVarP(&path, "output", "o", ".", "Output path") - cmd.Flags().BoolVar(&website, "website", false, "Website version") - cmd.Flags().BoolVar(&autogenTag, "autogenTag", true, "Determines if the generated docs should contain a timestamp") + cmd.Flags().StringVarP(&options.path, "output", "o", ".", "Output path") + cmd.Flags().BoolVar(&options.website, "website", false, "Website version") + cmd.Flags().BoolVar(&options.autogenTag, "autogenTag", true, "Determines if the generated docs should contain a timestamp") if err := cmd.MarkFlagDirname("output"); err != nil { log.Println("WARNING", err) } diff --git a/cmd/cli/kubectl-kyverno/commands/docs/command_test.go b/cmd/cli/kubectl-kyverno/commands/docs/command_test.go new file mode 100644 index 0000000000..4b714eaf0d --- /dev/null +++ b/cmd/cli/kubectl-kyverno/commands/docs/command_test.go @@ -0,0 +1,23 @@ +package docs + +import ( + "testing" + + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" +) + +func TestCommandWithNilRoot(t *testing.T) { + cmd := Command(nil) + assert.NotNil(t, cmd) + cmd.SetArgs([]string{"-o", "foo"}) + err := cmd.Execute() + assert.Error(t, err) +} + +func TestCommandWithoutArgs(t *testing.T) { + cmd := Command(&cobra.Command{}) + assert.NotNil(t, cmd) + err := cmd.Execute() + assert.Error(t, err) +} diff --git a/cmd/cli/kubectl-kyverno/commands/docs/options.go b/cmd/cli/kubectl-kyverno/commands/docs/options.go new file mode 100644 index 0000000000..3c756c6b2e --- /dev/null +++ b/cmd/cli/kubectl-kyverno/commands/docs/options.go @@ -0,0 +1,41 @@ +package docs + +import ( + "errors" + "os" + + "github.com/spf13/cobra" + "github.com/spf13/cobra/doc" +) + +type options struct { + path string + website bool + autogenTag bool +} + +func (o options) validate(root *cobra.Command) error { + if o.path == "" { + return errors.New("path is required") + } + if root == nil { + return errors.New("root command is required") + } + return nil +} + +func (o options) execute(root *cobra.Command) error { + prepender := empty + linkHandler := identity + if o.website { + prepender = websitePrepender + linkHandler = websiteLinkHandler + } + if _, err := os.Stat(o.path); errors.Is(err, os.ErrNotExist) { + if err := os.MkdirAll(o.path, os.ModeDir|os.ModePerm); err != nil { + return err + } + } + root.DisableAutoGenTag = !o.autogenTag + return doc.GenMarkdownTreeCustom(root, o.path, prepender, linkHandler) +} diff --git a/cmd/cli/kubectl-kyverno/commands/docs/utils.go b/cmd/cli/kubectl-kyverno/commands/docs/utils.go new file mode 100644 index 0000000000..14250f24a0 --- /dev/null +++ b/cmd/cli/kubectl-kyverno/commands/docs/utils.go @@ -0,0 +1,35 @@ +package docs + +import ( + "fmt" + "path" + "path/filepath" + "strings" + "time" +) + +const fmTemplate = `--- +date: %s +title: "%s" +weight: 35 +--- +` + +func websitePrepender(filename string) string { + now := time.Now().Format(time.RFC3339) + name := filepath.Base(filename) + base := strings.TrimSuffix(name, path.Ext(name)) + return fmt.Sprintf(fmTemplate, now, strings.Replace(base, "_", " ", -1)) +} + +func websiteLinkHandler(filename string) string { + return "../" + strings.TrimSuffix(filename, filepath.Ext(filename)) +} + +func identity(s string) string { + return s +} + +func empty(s string) string { + return "" +} diff --git a/cmd/cli/kubectl-kyverno/commands/fix/command.go b/cmd/cli/kubectl-kyverno/commands/fix/command.go index e01adbe0fc..392745cf1b 100644 --- a/cmd/cli/kubectl-kyverno/commands/fix/command.go +++ b/cmd/cli/kubectl-kyverno/commands/fix/command.go @@ -8,10 +8,13 @@ import ( func Command() *cobra.Command { cmd := &cobra.Command{ - Use: "fix", - Short: command.FormatDescription(true, websiteUrl, true, description...), - Long: command.FormatDescription(false, websiteUrl, true, description...), - Example: command.FormatExamples(examples...), + Use: "fix", + Short: command.FormatDescription(true, websiteUrl, true, description...), + Long: command.FormatDescription(false, websiteUrl, true, description...), + Example: command.FormatExamples(examples...), + Args: cobra.NoArgs, + SilenceErrors: true, + SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) error { return cmd.Help() }, diff --git a/cmd/cli/kubectl-kyverno/commands/fix/command_test.go b/cmd/cli/kubectl-kyverno/commands/fix/command_test.go new file mode 100644 index 0000000000..ed81523f55 --- /dev/null +++ b/cmd/cli/kubectl-kyverno/commands/fix/command_test.go @@ -0,0 +1,22 @@ +package fix + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCommand(t *testing.T) { + cmd := Command() + assert.NotNil(t, cmd) + err := cmd.Execute() + assert.NoError(t, err) +} + +func TestCommandWithArgs(t *testing.T) { + cmd := Command() + assert.NotNil(t, cmd) + cmd.SetArgs([]string{"foo"}) + err := cmd.Execute() + assert.Error(t, err) +} diff --git a/cmd/cli/kubectl-kyverno/commands/fix/test/command.go b/cmd/cli/kubectl-kyverno/commands/fix/test/command.go index c3435f05f9..e847bb03cf 100644 --- a/cmd/cli/kubectl-kyverno/commands/fix/test/command.go +++ b/cmd/cli/kubectl-kyverno/commands/fix/test/command.go @@ -8,17 +8,18 @@ import ( func Command() *cobra.Command { var options options cmd := &cobra.Command{ - Use: "test [folder]...", + Use: "test [dir]...", Short: command.FormatDescription(true, websiteUrl, true, description...), Long: command.FormatDescription(false, websiteUrl, true, description...), Example: command.FormatExamples(examples...), + Args: cobra.MinimumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - if err := options.validate(); err != nil { + if err := options.validate(args...); err != nil { return err } cmd.SilenceUsage = true cmd.SilenceErrors = true - return options.execute(args...) + return options.execute(cmd.OutOrStdout(), args...) }, } cmd.Flags().StringVarP(&options.fileName, "file-name", "f", "kyverno-test.yaml", "Test filename") diff --git a/cmd/cli/kubectl-kyverno/commands/fix/test/command_test.go b/cmd/cli/kubectl-kyverno/commands/fix/test/command_test.go new file mode 100644 index 0000000000..2224fd8582 --- /dev/null +++ b/cmd/cli/kubectl-kyverno/commands/fix/test/command_test.go @@ -0,0 +1,22 @@ +package test + +import ( + "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) +} diff --git a/cmd/cli/kubectl-kyverno/commands/fix/test/options.go b/cmd/cli/kubectl-kyverno/commands/fix/test/options.go index d2dc94cda4..6d971da6ba 100644 --- a/cmd/cli/kubectl-kyverno/commands/fix/test/options.go +++ b/cmd/cli/kubectl-kyverno/commands/fix/test/options.go @@ -3,6 +3,7 @@ package test import ( "errors" "fmt" + "io" "os" "path/filepath" @@ -17,16 +18,19 @@ type options struct { compress bool } -func (o options) validate() error { +func (o options) validate(dirs ...string) error { if o.fileName == "" { return errors.New("file-name must not be set to an empty string") } + if len(dirs) == 0 { + return errors.New("at least one test directory is required") + } return nil } -func (o options) execute(args ...string) error { +func (o options) execute(out io.Writer, dirs ...string) error { var testCases []test.TestCase - for _, arg := range args { + for _, arg := range dirs { tests, err := test.LoadTests(arg, o.fileName) if err != nil { return err @@ -34,48 +38,48 @@ func (o options) execute(args ...string) error { testCases = append(testCases, tests...) } for _, testCase := range testCases { - fmt.Printf("Processing test file (%s)...", testCase.Path) - fmt.Println() + fmt.Fprintf(out, "Processing test file (%s)...", testCase.Path) + fmt.Fprintln(out) if testCase.Err != nil { - fmt.Printf(" ERROR: loading test file (%s): %s", testCase.Path, testCase.Err) - fmt.Println() + fmt.Fprintf(out, " ERROR: loading test file (%s): %s", testCase.Path, testCase.Err) + fmt.Fprintln(out) continue } test := testCase.Test needsSave := false if test.Name == "" { - fmt.Println(" WARNING: name is not set") + fmt.Fprintln(out, " WARNING: name is not set") test.Name = filepath.Base(testCase.Path) needsSave = true } if len(test.Policies) == 0 { - fmt.Println(" WARNING: test has no policies") + fmt.Fprintln(out, " WARNING: test has no policies") } if len(test.Resources) == 0 { - fmt.Println(" WARNING: test has no resources") + fmt.Fprintln(out, " WARNING: test has no resources") } for i := range test.Results { result := &test.Results[i] if result.Resource != "" && len(result.Resources) != 0 { - fmt.Println(" WARNING: test result should not use both `resource` and `resources` fields") + fmt.Fprintln(out, " WARNING: test result should not use both `resource` and `resources` fields") } if result.Resource != "" { - fmt.Println(" WARNING: test result uses deprecated `resource` field, moving it into the `resources` field") + fmt.Fprintln(out, " WARNING: test result uses deprecated `resource` field, moving it into the `resources` field") result.Resources = append(result.Resources, result.Resource) result.Resource = "" needsSave = true } if result.Namespace != "" { - fmt.Println(" WARNING: test result uses deprecated `namespace` field, replacing `policy` with a `/` pattern") + fmt.Fprintln(out, " WARNING: test result uses deprecated `namespace` field, replacing `policy` with a `/` pattern") result.Policy = fmt.Sprintf("%s/%s", result.Namespace, result.Policy) result.Namespace = "" needsSave = true } if result.Status != "" && result.Result != "" { - fmt.Println(" ERROR: test result should not use both `status` and `result` fields") + fmt.Fprintln(out, " ERROR: test result should not use both `status` and `result` fields") } if result.Status != "" && result.Result == "" { - fmt.Println(" WARNING: test result uses deprecated `status` field, moving it into the `result` field") + fmt.Fprintln(out, " WARNING: test result uses deprecated `status` field, moving it into the `result` field") result.Result = result.Status result.Status = "" needsSave = true @@ -98,23 +102,23 @@ func (o options) execute(args ...string) error { } } if o.save && needsSave { - fmt.Printf(" Saving test file (%s)...", testCase.Path) - fmt.Println() + fmt.Fprintf(out, " Saving test file (%s)...", testCase.Path) + fmt.Fprintln(out) yamlBytes, err := yaml.Marshal(test) if err != nil { - fmt.Printf(" ERROR: converting test to yaml: %s", err) - fmt.Println() + fmt.Fprintf(out, " ERROR: converting test to yaml: %s", err) + fmt.Fprintln(out) continue } if err := os.WriteFile(testCase.Path, yamlBytes, os.ModePerm); err != nil { - fmt.Printf(" ERROR: saving test file (%s): %s", testCase.Path, err) - fmt.Println() + fmt.Fprintf(out, " ERROR: saving test file (%s): %s", testCase.Path, err) + fmt.Fprintln(out) continue } - fmt.Println(" OK") + fmt.Fprintln(out, " OK") } - fmt.Println() + fmt.Fprintln(out) } - fmt.Println("Done.") + fmt.Fprintln(out, "Done.") return nil } diff --git a/cmd/cli/kubectl-kyverno/commands/jp/command.go b/cmd/cli/kubectl-kyverno/commands/jp/command.go index 666e856db6..7dce3a8107 100644 --- a/cmd/cli/kubectl-kyverno/commands/jp/command.go +++ b/cmd/cli/kubectl-kyverno/commands/jp/command.go @@ -10,16 +10,21 @@ import ( func Command() *cobra.Command { cmd := &cobra.Command{ - Use: "jp", - Short: command.FormatDescription(true, websiteUrl, false, description...), - Long: command.FormatDescription(false, websiteUrl, false, description...), - Example: command.FormatExamples(examples...), - RunE: func(cmd *cobra.Command, args []string) error { + Use: "jp", + Short: command.FormatDescription(true, websiteUrl, false, description...), + Long: command.FormatDescription(false, websiteUrl, false, description...), + Example: command.FormatExamples(examples...), + Args: cobra.NoArgs, + SilenceErrors: true, + SilenceUsage: true, + RunE: func(cmd *cobra.Command, _ []string) error { return cmd.Help() }, } - cmd.AddCommand(query.Command()) - cmd.AddCommand(function.Command()) - cmd.AddCommand(parse.Command()) + cmd.AddCommand( + function.Command(), + parse.Command(), + query.Command(), + ) return cmd } diff --git a/cmd/cli/kubectl-kyverno/commands/jp/command_test.go b/cmd/cli/kubectl-kyverno/commands/jp/command_test.go new file mode 100644 index 0000000000..ddfda04df5 --- /dev/null +++ b/cmd/cli/kubectl-kyverno/commands/jp/command_test.go @@ -0,0 +1,22 @@ +package jp + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCommand(t *testing.T) { + cmd := Command() + assert.NotNil(t, cmd) + err := cmd.Execute() + assert.NoError(t, err) +} + +func TestCommandWithArgs(t *testing.T) { + cmd := Command() + assert.NotNil(t, cmd) + cmd.SetArgs([]string{"foo"}) + err := cmd.Execute() + assert.Error(t, err) +} diff --git a/cmd/cli/kubectl-kyverno/commands/jp/function/command.go b/cmd/cli/kubectl-kyverno/commands/jp/function/command.go index f2d032e74e..981c8b9ec0 100644 --- a/cmd/cli/kubectl-kyverno/commands/jp/function/command.go +++ b/cmd/cli/kubectl-kyverno/commands/jp/function/command.go @@ -2,6 +2,7 @@ package function import ( "fmt" + "io" "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/command" "github.com/kyverno/kyverno/pkg/config" @@ -13,18 +14,19 @@ import ( func Command() *cobra.Command { return &cobra.Command{ - Use: "function [function_name]...", - Short: command.FormatDescription(true, websiteUrl, false, description...), - Long: command.FormatDescription(false, websiteUrl, false, description...), - Example: command.FormatExamples(examples...), - SilenceUsage: true, + Use: "function [function_name]...", + Short: command.FormatDescription(true, websiteUrl, false, description...), + Long: command.FormatDescription(false, websiteUrl, false, description...), + Example: command.FormatExamples(examples...), + SilenceErrors: true, + SilenceUsage: true, Run: func(cmd *cobra.Command, args []string) { - printFunctions(args...) + printFunctions(cmd.OutOrStdout(), args...) }, } } -func printFunctions(names ...string) { +func printFunctions(out io.Writer, names ...string) { functions := jmespath.GetFunctions(config.NewDefaultConfiguration(false)) slices.SortFunc(functions, func(a, b jmespath.FunctionEntry) bool { return a.String() < b.String() @@ -34,12 +36,12 @@ func printFunctions(names ...string) { if len(namesSet) == 0 || namesSet.Has(function.Name) { note := function.Note function.Note = "" - fmt.Println("Name:", function.Name) - fmt.Println(" Signature:", function.String()) + fmt.Fprintln(out, "Name:", function.Name) + fmt.Fprintln(out, " Signature:", function.String()) if note != "" { - fmt.Println(" Note: ", note) + fmt.Fprintln(out, " Note: ", note) } - fmt.Println() + fmt.Fprintln(out) } } } diff --git a/cmd/cli/kubectl-kyverno/commands/jp/function/command_test.go b/cmd/cli/kubectl-kyverno/commands/jp/function/command_test.go new file mode 100644 index 0000000000..efafa96037 --- /dev/null +++ b/cmd/cli/kubectl-kyverno/commands/jp/function/command_test.go @@ -0,0 +1,30 @@ +package function + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCommand(t *testing.T) { + cmd := Command() + assert.NotNil(t, cmd) + err := cmd.Execute() + assert.NoError(t, err) +} + +func TestCommandWithOneArg(t *testing.T) { + cmd := Command() + assert.NotNil(t, cmd) + cmd.SetArgs([]string{"truncate"}) + err := cmd.Execute() + assert.NoError(t, err) +} + +func TestCommandWithArgs(t *testing.T) { + cmd := Command() + assert.NotNil(t, cmd) + cmd.SetArgs([]string{"truncate", "to_upper"}) + err := cmd.Execute() + assert.NoError(t, err) +} diff --git a/cmd/cli/kubectl-kyverno/commands/jp/parse/command.go b/cmd/cli/kubectl-kyverno/commands/jp/parse/command.go index 0bd6f873c2..ad64005afd 100644 --- a/cmd/cli/kubectl-kyverno/commands/jp/parse/command.go +++ b/cmd/cli/kubectl-kyverno/commands/jp/parse/command.go @@ -14,18 +14,19 @@ import ( func Command() *cobra.Command { var files []string cmd := &cobra.Command{ - Use: "parse [-f file|expression]...", - Short: command.FormatDescription(true, websiteUrl, false, description...), - Long: command.FormatDescription(false, websiteUrl, false, description...), - Example: command.FormatExamples(examples...), - SilenceUsage: true, + Use: "parse [-f file|expression]...", + Short: command.FormatDescription(true, websiteUrl, false, description...), + Long: command.FormatDescription(false, websiteUrl, false, description...), + Example: command.FormatExamples(examples...), + SilenceErrors: true, + 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 { + if err := printAst(cmd.OutOrStdout(), expression); err != nil { return err } } @@ -44,14 +45,14 @@ func readFile(reader io.Reader) (string, error) { return string(data), nil } -func loadFile(file string) (string, error) { +func loadFile(cmd *cobra.Command, 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) + fmt.Fprintf(cmd.OutOrStdout(), "Error closing file: %s\n", err) } }() content, err := readFile(reader) @@ -65,15 +66,15 @@ func loadExpressions(cmd *cobra.Command, args []string, files []string) ([]strin var expressions []string expressions = append(expressions, args...) for _, file := range files { - expression, err := loadFile(file) + expression, err := loadFile(cmd, 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 jmespath expression and hit Ctrl+D.") + 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 nil, fmt.Errorf("failed to read file STDIN: %v", err) @@ -85,7 +86,7 @@ func loadExpressions(cmd *cobra.Command, args []string, files []string) ([]strin // The following function has been adapted from // https://github.com/jmespath/jp/blob/54882e03bd277fc4475a677fab1d35eaa478b839/jp.go -func printAst(expression string) error { +func printAst(out io.Writer, expression string) error { parser := gojmespath.NewParser() parsed, err := parser.Parse(expression) if err != nil { @@ -94,7 +95,7 @@ func printAst(expression string) error { } return err } - fmt.Println("#", expression) - fmt.Println(parsed) + fmt.Fprintln(out, "#", expression) + fmt.Fprintln(out, parsed) return nil } diff --git a/cmd/cli/kubectl-kyverno/commands/jp/parse/command_test.go b/cmd/cli/kubectl-kyverno/commands/jp/parse/command_test.go new file mode 100644 index 0000000000..5dede0c42c --- /dev/null +++ b/cmd/cli/kubectl-kyverno/commands/jp/parse/command_test.go @@ -0,0 +1,15 @@ +package parse + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCommand(t *testing.T) { + cmd := Command() + assert.NotNil(t, cmd) + cmd.SetArgs([]string{"request.object.metadata.name | truncate(@, `9`)"}) + err := cmd.Execute() + assert.NoError(t, err) +} diff --git a/cmd/cli/kubectl-kyverno/commands/jp/query/command.go b/cmd/cli/kubectl-kyverno/commands/jp/query/command.go index 0156481d16..53d1d6fcd7 100644 --- a/cmd/cli/kubectl-kyverno/commands/jp/query/command.go +++ b/cmd/cli/kubectl-kyverno/commands/jp/query/command.go @@ -21,17 +21,18 @@ func Command() *cobra.Command { 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, + 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...), + SilenceErrors: true, + SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) error { - queries, err := loadQueries(args, queries) + queries, err := loadQueries(cmd, args, queries) if err != nil { return err } - input, err := loadInput(input) + input, err := loadInput(cmd, input) if err != nil { return err } @@ -57,7 +58,7 @@ func Command() *cobra.Command { if err != nil { return err } - if err := printResult(query, result, unquoted, compact); err != nil { + if err := printResult(cmd, query, result, unquoted, compact); err != nil { return err } } @@ -79,14 +80,14 @@ func readFile(reader io.Reader) ([]byte, error) { return data, nil } -func loadFile(file string) ([]byte, error) { +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.Printf("Error closing file: %s\n", err) + fmt.Fprintf(cmd.OutOrStdout(), "Error closing file: %s\n", err) } }() content, err := readFile(reader) @@ -97,8 +98,8 @@ func loadFile(file string) ([]byte, error) { } func readQuery(cmd *cobra.Command) (string, error) { - fmt.Println("Reading from terminal input.") - fmt.Println("Enter a jmespath expression and hit Ctrl+D.") + 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 @@ -106,11 +107,11 @@ func readQuery(cmd *cobra.Command) (string, error) { return string(data), nil } -func loadQueries(args []string, files []string) ([]string, error) { +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(file) + query, err := loadFile(cmd, file) if err != nil { return nil, err } @@ -120,8 +121,8 @@ func loadQueries(args []string, files []string) ([]string, error) { } func readInput(cmd *cobra.Command) (interface{}, error) { - fmt.Println("Reading from terminal input.") - fmt.Println("Enter input object and hit Ctrl+D.") + 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 @@ -133,11 +134,11 @@ func readInput(cmd *cobra.Command) (interface{}, error) { return input, nil } -func loadInput(file string) (interface{}, error) { +func loadInput(cmd *cobra.Command, file string) (interface{}, error) { if file == "" { return nil, nil } - data, err := loadFile(file) + data, err := loadFile(cmd, file) if err != nil { return nil, err } @@ -164,11 +165,11 @@ func evaluate(input interface{}, query string) (interface{}, error) { return result, nil } -func printResult(query string, result interface{}, unquoted bool, compact bool) error { +func printResult(cmd *cobra.Command, query string, result interface{}, unquoted bool, compact bool) error { converted, isString := result.(string) - fmt.Println("#", query) + fmt.Fprintln(cmd.OutOrStdout(), "#", query) if unquoted && isString { - fmt.Println(converted) + fmt.Fprintln(cmd.OutOrStdout(), converted) } else { var toJSON []byte var err error @@ -180,7 +181,7 @@ func printResult(query string, result interface{}, unquoted bool, compact bool) if err != nil { return fmt.Errorf("error marshalling result to JSON: %w", err) } - fmt.Println(string(toJSON)) + fmt.Fprintln(cmd.OutOrStdout(), string(toJSON)) } return nil } diff --git a/cmd/cli/kubectl-kyverno/commands/jp/query/command_test.go b/cmd/cli/kubectl-kyverno/commands/jp/query/command_test.go new file mode 100644 index 0000000000..cfbf9f68dd --- /dev/null +++ b/cmd/cli/kubectl-kyverno/commands/jp/query/command_test.go @@ -0,0 +1,15 @@ +package query + +import ( + "testing" + + "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) +} diff --git a/cmd/cli/kubectl-kyverno/commands/oci/command.go b/cmd/cli/kubectl-kyverno/commands/oci/command.go index c391bd30aa..5ee3491ac5 100644 --- a/cmd/cli/kubectl-kyverno/commands/oci/command.go +++ b/cmd/cli/kubectl-kyverno/commands/oci/command.go @@ -19,11 +19,14 @@ func Command() *cobra.Command { registryclient.AzureKeychain, ) cmd := &cobra.Command{ - Use: "oci", - Short: command.FormatDescription(true, websiteUrl, true, description...), - Long: command.FormatDescription(false, websiteUrl, true, description...), - Example: command.FormatExamples(examples...), - RunE: func(cmd *cobra.Command, args []string) error { + Use: "oci", + Short: command.FormatDescription(true, websiteUrl, true, description...), + Long: command.FormatDescription(false, websiteUrl, true, description...), + Example: command.FormatExamples(examples...), + Args: cobra.NoArgs, + SilenceErrors: true, + SilenceUsage: true, + RunE: func(cmd *cobra.Command, _ []string) error { return cmd.Help() }, } diff --git a/cmd/cli/kubectl-kyverno/commands/oci/command_test.go b/cmd/cli/kubectl-kyverno/commands/oci/command_test.go new file mode 100644 index 0000000000..506e0e0fe2 --- /dev/null +++ b/cmd/cli/kubectl-kyverno/commands/oci/command_test.go @@ -0,0 +1,22 @@ +package oci + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCommand(t *testing.T) { + cmd := Command() + assert.NotNil(t, cmd) + err := cmd.Execute() + assert.NoError(t, err) +} + +func TestCommandWithArgs(t *testing.T) { + cmd := Command() + assert.NotNil(t, cmd) + cmd.SetArgs([]string{"foo"}) + err := cmd.Execute() + assert.Error(t, err) +} diff --git a/cmd/cli/kubectl-kyverno/commands/oci/pull/command.go b/cmd/cli/kubectl-kyverno/commands/oci/pull/command.go index d56fdd825f..c7c8bcfe9c 100644 --- a/cmd/cli/kubectl-kyverno/commands/oci/pull/command.go +++ b/cmd/cli/kubectl-kyverno/commands/oci/pull/command.go @@ -1,121 +1,34 @@ package pull import ( - "errors" - "fmt" - "io" - "os" - "path/filepath" + "log" - securejoin "github.com/cyphar/filepath-securejoin" "github.com/google/go-containerregistry/pkg/authn" - "github.com/google/go-containerregistry/pkg/name" - "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/command" - "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/commands/oci/internal" - policyutils "github.com/kyverno/kyverno/pkg/utils/policy" - yamlutils "github.com/kyverno/kyverno/pkg/utils/yaml" "github.com/spf13/cobra" ) func Command(keychain authn.Keychain) *cobra.Command { - var dir string - var imageRef string + var options options cmd := &cobra.Command{ - Use: "pull", + Use: "pull [dir]", Short: command.FormatDescription(true, websiteUrl, true, description...), Long: command.FormatDescription(false, websiteUrl, true, description...), Example: command.FormatExamples(examples...), + Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - if imageRef == "" { - return errors.New("image reference is required") + dir := args[0] + if err := options.validate(dir); err != nil { + return err } - - dir = filepath.Clean(dir) - if !filepath.IsAbs(dir) { - cwd, err := os.Getwd() - if err != nil { - return err - } - - dir, err = securejoin.SecureJoin(cwd, dir) - if err != nil { - return err - } - } - - fi, err := os.Lstat(dir) - // Dir does not need to exist, as it can later be created. - if err != nil && errors.Is(err, os.ErrNotExist) { - if err := os.MkdirAll(dir, 0o750); err != nil { - return fmt.Errorf("unable to create directory %s: %w", dir, err) - } - } - - if err == nil && !fi.IsDir() { - return fmt.Errorf("dir '%s' must be a directory", dir) - } - - ref, err := name.ParseReference(imageRef) - if err != nil { - return fmt.Errorf("parsing image reference: %v", err) - } - - fmt.Fprintf(os.Stderr, "Downloading policies from an image [%s]...\n", ref.Name()) - rmt, err := remote.Get(ref, remote.WithContext(cmd.Context()), remote.WithAuthFromKeychain(keychain)) - if err != nil { - return fmt.Errorf("getting image: %v", err) - } - - img, err := rmt.Image() - if err != nil { - return fmt.Errorf("getting image: %v", err) - } - - l, err := img.Layers() - if err != nil { - return fmt.Errorf("getting image layers: %v", err) - } - - for _, layer := range l { - lmt, err := layer.MediaType() - if err != nil { - return fmt.Errorf("getting layer media type: %v", err) - } - - if lmt == internal.PolicyLayerMediaType { - blob, err := layer.Compressed() - if err != nil { - return fmt.Errorf("getting layer blob: %v", err) - } - defer blob.Close() - - layerBytes, err := io.ReadAll(blob) - if err != nil { - return fmt.Errorf("reading layer blob: %v", err) - } - policies, _, err := yamlutils.GetPolicy(layerBytes) - if err != nil { - return fmt.Errorf("unmarshaling layer blob: %v", err) - } - for _, policy := range policies { - policyBytes, err := policyutils.ToYaml(policy) - if err != nil { - return fmt.Errorf("converting policy to yaml: %v", err) - } - pp := filepath.Join(dir, policy.GetName()+".yaml") - fmt.Fprintf(os.Stderr, "Saving policy into disk [%s]...\n", pp) - if err := os.WriteFile(pp, policyBytes, 0o600); err != nil { - return fmt.Errorf("creating file: %v", err) - } - } - } - } - fmt.Fprintf(os.Stderr, "Done.") - return nil + cmd.SilenceUsage = true + cmd.SilenceErrors = true + return options.execute(cmd.Context(), dir, keychain) }, } - cmd.Flags().StringVarP(&dir, "directory", "d", ".", "path to a directory") - cmd.Flags().StringVarP(&imageRef, "image", "i", "", "image reference to push to or pull from") + cmd.Flags().StringVarP(&options.imageRef, "image", "i", "", "image reference to push to or pull from") + if err := cmd.MarkFlagRequired("image"); err != nil { + log.Println("WARNING", err) + } return cmd } diff --git a/cmd/cli/kubectl-kyverno/commands/oci/pull/command_test.go b/cmd/cli/kubectl-kyverno/commands/oci/pull/command_test.go new file mode 100644 index 0000000000..4ad2d94861 --- /dev/null +++ b/cmd/cli/kubectl-kyverno/commands/oci/pull/command_test.go @@ -0,0 +1,33 @@ +package pull + +import ( + "testing" + + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/authn/github" + "github.com/kyverno/kyverno/pkg/registryclient" + "github.com/stretchr/testify/assert" +) + +var keychain = authn.NewMultiKeychain( + authn.DefaultKeychain, + github.Keychain, + registryclient.AWSKeychain, + registryclient.GCPKeychain, + registryclient.AzureKeychain, +) + +func TestCommandNoImageRef(t *testing.T) { + cmd := Command(keychain) + assert.NotNil(t, cmd) + err := cmd.Execute() + assert.Error(t, err) +} + +func TestCommandWithArgs(t *testing.T) { + cmd := Command(keychain) + assert.NotNil(t, cmd) + cmd.SetArgs([]string{"foo"}) + err := cmd.Execute() + assert.Error(t, err) +} diff --git a/cmd/cli/kubectl-kyverno/commands/oci/pull/doc.go b/cmd/cli/kubectl-kyverno/commands/oci/pull/doc.go index 55e2c08fcb..8a5b438c83 100644 --- a/cmd/cli/kubectl-kyverno/commands/oci/pull/doc.go +++ b/cmd/cli/kubectl-kyverno/commands/oci/pull/doc.go @@ -9,6 +9,6 @@ var description = []string{ var examples = [][]string{ { `# Pull policy from an OCI image and save it to the specific directory`, - `kyverno oci pull -i -d policies`, + `kyverno oci pull . -i `, }, } diff --git a/cmd/cli/kubectl-kyverno/commands/oci/pull/options.go b/cmd/cli/kubectl-kyverno/commands/oci/pull/options.go new file mode 100644 index 0000000000..181ff811d3 --- /dev/null +++ b/cmd/cli/kubectl-kyverno/commands/oci/pull/options.go @@ -0,0 +1,108 @@ +package pull + +import ( + "context" + "errors" + "fmt" + "io" + "os" + "path/filepath" + + securejoin "github.com/cyphar/filepath-securejoin" + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/commands/oci/internal" + policyutils "github.com/kyverno/kyverno/pkg/utils/policy" + yamlutils "github.com/kyverno/kyverno/pkg/utils/yaml" +) + +type options struct { + imageRef string +} + +func (o options) validate(dir string) error { + if o.imageRef == "" { + return errors.New("image is required") + } + if dir == "" { + return errors.New("dir is required") + } + return nil +} + +func (o options) execute(ctx context.Context, dir string, keychain authn.Keychain) error { + dir = filepath.Clean(dir) + if !filepath.IsAbs(dir) { + cwd, err := os.Getwd() + if err != nil { + return err + } + dir, err = securejoin.SecureJoin(cwd, dir) + if err != nil { + return err + } + } + fi, err := os.Lstat(dir) + // Dir does not need to exist, as it can later be created. + if err != nil && errors.Is(err, os.ErrNotExist) { + if err := os.MkdirAll(dir, 0o750); err != nil { + return fmt.Errorf("unable to create directory %s: %w", dir, err) + } + } + if err == nil && !fi.IsDir() { + return fmt.Errorf("dir '%s' must be a directory", dir) + } + ref, err := name.ParseReference(o.imageRef) + if err != nil { + return fmt.Errorf("parsing image reference: %v", err) + } + fmt.Fprintf(os.Stderr, "Downloading policies from an image [%s]...\n", ref.Name()) + rmt, err := remote.Get(ref, remote.WithContext(ctx), remote.WithAuthFromKeychain(keychain)) + if err != nil { + return fmt.Errorf("getting image: %v", err) + } + img, err := rmt.Image() + if err != nil { + return fmt.Errorf("getting image: %v", err) + } + l, err := img.Layers() + if err != nil { + return fmt.Errorf("getting image layers: %v", err) + } + for _, layer := range l { + lmt, err := layer.MediaType() + if err != nil { + return fmt.Errorf("getting layer media type: %v", err) + } + if lmt == internal.PolicyLayerMediaType { + blob, err := layer.Compressed() + if err != nil { + return fmt.Errorf("getting layer blob: %v", err) + } + defer blob.Close() + + layerBytes, err := io.ReadAll(blob) + if err != nil { + return fmt.Errorf("reading layer blob: %v", err) + } + policies, _, err := yamlutils.GetPolicy(layerBytes) + if err != nil { + return fmt.Errorf("unmarshaling layer blob: %v", err) + } + for _, policy := range policies { + policyBytes, err := policyutils.ToYaml(policy) + if err != nil { + return fmt.Errorf("converting policy to yaml: %v", err) + } + pp := filepath.Join(dir, policy.GetName()+".yaml") + fmt.Fprintf(os.Stderr, "Saving policy into disk [%s]...\n", pp) + if err := os.WriteFile(pp, policyBytes, 0o600); err != nil { + return fmt.Errorf("creating file: %v", err) + } + } + } + } + fmt.Fprintf(os.Stderr, "Done.") + return nil +} diff --git a/cmd/cli/kubectl-kyverno/commands/oci/push/command.go b/cmd/cli/kubectl-kyverno/commands/oci/push/command.go index e6be994a09..42c0257b3b 100644 --- a/cmd/cli/kubectl-kyverno/commands/oci/push/command.go +++ b/cmd/cli/kubectl-kyverno/commands/oci/push/command.go @@ -1,91 +1,34 @@ package push import ( - "errors" - "fmt" - "os" + "log" "github.com/google/go-containerregistry/pkg/authn" - "github.com/google/go-containerregistry/pkg/name" - "github.com/google/go-containerregistry/pkg/v1/empty" - "github.com/google/go-containerregistry/pkg/v1/mutate" - "github.com/google/go-containerregistry/pkg/v1/remote" - "github.com/google/go-containerregistry/pkg/v1/static" - "github.com/google/go-containerregistry/pkg/v1/types" "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/command" - "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/commands/oci/internal" - "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/log" - "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/policy" - "github.com/kyverno/kyverno/pkg/config" - "github.com/kyverno/kyverno/pkg/openapi" - policyutils "github.com/kyverno/kyverno/pkg/utils/policy" - policyvalidation "github.com/kyverno/kyverno/pkg/validation/policy" "github.com/spf13/cobra" ) func Command(keychain authn.Keychain) *cobra.Command { - var policyRef string - var imageRef string + var options options cmd := &cobra.Command{ Use: "push", Short: command.FormatDescription(true, websiteUrl, true, description...), Long: command.FormatDescription(false, websiteUrl, true, description...), Example: command.FormatExamples(examples...), + Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - if imageRef == "" { - return errors.New("image reference is required") + dir := args[0] + if err := options.validate(dir); err != nil { + return err } - - policies, _, err := policy.Load(nil, "", policyRef) - if err != nil { - return fmt.Errorf("unable to read policy file or directory %s: %w", policyRef, err) - } - - openApiManager, err := openapi.NewManager(log.Log) - if err != nil { - return fmt.Errorf("creating openapi manager: %v", err) - } - for _, policy := range policies { - if _, err := policyvalidation.Validate(policy, nil, nil, true, openApiManager, config.KyvernoUserName(config.KyvernoServiceAccountName())); err != nil { - return fmt.Errorf("validating policy %s: %v", policy.GetName(), err) - } - } - - img := mutate.MediaType(empty.Image, types.OCIManifestSchema1) - img = mutate.ConfigMediaType(img, internal.PolicyConfigMediaType) - ref, err := name.ParseReference(imageRef) - if err != nil { - return fmt.Errorf("parsing image reference: %v", err) - } - - for _, policy := range policies { - if policy.IsNamespaced() { - fmt.Fprintf(os.Stderr, "Adding policy [%s]\n", policy.GetName()) - } else { - fmt.Fprintf(os.Stderr, "Adding cluster policy [%s]\n", policy.GetName()) - } - policyBytes, err := policyutils.ToYaml(policy) - if err != nil { - return fmt.Errorf("converting policy to yaml: %v", err) - } - policyLayer := static.NewLayer(policyBytes, internal.PolicyLayerMediaType) - img, err = mutate.Append(img, mutate.Addendum{ - Layer: policyLayer, - Annotations: internal.Annotations(policy), - }) - if err != nil { - return fmt.Errorf("mutating image: %v", err) - } - } - fmt.Fprintf(os.Stderr, "Uploading [%s]...\n", ref.Name()) - if err = remote.Write(ref, img, remote.WithContext(cmd.Context()), remote.WithAuthFromKeychain(keychain)); err != nil { - return fmt.Errorf("writing image: %v", err) - } - fmt.Fprintf(os.Stderr, "Done.") - return nil + cmd.SilenceUsage = true + cmd.SilenceErrors = true + return options.execute(cmd.Context(), dir, keychain) }, } - cmd.Flags().StringVarP(&policyRef, "policy", "p", "", "path to policie(s)") - cmd.Flags().StringVarP(&imageRef, "image", "i", "", "image reference to push to or pull from") + cmd.Flags().StringVarP(&options.imageRef, "image", "i", "", "image reference to push to or pull from") + if err := cmd.MarkFlagRequired("image"); err != nil { + log.Println("WARNING", err) + } return cmd } diff --git a/cmd/cli/kubectl-kyverno/commands/oci/push/command_test.go b/cmd/cli/kubectl-kyverno/commands/oci/push/command_test.go new file mode 100644 index 0000000000..fb97f2d196 --- /dev/null +++ b/cmd/cli/kubectl-kyverno/commands/oci/push/command_test.go @@ -0,0 +1,33 @@ +package push + +import ( + "testing" + + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/authn/github" + "github.com/kyverno/kyverno/pkg/registryclient" + "github.com/stretchr/testify/assert" +) + +var keychain = authn.NewMultiKeychain( + authn.DefaultKeychain, + github.Keychain, + registryclient.AWSKeychain, + registryclient.GCPKeychain, + registryclient.AzureKeychain, +) + +func TestCommandNoImageRef(t *testing.T) { + cmd := Command(keychain) + assert.NotNil(t, cmd) + err := cmd.Execute() + assert.Error(t, err) +} + +func TestCommandWithArgs(t *testing.T) { + cmd := Command(keychain) + assert.NotNil(t, cmd) + cmd.SetArgs([]string{"foo"}) + err := cmd.Execute() + assert.Error(t, err) +} diff --git a/cmd/cli/kubectl-kyverno/commands/oci/push/doc.go b/cmd/cli/kubectl-kyverno/commands/oci/push/doc.go index e4728b5385..1f0d880430 100644 --- a/cmd/cli/kubectl-kyverno/commands/oci/push/doc.go +++ b/cmd/cli/kubectl-kyverno/commands/oci/push/doc.go @@ -9,10 +9,10 @@ var description = []string{ var examples = [][]string{ { `# Push policy to an OCI image from a given policy file`, - `kyverno oci push -p policy.yaml -i `, + `kyverno oci push ./policy.yaml -i `, }, { `# Push multiple policies to an OCI image from a given directory that includes policies`, - `kyverno oci push -p policies. -i `, + `kyverno oci push . -i `, }, } diff --git a/cmd/cli/kubectl-kyverno/commands/oci/push/options.go b/cmd/cli/kubectl-kyverno/commands/oci/push/options.go new file mode 100644 index 0000000000..f1491891dd --- /dev/null +++ b/cmd/cli/kubectl-kyverno/commands/oci/push/options.go @@ -0,0 +1,84 @@ +package push + +import ( + "context" + "errors" + "fmt" + "os" + + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/empty" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/google/go-containerregistry/pkg/v1/static" + "github.com/google/go-containerregistry/pkg/v1/types" + "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/commands/oci/internal" + clilog "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/log" + "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/policy" + "github.com/kyverno/kyverno/pkg/config" + "github.com/kyverno/kyverno/pkg/openapi" + policyutils "github.com/kyverno/kyverno/pkg/utils/policy" + policyvalidation "github.com/kyverno/kyverno/pkg/validation/policy" +) + +type options struct { + imageRef string +} + +func (o options) validate(policy string) error { + if o.imageRef == "" { + return errors.New("image is required") + } + if policy == "" { + return errors.New("policy is required") + } + return nil +} + +func (o options) execute(ctx context.Context, dir string, keychain authn.Keychain) error { + policies, _, err := policy.Load(nil, "", dir) + if err != nil { + return fmt.Errorf("unable to read policy file or directory %s (%w)", dir, err) + } + openApiManager, err := openapi.NewManager(clilog.Log) + if err != nil { + return fmt.Errorf("creating openapi manager: %v", err) + } + for _, policy := range policies { + if _, err := policyvalidation.Validate(policy, nil, nil, true, openApiManager, config.KyvernoUserName(config.KyvernoServiceAccountName())); err != nil { + return fmt.Errorf("validating policy %s: %v", policy.GetName(), err) + } + } + img := mutate.MediaType(empty.Image, types.OCIManifestSchema1) + img = mutate.ConfigMediaType(img, internal.PolicyConfigMediaType) + ref, err := name.ParseReference(o.imageRef) + if err != nil { + return fmt.Errorf("parsing image reference: %v", err) + } + for _, policy := range policies { + if policy.IsNamespaced() { + fmt.Fprintf(os.Stderr, "Adding policy [%s]\n", policy.GetName()) + } else { + fmt.Fprintf(os.Stderr, "Adding cluster policy [%s]\n", policy.GetName()) + } + policyBytes, err := policyutils.ToYaml(policy) + if err != nil { + return fmt.Errorf("converting policy to yaml: %v", err) + } + policyLayer := static.NewLayer(policyBytes, internal.PolicyLayerMediaType) + img, err = mutate.Append(img, mutate.Addendum{ + Layer: policyLayer, + Annotations: internal.Annotations(policy), + }) + if err != nil { + return fmt.Errorf("mutating image: %v", err) + } + } + fmt.Fprintf(os.Stderr, "Uploading [%s]...\n", ref.Name()) + if err = remote.Write(ref, img, remote.WithContext(ctx), remote.WithAuthFromKeychain(keychain)); err != nil { + return fmt.Errorf("writing image: %v", err) + } + fmt.Fprintf(os.Stderr, "Done.") + return nil +} diff --git a/cmd/cli/kubectl-kyverno/commands/version/command.go b/cmd/cli/kubectl-kyverno/commands/version/command.go index 41419ac96f..d1e1fda6a1 100644 --- a/cmd/cli/kubectl-kyverno/commands/version/command.go +++ b/cmd/cli/kubectl-kyverno/commands/version/command.go @@ -14,10 +14,10 @@ func Command() *cobra.Command { Short: command.FormatDescription(true, websiteUrl, false, description...), Long: command.FormatDescription(false, websiteUrl, false, description...), Example: command.FormatExamples(examples...), + Args: cobra.NoArgs, SilenceErrors: true, SilenceUsage: true, - Args: cobra.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, _ []string) error { fmt.Fprintf(cmd.OutOrStdout(), "Version: %s\n", version.Version()) fmt.Fprintf(cmd.OutOrStdout(), "Time: %s\n", version.Time()) fmt.Fprintf(cmd.OutOrStdout(), "Git commit ID: %s\n", version.Hash()) diff --git a/cmd/cli/kubectl-kyverno/commands/version/command_test.go b/cmd/cli/kubectl-kyverno/commands/version/command_test.go index 6cb9bc72e5..d4920720a6 100644 --- a/cmd/cli/kubectl-kyverno/commands/version/command_test.go +++ b/cmd/cli/kubectl-kyverno/commands/version/command_test.go @@ -13,6 +13,7 @@ import ( func TestCommand(t *testing.T) { version.BuildVersion = "test" cmd := Command() + assert.NotNil(t, cmd) b := bytes.NewBufferString("") cmd.SetOut(b) err := cmd.Execute() @@ -28,6 +29,7 @@ Git commit ID: ---` func TestCommandWithArgs(t *testing.T) { cmd := Command() + assert.NotNil(t, cmd) cmd.SetArgs([]string{"test"}) err := cmd.Execute() assert.Error(t, err) diff --git a/docs/user/cli/kyverno_fix_test.md b/docs/user/cli/kyverno_fix_test.md index 1e6eb4f4cf..a17b477543 100644 --- a/docs/user/cli/kyverno_fix_test.md +++ b/docs/user/cli/kyverno_fix_test.md @@ -9,7 +9,7 @@ Fix inconsistencies and deprecated usage in Kyverno test files. NOTE: This is an experimental command, use `KYVERNO_EXPERIMENTAL=true` to enable it. ``` -kyverno fix test [folder]... [flags] +kyverno fix test [dir]... [flags] ``` ### Examples diff --git a/docs/user/cli/kyverno_oci_pull.md b/docs/user/cli/kyverno_oci_pull.md index 538061696e..b13f18067d 100644 --- a/docs/user/cli/kyverno_oci_pull.md +++ b/docs/user/cli/kyverno_oci_pull.md @@ -11,22 +11,21 @@ Pulls policie(s) that are included in an OCI image from OCI registry and saves t For more information visit https://kyverno.io/docs/kyverno-cli/#pulling ``` -kyverno oci pull [flags] +kyverno oci pull [dir] [flags] ``` ### Examples ``` # Pull policy from an OCI image and save it to the specific directory - kyverno oci pull -i -d policies + kyverno oci pull . -i ``` ### Options ``` - -d, --directory string path to a directory (default ".") - -h, --help help for pull - -i, --image string image reference to push to or pull from + -h, --help help for pull + -i, --image string image reference to push to or pull from ``` ### Options inherited from parent commands diff --git a/docs/user/cli/kyverno_oci_push.md b/docs/user/cli/kyverno_oci_push.md index a11a85101e..c36e944fac 100644 --- a/docs/user/cli/kyverno_oci_push.md +++ b/docs/user/cli/kyverno_oci_push.md @@ -18,18 +18,17 @@ kyverno oci push [flags] ``` # Push policy to an OCI image from a given policy file - kyverno oci push -p policy.yaml -i + kyverno oci push ./policy.yaml -i # Push multiple policies to an OCI image from a given directory that includes policies - kyverno oci push -p policies. -i + kyverno oci push . -i ``` ### Options ``` - -h, --help help for push - -i, --image string image reference to push to or pull from - -p, --policy string path to policie(s) + -h, --help help for push + -i, --image string image reference to push to or pull from ``` ### Options inherited from parent commands diff --git a/test/cli/test-mutate/karpenter-annotations-to-nodeselector/kyverno-test.yaml b/test/cli/test-mutate/karpenter-annotations-to-nodeselector/kyverno-test.yaml index 24f0c37c8e..20fbedd282 100644 --- a/test/cli/test-mutate/karpenter-annotations-to-nodeselector/kyverno-test.yaml +++ b/test/cli/test-mutate/karpenter-annotations-to-nodeselector/kyverno-test.yaml @@ -1,17 +1,19 @@ name: karpenter-annotations-to-nodeselector policies: - - policy.yaml +- policy.yaml resources: - - resource.yaml +- resource.yaml results: - - policy: karpenter-annotations-to-nodeselector - rule: hard-nodeselector-lifecycle-on-demand - resource: soft-pod-antiaffinity-1 - patchedResource: patched.yaml - kind: Pod - result: pass - - policy: karpenter-annotations-to-nodeselector - rule: hard-nodeselector-lifecycle-on-demand - resource: soft-pod-antiaffinity-1-copy - kind: Pod - result: pass \ No newline at end of file +- kind: Pod + patchedResource: patched.yaml + policy: karpenter-annotations-to-nodeselector + resources: + - soft-pod-antiaffinity-1 + result: pass + rule: hard-nodeselector-lifecycle-on-demand +- kind: Pod + policy: karpenter-annotations-to-nodeselector + resources: + - soft-pod-antiaffinity-1-copy + result: pass + rule: hard-nodeselector-lifecycle-on-demand