Skip to content

Commit

Permalink
Add basic framework for writing a CLI plugin
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
Ian Campbell committed Jan 29, 2019
1 parent 8cf946d commit e962404
Show file tree
Hide file tree
Showing 10 changed files with 248 additions and 0 deletions.
12 changes: 12 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
38 changes: 38 additions & 0 deletions cli-plugins/examples/helloworld/main.go
Original file line number Diff line number Diff line change
@@ -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",
})
}
25 changes: 25 additions & 0 deletions cli-plugins/manager/metadata.go
Original file line number Diff line number Diff line change
@@ -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
}
96 changes: 96 additions & 0 deletions cli-plugins/plugin/plugin.go
Original file line number Diff line number Diff line change
@@ -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
}
12 changes: 12 additions & 0 deletions cli/cobra.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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 {
Expand Down
11 changes: 11 additions & 0 deletions docker.Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) \
Expand Down
1 change: 1 addition & 0 deletions dockerfiles/Dockerfile.e2e
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 21 additions & 0 deletions scripts/build/plugins
Original file line number Diff line number Diff line change
@@ -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 changes: 18 additions & 0 deletions scripts/build/plugins-osx
Original file line number Diff line number Diff line change
@@ -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 changes: 14 additions & 0 deletions scripts/build/plugins-windows
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit e962404

Please sign in to comment.