Skip to content

Commit 43ee7fc

Browse files
pPrecelanoipmgrego952
authored
Propose command standards (#2751)
Co-authored-by: Marcin Dobrochowski <39153236+anoipm@users.noreply.github.com> Co-authored-by: Grzegorz Karaluch <grzegorz.karaluch@sap.com>
1 parent fa6066a commit 43ee7fc

File tree

2 files changed

+293
-1
lines changed

2 files changed

+293
-1
lines changed
Lines changed: 292 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
1+
# Command Structure Standards
2+
3+
Creation date: 2025.11.21
4+
5+
## Description
6+
7+
Let's collect and propose all requirements and good practices that all new commands must meet before moving them out of the `alpha` group.
8+
9+
This document is focused on what users see, and it's not about command functionalities.
10+
11+
## Name
12+
13+
The command's name field is defined under the `cobra.Command{}.Use` field. This field must contain the command name and a symbolic visualization of the allowed arguments and flags.
14+
15+
### Names in Practice
16+
17+
Command groups:
18+
19+
```yaml
20+
function <command> [flags] # function command group that is not runnable and can receive optional flags
21+
```
22+
23+
Runnable commands:
24+
25+
```yaml
26+
explain [flags] # runnable command without argument
27+
create <name> [flags] # runnable command that receive one required arg and optional flags
28+
scale <name> <replicas> [flags] # runnable command that can receive two arguments and optional flags
29+
delete <name>... # runnable command that receive at least one argument and has no flags
30+
get [<name>...] [flags] # runnable command that receive optional one argument and optional flags
31+
module-deploy (<name>|<namespacedName>|<filepath>)... [flags] # runnable command that deploy module based on at least one argument in one of three types
32+
```
33+
34+
Important elements:
35+
36+
- `<>` - means this is a positional argument
37+
- `[]` - means elements are optional
38+
- `...` - means at least one element is required (this applies only for positional arguments)
39+
- `(a|b)` - means element `a` or `b` is possible
40+
41+
### Name Details
42+
43+
The command name must be a verb or noun. In most cases, it is recommended to use nouns to define domain-related command groups (like `kyma function`, `kyma app`, or `kyma apirule`) and verbs to define runnable operations around the domain (like `kyma app push`, `kyma function create`, `kyma apirule expose`). This rule is only a suggestion, and it depends on the use case. For example, the `kyma diagnose` command works as a runnable command and command group at the same (`kyma diagnose logs`) time, and because of this, it's not a noun. On the other hand, the `kyma registry config` in another example, where after the noun is another noun (runnable noun), but in this case, the `config` word is shorter than verbs like `get-config` or `configure`.
44+
45+
After the first word, there must be a description of possible arguments/commands. If the command is a non-runnable command group, then it should contain `<command>`, which means that this command accepts only one argument, and this argument is the command name. If command is runnable then it must describe possible inputs (if allowed) in the following format: `<arg_name>` for single, required argument, `<arg_name>...` for at least one required argument, `[<arg_name>...]` for one optional argument, `<arg_name>...` for optional arguments list of the same type. If the command receives more than one argument type, then it is possible to describe many arguments separated by a space. For example: `scale <name> <replicas>`.
46+
47+
The last element must be optional flags represented by the `[flags]` element. In our case, it must be a part of every command because we add persistent flags for the parent `kyma` command, and these flags are valid for every sub-command.
48+
49+
## Descriptions
50+
51+
There are two types of description under the `cobra.Command{}.Short` and the `cobra.Command{}.Long` fields. The first one represents a shorter description that is displayed when running parent commands help, and the second one is displayed when running the current command.
52+
53+
### Descriptions in Practice
54+
55+
For the long and short description:
56+
57+
```yaml
58+
Lists modules catalog # short description of the catalog command
59+
Use this command to list all available Kyma modules. # long description of the catalog command
60+
```
61+
62+
The parents help:
63+
64+
```console
65+
$ kyma module --help
66+
67+
Use this command to manage modules in the Kyma cluster.
68+
69+
Usage:
70+
kyma module [command]
71+
72+
Available Commands:
73+
catalog Lists modules catalog # short description of the catalog command
74+
```
75+
76+
The commands help:
77+
78+
```console
79+
$ kyma module catalog --help
80+
81+
Use this command to list all available Kyma modules. # long description of the catalog command
82+
83+
Usage:
84+
kyma module catalog [flags]
85+
```
86+
87+
### Description Details
88+
89+
The short desc helps users choose the right sub-command in the context of the domain they are in. This description must start with a capital letter and end without a period.
90+
91+
The longer one describes exactly what the command is doing. It can be multiline, describing all use-cases. It must start with a capital letter and always end with a period.
92+
93+
## Flags
94+
95+
Flags are elements of the command that can be added using the `cobra.Command{}.Flags()` and `cobra.Command{}.PersistentFlags()` functions.
96+
97+
### Flags in Practice
98+
99+
For flags configuration:
100+
101+
```golang
102+
cmd.Flags().StringVarP(&cfg.channel, "channel", "c", "fast", "Name of the Kyma channel to use for the module")
103+
cmd.Flags().StringVar(&cfg.crPath, "cr-path", "", "Path to the custom resource file")
104+
cmd.Flags().BoolVar(&cfg.defaultCR, "default-cr", false, "Deploys the module with the default CR")
105+
cmd.Flags().BoolVar(&cfg.autoApprove, "auto-approve", false, "Automatically approve community module installation")
106+
cmd.Flags().StringVar(&cfg.version, "version", "", "Specifies version of the community module to install")
107+
cmd.Flags().StringVar(&cfg.modulePath, "origin", "", "Specifies the source of the module (kyma or custom name)")
108+
_ = cmd.Flags().MarkHidden("origin")
109+
cmd.Flags().BoolVar(&cfg.community, "community", false, "Installs a community module (no official support, no binding SLA)")
110+
_ = cmd.Flags().MarkHidden("community")
111+
```
112+
113+
The commands help looks:
114+
115+
```console
116+
$ kyma module add --help
117+
118+
...
119+
120+
Flags:
121+
--auto-approve Automatically approve community module installation
122+
-c, --channel string Name of the Kyma channel to use for the module (default "fast")
123+
--cr-path string Path to the custom resource file
124+
--default-cr Deploys the module with the default CR
125+
--version string Specifies version of the community module to install
126+
```
127+
128+
### Flags Details
129+
130+
The most important thing from the user perspective is flag description, defaulting and validation.
131+
132+
The description should be as minimalistic as possible, but describe for which flag it can be used. If the flag is related to only one command's use case, then it's a good opportunity to add example usage of such a command (read more in the [Examples](#examples) section). Every description should start with a capital letter and end with no period.
133+
134+
If possible, every flag should have its own default value passed to the flag building function (to display it in commands' help).
135+
136+
Flag shorthand should be added only to flags that are often used in most cases to speed up typing. The shorthand should be the first letter of the full flag name (for example, `-c` for `--channel`).
137+
138+
Flags can be marked as hidden. This functionality may be helpful while some flags are deprecated and their functionality is removed or moved. Hiding the flag allows us to validate later if the user uses it and display a well-crafted error with detailed information on what is happening and where such functionality was moved to.
139+
140+
To validate flags, the [flags](../../../internal/flags/validate.go) package must be used to keep all validations of all commands in the same shape. Functionality of this package can be run in the `cobra.Command{}.PreRun`.
141+
142+
Persistent flags must meet all the requirements above and, in addition, should be implemented only in common use cases for all referred commands. These flag types can introduce confusion when implemented for commands and don't provide any functionality.
143+
144+
## Examples
145+
146+
`Examples` is an optional field that is highly recommended to use. It's under the `cobra.Command{}.Examples` field and can be used to propose the most common use cases or propositions of flag usage.
147+
148+
### Examples in Practice
149+
150+
For the following examples:
151+
152+
```yaml
153+
# Add a Kyma module with the default CR
154+
kyma module add kyma-module --default-cr
155+
156+
# Add a Kyma module with a custom CR from a file
157+
kyma module add kyma-module --cr-path ./kyma-module-cr.yaml
158+
159+
## Add a community module with a default CR and auto-approve the SLA
160+
# passed argument must be in the format <namespace>/<module-template-name>
161+
kyma module add my-namespace/my-module-template-name --default-cr --auto-approve
162+
```
163+
164+
The help displays:
165+
166+
```console
167+
$ kyma module add --help
168+
169+
Use this command to add a module.
170+
171+
Usage:
172+
kyma module add <module> [flags]
173+
174+
Examples:
175+
# Add a Kyma module with the default CR
176+
kyma module add kyma-module --default-cr
177+
178+
# Add a Kyma module with a custom CR from a file
179+
kyma module add kyma-module --cr-path ./kyma-module-cr.yaml
180+
181+
## Add a community module with a default CR and auto-approve the SLA
182+
# passed argument must be in the format <namespace>/<module-template-name>
183+
kyma module add my-namespace/my-module-template-name --default-cr --auto-approve
184+
```
185+
186+
### Examples Details
187+
188+
Every line of the examples must start with double spaces to display them correctly in the terminal.
189+
190+
All examples for a single command must be separated by an empty line and include their own description, starting with the `#` symbol. If the example description is longer than one line, then the first line should start with the `##` prefix, and then every next line should start with `#` and two spaces after.
191+
192+
Examples should reflect real use cases, so, if possible, they should use real data as arguments and flags. If not, then use fiction one representing real data, like `my-module`, `my-resource`, `my-something`.
193+
194+
## Aliases
195+
196+
The alias is an array table in the `cobra.Command{}.Aliases`, and there are no specific requirements about command aliases. The good idea is always to use this functionality to provide a shorthand of the command (`del` for `delete` command), or a word form that can help with avoiding small typos (`modules` for `module` command).
197+
198+
### Aliases in Practice
199+
200+
```go
201+
cmd := &cobra.Command{
202+
Use: "delete <module> [flags]",
203+
Aliases: []string{"del"},
204+
}
205+
```
206+
207+
## Errors and Hints
208+
209+
Errors were proposed in the [ADR 001](001-error-output-format.md) proposal and implemented quite a while after that. With this functionality, users can understand what is going on at three levels of abstraction. The general message called `Error` should contain the last, user-understandable operation that fails. The second thing is `Error Details`, which contains an internal error message generated by the library or the server. The `Hints` section is designed to help users identify how to fix the problem. Every hint should be in one of two formats:
210+
211+
- `to <what>, <do>` - format used to describe possible optional configurations that may be used or misconfigured
212+
- `ensure <what>` - format used to describe the required configuration user may have misconfigured
213+
214+
The CLI is designed to always return `clierror.Error` instead of pure `error`. Both errors are not compatible with each other, and to avoid user confusion, we should not use the `cobra.Command{}.RunE` and instead of that use the `cobra.Command{}.Run` and check the error manually inside of it:
215+
216+
```go
217+
cmd := &cobra.Command{
218+
Run: func(cmd *cobra.Command, _ []string) {
219+
clierror.Check(runAdd(&cfg)) // check manually using the `clierror.Check` function
220+
},
221+
}
222+
```
223+
224+
### Example Hints
225+
226+
```yaml
227+
"ensure you provide a valid module name and channel (or version)",
228+
"to list available modules, call the `kyma module catalog` command",
229+
"to pull available modules, call the `kyma module pull` command",
230+
"to add a community module, use the `--origin` flag",
231+
```
232+
233+
## Command Messaging
234+
235+
It's not allowed to use the `os.Stdout`/`os.Stderr` and `fmt.Print` functionalities. Instead of that we must use the `internal/out` package. The main reason of using it is to keep control over `stdout` and `stderr` channels in one pleace. This allows us to:
236+
237+
- split messages by calling specific functions like `out.Err`, to send a message to `stderr`, or one of `out.Msg`, `out.Prio`, `out.Verbose`, or `out.Debug` to send it to the `stdout`
238+
- mute less crucial output by calling the `out.DisableMsg` function that disables the `out.Msg`
239+
- enable cli verbosity by calling the `out.EnableVerbose` function that enables the `out.Verbose`
240+
- enable command's debug info by running it with the `--debug` flag. It allows printing messages passed to the `out.Debug` function. This solution is designed for developers who are working on the CLI. This flag is not mentioned in the user documentation. The `out.Debug` functionality is the only one that should not be re-configured in commands business logic - the `--debug` flag is defined as a persistent flag for all sub-commands, and no command should re-define this flag on its own or mask by defining a local flag with the same name
241+
242+
## Example Command in Code
243+
244+
The command configuration below applies all rules described above:
245+
246+
```go
247+
func newCMD() *cobra.Command {
248+
cfg := addConfig{
249+
KymaConfig: kymaConfig,
250+
}
251+
252+
cmd := &cobra.Command{
253+
Use: "add <module> [flags]",
254+
Short: "Add a module",
255+
Long: "Use this command to add a module.",
256+
Example: ` # Add a Kyma module with the default CR
257+
kyma module add kyma-module --default-cr
258+
259+
# Add a Kyma module with a custom CR from a file
260+
kyma module add kyma-module --cr-path ./kyma-module-cr.yaml
261+
262+
## Add a community module with a default CR and auto-approve the SLA
263+
# passed argument must be in the format <namespace>/<module-template-name>
264+
kyma module add my-namespace/my-module-template-name --default-cr --auto-approve`,
265+
266+
Args: cobra.ExactArgs(1),
267+
PreRun: func(cmd *cobra.Command, _ []string) {
268+
clierror.Check(flags.Validate(cmd.Flags(),
269+
flags.MarkMutuallyExclusive("cr-path", "default-cr"),
270+
flags.MarkUnsupported("community", "the --community flag is no longer supported - community modules need to be pulled first using 'kyma module pull' command, then installed"),
271+
flags.MarkUnsupported("origin", "the --origin flag is no longer supported - use commands argument instead"),
272+
))
273+
},
274+
Run: func(cmd *cobra.Command, args []string) {
275+
cfg.complete(args)
276+
clierror.Check(runAdd(&cfg))
277+
},
278+
}
279+
280+
cmd.Flags().StringVarP(&cfg.channel, "channel", "c", "", "Name of the Kyma channel to use for the module")
281+
cmd.Flags().StringVar(&cfg.crPath, "cr-path", "", "Path to the custom resource file")
282+
cmd.Flags().BoolVar(&cfg.defaultCR, "default-cr", false, "Deploys the module with the default CR")
283+
cmd.Flags().BoolVar(&cfg.autoApprove, "auto-approve", false, "Automatically approve community module installation")
284+
cmd.Flags().StringVar(&cfg.version, "version", "", "Specifies version of the community module to install")
285+
cmd.Flags().StringVar(&cfg.modulePath, "origin", "", "Specifies the source of the module (kyma or custom name)")
286+
_ = cmd.Flags().MarkHidden("origin")
287+
cmd.Flags().BoolVar(&cfg.community, "community", false, "Install a community module (no official support, no binding SLA)")
288+
_ = cmd.Flags().MarkHidden("community")
289+
290+
return cmd
291+
}
292+
```

internal/out/out.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// - General output messages (Msg, Msgf, Msgln, Msgfln) write to stdout by default and can be disabled
44
// - Priority messages (Prio, Priof, Prioln, Priofln) write to stdout by default and cannot be disabled
55
// - Verbose messages (Verbose, Verbosef, Verboseln, Verbosefln) write to stdout when enabled
6-
// - Debug messages (Dev, Devf, Devln, Devfln) write to stdout when enabled
6+
// - Debug messages (Dev, Devf, Devln, Devfln) write to stdout when enabled via --debug flag
77
// - Error messages (Err, Errf, Errln, Errfln) write to stderr by default
88
// Printer can be configured with different writers to redirect output as needed to files, buffers, etc.
99
package out

0 commit comments

Comments
 (0)