mirror of https://github.com/docker/cli.git
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:
parent
8cf946d1bc
commit
e96240427f
12
Makefile
12
Makefile
|
@ -34,6 +34,10 @@ binary: ## build executable for Linux
|
||||||
@echo "WARNING: binary creates a Linux executable. Use cross for macOS or Windows."
|
@echo "WARNING: binary creates a Linux executable. Use cross for macOS or Windows."
|
||||||
./scripts/build/binary
|
./scripts/build/binary
|
||||||
|
|
||||||
|
.PHONY: plugins
|
||||||
|
plugins: ## build example CLI plugins
|
||||||
|
./scripts/build/plugins
|
||||||
|
|
||||||
.PHONY: cross
|
.PHONY: cross
|
||||||
cross: ## build executable for macOS and Windows
|
cross: ## build executable for macOS and Windows
|
||||||
./scripts/build/cross
|
./scripts/build/cross
|
||||||
|
@ -42,10 +46,18 @@ cross: ## build executable for macOS and Windows
|
||||||
binary-windows: ## build executable for Windows
|
binary-windows: ## build executable for Windows
|
||||||
./scripts/build/windows
|
./scripts/build/windows
|
||||||
|
|
||||||
|
.PHONY: plugins-windows
|
||||||
|
plugins-windows: ## build example CLI plugins for Windows
|
||||||
|
./scripts/build/plugins-windows
|
||||||
|
|
||||||
.PHONY: binary-osx
|
.PHONY: binary-osx
|
||||||
binary-osx: ## build executable for macOS
|
binary-osx: ## build executable for macOS
|
||||||
./scripts/build/osx
|
./scripts/build/osx
|
||||||
|
|
||||||
|
.PHONY: plugins-osx
|
||||||
|
plugins-osx: ## build example CLI plugins for macOS
|
||||||
|
./scripts/build/plugins-osx
|
||||||
|
|
||||||
.PHONY: dynbinary
|
.PHONY: dynbinary
|
||||||
dynbinary: ## build dynamically linked binary
|
dynbinary: ## build dynamically linked binary
|
||||||
./scripts/build/dynbinary
|
./scripts/build/dynbinary
|
||||||
|
|
|
@ -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",
|
||||||
|
})
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
cli/cobra.go
12
cli/cobra.go
|
@ -12,6 +12,8 @@ import (
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// setupCommonRootCommand contains the setup common to
|
||||||
|
// SetupRootCommand and SetupPluginRootCommand.
|
||||||
func setupCommonRootCommand(rootCmd *cobra.Command) (*cliflags.ClientOptions, *pflag.FlagSet) {
|
func setupCommonRootCommand(rootCmd *cobra.Command) (*cliflags.ClientOptions, *pflag.FlagSet) {
|
||||||
opts := cliflags.NewClientOptions()
|
opts := cliflags.NewClientOptions()
|
||||||
flags := rootCmd.Flags()
|
flags := rootCmd.Flags()
|
||||||
|
@ -47,6 +49,16 @@ func SetupRootCommand(rootCmd *cobra.Command) (*cliflags.ClientOptions, *pflag.F
|
||||||
return opts, flags
|
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
|
// FlagErrorFunc prints an error message which matches the format of the
|
||||||
// docker/cli/cli error messages
|
// docker/cli/cli error messages
|
||||||
func FlagErrorFunc(cmd *cobra.Command, err error) error {
|
func FlagErrorFunc(cmd *cobra.Command, err error) error {
|
||||||
|
|
|
@ -56,6 +56,9 @@ binary: build_binary_native_image ## build the CLI
|
||||||
|
|
||||||
build: binary ## alias for binary
|
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
|
.PHONY: clean
|
||||||
clean: build_docker_image ## clean build artifacts
|
clean: build_docker_image ## clean build artifacts
|
||||||
docker run --rm $(ENVVARS) $(MOUNTS) $(DEV_DOCKER_IMAGE_NAME) make clean
|
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
|
binary-windows: build_cross_image ## build the CLI for Windows
|
||||||
docker run --rm $(ENVVARS) $(MOUNTS) $(CROSS_IMAGE_NAME) make $@
|
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
|
.PHONY: binary-osx
|
||||||
binary-osx: build_cross_image ## build the CLI for macOS
|
binary-osx: build_cross_image ## build the CLI for macOS
|
||||||
docker run --rm $(ENVVARS) $(MOUNTS) $(CROSS_IMAGE_NAME) make $@
|
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
|
.PHONY: dev
|
||||||
dev: build_docker_image ## start a build container in interactive mode for in-container development
|
dev: build_docker_image ## start a build container in interactive mode for in-container development
|
||||||
docker run -ti --rm $(ENVVARS) $(MOUNTS) \
|
docker run -ti --rm $(ENVVARS) $(MOUNTS) \
|
||||||
|
|
|
@ -38,5 +38,6 @@ ARG VERSION
|
||||||
ARG GITCOMMIT
|
ARG GITCOMMIT
|
||||||
ENV VERSION=${VERSION} GITCOMMIT=${GITCOMMIT}
|
ENV VERSION=${VERSION} GITCOMMIT=${GITCOMMIT}
|
||||||
RUN ./scripts/build/binary
|
RUN ./scripts/build/binary
|
||||||
|
RUN ./scripts/build/plugins
|
||||||
|
|
||||||
CMD ./scripts/test/e2e/entry
|
CMD ./scripts/test/e2e/entry
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
Loading…
Reference in New Issue