Skip to content

Latest commit

 

History

History
345 lines (264 loc) · 14.9 KB

cli_hacking_guide.adoc

File metadata and controls

345 lines (264 loc) · 14.9 KB

CLI Hacking Guide

The OpenShift 3 Command Line Interface (CLI) is a set of command-line tools designed for managing OpenShift servers and performing multiple client actions against them.

This document provides information about how to contribute to the CLI. For usage and other end-user information check the official documentation and cli.md.

Getting started

The OpenShift CLI is distributed as a single binary that can act as a different tool depending on its name and/or symlinks. So if named as (or have a symlink created with the name) oc it will provide higher-level commands generally targeted for end-users. If renamed to kubectl, it will provide only the functionality of a kubectl binary.

Contributing

The Commander

We make use of Cobra and pflag as the base commander that allows fully compliant POSIX commands. We are not going to cover Cobra and pflag in this document, so please refer to their documentation for information about flags, hooks or general commander usage.

CLI Code Organization

Commands are organized in the package structure as:

Command Structure

For every command we have a NewCmd<CommandName> function that creates the command and returns a pointer to a cobra.Command, which can later be added to other parent commands to compose the structure tree.

We have a <CommandName>Options struct with a variable to every flag and argument declared by the command (and any other variable required for the command to run). Each Options struct has a New<CommandName>Options function that instantiates the command options struct, and sets any default values for flag fields. When declaring a command’s flags, each flag is bound to its corresponding field in the options struct via the <FlagType>Var Cobra method. This makes tests and mocking easier. The options struct exposes three functions:

  • Complete: Completes the struct variables with values that may not be directly provided via flags. Here you will usually take the args slice and set the values as appropriate variables, instantiate configs or clients, etc.

  • Validate: performs validation and returns errors.

  • Run: runs the actual command, assuming that the struct is complete with all required values to run, and that they are valid.

For every command that requires printing information to the screen, we provide a "printing stack" which allows every command to handle binding printing flags, printing flag values, and displaying a consistent output format for various operations to the user.

Sample command skeleton:

// MineRecommendedCommandName is the recommended command name
const MineRecommendedCommandName = "mine"

// MineOptions contains all the options for running the mine cli command
type MineOptions struct {
  PrintFlags *genericclioptions.PrintFlags // printer flags provide several methods to bind flags and obtain a suitable printer
  Printer printers.ResourcePrinter // This field is set in the "Complete" method (usually).

  mineLatest bool

  genericclioptions.IOStreams // this field is always provided last. It is inlined in the options struct, and set during options instantiation.
}

var (
  mineLong = templates.LongDesc(`
    Some long description
    for my command.`)

  mineExample = templates.Examples(`
    # Run my command's first action
    %[1]s first

    # Run my command's second action on latest stuff
    %[1]s second --latest`)
)

func NewMineOptions(streams genericclioptions.IOStreams) *MineOptions {
  return &MineOptions{
    PrintFlags: genericclioptions.NewPrintFlags("this message is printed on command success").WithTypeSetter(scheme.Scheme), // here we instantiate our PrintFlags

    mineLatest: true, // perform flag defaulting here
    IOStreams: streams,
  }
}

// NewCmdMine implement the OpenShift cli mine command.
func NewCmdMine(name, fullName string, f *clientcmd.Factory, streams genericclioptions.IOStreams) *cobra.Command {
  o := NewMineOptions(streams)
  cmd := &cobra.Command{
    Use:     fmt.Sprintf("%s [--latest]", name),
    Short:   "Run my command",
    Long:    mineLong,
    Example: fmt.Sprintf(mineExample, fullName),
    Run: func(cmd *cobra.Command, args []string) {
      kcmdutil.CheckErr(o.Complete(f, cmd, args))
      kcmdutil.CheckErr(o.Validate())
      kcmdutil.CheckErr(o.Run())
    },
  }

  // always use <Type>Var cobra methods, as they allow us to bind flag values
  // directly to struct fields by passing their address as the first parameter.
  cmd.Flags().BoolVar(&o.mineLatest, "latest", o.mineLatest, "Use latest stuff")

  // make sure to bind any printing flags if the command makes use of the printing stack
  o.PrintFlags.AddFlags(cmd)
  return cmd
}

