Add basic framework for writing a CLI plugin

That is, the helper to be used from the plugin's `main`.

Also add a `helloworld` plugin example and build integration.

Signed-off-by: Ian Campbell <ijc@docker.com>
This commit is contained in:
Ian Campbell 2018-12-10 15:30:19 +00:00
parent 8cf946d1bc
commit e96240427f
10 changed files with 248 additions and 0 deletions

View File

@ -34,6 +34,10 @@ binary: ## build executable for Linux
@echo "WARNING: binary creates a Linux executable. Use cross for macOS or Windows."
./scripts/build/binary
.PHONY: plugins
plugins: ## build example CLI plugins
./scripts/build/plugins
.PHONY: cross
cross: ## build executable for macOS and Windows
./scripts/build/cross
@ -42,10 +46,18 @@ cross: ## build executable for macOS and Windows
binary-windows: ## build executable for Windows
./scripts/build/windows
.PHONY: plugins-windows
plugins-windows: ## build example CLI plugins for Windows
./scripts/build/plugins-windows
.PHONY: binary-osx
binary-osx: ## build executable for macOS
./scripts/build/osx
.PHONY: plugins-osx
plugins-osx: ## build example CLI plugins for macOS
./scripts/build/plugins-osx
.PHONY: dynbinary
dynbinary: ## build dynamically linked binary
./scripts/build/dynbinary

View File

@ -0,0 +1,38 @@
package main
import (
"fmt"
"github.com/docker/cli/cli-plugins/manager"
"github.com/docker/cli/cli-plugins/plugin"
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
)
func main() {
plugin.Run(func(dockerCli command.Cli) *cobra.Command {
goodbye := &cobra.Command{
Use: "goodbye",
Short: "Say Goodbye instead of Hello",
Run: func(cmd *cobra.Command, _ []string) {
fmt.Fprintln(dockerCli.Out(), "Goodbye World!")
},
}
cmd := &cobra.Command{
Use: "helloworld",
Short: "A basic Hello World plugin for tests",
Run: func(cmd *cobra.Command, args []string) {
fmt.Fprintln(dockerCli.Out(), "Hello World!")
},
}
cmd.AddCommand(goodbye)
return cmd
},
manager.Metadata{
SchemaVersion: "0.1.0",
Vendor: "Docker Inc.",
Version: "0.1.0",
})
}

View File

@ -0,0 +1,25 @@
package manager
const (
// NamePrefix is the prefix required on all plugin binary names
NamePrefix = "docker-"
// MetadataSubcommandName is the name of the plugin subcommand
// which must be supported by every plugin and returns the
// plugin metadata.
MetadataSubcommandName = "docker-cli-plugin-metadata"
)
// Metadata provided by the plugin
type Metadata struct {
// SchemaVersion describes the version of this struct. Mandatory, must be "0.1.0"
SchemaVersion string
// Vendor is the name of the plugin vendor. Mandatory
Vendor string
// Version is the optional version of this plugin.
Version string
// ShortDescription should be suitable for a single line help message.
ShortDescription string
// URL is a pointer to the plugin's homepage.
URL string
}

View File

@ -0,0 +1,96 @@
package plugin
import (
"encoding/json"
"fmt"
"os"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli-plugins/manager"
"github.com/docker/cli/cli/command"
cliflags "github.com/docker/cli/cli/flags"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
// Run is the top-level entry point to the CLI plugin framework. It should be called from your plugin's `main()` function.
func Run(makeCmd func(command.Cli) *cobra.Command, meta manager.Metadata) {
dockerCli, err := command.NewDockerCli()
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
plugin := makeCmd(dockerCli)
cmd := newPluginCommand(dockerCli, plugin, meta)
if err := cmd.Execute(); err != nil {
if sterr, ok := err.(cli.StatusError); ok {
if sterr.Status != "" {
fmt.Fprintln(dockerCli.Err(), sterr.Status)
}
// StatusError should only be used for errors, and all errors should
// have a non-zero exit status, so never exit with 0
if sterr.StatusCode == 0 {
os.Exit(1)
}
os.Exit(sterr.StatusCode)
}
fmt.Fprintln(dockerCli.Err(), err)
os.Exit(1)
}
}
func newPluginCommand(dockerCli *command.DockerCli, plugin *cobra.Command, meta manager.Metadata) *cobra.Command {
var (
opts *cliflags.ClientOptions
flags *pflag.FlagSet
)
name := plugin.Use
fullname := manager.NamePrefix + name
cmd := &cobra.Command{
Use: fmt.Sprintf("docker [OPTIONS] %s [ARG...]", name),
Short: fullname + " is a Docker CLI plugin",
SilenceUsage: true,
SilenceErrors: true,
TraverseChildren: true,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
// flags must be the top-level command flags, not cmd.Flags()
opts.Common.SetDefaultOptions(flags)
return dockerCli.Initialize(opts)
},
DisableFlagsInUseLine: true,
}
opts, flags = cli.SetupPluginRootCommand(cmd)
cmd.SetOutput(dockerCli.Out())
cmd.AddCommand(
plugin,
newMetadataSubcommand(plugin, meta),
)
cli.DisableFlagsInUseLine(cmd)
return cmd
}
func newMetadataSubcommand(plugin *cobra.Command, meta manager.Metadata) *cobra.Command {
if meta.ShortDescription == "" {
meta.ShortDescription = plugin.Short
}
cmd := &cobra.Command{
Use: manager.MetadataSubcommandName,
Hidden: true,
RunE: func(cmd *cobra.Command, args []string) error {
enc := json.NewEncoder(os.Stdout)
enc.SetEscapeHTML(false)
enc.SetIndent("", " ")
return enc.Encode(meta)
},
}
return cmd
}

