diff --git a/.github/workflows/codegen.yaml b/.github/workflows/codegen.yaml index 0b4d3ddd..ab98da9c 100644 --- a/.github/workflows/codegen.yaml +++ b/.github/workflows/codegen.yaml @@ -18,6 +18,10 @@ jobs: steps: - name: Checkout uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 + with: + go-version-file: go.mod + cache-dependency-path: go.sum - name: Verify codegen run: | set -e diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index b7ae8f0b..8848b575 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -20,6 +20,7 @@ jobs: uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: - go-version: ~1.21.1 + go-version-file: go.mod + cache-dependency-path: go.sum - name: golangci-lint uses: golangci/golangci-lint-action@3a919529898de77ec3da873e3063ca4b10e7f5cc # v3.7.0 diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 8b831f36..8464a221 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -20,6 +20,7 @@ jobs: uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 with: - go-version: ~1.21.1 + go-version-file: go.mod + cache-dependency-path: go.sum - name: Run tests run: make tests diff --git a/scripts/boilerplate.go.txt b/.hack/boilerplate.go.txt similarity index 100% rename from scripts/boilerplate.go.txt rename to .hack/boilerplate.go.txt diff --git a/Makefile b/Makefile index 38c1eb5c..b1db38bd 100644 --- a/Makefile +++ b/Makefile @@ -68,14 +68,14 @@ $(PACKAGE_SHIM): $(GOPATH_SHIM) codegen-register: $(PACKAGE_SHIM) $(REGISTER_GEN) ## Generate types registrations @echo Generate registration... >&2 @GOPATH=$(GOPATH_SHIM) $(REGISTER_GEN) \ - --go-header-file=./scripts/boilerplate.go.txt \ + --go-header-file=./.hack/boilerplate.go.txt \ --input-dirs=$(INPUT_DIRS) .PHONY: codegen-deepcopy codegen-deepcopy: $(PACKAGE_SHIM) $(DEEPCOPY_GEN) ## Generate deep copy functions @echo Generate deep copy functions... >&2 @GOPATH=$(GOPATH_SHIM) $(DEEPCOPY_GEN) \ - --go-header-file=./scripts/boilerplate.go.txt \ + --go-header-file=./.hack/boilerplate.go.txt \ --input-dirs=$(INPUT_DIRS) \ --output-file-base=zz_generated.deepcopy @@ -103,8 +103,17 @@ codegen-api-docs-html: $(REFERENCE_DOCS) ## Generate html API docs .PHONY: codegen-api-docs codegen-api-docs: codegen-api-docs-md codegen-api-docs-html ## Generate API docs +.PHONY: codegen-cli-docs +codegen-cli-docs: build ## Generate CLI docs + @echo Generate cli docs... >&2 + @rm -rf docs/user/commands && mkdir -p docs/user/commands + @./kyverno-json docs -o docs/user/commands --autogenTag=false + +.PHONY: codegen-docs +codegen-docs: codegen-api-docs-md codegen-cli-docs ## Generate docs + .PHONY: codegen-all -codegen-all: codegen-crds codegen-deepcopy codegen-register codegen-api-docs-md ## Rebuild all generated code and docs +codegen-all: codegen-crds codegen-deepcopy codegen-register codegen-docs ## Rebuild all generated code and docs .PHONY: verify-codegen verify-codegen: codegen-all ## Verify all generated code and docs are up to date @@ -131,7 +140,7 @@ vet: ## Run go vet @go vet ./... .PHONY: build -build: fmt vet codegen-all ## Build +build: fmt vet codegen-crds codegen-deepcopy codegen-register ## Build @echo Building... >&2 @go build -ldflags=$(LD_FLAGS) . diff --git a/README.md b/README.md index 859abb4e..fbb47e2d 100644 --- a/README.md +++ b/README.md @@ -263,6 +263,7 @@ spec: ``` All binding are available to descendants, if a descendant creates a binding with a name that already exists the binding will be overriden for descendants only and it doesn't affect the bindings at upper levels in the tree. + In other words, a node in the tree always sees bindings that are definied in the parents and if a name is reused, the first binding with the given name wins when winding up the tree. As a consequence, the policy below is perfectly valid: @@ -422,5 +423,5 @@ There is no limitation in a preprocessing [jmespath](https://jmespath.site) expr ## Documentation -User documentation can be found in [docs/user](./docs/user/README.md) -Dev documentation can be found in [docs/dev](./docs/dev/README.md) +- User documentation can be found in [docs/user](./docs/user/README.md) +- Dev documentation can be found in [docs/dev](./docs/dev/README.md) diff --git a/docs/user/README.md b/docs/user/README.md index acdda5ed..cd6c7f30 100644 --- a/docs/user/README.md +++ b/docs/user/README.md @@ -3,3 +3,4 @@ This documentation contains the following user docs: - [API reference docs](./apis/README.md) +- [CLI commands docs](./commands/kyverno-json.md) diff --git a/docs/user/commands/kyverno-json.md b/docs/user/commands/kyverno-json.md new file mode 100644 index 00000000..75f38927 --- /dev/null +++ b/docs/user/commands/kyverno-json.md @@ -0,0 +1,26 @@ +## kyverno-json + +kyverno-json + +### Synopsis + +kyverno-json is a CLI tool to apply policies to json resources + +``` +kyverno-json [flags] +``` + +### Options + +``` + -h, --help help for kyverno-json + --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 +``` + +### SEE ALSO + +* [kyverno-json completion](kyverno-json_completion.md) - Generate the autocompletion script for the specified shell +* [kyverno-json docs](kyverno-json_docs.md) - Generates reference documentation. + diff --git a/docs/user/commands/kyverno-json_completion.md b/docs/user/commands/kyverno-json_completion.md new file mode 100644 index 00000000..f642dd6c --- /dev/null +++ b/docs/user/commands/kyverno-json_completion.md @@ -0,0 +1,24 @@ +## kyverno-json completion + +Generate the autocompletion script for the specified shell + +### Synopsis + +Generate the autocompletion script for kyverno-json for the specified shell. +See each sub-command's help for details on how to use the generated script. + + +### Options + +``` + -h, --help help for completion +``` + +### SEE ALSO + +* [kyverno-json](kyverno-json.md) - kyverno-json +* [kyverno-json completion bash](kyverno-json_completion_bash.md) - Generate the autocompletion script for bash +* [kyverno-json completion fish](kyverno-json_completion_fish.md) - Generate the autocompletion script for fish +* [kyverno-json completion powershell](kyverno-json_completion_powershell.md) - Generate the autocompletion script for powershell +* [kyverno-json completion zsh](kyverno-json_completion_zsh.md) - Generate the autocompletion script for zsh + diff --git a/docs/user/commands/kyverno-json_completion_bash.md b/docs/user/commands/kyverno-json_completion_bash.md new file mode 100644 index 00000000..78a2425a --- /dev/null +++ b/docs/user/commands/kyverno-json_completion_bash.md @@ -0,0 +1,43 @@ +## kyverno-json completion bash + +Generate the autocompletion script for bash + +### Synopsis + +Generate the autocompletion script for the bash shell. + +This script depends on the 'bash-completion' package. +If it is not installed already, you can install it via your OS's package manager. + +To load completions in your current shell session: + + source <(kyverno-json completion bash) + +To load completions for every new session, execute once: + +#### Linux: + + kyverno-json completion bash > /etc/bash_completion.d/kyverno-json + +#### macOS: + + kyverno-json completion bash > $(brew --prefix)/etc/bash_completion.d/kyverno-json + +You will need to start a new shell for this setup to take effect. + + +``` +kyverno-json completion bash +``` + +### Options + +``` + -h, --help help for bash + --no-descriptions disable completion descriptions +``` + +### SEE ALSO + +* [kyverno-json completion](kyverno-json_completion.md) - Generate the autocompletion script for the specified shell + diff --git a/docs/user/commands/kyverno-json_completion_fish.md b/docs/user/commands/kyverno-json_completion_fish.md new file mode 100644 index 00000000..e29cc379 --- /dev/null +++ b/docs/user/commands/kyverno-json_completion_fish.md @@ -0,0 +1,34 @@ +## kyverno-json completion fish + +Generate the autocompletion script for fish + +### Synopsis + +Generate the autocompletion script for the fish shell. + +To load completions in your current shell session: + + kyverno-json completion fish | source + +To load completions for every new session, execute once: + + kyverno-json completion fish > ~/.config/fish/completions/kyverno-json.fish + +You will need to start a new shell for this setup to take effect. + + +``` +kyverno-json completion fish [flags] +``` + +### Options + +``` + -h, --help help for fish + --no-descriptions disable completion descriptions +``` + +### SEE ALSO + +* [kyverno-json completion](kyverno-json_completion.md) - Generate the autocompletion script for the specified shell + diff --git a/docs/user/commands/kyverno-json_completion_powershell.md b/docs/user/commands/kyverno-json_completion_powershell.md new file mode 100644 index 00000000..77829e69 --- /dev/null +++ b/docs/user/commands/kyverno-json_completion_powershell.md @@ -0,0 +1,31 @@ +## kyverno-json completion powershell + +Generate the autocompletion script for powershell + +### Synopsis + +Generate the autocompletion script for powershell. + +To load completions in your current shell session: + + kyverno-json completion powershell | Out-String | Invoke-Expression + +To load completions for every new session, add the output of the above command +to your powershell profile. + + +``` +kyverno-json completion powershell [flags] +``` + +### Options + +``` + -h, --help help for powershell + --no-descriptions disable completion descriptions +``` + +### SEE ALSO + +* [kyverno-json completion](kyverno-json_completion.md) - Generate the autocompletion script for the specified shell + diff --git a/docs/user/commands/kyverno-json_completion_zsh.md b/docs/user/commands/kyverno-json_completion_zsh.md new file mode 100644 index 00000000..a4cdcd69 --- /dev/null +++ b/docs/user/commands/kyverno-json_completion_zsh.md @@ -0,0 +1,45 @@ +## kyverno-json completion zsh + +Generate the autocompletion script for zsh + +### Synopsis + +Generate the autocompletion script for the zsh shell. + +If shell completion is not already enabled in your environment you will need +to enable it. You can execute the following once: + + echo "autoload -U compinit; compinit" >> ~/.zshrc + +To load completions in your current shell session: + + source <(kyverno-json completion zsh) + +To load completions for every new session, execute once: + +#### Linux: + + kyverno-json completion zsh > "${fpath[1]}/_kyverno-json" + +#### macOS: + + kyverno-json completion zsh > $(brew --prefix)/share/zsh/site-functions/_kyverno-json + +You will need to start a new shell for this setup to take effect. + + +``` +kyverno-json completion zsh [flags] +``` + +### Options + +``` + -h, --help help for zsh + --no-descriptions disable completion descriptions +``` + +### SEE ALSO + +* [kyverno-json completion](kyverno-json_completion.md) - Generate the autocompletion script for the specified shell + diff --git a/docs/user/commands/kyverno-json_docs.md b/docs/user/commands/kyverno-json_docs.md new file mode 100644 index 00000000..a4f76388 --- /dev/null +++ b/docs/user/commands/kyverno-json_docs.md @@ -0,0 +1,29 @@ +## kyverno-json docs + +Generates reference documentation. + +### Synopsis + +Generates reference documentation. + + The docs command generates CLI reference documentation. + + It can be used to generate simple markdown files or markdown to be used for the website. + +``` +kyverno-json docs [flags] +``` + +### Options + +``` + --autogenTag Determines if the generated docs should contain a timestamp (default true) + -h, --help help for docs + -o, --output string Output path (default ".") + --website Website version +``` + +### SEE ALSO + +* [kyverno-json](kyverno-json.md) - kyverno-json + diff --git a/go.mod b/go.mod index bea5e6be..516da380 100644 --- a/go.mod +++ b/go.mod @@ -86,6 +86,7 @@ require ( github.com/coreos/go-oidc/v3 v3.6.0 // indirect github.com/coreos/go-semver v0.3.1 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/cyberphone/json-canonicalization v0.0.0-20230710064741-aa7fe85c7dbd // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect @@ -202,6 +203,7 @@ require ( github.com/puzpuzpuz/xsync/v2 v2.5.0 // indirect github.com/r3labs/diff v1.1.0 // indirect github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sassoftware/relic v7.2.1+incompatible // indirect github.com/secure-systems-lab/go-securesystemslib v0.7.0 // indirect github.com/segmentio/asm v1.2.0 // indirect diff --git a/go.sum b/go.sum index db4afa23..60cf04ba 100644 --- a/go.sum +++ b/go.sum @@ -354,6 +354,7 @@ github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfc github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= @@ -1271,6 +1272,7 @@ github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncj github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryancurrah/gomodguard v1.2.0/go.mod h1:rNqbC4TOIdUDcVMSIpNNAzTbzXAZa6W5lnUepvuMMgQ= github.com/ryanrolds/sqlclosecheck v0.3.0/go.mod h1:1gREqxyTGR3lVtpngyFo3hZAgk0KCtEdgEkHwDbigdA= diff --git a/pkg/commands/docs/command.go b/pkg/commands/docs/command.go new file mode 100644 index 00000000..abb2b33a --- /dev/null +++ b/pkg/commands/docs/command.go @@ -0,0 +1,35 @@ +package docs + +import ( + "log" + + "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/command" + "github.com/spf13/cobra" +) + +func Command(root *cobra.Command) *cobra.Command { + var options options + cmd := &cobra.Command{ + Use: "docs", + Short: command.FormatDescription(true, websiteUrl, false, description...), + Long: command.FormatDescription(false, websiteUrl, false, description...), + Args: cobra.NoArgs, + SilenceUsage: true, + RunE: func(cmd *cobra.Command, _ []string) error { + if err := options.validate(root); err != nil { + return err + } + return options.execute(root) + }, + } + 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) + } + if err := cmd.MarkFlagRequired("output"); err != nil { + log.Println("WARNING", err) + } + return cmd +} diff --git a/pkg/commands/docs/command_test.go b/pkg/commands/docs/command_test.go new file mode 100644 index 00000000..d0eca2ce --- /dev/null +++ b/pkg/commands/docs/command_test.go @@ -0,0 +1,67 @@ +package docs + +import ( + "bytes" + "io" + "strings" + "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) +} + +func TestCommandWithInvalidArg(t *testing.T) { + cmd := Command(&cobra.Command{}) + assert.NotNil(t, cmd) + b := bytes.NewBufferString("") + cmd.SetErr(b) + cmd.SetArgs([]string{"foo"}) + err := cmd.Execute() + assert.Error(t, err) + out, err := io.ReadAll(b) + assert.NoError(t, err) + expected := `Error: unknown command "foo" for "docs"` + assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(out))) +} + +func TestCommandWithInvalidFlag(t *testing.T) { + cmd := Command(&cobra.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(&cobra.Command{}) + assert.NotNil(t, cmd) + b := bytes.NewBufferString("") + cmd.SetOut(b) + cmd.SetArgs([]string{"--help"}) + err := cmd.Execute() + assert.NoError(t, err) + out, err := io.ReadAll(b) + assert.NoError(t, err) + assert.True(t, strings.HasPrefix(string(out), cmd.Long)) +} diff --git a/pkg/commands/docs/doc.go b/pkg/commands/docs/doc.go new file mode 100644 index 00000000..53545e0c --- /dev/null +++ b/pkg/commands/docs/doc.go @@ -0,0 +1,12 @@ +package docs + +// TODO +var websiteUrl = `` + +var description = []string{ + `Generates reference documentation.`, + ``, + `The docs command generates CLI reference documentation.`, + ``, + `It can be used to generate simple markdown files or markdown to be used for the website.`, +} diff --git a/pkg/commands/docs/options.go b/pkg/commands/docs/options.go new file mode 100644 index 00000000..3c756c6b --- /dev/null +++ b/pkg/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/pkg/commands/docs/utils.go b/pkg/commands/docs/utils.go new file mode 100644 index 00000000..14250f24 --- /dev/null +++ b/pkg/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/pkg/commands/root.go b/pkg/commands/root.go index 2e2b8a1b..c72031b7 100644 --- a/pkg/commands/root.go +++ b/pkg/commands/root.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" + "github.com/kyverno/kyverno-json/pkg/commands/docs" "github.com/kyverno/kyverno-json/pkg/engine/template" jsonengine "github.com/kyverno/kyverno-json/pkg/json-engine" "github.com/kyverno/kyverno-json/pkg/payload" @@ -86,5 +87,8 @@ func NewRootCommand() *cobra.Command { cmd.Flags().StringVar(&command.payload, "payload", "", "Path to payload (json or yaml file)") cmd.Flags().StringSliceVar(&command.preprocessors, "pre-process", nil, "JmesPath expression used to pre process payload") cmd.Flags().StringSliceVar(&command.policies, "policy", nil, "Path to kyverno-json policies") + cmd.AddCommand( + docs.Command(cmd), + ) return cmd }