// Complete completes all the required options for mine.
func (o *MineOptions) Complete(f *clientcmd.Factory, cmd *cobra.Command, args []string) error {
  // obtain a printer from our PrintFlags
  var err error
  o.Printer, err = o.PrintFlags.ToPrinter()
  return err
}

// Validate validates all the required options for mine.
func (o *MineOptions) Validate() error {
  return nil
}

// Run implements all the necessary functionality for mine.
func (o *MineOptions) Run() error {
  return nil
}

Writing Usage

When writing a usage string, make sure you cover the most important path for the given command. Use the following conventions:

  • Arguments and flag values names in upper case, e.g. RESOURCE, -n NAME.

  • Optional arguments or flags between brackets, e.g. [RESOURCE], [-f FILENAME].

  • Mutually exclusive required arguments and/or flags with the OR operator, e.g. --add|--remove|--list, with parenthesis if they are of mixed types (arguments and flags), e.g. (RESOURCE | -f FILENAME).

  • If multiple values are supported for a given argument use three dots, e.g. KEY_1=VAL_1 …​ KEY_N=VAL_N.

  • Arguments don’t have names, but we have to reference them somehow in usage. Try to be concise with the names already used by the usage of other commands. For example, these are some very recurring names: BUILD (meaning a build name or ID), DEPLOYMENT (meaning a deployment name or ID), RESOURCE (e.g. pod, pods, replicationcontroller, rc, deploymentconfig, dc, build, etc), NAME, RESOURCE/NAME (e.g. pod/mypodname, rc/myrcname, etc), URL, TEMPLATE, KEY=VALUE, FILENAME and so on.

A few examples:

cancel-build BUILD
deploy DEPLOYMENTCONFIG
login [URL]
edit (RESOURCE/NAME | -f FILENAME)
new-app (IMAGE | IMAGESTREAM | TEMPLATE | PATH | URL ...)
process (TEMPLATE | -f FILENAME) [-v KEY=VALUE]

Writing Examples