View File

@ -12,6 +12,8 @@ import (
"github.com/spf13/pflag"
)
// setupCommonRootCommand contains the setup common to
// SetupRootCommand and SetupPluginRootCommand.
func setupCommonRootCommand(rootCmd *cobra.Command) (*cliflags.ClientOptions, *pflag.FlagSet) {
opts := cliflags.NewClientOptions()
flags := rootCmd.Flags()
@ -47,6 +49,16 @@ func SetupRootCommand(rootCmd *cobra.Command) (*cliflags.ClientOptions, *pflag.F
return opts, flags
}
// SetupPluginRootCommand sets default usage, help and error handling for a plugin root command.
func SetupPluginRootCommand(rootCmd *cobra.Command) (*cliflags.ClientOptions, *pflag.FlagSet) {
opts, flags := setupCommonRootCommand(rootCmd)
rootCmd.PersistentFlags().BoolP("help", "", false, "Print usage")
rootCmd.PersistentFlags().Lookup("help").Hidden = true
return opts, flags
}
// FlagErrorFunc prints an error message which matches the format of the
// docker/cli/cli error messages
func FlagErrorFunc(cmd *cobra.Command, err error) error {

View File

@ -56,6 +56,9 @@ binary: build_binary_native_image ## build the CLI
build: binary ## alias for binary
plugins: build_binary_native_image ## build the CLI plugin examples
docker run --rm $(ENVVARS) $(MOUNTS) $(BINARY_NATIVE_IMAGE_NAME) ./scripts/build/plugins
.PHONY: clean
clean: build_docker_image ## clean build artifacts
docker run --rm $(ENVVARS) $(MOUNTS) $(DEV_DOCKER_IMAGE_NAME) make clean
@ -76,10 +79,18 @@ cross: build_cross_image ## build the CLI for macOS and Windows
binary-windows: build_cross_image ## build the CLI for Windows
docker run --rm $(ENVVARS) $(MOUNTS) $(CROSS_IMAGE_NAME) make $@
.PHONY: plugins-windows
plugins-windows: build_cross_image ## build the example CLI plugins for Windows
docker run --rm $(ENVVARS) $(MOUNTS) $(CROSS_IMAGE_NAME) make $@
.PHONY: binary-osx
binary-osx: build_cross_image ## build the CLI for macOS
docker run --rm $(ENVVARS) $(MOUNTS) $(CROSS_IMAGE_NAME) make $@
.PHONY: plugins-osx
plugins-osx: build_cross_image ## build the example CLI plugins for macOS
docker run --rm $(ENVVARS) $(MOUNTS) $(CROSS_IMAGE_NAME) make $@
.PHONY: dev
dev: build_docker_image ## start a build container in interactive mode for in-container development
docker run -ti --rm $(ENVVARS) $(MOUNTS) \

View File

@ -38,5 +38,6 @@ ARG VERSION
ARG GITCOMMIT
ENV VERSION=${VERSION} GITCOMMIT=${GITCOMMIT}
RUN ./scripts/build/binary
RUN ./scripts/build/plugins
CMD ./scripts/test/e2e/entry

21
scripts/build/plugins Executable file
View File

@ -0,0 +1,21 @@
#!/usr/bin/env bash
#
# Build a static binary for the host OS/ARCH
#
set -eu -o pipefail
source ./scripts/build/.variables
mkdir -p "build/plugins-${GOOS}-${GOARCH}"
for p in cli-plugins/examples/* ; do
[ -d "$p" ] || continue
n=$(basename "$p")
TARGET="build/plugins-${GOOS}-${GOARCH}/docker-${n}"
echo "Building statically linked $TARGET"
export CGO_ENABLED=0
go build -o "${TARGET}" --ldflags "${LDFLAGS}" "github.com/docker/cli/${p}"
done

18
scripts/build/plugins-osx Executable file
View File

@ -0,0 +1,18 @@
#!/usr/bin/env bash
#
# Build a static binary for the host OS/ARCH
#
set -eu -o pipefail
source ./scripts/build/.variables
export CGO_ENABLED=1
export GOOS=darwin
export GOARCH=amd64
export CC=o64-clang
export CXX=o64-clang++
export LDFLAGS="$LDFLAGS -linkmode external -s"
export LDFLAGS_STATIC_DOCKER='-extld='${CC}
source ./scripts/build/plugins

14
scripts/build/plugins-windows Executable file
View File

@ -0,0 +1,14 @@
#!/usr/bin/env bash
#
# Build a static binary for the host OS/ARCH
#
set -eu -o pipefail
source ./scripts/build/.variables
export CC=x86_64-w64-mingw32-gcc
export CGO_ENABLED=1
export GOOS=windows
export GOARCH=amd64
source ./scripts/build/plugins