airshipctl/docs/plugins.md
Ian Howell 6e41a56a7d Rename module to reflect its new location
Change-Id: I5c8c524c4d3f569b21b5b829722b3c49952cb0ab
2019-07-01 12:15:29 -05:00

5.8 KiB

Plugin Support

Table of Contents

Our requirements for airshipctl contain two very conflicting concepts. One, we'd like to assert that airshipctl is a statically linked executable, such that it can be easily distributed. Two, we'd like to have plugin support. These requirements can't coincide within the same project under the standard definition of a plugin. Our solution is to provide a more refined definition of what a plugin actually is.

Compile-In Plugins

In order to support plugins to an independent binary file, we use the concept of compile-in plugins. A compile-in plugin is an add-on that is built into the main application at compile time, as opposed to runtime. This means that while airshipctl is a standalone application, it also acts as a sort of library. In fact, taking a deeper look at airshipctl reveals that the base application is incredibly simple. At its core, airshipctl provides exactly 2 commands: version and help. Take a look at the following snippet to see what this looks like:

package main

import (
	"os"
	"opendev.org/airship/airshipctl/cmd"
)

func main() {
	rootCmd, _, err := cmd.NewRootCmd(os.Stdout)
	if err != nil {
		panic(err)
	}
	rootCmd.Execute()
}

Compiling and running the above gives the following output:

$ ./airshipctl
airshipctl is a unified entrypoint to various airship components

Usage:
  airshipctl [command]

Available Commands:
  help        Help about any command
  version     Show the version number of airshipctl

Flags:
      --debug   enable verbose output
  -h, --help    help for airshipctl

Use "airshipctl [command] --help" for more information about a command.

Every other command is treated as a plugin. Changing main to the following adds the default commands, or "plugins", to theairshipctl tool:

func main() {
	rootCmd, settings, err := cmd.NewRootCmd(os.Stdout)
	if err != nil {
		panic(err)
	}
	cmd.AddDefaultAirshipCTLCommands(rootCmd, settings)
	rootCmd.Execute()
}

Compiling and running now provides the following commands:

Available Commands:
  bootstrap   bootstraps airshipctl
  help        Help about any command
  version     Show the version number of airshipctl
  ------ more commands TBD ------

Downloading and building the main airshipctl project will default to providing the builtin commands (such as bootstrap), much like the above. A plugin author wishing to use airshipctl can then use the rootCmd as the first of a series of building blocks. The following demonstrates the addition of a new command, hello:

package main

import (
	"fmt"
	"os"
	"opendev.org/airship/airshipctl/cmd"
	"github.com/spf13/cobra"
)

func main() {
	rootCmd, settings, err := cmd.NewRootCmd(os.Stdout)
	if err != nil {
		panic(err)
	}

	cmd.AddDefaultAirshipCTLCommands(rootCmd, settings)

	helloCmd := &cobra.Command{
		Use: "hello",
		Short: "Prints a friendly message to the screen",
		Run: func(cmd *cobra.Command, args []string) {
			fmt.Println("Hello World!")
		},
	}
	rootCmd.AddCommand(helloCmd)

	rootCmd.Execute()
}

Fine Tuning a Build

There are a couple of ways in which a plugin author can fine tune their version of airshipctl. These manifest as an ability to pick and choose various plugins (including the defaults), and capabilities for accessing the same settings as other airshipctl commands.

Command Selection

In the previous section, we introduced the AddDefaultAirshipCTLCommands function. That command will simply dump all of the builtin commands onto the root. But a plugin author might not need all of the builtins. To deal with this, the author can pick and choose specific commands to add to their airshipctl, much like the following:

package main

import (
	"os"
	"opendev.org/airship/airshipctl/cmd"
	"opendev.org/airship/airshipctl/cmd/bootstrap"
)

func main() {
	rootCmd, settings, err := cmd.NewRootCmd(os.Stdout)
	if err != nil {
		panic(err)
	}
	rootCmd.AddCommand(bootstrap.NewBootstrapCommand(settings))
	rootCmd.Execute()
}

This variant of airshipctl will have the bootstrap command, but will not have any other builtins.

This can be particularly useful if a plugin author desires to "override" a specific functionality provided by a builtin command. For example, you might write your own bootstrap command and use it in place of the builtin.

Accessing airshipctl settings

The airshipctl will contain several settings which may be useful to a plugin author. The following snippet demonstrates how to use the debug flag, provided by airshipctl, as well as a custom alt-message flag, provided by the plugin.

package main

import (
	"fmt"
	"os"
	"opendev.org/airship/airshipctl/cmd"
	"opendev.org/airship/airshipctl/pkg/environment"
	"github.com/spf13/cobra"
)

type Settings struct {
	*environment.AirshipCTLSettings

	AltMessage bool
}

func main() {
	rootCmd, rootSettings, err := cmd.NewRootCmd(os.Stdout)
	if err != nil {
		panic(err)
	}

	settings := Settings{AirshipCTLSettings: rootSettings}
	helloCmd := &cobra.Command{
		Use:   "hello",
		Short: "Prints a friendly message to the screen",
		Run: func(cmd *cobra.Command, args []string) {
			if settings.Debug {
				fmt.Println("DEBUG: a debugging message")
			}
			if settings.AltMessage {
				fmt.Println("Goodbye World!")
			} else {
				fmt.Println("Hello World!")
			}
		},
	}
	helloCmd.PersistentFlags().BoolVar(&settings.AltMessage, "alt-message", false, "display an alternate message")
	rootCmd.AddCommand(helloCmd)

	rootCmd.Execute()
}

The AirshipCTLSettings object can be found here. Future documentation TBD.