Examples must have 2-space tabbing. Always try to have a consistent explanation for every example as a comment (starting with #). The full command name is parameterized for every example (usually with %[1]s) so that the examples are still valid if the command is used by different parent commands. Make sure you don’t have a newline character at the end of the string.

Example:

  deployExample = templates.Examples(`
    # Display the latest deployment for the 'database' deployment config
    %[1]s database

    # Start a new deployment based on the 'database' deployment config
    %[1]s database --latest`)

Bash Completions

When introducing modifications to the structure of the commands set (changes in flags, command names, arguments, etc) you may need to update the bash completions files. To check if an update to completions is needed, you can run the command:

$ hack/verify-generated-completions.sh

To update completions, run:

$ hack/update-generated-completions.sh

In case you need additional control over how flags behave in terms of code completion, there are some helper functions:

cmd.MarkFlagFilename("my-flag-name")

allows the given flag to autocomplete as a path to file or directory.

cmd.MarkFlagFilename("my-flag-name", "yaml", "yml")

consider the given file extensions when doing autocomplete.

cmd.MarkFlagRequired("my-flag-name")

mark a flag as required.

Handling Errors

When delcaring the Run: field in the cobra comand, make sure to call the Complete, Validate, Run methods within the k8s.io/kubernetes/pkg/kubectl/cmd/util#CheckErr helper, which will take care of exiting with the correct exit code in the event of an error:

cmd := &cobra.Command{
  Use:     "foo [flags]",
  Short:   "short command description",
  Long:    descLong,
  Example: fmt.Sprintf(fooExample, fullName),
  Run: func(cmd *cobra.Command, args []string) {
    kcmdutil.CheckErr(o.Complete(f, cmd, args))
    kcmdutil.CheckErr(o.Validate())
    kcmdutil.CheckErr(o.Run())
  },
}

Helper Functions

There are a number of helper functions available in cmdutil and kcmdutil. Import them with:

import (
  // other imports...
  kcmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
  cmdutil "github.com/openshift/oc/pkg/helpers/cmd"
)

Examples:

kcmdutil.CheckErr(err error)

handles an error (check for nil and exit the program accordingly), this should always be used instead of handling the err manually.

kcmdutil.GetFlag<Type>(cmd *cobra.Command, flagName string)

gets the instance of a declared flag, by type. If possible, use the struct var binding to get flag values instead.

cmdutil.IsTerminalReader(r io.Reader)

checks if the given io.Reader is a terminal.

Commented Example

Taking the oc deploy command as an example, the code structure for a command will usually look like the one below.

// 1.
type DeployOptions struct {
  PrintFlags *genericclioptions.PrintFlags

  Printer printers.ResourcePrinter

  // other fields...
  deployLatest bool
  retryDeploy  bool

  // inlined IOStreams provide standard error, standard out, and standard input streams
  genericclioptions.IOStreams
}

var (
  // 2.
  deployLong = templates.LongDesc(`
    Some long description
    for the deploy command.`)

  // 3.
  deployExample = templates.Examples(`
    # Display the latest deployment for the 'database' DeploymentConfig
    %[1]s database

    # Start a new deployment based on the 'database' DeploymentConfig
    %[1]s database --latest`)
)

// 4
func NewDeployOptions(streams genericclioptions.IOStreams) *DeployOptions {
  return &DeployOptions{
    PrintFlags: genericclioptions.NewPrintFlags("deployed").WithTypeSetter(scheme.Scheme),
    IOStreams: streams,
  }
}

// 5
func NewCmdDeploy(name, fullName string, f *clientcmd.Factory, streams genericclioptions.IOStreams) *cobra.Command {
  o := NewDeployOptions(streams)

  cmd := &cobra.Command{
    // 6.
    Use:     fmt.Sprintf("%s DEPLOYMENTCONFIG", name),
    Short:   "View, start, cancel, or retry deployments",
    Long:    deployLong,
    Example: fmt.Sprintf(deployExample, fullName),
    Run: func(cmd *cobra.Command, args []string) {
      // 7.
      kcmdutil.CheckErr(o.Complete(f, cmd, args))

      // 8.
      kcmdutil.CheckErr(o.Validate())

      // 9.
      kcmdutil.CheckErr(o.Run())
    },
  }

  cmd.Flags().BoolVar(&options.deployLatest, "latest", false, "Start a new deployment now.")
  cmd.Flags().BoolVar(&options.retryDeploy, "retry", false, "Retry the latest failed deployment.")

  // 10.
  o.PrintFlags.AddFlags(cmd)
  return cmd
}

func (o *DeployOptions) Complete(f *clientcmd.Factory, cmd *cobra.Command, args []string) error {
  // 11.
  var err error
  o.Printer, err = o.PrintFlags.ToPrinter()
  return err
}

func (o DeployOptions) Validate() error {
  return nil
}

func (o DeployOptions) Run() error {
  return nil
}
  1. Create a struct to contain vars for every flag declared (and other vars that the command may need). This struct will usually have the Complete, Validate and Run<Command> methods (explained below).

  2. Multiple lines describing the command.

  3. Command examples. Try to cover every important command path (flags, arguments, etc).

  4. Create a "constructor" for the command options struct. Here you will instantiate command options, default any values, and set the IO streams for writing to the screen.

  5. This function creates the command. Notice it takes the parent command name as argument and also a io.Writer that will be used to print messages.

  6. Command usage.

  7. Complete(f *clientcmd.Factory, cmd *cobra.Command, args []string) error is used to populate any object or variable that will be required to run the command and is still missing at this point. For example, if the command will make use of an API client it can be created from the factory in this method. Can also be used to take argument values from the args slice and hold it in explicit variables in your struct, store the io.Writer that will be used later, etc.

  8. Validate() error perform validations on anything required in order to run this command. Notice that if the Complete and Validate methods implementations are simple enough, you may have only one of them that does both.

  9. Run() error does the actual command logic and returns errors as required. Notice that this method does not take anything as argument - it’s expected that you previously extracted and stored in the struct anything that will be needed to run this command. This makes commands more easily testable once you can run and populate the command struct with the values you want to test and then just run this method and check for the returned error(s). Try to always use the functions in k8s.io/kubernetes/pkg/kubectl/cmd/util to check and handle errors. It is not expected that commands call glog.Fatalf, os.Exit or anything similar directly.

  10. Always remember to bind printer-related flags, if your command makes use of the printing stack.

  11. Similarly, remember to retrieve a valid printer from the printer-related flags struct in your Complete method. The printer obtained in this step will always be the correct printer based on the output-format specified by the user.