| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [View Raw Code] [Original HTTPS Page] |
roxctl is the CLI for Stackrox.
It currently serves as a multi-purpose CLI that handles things such as:
The goal of this README is to provide an overview of writing and maintaining commands of roxctl.
Commands must follow the common name convention: roxctl <noun> [subcommands...]. For more information on why this decision was made, an ADR exists explaining it.
Within the codebase, roxctl makes heavy use of github.com/spf13/cobra as the CLI library of choice.
Besides github.com/spf13/cobra, one integral part how commands are structured is the roxctl/common/environment package. The Environment interface provides abstractions for generic functionality that may be required by a command:
Each command used in roxctl must use the Environment interface. It provides a clear structure to all commands, as well as the possibility to easily mock external dependencies to allow for better and sophisticated unit testing.
A typical command structure within Go should look as follows:
package sample
// Command exposes the *cobra.Command for your specific command.
// It will **always** take a environment.Environment as input.
func Command(cliEnvironment environment.Environment) *cobra.Command {
sampleCmd := &sampleCmd{env: cliEnvironment}
cmd := &cobra.Command{
Use: "sample",
Short: "A sample command",
Long: `A sample command.
This is a sample for a multi-line detailed description for the sample command`,
Args: cobra.NoArgs,
// Each command **must** always use `RunE` and return an error.
RunE: func(cmd *cobra.Command, args []string) error {
if err := sampleCmd.Construct(cmd); err != nil {
return err
}
return sampleCmd.sample()
},
}
// Each custom flag for a command **must** be bound to the struct holding
// all relevant properties for a command.
cmd.Flags().StringVarP(&sampleCmd.name, "name", "n", "", "name to say hello to.")
return cmd
}
// Sample command holds the cli environment, as well as all resolved flag values. This makes unit testing easier.
type sampleCmd struct {
env environment.Environment
name string
}
// Construct ensure all flag values are resolved and possibly validated.
// Any additional external dependency that you may require (e.g. a gRPC client)
// may be constructed here as well.
func (s *sampleCmd) Construct(cmd *cobra.Command) error {
return nil
}
// Sample is an example for any possible business logic your command may execute.
func (s *sampleCmd) sample() error {
s.env.Logger().InfofLn("Hello world, %s!", s.name)
return nil
}One of the most important things about maintaining commands for roxctl is to ensure changes of behavior have no side effects.
Since roxctl is typically used within CI as well as automation (e.g. scripts) by users, it is sensitive to behavior changes.
A behavior change may consist of:
Since this is a sensitive topic, the following points must be considered when maintaining commands:
When changing flags or commands, aliases must be in place to avoid breaking users of said commands.
Currently, two options exist for this:
For Cobra's alias feature, the following limitations currently apply:
In case your change falls under the limitations of the cobra alias feature, it's recommended to handle the alias within the command's code itself.
The below code highlights changes for a command:
package sample
func OldCommand(cliEnvironment environment.Environment) *cobra.Command {
sampleCmd := &sampleCmd{env: cliEnvironment}
cmd := &cobra.Command{
Use: "old-sample",
Short: "A sample command",
Long: `A sample command.
This is a sample for a multi-line detailed description for the sample command.
Also, please use the new-sample command since this one is deprecated.`,
Hidden: true,
Args: cobra.NoArgs,
// Each command **must** always use `RunE` and return an error.
RunE: func(cmd *cobra.Command, args []string) error {
cliEnvironment.Logger().WarnLn("old-sample is deprecated. Please use new-sample.")
return sampleCmd.RunE(cmd, args)
},
}
cmd.Flags().StringVarP(&sampleCmd.name, "name", "n", "", "name to say hello to.")
return cmd
}
func NewCommand(cliEnvironment environment.Environment) *cobra.Command {
sampleCmd := &sampleCmd{env: cliEnvironment}
cmd := &cobra.Command{
Use: "new-sample",
Short: "A sample command",
Long: `A sample command.
This is a sample for a multi-line detailed description for the sample command`,
Args: cobra.NoArgs,
// Each command **must** always use `RunE` and return an error.
RunE: sampleCmd.RunE,
}
cmd.Flags().StringVarP(&sampleCmd.name, "name", "n", "", "name to say hello to.")
return cmd
}
// While the sample command now lives in the same package, it may live in a completely different package.
type sampleCmd struct {
env environment.Environment
name string
}
// Aliases can share code and reuse functions, e.g.:
func (s *sampleCmd) RunE(cmd *cobra.Command, args []string) error { ... }
func (s *sampleCmd) Construct(cmd *cobra.Command) error { ... }
func (s *sampleCmd) sample() error { ... }| Back | FazBrowse Home | New Git URL |