mirror of https://github.com/docker/cli.git
Merge pull request #1564 from ijc/plugins
Basic framework for writing and running CLI plugins
This commit is contained in:
commit
2e5639da02
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,59 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"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!")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
apiversion := &cobra.Command{
|
||||||
|
Use: "apiversion",
|
||||||
|
Short: "Print the API version of the server",
|
||||||
|
RunE: func(_ *cobra.Command, _ []string) error {
|
||||||
|
cli := dockerCli.Client()
|
||||||
|
ping, err := cli.Ping(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Println(ping.APIVersion)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var who string
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "helloworld",
|
||||||
|
Short: "A basic Hello World plugin for tests",
|
||||||
|
// This is redundant but included to exercise
|
||||||
|
// the path where a plugin overrides this
|
||||||
|
// hook.
|
||||||
|
PersistentPreRunE: plugin.PersistentPreRunE,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
fmt.Fprintf(dockerCli.Out(), "Hello %s!\n", who)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
flags := cmd.Flags()
|
||||||
|
flags.StringVar(&who, "who", "World", "Who are we addressing?")
|
||||||
|
|
||||||
|
cmd.AddCommand(goodbye, apiversion)
|
||||||
|
return cmd
|
||||||
|
},
|
||||||
|
manager.Metadata{
|
||||||
|
SchemaVersion: "0.1.0",
|
||||||
|
Vendor: "Docker Inc.",
|
||||||
|
Version: "testing",
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
package manager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os/exec"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Candidate represents a possible plugin candidate, for mocking purposes
|
||||||
|
type Candidate interface {
|
||||||
|
Path() string
|
||||||
|
Metadata() ([]byte, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type candidate struct {
|
||||||
|
path string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *candidate) Path() string {
|
||||||
|
return c.path
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *candidate) Metadata() ([]byte, error) {
|
||||||
|
return exec.Command(c.path, MetadataSubcommandName).Output()
|
||||||
|
}
|
|
@ -0,0 +1,93 @@
|
||||||
|
package manager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"gotest.tools/assert"
|
||||||
|
"gotest.tools/assert/cmp"
|
||||||
|
)
|
||||||
|
|
||||||
|
type fakeCandidate struct {
|
||||||
|
path string
|
||||||
|
exec bool
|
||||||
|
meta string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *fakeCandidate) Path() string {
|
||||||
|
return c.path
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *fakeCandidate) Metadata() ([]byte, error) {
|
||||||
|
if !c.exec {
|
||||||
|
return nil, fmt.Errorf("faked a failure to exec %q", c.path)
|
||||||
|
}
|
||||||
|
return []byte(c.meta), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateCandidate(t *testing.T) {
|
||||||
|
var (
|
||||||
|
goodPluginName = NamePrefix + "goodplugin"
|
||||||
|
|
||||||
|
builtinName = NamePrefix + "builtin"
|
||||||
|
builtinAlias = NamePrefix + "alias"
|
||||||
|
|
||||||
|
badPrefixPath = "/usr/local/libexec/cli-plugins/wobble"
|
||||||
|
badNamePath = "/usr/local/libexec/cli-plugins/docker-123456"
|
||||||
|
goodPluginPath = "/usr/local/libexec/cli-plugins/" + goodPluginName
|
||||||
|
)
|
||||||
|
|
||||||
|
fakeroot := &cobra.Command{Use: "docker"}
|
||||||
|
fakeroot.AddCommand(&cobra.Command{
|
||||||
|
Use: strings.TrimPrefix(builtinName, NamePrefix),
|
||||||
|
Aliases: []string{
|
||||||
|
strings.TrimPrefix(builtinAlias, NamePrefix),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, tc := range []struct {
|
||||||
|
c *fakeCandidate
|
||||||
|
|
||||||
|
// Either err or invalid may be non-empty, but not both (both can be empty for a good plugin).
|
||||||
|
err string
|
||||||
|
invalid string
|
||||||
|
}{
|
||||||
|
/* Each failing one of the tests */
|
||||||
|
{c: &fakeCandidate{path: ""}, err: "plugin candidate path cannot be empty"},
|
||||||
|
{c: &fakeCandidate{path: badPrefixPath}, err: fmt.Sprintf("does not have %q prefix", NamePrefix)},
|
||||||
|
{c: &fakeCandidate{path: badNamePath}, invalid: "did not match"},
|
||||||
|
{c: &fakeCandidate{path: builtinName}, invalid: `plugin "builtin" duplicates builtin command`},
|
||||||
|
{c: &fakeCandidate{path: builtinAlias}, invalid: `plugin "alias" duplicates an alias of builtin command "builtin"`},
|
||||||
|
{c: &fakeCandidate{path: goodPluginPath, exec: false}, invalid: fmt.Sprintf("failed to fetch metadata: faked a failure to exec %q", goodPluginPath)},
|
||||||
|
{c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `xyzzy`}, invalid: "invalid character"},
|
||||||
|
{c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{}`}, invalid: `plugin SchemaVersion "" is not valid`},
|
||||||
|
{c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "xyzzy"}`}, invalid: `plugin SchemaVersion "xyzzy" is not valid`},
|
||||||
|
{c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0"}`}, invalid: "plugin metadata does not define a vendor"},
|
||||||
|
{c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0", "Vendor": ""}`}, invalid: "plugin metadata does not define a vendor"},
|
||||||
|
// This one should work
|
||||||
|
{c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0", "Vendor": "e2e-testing"}`}},
|
||||||
|
} {
|
||||||
|
p, err := newPlugin(tc.c, fakeroot)
|
||||||
|
if tc.err != "" {
|
||||||
|
assert.ErrorContains(t, err, tc.err)
|
||||||
|
} else if tc.invalid != "" {
|
||||||
|
assert.NilError(t, err)
|
||||||
|
assert.Assert(t, cmp.ErrorType(p.Err, reflect.TypeOf(&pluginError{})))
|
||||||
|
assert.ErrorContains(t, p.Err, tc.invalid)
|
||||||
|
} else {
|
||||||
|
assert.NilError(t, err)
|
||||||
|
assert.Equal(t, NamePrefix+p.Name, goodPluginName)
|
||||||
|
assert.Equal(t, p.SchemaVersion, "0.1.0")
|
||||||
|
assert.Equal(t, p.Vendor, "e2e-testing")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCandidatePath(t *testing.T) {
|
||||||
|
exp := "/some/path"
|
||||||
|
cand := &candidate{path: exp}
|
||||||
|
assert.Equal(t, exp, cand.Path())
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
package manager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/docker/cli/cli/command"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// CommandAnnotationPlugin is added to every stub command added by
|
||||||
|
// AddPluginCommandStubs with the value "true" and so can be
|
||||||
|
// used to distinguish plugin stubs from regular commands.
|
||||||
|
CommandAnnotationPlugin = "com.docker.cli.plugin"
|
||||||
|
|
||||||
|
// CommandAnnotationPluginVendor is added to every stub command
|
||||||
|
// added by AddPluginCommandStubs and contains the vendor of
|
||||||
|
// that plugin.
|
||||||
|
CommandAnnotationPluginVendor = "com.docker.cli.plugin.vendor"
|
||||||
|
|
||||||
|
// CommandAnnotationPluginInvalid is added to any stub command
|
||||||
|
// added by AddPluginCommandStubs for an invalid command (that
|
||||||
|
// is, one which failed it's candidate test) and contains the
|
||||||
|
// reason for the failure.
|
||||||
|
CommandAnnotationPluginInvalid = "com.docker.cli.plugin-invalid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AddPluginCommandStubs adds a stub cobra.Commands for each valid and invalid
|
||||||
|
// plugin. The command stubs will have several annotations added, see
|
||||||
|
// `CommandAnnotationPlugin*`.
|
||||||
|
func AddPluginCommandStubs(dockerCli command.Cli, cmd *cobra.Command) error {
|
||||||
|
plugins, err := ListPlugins(dockerCli, cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, p := range plugins {
|
||||||
|
vendor := p.Vendor
|
||||||
|
if vendor == "" {
|
||||||
|
vendor = "unknown"
|
||||||
|
}
|
||||||
|
annotations := map[string]string{
|
||||||
|
CommandAnnotationPlugin: "true",
|
||||||
|
CommandAnnotationPluginVendor: vendor,
|
||||||
|
}
|
||||||
|
if p.Err != nil {
|
||||||
|
annotations[CommandAnnotationPluginInvalid] = p.Err.Error()
|
||||||
|
}
|
||||||
|
cmd.AddCommand(&cobra.Command{
|
||||||
|
Use: p.Name,
|
||||||
|
Short: p.ShortDescription,
|
||||||
|
Run: func(_ *cobra.Command, _ []string) {},
|
||||||
|
Annotations: annotations,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
package manager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// pluginError is set as Plugin.Err by NewPlugin if the plugin
|
||||||
|
// candidate fails one of the candidate tests. This exists primarily
|
||||||
|
// to implement encoding.TextMarshaller such that rendering a plugin as JSON
|
||||||
|
// (e.g. for `docker info -f '{{json .CLIPlugins}}'`) renders the Err
|
||||||
|
// field as a useful string and not just `{}`. See
|
||||||
|
// https://github.com/golang/go/issues/10748 for some discussion
|
||||||
|
// around why the builtin error type doesn't implement this.
|
||||||
|
type pluginError struct {
|
||||||
|
cause error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error satisfies the core error interface for pluginError.
|
||||||
|
func (e *pluginError) Error() string {
|
||||||
|
return e.cause.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cause satisfies the errors.causer interface for pluginError.
|
||||||
|
func (e *pluginError) Cause() error {
|
||||||
|
return e.cause
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalText marshalls the pluginError into a textual form.
|
||||||
|
func (e *pluginError) MarshalText() (text []byte, err error) {
|
||||||
|
return []byte(e.cause.Error()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// wrapAsPluginError wraps an error in a pluginError with an
|
||||||
|
// additional message, analogous to errors.Wrapf.
|
||||||
|
func wrapAsPluginError(err error, msg string) error {
|
||||||
|
return &pluginError{cause: errors.Wrap(err, msg)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPluginError creates a new pluginError, analogous to
|
||||||
|
// errors.Errorf.
|
||||||
|
func NewPluginError(msg string, args ...interface{}) error {
|
||||||
|
return &pluginError{cause: errors.Errorf(msg, args...)}
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
package manager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
"gotest.tools/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPluginError(t *testing.T) {
|
||||||
|
err := NewPluginError("new error")
|
||||||
|
assert.Error(t, err, "new error")
|
||||||
|
|
||||||
|
inner := fmt.Errorf("testing")
|
||||||
|
err = wrapAsPluginError(inner, "wrapping")
|
||||||
|
assert.Error(t, err, "wrapping: testing")
|
||||||
|
assert.Equal(t, inner, errors.Cause(err))
|
||||||
|
|
||||||
|
actual, err := yaml.Marshal(err)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
assert.Equal(t, "'wrapping: testing'\n", string(actual))
|
||||||
|
}
|
|
@ -0,0 +1,161 @@
|
||||||
|
package manager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/docker/cli/cli/command"
|
||||||
|
"github.com/docker/cli/cli/config"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// errPluginNotFound is the error returned when a plugin could not be found.
|
||||||
|
type errPluginNotFound string
|
||||||
|
|
||||||
|
func (e errPluginNotFound) NotFound() {}
|
||||||
|
|
||||||
|
func (e errPluginNotFound) Error() string {
|
||||||
|
return "Error: No such CLI plugin: " + string(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
type notFound interface{ NotFound() }
|
||||||
|
|
||||||
|
// IsNotFound is true if the given error is due to a plugin not being found.
|
||||||
|
func IsNotFound(err error) bool {
|
||||||
|
_, ok := err.(notFound)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPluginDirs(dockerCli command.Cli) []string {
|
||||||
|
var pluginDirs []string
|
||||||
|
|
||||||
|
if cfg := dockerCli.ConfigFile(); cfg != nil {
|
||||||
|
pluginDirs = append(pluginDirs, cfg.CLIPluginsExtraDirs...)
|
||||||
|
}
|
||||||
|
pluginDirs = append(pluginDirs, config.Path("cli-plugins"))
|
||||||
|
pluginDirs = append(pluginDirs, defaultSystemPluginDirs...)
|
||||||
|
return pluginDirs
|
||||||
|
}
|
||||||
|
|
||||||
|
func addPluginCandidatesFromDir(res map[string][]string, d string) error {
|
||||||
|
dentries, err := ioutil.ReadDir(d)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, dentry := range dentries {
|
||||||
|
switch dentry.Mode() & os.ModeType {
|
||||||
|
case 0, os.ModeSymlink:
|
||||||
|
// Regular file or symlink, keep going
|
||||||
|
default:
|
||||||
|
// Something else, ignore.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name := dentry.Name()
|
||||||
|
if !strings.HasPrefix(name, NamePrefix) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name = strings.TrimPrefix(name, NamePrefix)
|
||||||
|
var err error
|
||||||
|
if name, err = trimExeSuffix(name); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
res[name] = append(res[name], filepath.Join(d, dentry.Name()))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// listPluginCandidates returns a map from plugin name to the list of (unvalidated) Candidates. The list is in descending order of priority.
|
||||||
|
func listPluginCandidates(dirs []string) (map[string][]string, error) {
|
||||||
|
result := make(map[string][]string)
|
||||||
|
for _, d := range dirs {
|
||||||
|
// Silently ignore any directories which we cannot
|
||||||
|
// Stat (e.g. due to permissions or anything else) or
|
||||||
|
// which is not a directory.
|
||||||
|
if fi, err := os.Stat(d); err != nil || !fi.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := addPluginCandidatesFromDir(result, d); err != nil {
|
||||||
|
// Silently ignore paths which don't exist.
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return nil, err // Or return partial result?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListPlugins produces a list of the plugins available on the system
|
||||||
|
func ListPlugins(dockerCli command.Cli, rootcmd *cobra.Command) ([]Plugin, error) {
|
||||||
|
candidates, err := listPluginCandidates(getPluginDirs(dockerCli))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var plugins []Plugin
|
||||||
|
for _, paths := range candidates {
|
||||||
|
if len(paths) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
c := &candidate{paths[0]}
|
||||||
|
p, err := newPlugin(c, rootcmd)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
p.ShadowedPaths = paths[1:]
|
||||||
|
plugins = append(plugins, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
return plugins, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PluginRunCommand returns an "os/exec".Cmd which when .Run() will execute the named plugin.
|
||||||
|
// The rootcmd argument is referenced to determine the set of builtin commands in order to detect conficts.
|
||||||
|
// The error returned satisfies the IsNotFound() predicate if no plugin was found or if the first candidate plugin was invalid somehow.
|
||||||
|
func PluginRunCommand(dockerCli command.Cli, name string, rootcmd *cobra.Command) (*exec.Cmd, error) {
|
||||||
|
// This uses the full original args, not the args which may
|
||||||
|
// have been provided by cobra to our caller. This is because
|
||||||
|
// they lack e.g. global options which we must propagate here.
|
||||||
|
args := os.Args[1:]
|
||||||
|
if !pluginNameRe.MatchString(name) {
|
||||||
|
// We treat this as "not found" so that callers will
|
||||||
|
// fallback to their "invalid" command path.
|
||||||
|
return nil, errPluginNotFound(name)
|
||||||
|
}
|
||||||
|
exename := addExeSuffix(NamePrefix + name)
|
||||||
|
for _, d := range getPluginDirs(dockerCli) {
|
||||||
|
path := filepath.Join(d, exename)
|
||||||
|
|
||||||
|
// We stat here rather than letting the exec tell us
|
||||||
|
// ENOENT because the latter does not distinguish a
|
||||||
|
// file not existing from its dynamic loader or one of
|
||||||
|
// its libraries not existing.
|
||||||
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
c := &candidate{path: path}
|
||||||
|
plugin, err := newPlugin(c, rootcmd)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if plugin.Err != nil {
|
||||||
|
return nil, errPluginNotFound(name)
|
||||||
|
}
|
||||||
|
cmd := exec.Command(plugin.Path, args...)
|
||||||
|
// Using dockerCli.{In,Out,Err}() here results in a hang until something is input.
|
||||||
|
// See: - https://github.com/golang/go/issues/10338
|
||||||
|
// - https://github.com/golang/go/commit/d000e8742a173aa0659584aa01b7ba2834ba28ab
|
||||||
|
// os.Stdin is a *os.File which avoids this behaviour. We don't need the functionality
|
||||||
|
// of the wrappers here anyway.
|
||||||
|
cmd.Stdin = os.Stdin
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
|
||||||
|
return cmd, nil
|
||||||
|
}
|
||||||
|
return nil, errPluginNotFound(name)
|
||||||
|
}
|
|
@ -0,0 +1,108 @@
|
||||||
|
package manager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/cli/cli/config"
|
||||||
|
"github.com/docker/cli/cli/config/configfile"
|
||||||
|
"github.com/docker/cli/internal/test"
|
||||||
|
"gotest.tools/assert"
|
||||||
|
"gotest.tools/fs"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestListPluginCandidates(t *testing.T) {
|
||||||
|
// Populate a selection of directories with various shadowed and bogus/obscure plugin candidates.
|
||||||
|
// For the purposes of this test no contents is required and permissions are irrelevant.
|
||||||
|
dir := fs.NewDir(t, t.Name(),
|
||||||
|
fs.WithDir(
|
||||||
|
"plugins1",
|
||||||
|
fs.WithFile("docker-plugin1", ""), // This appears in each directory
|
||||||
|
fs.WithFile("not-a-plugin", ""), // Should be ignored
|
||||||
|
fs.WithFile("docker-symlinked1", ""), // This and ...
|
||||||
|
fs.WithSymlink("docker-symlinked2", "docker-symlinked1"), // ... this should both appear
|
||||||
|
fs.WithDir("ignored1"), // A directory should be ignored
|
||||||
|
),
|
||||||
|
fs.WithDir(
|
||||||
|
"plugins2",
|
||||||
|
fs.WithFile("docker-plugin1", ""),
|
||||||
|
fs.WithFile("also-not-a-plugin", ""),
|
||||||
|
fs.WithFile("docker-hardlink1", ""), // This and ...
|
||||||
|
fs.WithHardlink("docker-hardlink2", "docker-hardlink1"), // ... this should both appear
|
||||||
|
fs.WithDir("ignored2"),
|
||||||
|
),
|
||||||
|
fs.WithDir(
|
||||||
|
"plugins3-target", // Will be referenced as a symlink from below
|
||||||
|
fs.WithFile("docker-plugin1", ""),
|
||||||
|
fs.WithDir("ignored3"),
|
||||||
|
fs.WithSymlink("docker-brokensymlink", "broken"), // A broken symlink is still a candidate (but would fail tests later)
|
||||||
|
fs.WithFile("non-plugin-symlinked", ""), // This shouldn't appear, but ...
|
||||||
|
fs.WithSymlink("docker-symlinked", "non-plugin-symlinked"), // ... this link to it should.
|
||||||
|
),
|
||||||
|
fs.WithSymlink("plugins3", "plugins3-target"),
|
||||||
|
fs.WithFile("/plugins4", ""),
|
||||||
|
fs.WithSymlink("plugins5", "plugins5-nonexistent-target"),
|
||||||
|
)
|
||||||
|
defer dir.Remove()
|
||||||
|
|
||||||
|
var dirs []string
|
||||||
|
for _, d := range []string{"plugins1", "nonexistent", "plugins2", "plugins3", "plugins4", "plugins5"} {
|
||||||
|
dirs = append(dirs, dir.Join(d))
|
||||||
|
}
|
||||||
|
|
||||||
|
candidates, err := listPluginCandidates(dirs)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
exp := map[string][]string{
|
||||||
|
"plugin1": {
|
||||||
|
dir.Join("plugins1", "docker-plugin1"),
|
||||||
|
dir.Join("plugins2", "docker-plugin1"),
|
||||||
|
dir.Join("plugins3", "docker-plugin1"),
|
||||||
|
},
|
||||||
|
"symlinked1": {
|
||||||
|
dir.Join("plugins1", "docker-symlinked1"),
|
||||||
|
},
|
||||||
|
"symlinked2": {
|
||||||
|
dir.Join("plugins1", "docker-symlinked2"),
|
||||||
|
},
|
||||||
|
"hardlink1": {
|
||||||
|
dir.Join("plugins2", "docker-hardlink1"),
|
||||||
|
},
|
||||||
|
"hardlink2": {
|
||||||
|
dir.Join("plugins2", "docker-hardlink2"),
|
||||||
|
},
|
||||||
|
"brokensymlink": {
|
||||||
|
dir.Join("plugins3", "docker-brokensymlink"),
|
||||||
|
},
|
||||||
|
"symlinked": {
|
||||||
|
dir.Join("plugins3", "docker-symlinked"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.DeepEqual(t, candidates, exp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestErrPluginNotFound(t *testing.T) {
|
||||||
|
var err error = errPluginNotFound("test")
|
||||||
|
err.(errPluginNotFound).NotFound()
|
||||||
|
assert.Error(t, err, "Error: No such CLI plugin: test")
|
||||||
|
assert.Assert(t, IsNotFound(err))
|
||||||
|
assert.Assert(t, !IsNotFound(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetPluginDirs(t *testing.T) {
|
||||||
|
cli := test.NewFakeCli(nil)
|
||||||
|
|
||||||
|
expected := []string{config.Path("cli-plugins")}
|
||||||
|
expected = append(expected, defaultSystemPluginDirs...)
|
||||||
|
|
||||||
|
assert.Equal(t, strings.Join(expected, ":"), strings.Join(getPluginDirs(cli), ":"))
|
||||||
|
|
||||||
|
extras := []string{
|
||||||
|
"foo", "bar", "baz",
|
||||||
|
}
|
||||||
|
expected = append(extras, expected...)
|
||||||
|
cli.SetConfigFile(&configfile.ConfigFile{
|
||||||
|
CLIPluginsExtraDirs: extras,
|
||||||
|
})
|
||||||
|
assert.DeepEqual(t, expected, getPluginDirs(cli))
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
// +build !windows
|
||||||
|
|
||||||
|
package manager
|
||||||
|
|
||||||
|
var defaultSystemPluginDirs = []string{
|
||||||
|
"/usr/local/lib/docker/cli-plugins", "/usr/local/libexec/docker/cli-plugins",
|
||||||
|
"/usr/lib/docker/cli-plugins", "/usr/libexec/docker/cli-plugins",
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package manager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
var defaultSystemPluginDirs = []string{
|
||||||
|
filepath.Join(os.Getenv("ProgramData"), "Docker", "cli-plugins"),
|
||||||
|
}
|
|
@ -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. See docs/extend/cli_plugins.md for canonical information.
|
||||||
|
type Metadata struct {
|
||||||
|
// SchemaVersion describes the version of this struct. Mandatory, must be "0.1.0"
|
||||||
|
SchemaVersion string `json:",omitempty"`
|
||||||
|
// Vendor is the name of the plugin vendor. Mandatory
|
||||||
|
Vendor string `json:",omitempty"`
|
||||||
|
// Version is the optional version of this plugin.
|
||||||
|
Version string `json:",omitempty"`
|
||||||
|
// ShortDescription should be suitable for a single line help message.
|
||||||
|
ShortDescription string `json:",omitempty"`
|
||||||
|
// URL is a pointer to the plugin's homepage.
|
||||||
|
URL string `json:",omitempty"`
|
||||||
|
}
|
|
@ -0,0 +1,107 @@
|
||||||
|
package manager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
pluginNameRe = regexp.MustCompile("^[a-z][a-z0-9]*$")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Plugin represents a potential plugin with all it's metadata.
|
||||||
|
type Plugin struct {
|
||||||
|
Metadata
|
||||||
|
|
||||||
|
Name string `json:",omitempty"`
|
||||||
|
Path string `json:",omitempty"`
|
||||||
|
|
||||||
|
// Err is non-nil if the plugin failed one of the candidate tests.
|
||||||
|
Err error `json:",omitempty"`
|
||||||
|
|
||||||
|
// ShadowedPaths contains the paths of any other plugins which this plugin takes precedence over.
|
||||||
|
ShadowedPaths []string `json:",omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// newPlugin determines if the given candidate is valid and returns a
|
||||||
|
// Plugin. If the candidate fails one of the tests then `Plugin.Err`
|
||||||
|
// is set, and is always a `pluginError`, but the `Plugin` is still
|
||||||
|
// returned with no error. An error is only returned due to a
|
||||||
|
// non-recoverable error.
|
||||||
|
func newPlugin(c Candidate, rootcmd *cobra.Command) (Plugin, error) {
|
||||||
|
path := c.Path()
|
||||||
|
if path == "" {
|
||||||
|
return Plugin{}, errors.New("plugin candidate path cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// The candidate listing process should have skipped anything
|
||||||
|
// which would fail here, so there are all real errors.
|
||||||
|
fullname := filepath.Base(path)
|
||||||
|
if fullname == "." {
|
||||||
|
return Plugin{}, errors.Errorf("unable to determine basename of plugin candidate %q", path)
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
if fullname, err = trimExeSuffix(fullname); err != nil {
|
||||||
|
return Plugin{}, errors.Wrapf(err, "plugin candidate %q", path)
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(fullname, NamePrefix) {
|
||||||
|
return Plugin{}, errors.Errorf("plugin candidate %q: does not have %q prefix", path, NamePrefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
p := Plugin{
|
||||||
|
Name: strings.TrimPrefix(fullname, NamePrefix),
|
||||||
|
Path: path,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now apply the candidate tests, so these update p.Err.
|
||||||
|
if !pluginNameRe.MatchString(p.Name) {
|
||||||
|
p.Err = NewPluginError("plugin candidate %q did not match %q", p.Name, pluginNameRe.String())
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if rootcmd != nil {
|
||||||
|
for _, cmd := range rootcmd.Commands() {
|
||||||
|
// Ignore conflicts with commands which are
|
||||||
|
// just plugin stubs (i.e. from a previous
|
||||||
|
// call to AddPluginCommandStubs).
|
||||||
|
if p := cmd.Annotations[CommandAnnotationPlugin]; p == "true" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if cmd.Name() == p.Name {
|
||||||
|
p.Err = NewPluginError("plugin %q duplicates builtin command", p.Name)
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
if cmd.HasAlias(p.Name) {
|
||||||
|
p.Err = NewPluginError("plugin %q duplicates an alias of builtin command %q", p.Name, cmd.Name())
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We are supposed to check for relevant execute permissions here. Instead we rely on an attempt to execute.
|
||||||
|
meta, err := c.Metadata()
|
||||||
|
if err != nil {
|
||||||
|
p.Err = wrapAsPluginError(err, "failed to fetch metadata")
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(meta, &p.Metadata); err != nil {
|
||||||
|
p.Err = wrapAsPluginError(err, "invalid metadata")
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.Metadata.SchemaVersion != "0.1.0" {
|
||||||
|
p.Err = NewPluginError("plugin SchemaVersion %q is not valid, must be 0.1.0", p.Metadata.SchemaVersion)
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
if p.Metadata.Vendor == "" {
|
||||||
|
p.Err = NewPluginError("plugin metadata does not define a vendor")
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
return p, nil
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
// +build !windows
|
||||||
|
|
||||||
|
package manager
|
||||||
|
|
||||||
|
func trimExeSuffix(s string) (string, error) {
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
func addExeSuffix(s string) string {
|
||||||
|
return s
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
package manager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This is made slightly more complex due to needing to be case insensitive.
|
||||||
|
func trimExeSuffix(s string) (string, error) {
|
||||||
|
ext := filepath.Ext(s)
|
||||||
|
if ext == "" {
|
||||||
|
return "", errors.Errorf("path %q lacks required file extension", s)
|
||||||
|
}
|
||||||
|
|
||||||
|
exe := ".exe"
|
||||||
|
if !strings.EqualFold(ext, exe) {
|
||||||
|
return "", errors.Errorf("path %q lacks required %q suffix", s, exe)
|
||||||
|
}
|
||||||
|
return strings.TrimSuffix(s, ext), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func addExeSuffix(s string) string {
|
||||||
|
return s + ".exe"
|
||||||
|
}
|
|
@ -0,0 +1,126 @@
|
||||||
|
package plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// options encapsulates the ClientOptions and FlagSet constructed by
|
||||||
|
// `newPluginCommand` such that they can be finalized by our
|
||||||
|
// `PersistentPreRunE`. This is necessary because otherwise a plugin's
|
||||||
|
// own use of that hook will shadow anything we add to the top-level
|
||||||
|
// command meaning the CLI is never Initialized.
|
||||||
|
var options struct {
|
||||||
|
init, prerun sync.Once
|
||||||
|
opts *cliflags.ClientOptions
|
||||||
|
flags *pflag.FlagSet
|
||||||
|
dockerCli *command.DockerCli
|
||||||
|
}
|
||||||
|
|
||||||
|
// PersistentPreRunE must be called by any plugin command (or
|
||||||
|
// subcommand) which uses the cobra `PersistentPreRun*` hook. Plugins
|
||||||
|
// which do not make use of `PersistentPreRun*` do not need to call
|
||||||
|
// this (although it remains safe to do so). Plugins are recommended
|
||||||
|
// to use `PersistenPreRunE` to enable the error to be
|
||||||
|
// returned. Should not be called outside of a commands
|
||||||
|
// PersistentPreRunE hook and must not be run unless Run has been
|
||||||
|
// called.
|
||||||
|
func PersistentPreRunE(cmd *cobra.Command, args []string) error {
|
||||||
|
var err error
|
||||||
|
options.prerun.Do(func() {
|
||||||
|
if options.opts == nil || options.flags == nil || options.dockerCli == nil {
|
||||||
|
panic("PersistentPreRunE called without Run successfully called first")
|
||||||
|
}
|
||||||
|
// flags must be the original top-level command flags, not cmd.Flags()
|
||||||
|
options.opts.Common.SetDefaultOptions(options.flags)
|
||||||
|
err = options.dockerCli.Initialize(options.opts)
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPluginCommand(dockerCli *command.DockerCli, plugin *cobra.Command, meta manager.Metadata) *cobra.Command {
|
||||||
|
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: PersistentPreRunE,
|
||||||
|
DisableFlagsInUseLine: true,
|
||||||
|
}
|
||||||
|
opts, flags := cli.SetupPluginRootCommand(cmd)
|
||||||
|
|
||||||
|
cmd.SetOutput(dockerCli.Out())
|
||||||
|
|
||||||
|
cmd.AddCommand(
|
||||||
|
plugin,
|
||||||
|
newMetadataSubcommand(plugin, meta),
|
||||||
|
)
|
||||||
|
|
||||||
|
cli.DisableFlagsInUseLine(cmd)
|
||||||
|
|
||||||
|
options.init.Do(func() {
|
||||||
|
options.opts = opts
|
||||||
|
options.flags = flags
|
||||||
|
options.dockerCli = dockerCli
|
||||||
|
})
|
||||||
|
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
|
||||||
|
}
|
121
cli/cobra.go
121
cli/cobra.go
|
@ -4,29 +4,65 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
pluginmanager "github.com/docker/cli/cli-plugins/manager"
|
||||||
|
cliconfig "github.com/docker/cli/cli/config"
|
||||||
|
cliflags "github.com/docker/cli/cli/flags"
|
||||||
"github.com/docker/docker/pkg/term"
|
"github.com/docker/docker/pkg/term"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SetupRootCommand sets default usage, help, and error handling for the
|
// setupCommonRootCommand contains the setup common to
|
||||||
// root command.
|
// SetupRootCommand and SetupPluginRootCommand.
|
||||||
func SetupRootCommand(rootCmd *cobra.Command) {
|
func setupCommonRootCommand(rootCmd *cobra.Command) (*cliflags.ClientOptions, *pflag.FlagSet, *cobra.Command) {
|
||||||
|
opts := cliflags.NewClientOptions()
|
||||||
|
flags := rootCmd.Flags()
|
||||||
|
|
||||||
|
flags.StringVar(&opts.ConfigDir, "config", cliconfig.Dir(), "Location of client config files")
|
||||||
|
opts.Common.InstallFlags(flags)
|
||||||
|
|
||||||
cobra.AddTemplateFunc("hasSubCommands", hasSubCommands)
|
cobra.AddTemplateFunc("hasSubCommands", hasSubCommands)
|
||||||
cobra.AddTemplateFunc("hasManagementSubCommands", hasManagementSubCommands)
|
cobra.AddTemplateFunc("hasManagementSubCommands", hasManagementSubCommands)
|
||||||
|
cobra.AddTemplateFunc("hasInvalidPlugins", hasInvalidPlugins)
|
||||||
cobra.AddTemplateFunc("operationSubCommands", operationSubCommands)
|
cobra.AddTemplateFunc("operationSubCommands", operationSubCommands)
|
||||||
cobra.AddTemplateFunc("managementSubCommands", managementSubCommands)
|
cobra.AddTemplateFunc("managementSubCommands", managementSubCommands)
|
||||||
|
cobra.AddTemplateFunc("invalidPlugins", invalidPlugins)
|
||||||
cobra.AddTemplateFunc("wrappedFlagUsages", wrappedFlagUsages)
|
cobra.AddTemplateFunc("wrappedFlagUsages", wrappedFlagUsages)
|
||||||
|
cobra.AddTemplateFunc("commandVendor", commandVendor)
|
||||||
|
cobra.AddTemplateFunc("isFirstLevelCommand", isFirstLevelCommand) // is it an immediate sub-command of the root
|
||||||
|
cobra.AddTemplateFunc("invalidPluginReason", invalidPluginReason)
|
||||||
|
|
||||||
rootCmd.SetUsageTemplate(usageTemplate)
|
rootCmd.SetUsageTemplate(usageTemplate)
|
||||||
rootCmd.SetHelpTemplate(helpTemplate)
|
rootCmd.SetHelpTemplate(helpTemplate)
|
||||||
rootCmd.SetFlagErrorFunc(FlagErrorFunc)
|
rootCmd.SetFlagErrorFunc(FlagErrorFunc)
|
||||||
rootCmd.SetHelpCommand(helpCommand)
|
rootCmd.SetHelpCommand(helpCommand)
|
||||||
|
|
||||||
|
return opts, flags, helpCommand
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupRootCommand sets default usage, help, and error handling for the
|
||||||
|
// root command.
|
||||||
|
func SetupRootCommand(rootCmd *cobra.Command) (*cliflags.ClientOptions, *pflag.FlagSet, *cobra.Command) {
|
||||||
|
opts, flags, helpCmd := setupCommonRootCommand(rootCmd)
|
||||||
|
|
||||||
rootCmd.SetVersionTemplate("Docker version {{.Version}}\n")
|
rootCmd.SetVersionTemplate("Docker version {{.Version}}\n")
|
||||||
|
|
||||||
rootCmd.PersistentFlags().BoolP("help", "h", false, "Print usage")
|
rootCmd.PersistentFlags().BoolP("help", "h", false, "Print usage")
|
||||||
rootCmd.PersistentFlags().MarkShorthandDeprecated("help", "please use --help")
|
rootCmd.PersistentFlags().MarkShorthandDeprecated("help", "please use --help")
|
||||||
rootCmd.PersistentFlags().Lookup("help").Hidden = true
|
rootCmd.PersistentFlags().Lookup("help").Hidden = true
|
||||||
|
|
||||||
|
return opts, flags, helpCmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
@ -46,6 +82,25 @@ func FlagErrorFunc(cmd *cobra.Command, err error) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// VisitAll will traverse all commands from the root.
|
||||||
|
// This is different from the VisitAll of cobra.Command where only parents
|
||||||
|
// are checked.
|
||||||
|
func VisitAll(root *cobra.Command, fn func(*cobra.Command)) {
|
||||||
|
for _, cmd := range root.Commands() {
|
||||||
|
VisitAll(cmd, fn)
|
||||||
|
}
|
||||||
|
fn(root)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DisableFlagsInUseLine sets the DisableFlagsInUseLine flag on all
|
||||||
|
// commands within the tree rooted at cmd.
|
||||||
|
func DisableFlagsInUseLine(cmd *cobra.Command) {
|
||||||
|
VisitAll(cmd, func(ccmd *cobra.Command) {
|
||||||
|
// do not add a `[flags]` to the end of the usage line.
|
||||||
|
ccmd.DisableFlagsInUseLine = true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
var helpCommand = &cobra.Command{
|
var helpCommand = &cobra.Command{
|
||||||
Use: "help [command]",
|
Use: "help [command]",
|
||||||
Short: "Help about the command",
|
Short: "Help about the command",
|
||||||
|
@ -63,6 +118,10 @@ var helpCommand = &cobra.Command{
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isPlugin(cmd *cobra.Command) bool {
|
||||||
|
return cmd.Annotations[pluginmanager.CommandAnnotationPlugin] == "true"
|
||||||
|
}
|
||||||
|
|
||||||
func hasSubCommands(cmd *cobra.Command) bool {
|
func hasSubCommands(cmd *cobra.Command) bool {
|
||||||
return len(operationSubCommands(cmd)) > 0
|
return len(operationSubCommands(cmd)) > 0
|
||||||
}
|
}
|
||||||
|
@ -71,9 +130,16 @@ func hasManagementSubCommands(cmd *cobra.Command) bool {
|
||||||
return len(managementSubCommands(cmd)) > 0
|
return len(managementSubCommands(cmd)) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func hasInvalidPlugins(cmd *cobra.Command) bool {
|
||||||
|
return len(invalidPlugins(cmd)) > 0
|
||||||
|
}
|
||||||
|
|
||||||
func operationSubCommands(cmd *cobra.Command) []*cobra.Command {
|
func operationSubCommands(cmd *cobra.Command) []*cobra.Command {
|
||||||
cmds := []*cobra.Command{}
|
cmds := []*cobra.Command{}
|
||||||
for _, sub := range cmd.Commands() {
|
for _, sub := range cmd.Commands() {
|
||||||
|
if isPlugin(sub) && invalidPluginReason(sub) != "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if sub.IsAvailableCommand() && !sub.HasSubCommands() {
|
if sub.IsAvailableCommand() && !sub.HasSubCommands() {
|
||||||
cmds = append(cmds, sub)
|
cmds = append(cmds, sub)
|
||||||
}
|
}
|
||||||
|
@ -89,9 +155,27 @@ func wrappedFlagUsages(cmd *cobra.Command) string {
|
||||||
return cmd.Flags().FlagUsagesWrapped(width - 1)
|
return cmd.Flags().FlagUsagesWrapped(width - 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isFirstLevelCommand(cmd *cobra.Command) bool {
|
||||||
|
return cmd.Parent() == cmd.Root()
|
||||||
|
}
|
||||||
|
|
||||||
|
func commandVendor(cmd *cobra.Command) string {
|
||||||
|
width := 13
|
||||||
|
if v, ok := cmd.Annotations[pluginmanager.CommandAnnotationPluginVendor]; ok {
|
||||||
|
if len(v) > width-2 {
|
||||||
|
v = v[:width-3] + "…"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%-*s", width, "("+v+")")
|
||||||
|
}
|
||||||
|
return strings.Repeat(" ", width)
|
||||||
|
}
|
||||||
|
|
||||||
func managementSubCommands(cmd *cobra.Command) []*cobra.Command {
|
func managementSubCommands(cmd *cobra.Command) []*cobra.Command {
|
||||||
cmds := []*cobra.Command{}
|
cmds := []*cobra.Command{}
|
||||||
for _, sub := range cmd.Commands() {
|
for _, sub := range cmd.Commands() {
|
||||||
|
if isPlugin(sub) && invalidPluginReason(sub) != "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if sub.IsAvailableCommand() && sub.HasSubCommands() {
|
if sub.IsAvailableCommand() && sub.HasSubCommands() {
|
||||||
cmds = append(cmds, sub)
|
cmds = append(cmds, sub)
|
||||||
}
|
}
|
||||||
|
@ -99,6 +183,23 @@ func managementSubCommands(cmd *cobra.Command) []*cobra.Command {
|
||||||
return cmds
|
return cmds
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func invalidPlugins(cmd *cobra.Command) []*cobra.Command {
|
||||||
|
cmds := []*cobra.Command{}
|
||||||
|
for _, sub := range cmd.Commands() {
|
||||||
|
if !isPlugin(sub) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if invalidPluginReason(sub) != "" {
|
||||||
|
cmds = append(cmds, sub)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cmds
|
||||||
|
}
|
||||||
|
|
||||||
|
func invalidPluginReason(cmd *cobra.Command) string {
|
||||||
|
return cmd.Annotations[pluginmanager.CommandAnnotationPluginInvalid]
|
||||||
|
}
|
||||||
|
|
||||||
var usageTemplate = `Usage:
|
var usageTemplate = `Usage:
|
||||||
|
|
||||||
{{- if not .HasSubCommands}} {{.UseLine}}{{end}}
|
{{- if not .HasSubCommands}} {{.UseLine}}{{end}}
|
||||||
|
@ -129,7 +230,7 @@ Options:
|
||||||
Management Commands:
|
Management Commands:
|
||||||
|
|
||||||
{{- range managementSubCommands . }}
|
{{- range managementSubCommands . }}
|
||||||
{{rpad .Name .NamePadding }} {{.Short}}
|
{{rpad .Name .NamePadding }} {{ if isFirstLevelCommand .}}{{commandVendor .}} {{ end}}{{.Short}}
|
||||||
{{- end}}
|
{{- end}}
|
||||||
|
|
||||||
{{- end}}
|
{{- end}}
|
||||||
|
@ -138,10 +239,20 @@ Management Commands:
|
||||||
Commands:
|
Commands:
|
||||||
|
|
||||||
{{- range operationSubCommands . }}
|
{{- range operationSubCommands . }}
|
||||||
{{rpad .Name .NamePadding }} {{.Short}}
|
{{rpad .Name .NamePadding }} {{ if isFirstLevelCommand .}}{{commandVendor .}} {{ end}}{{.Short}}
|
||||||
{{- end}}
|
{{- end}}
|
||||||
{{- end}}
|
{{- end}}
|
||||||
|
|
||||||
|
{{- if hasInvalidPlugins . }}
|
||||||
|
|
||||||
|
Invalid Plugins:
|
||||||
|
|
||||||
|
{{- range invalidPlugins . }}
|
||||||
|
{{rpad .Name .NamePadding }} {{invalidPluginReason .}}
|
||||||
|
{{- end}}
|
||||||
|
|
||||||
|
{{- end}}
|
||||||
|
|
||||||
{{- if .HasSubCommands }}
|
{{- if .HasSubCommands }}
|
||||||
|
|
||||||
Run '{{.CommandPath}} COMMAND --help' for more information on a command.
|
Run '{{.CommandPath}} COMMAND --help' for more information on a command.
|
||||||
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
pluginmanager "github.com/docker/cli/cli-plugins/manager"
|
||||||
|
"github.com/google/go-cmp/cmp/cmpopts"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"gotest.tools/assert"
|
||||||
|
is "gotest.tools/assert/cmp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestVisitAll(t *testing.T) {
|
||||||
|
root := &cobra.Command{Use: "root"}
|
||||||
|
sub1 := &cobra.Command{Use: "sub1"}
|
||||||
|
sub1sub1 := &cobra.Command{Use: "sub1sub1"}
|
||||||
|
sub1sub2 := &cobra.Command{Use: "sub1sub2"}
|
||||||
|
sub2 := &cobra.Command{Use: "sub2"}
|
||||||
|
|
||||||
|
root.AddCommand(sub1, sub2)
|
||||||
|
sub1.AddCommand(sub1sub1, sub1sub2)
|
||||||
|
|
||||||
|
// Take the opportunity to test DisableFlagsInUseLine too
|
||||||
|
DisableFlagsInUseLine(root)
|
||||||
|
|
||||||
|
var visited []string
|
||||||
|
VisitAll(root, func(ccmd *cobra.Command) {
|
||||||
|
visited = append(visited, ccmd.Name())
|
||||||
|
assert.Assert(t, ccmd.DisableFlagsInUseLine, "DisableFlagsInUseLine not set on %q", ccmd.Name())
|
||||||
|
})
|
||||||
|
expected := []string{"sub1sub1", "sub1sub2", "sub1", "sub2", "root"}
|
||||||
|
assert.DeepEqual(t, expected, visited)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCommandVendor(t *testing.T) {
|
||||||
|
// Non plugin.
|
||||||
|
assert.Equal(t, commandVendor(&cobra.Command{Use: "test"}), " ")
|
||||||
|
|
||||||
|
// Plugins with various lengths of vendor.
|
||||||
|
for _, tc := range []struct {
|
||||||
|
vendor string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{vendor: "vendor", expected: "(vendor) "},
|
||||||
|
{vendor: "vendor12345", expected: "(vendor12345)"},
|
||||||
|
{vendor: "vendor123456", expected: "(vendor1234…)"},
|
||||||
|
{vendor: "vendor1234567", expected: "(vendor1234…)"},
|
||||||
|
} {
|
||||||
|
t.Run(tc.vendor, func(t *testing.T) {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "test",
|
||||||
|
Annotations: map[string]string{
|
||||||
|
pluginmanager.CommandAnnotationPluginVendor: tc.vendor,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
assert.Equal(t, commandVendor(cmd), tc.expected)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInvalidPlugin(t *testing.T) {
|
||||||
|
root := &cobra.Command{Use: "root"}
|
||||||
|
sub1 := &cobra.Command{Use: "sub1"}
|
||||||
|
sub1sub1 := &cobra.Command{Use: "sub1sub1"}
|
||||||
|
sub1sub2 := &cobra.Command{Use: "sub1sub2"}
|
||||||
|
sub2 := &cobra.Command{Use: "sub2"}
|
||||||
|
|
||||||
|
assert.Assert(t, is.Len(invalidPlugins(root), 0))
|
||||||
|
|
||||||
|
sub1.Annotations = map[string]string{
|
||||||
|
pluginmanager.CommandAnnotationPlugin: "true",
|
||||||
|
pluginmanager.CommandAnnotationPluginInvalid: "foo",
|
||||||
|
}
|
||||||
|
root.AddCommand(sub1, sub2)
|
||||||
|
sub1.AddCommand(sub1sub1, sub1sub2)
|
||||||
|
|
||||||
|
assert.DeepEqual(t, invalidPlugins(root), []*cobra.Command{sub1}, cmpopts.IgnoreUnexported(cobra.Command{}))
|
||||||
|
}
|
|
@ -8,7 +8,6 @@ import (
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/docker/cli/cli"
|
|
||||||
"github.com/docker/cli/cli/config"
|
"github.com/docker/cli/cli/config"
|
||||||
cliconfig "github.com/docker/cli/cli/config"
|
cliconfig "github.com/docker/cli/cli/config"
|
||||||
"github.com/docker/cli/cli/config/configfile"
|
"github.com/docker/cli/cli/config/configfile"
|
||||||
|
@ -16,11 +15,13 @@ import (
|
||||||
"github.com/docker/cli/cli/context/docker"
|
"github.com/docker/cli/cli/context/docker"
|
||||||
kubcontext "github.com/docker/cli/cli/context/kubernetes"
|
kubcontext "github.com/docker/cli/cli/context/kubernetes"
|
||||||
"github.com/docker/cli/cli/context/store"
|
"github.com/docker/cli/cli/context/store"
|
||||||
|
"github.com/docker/cli/cli/debug"
|
||||||
cliflags "github.com/docker/cli/cli/flags"
|
cliflags "github.com/docker/cli/cli/flags"
|
||||||
manifeststore "github.com/docker/cli/cli/manifest/store"
|
manifeststore "github.com/docker/cli/cli/manifest/store"
|
||||||
registryclient "github.com/docker/cli/cli/registry/client"
|
registryclient "github.com/docker/cli/cli/registry/client"
|
||||||
"github.com/docker/cli/cli/streams"
|
"github.com/docker/cli/cli/streams"
|
||||||
"github.com/docker/cli/cli/trust"
|
"github.com/docker/cli/cli/trust"
|
||||||
|
"github.com/docker/cli/cli/version"
|
||||||
"github.com/docker/cli/internal/containerizedengine"
|
"github.com/docker/cli/internal/containerizedengine"
|
||||||
dopts "github.com/docker/cli/opts"
|
dopts "github.com/docker/cli/opts"
|
||||||
clitypes "github.com/docker/cli/types"
|
clitypes "github.com/docker/cli/types"
|
||||||
|
@ -177,6 +178,16 @@ func (cli *DockerCli) RegistryClient(allowInsecure bool) registryclient.Registry
|
||||||
// Initialize the dockerCli runs initialization that must happen after command
|
// Initialize the dockerCli runs initialization that must happen after command
|
||||||
// line flags are parsed.
|
// line flags are parsed.
|
||||||
func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions) error {
|
func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions) error {
|
||||||
|
cliflags.SetLogLevel(opts.Common.LogLevel)
|
||||||
|
|
||||||
|
if opts.ConfigDir != "" {
|
||||||
|
cliconfig.SetDir(opts.ConfigDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.Common.Debug {
|
||||||
|
debug.Enable()
|
||||||
|
}
|
||||||
|
|
||||||
cli.configFile = cliconfig.LoadDefaultConfigFile(cli.err)
|
cli.configFile = cliconfig.LoadDefaultConfigFile(cli.err)
|
||||||
var err error
|
var err error
|
||||||
cli.contextStore = store.New(cliconfig.ContextStoreDir(), cli.contextStoreConfig)
|
cli.contextStore = store.New(cliconfig.ContextStoreDir(), cli.contextStoreConfig)
|
||||||
|
@ -461,7 +472,7 @@ func getServerHost(hosts []string, tlsOptions *tlsconfig.Options) (string, error
|
||||||
|
|
||||||
// UserAgent returns the user agent string used for making API requests
|
// UserAgent returns the user agent string used for making API requests
|
||||||
func UserAgent() string {
|
func UserAgent() string {
|
||||||
return "Docker-Client/" + cli.Version + " (" + runtime.GOOS + ")"
|
return "Docker-Client/" + version.Version + " (" + runtime.GOOS + ")"
|
||||||
}
|
}
|
||||||
|
|
||||||
// resolveContextName resolves the current context name with the following rules:
|
// resolveContextName resolves the current context name with the following rules:
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gotest.tools/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func contentTrustEnabled(t *testing.T) bool {
|
||||||
|
var cli DockerCli
|
||||||
|
assert.NilError(t, WithContentTrustFromEnv()(&cli))
|
||||||
|
return cli.contentTrust
|
||||||
|
}
|
||||||
|
|
||||||
|
// NB: Do not t.Parallel() this test -- it messes with the process environment.
|
||||||
|
func TestWithContentTrustFromEnv(t *testing.T) {
|
||||||
|
envvar := "DOCKER_CONTENT_TRUST"
|
||||||
|
if orig, ok := os.LookupEnv(envvar); ok {
|
||||||
|
defer func() {
|
||||||
|
os.Setenv(envvar, orig)
|
||||||
|
}()
|
||||||
|
} else {
|
||||||
|
defer func() {
|
||||||
|
os.Unsetenv(envvar)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
os.Setenv(envvar, "true")
|
||||||
|
assert.Assert(t, contentTrustEnabled(t))
|
||||||
|
os.Setenv(envvar, "false")
|
||||||
|
assert.Assert(t, !contentTrustEnabled(t))
|
||||||
|
os.Setenv(envvar, "invalid")
|
||||||
|
assert.Assert(t, contentTrustEnabled(t))
|
||||||
|
os.Unsetenv(envvar)
|
||||||
|
assert.Assert(t, !contentTrustEnabled(t))
|
||||||
|
}
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/docker/cli/cli"
|
"github.com/docker/cli/cli"
|
||||||
|
pluginmanager "github.com/docker/cli/cli-plugins/manager"
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
"github.com/docker/cli/cli/debug"
|
"github.com/docker/cli/cli/debug"
|
||||||
"github.com/docker/cli/templates"
|
"github.com/docker/cli/templates"
|
||||||
|
@ -23,6 +24,7 @@ type infoOptions struct {
|
||||||
|
|
||||||
type clientInfo struct {
|
type clientInfo struct {
|
||||||
Debug bool
|
Debug bool
|
||||||
|
Plugins []pluginmanager.Plugin
|
||||||
Warnings []string
|
Warnings []string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,7 +49,7 @@ func NewInfoCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
Short: "Display system-wide information",
|
Short: "Display system-wide information",
|
||||||
Args: cli.NoArgs,
|
Args: cli.NoArgs,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return runInfo(dockerCli, &opts)
|
return runInfo(cmd, dockerCli, &opts)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,7 +60,7 @@ func NewInfoCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func runInfo(dockerCli command.Cli, opts *infoOptions) error {
|
func runInfo(cmd *cobra.Command, dockerCli command.Cli, opts *infoOptions) error {
|
||||||
var info info
|
var info info
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
@ -71,6 +73,11 @@ func runInfo(dockerCli command.Cli, opts *infoOptions) error {
|
||||||
info.ClientInfo = &clientInfo{
|
info.ClientInfo = &clientInfo{
|
||||||
Debug: debug.IsEnabled(),
|
Debug: debug.IsEnabled(),
|
||||||
}
|
}
|
||||||
|
if plugins, err := pluginmanager.ListPlugins(dockerCli, cmd.Root()); err == nil {
|
||||||
|
info.ClientInfo.Plugins = plugins
|
||||||
|
} else {
|
||||||
|
info.ClientErrors = append(info.ClientErrors, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
if opts.format == "" {
|
if opts.format == "" {
|
||||||
return prettyPrintInfo(dockerCli, info)
|
return prettyPrintInfo(dockerCli, info)
|
||||||
|
@ -109,6 +116,17 @@ func prettyPrintInfo(dockerCli command.Cli, info info) error {
|
||||||
func prettyPrintClientInfo(dockerCli command.Cli, info clientInfo) error {
|
func prettyPrintClientInfo(dockerCli command.Cli, info clientInfo) error {
|
||||||
fmt.Fprintln(dockerCli.Out(), " Debug Mode:", info.Debug)
|
fmt.Fprintln(dockerCli.Out(), " Debug Mode:", info.Debug)
|
||||||
|
|
||||||
|
if len(info.Plugins) > 0 {
|
||||||
|
fmt.Fprintln(dockerCli.Out(), " Plugins:")
|
||||||
|
for _, p := range info.Plugins {
|
||||||
|
if p.Err == nil {
|
||||||
|
fmt.Fprintf(dockerCli.Out(), " %s: (%s, %s) %s\n", p.Name, p.Version, p.Vendor, p.ShortDescription)
|
||||||
|
} else {
|
||||||
|
info.Warnings = append(info.Warnings, fmt.Sprintf("WARNING: Plugin %q is not valid: %s", p.Path, p.Err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if len(info.Warnings) > 0 {
|
if len(info.Warnings) > 0 {
|
||||||
fmt.Fprintln(dockerCli.Err(), strings.Join(info.Warnings, "\n"))
|
fmt.Fprintln(dockerCli.Err(), strings.Join(info.Warnings, "\n"))
|
||||||
}
|
}
|
||||||
|
@ -447,6 +465,11 @@ func getBackingFs(info types.Info) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatInfo(dockerCli command.Cli, info info, format string) error {
|
func formatInfo(dockerCli command.Cli, info info, format string) error {
|
||||||
|
// Ensure slice/array fields render as `[]` not `null`
|
||||||
|
if info.ClientInfo != nil && info.ClientInfo.Plugins == nil {
|
||||||
|
info.ClientInfo.Plugins = make([]pluginmanager.Plugin, 0)
|
||||||
|
}
|
||||||
|
|
||||||
tmpl, err := templates.Parse(format)
|
tmpl, err := templates.Parse(format)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cli.StatusError{StatusCode: 64,
|
return cli.StatusError{StatusCode: 64,
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
pluginmanager "github.com/docker/cli/cli-plugins/manager"
|
||||||
"github.com/docker/cli/internal/test"
|
"github.com/docker/cli/internal/test"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/docker/docker/api/types/registry"
|
"github.com/docker/docker/api/types/registry"
|
||||||
|
@ -192,6 +193,24 @@ PQQDAgNIADBFAiEAo9fTQNM5DP9bHVcTJYfl2Cay1bFu1E+lnpmN+EYJfeACIGKH
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var samplePluginsInfo = []pluginmanager.Plugin{
|
||||||
|
{
|
||||||
|
Name: "goodplugin",
|
||||||
|
Path: "/path/to/docker-goodplugin",
|
||||||
|
Metadata: pluginmanager.Metadata{
|
||||||
|
SchemaVersion: "0.1.0",
|
||||||
|
ShortDescription: "unit test is good",
|
||||||
|
Vendor: "ACME Corp",
|
||||||
|
Version: "0.1.0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "badplugin",
|
||||||
|
Path: "/path/to/docker-badplugin",
|
||||||
|
Err: pluginmanager.NewPluginError("something wrong"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
func TestPrettyPrintInfo(t *testing.T) {
|
func TestPrettyPrintInfo(t *testing.T) {
|
||||||
infoWithSwarm := sampleInfoNoSwarm
|
infoWithSwarm := sampleInfoNoSwarm
|
||||||
infoWithSwarm.Swarm = sampleSwarmInfo
|
infoWithSwarm.Swarm = sampleSwarmInfo
|
||||||
|
@ -228,8 +247,9 @@ func TestPrettyPrintInfo(t *testing.T) {
|
||||||
sampleInfoBadSecurity.SecurityOptions = []string{"foo="}
|
sampleInfoBadSecurity.SecurityOptions = []string{"foo="}
|
||||||
|
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
doc string
|
doc string
|
||||||
dockerInfo info
|
dockerInfo info
|
||||||
|
|
||||||
prettyGolden string
|
prettyGolden string
|
||||||
warningsGolden string
|
warningsGolden string
|
||||||
jsonGolden string
|
jsonGolden string
|
||||||
|
@ -245,6 +265,19 @@ func TestPrettyPrintInfo(t *testing.T) {
|
||||||
jsonGolden: "docker-info-no-swarm",
|
jsonGolden: "docker-info-no-swarm",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
doc: "info with plugins",
|
||||||
|
dockerInfo: info{
|
||||||
|
Info: &sampleInfoNoSwarm,
|
||||||
|
ClientInfo: &clientInfo{
|
||||||
|
Plugins: samplePluginsInfo,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
prettyGolden: "docker-info-plugins",
|
||||||
|
jsonGolden: "docker-info-plugins",
|
||||||
|
warningsGolden: "docker-info-plugins-warnings",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
|
||||||
doc: "info with swarm",
|
doc: "info with swarm",
|
||||||
dockerInfo: info{
|
dockerInfo: info{
|
||||||
Info: &infoWithSwarm,
|
Info: &infoWithSwarm,
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
{"ID":"EKHL:QDUU:QZ7U:MKGD:VDXK:S27Q:GIPU:24B7:R7VT:DGN6:QCSF:2UBX","Containers":0,"ContainersRunning":0,"ContainersPaused":0,"ContainersStopped":0,"Images":0,"Driver":"aufs","DriverStatus":[["Root Dir","/var/lib/docker/aufs"],["Backing Filesystem","extfs"],["Dirs","0"],["Dirperm1 Supported","true"]],"SystemStatus":null,"Plugins":{"Volume":["local"],"Network":["bridge","host","macvlan","null","overlay"],"Authorization":null,"Log":["awslogs","fluentd","gcplogs","gelf","journald","json-file","logentries","splunk","syslog"]},"MemoryLimit":true,"SwapLimit":true,"KernelMemory":true,"KernelMemoryTCP":false,"CpuCfsPeriod":true,"CpuCfsQuota":true,"CPUShares":true,"CPUSet":true,"IPv4Forwarding":true,"BridgeNfIptables":true,"BridgeNfIp6tables":true,"Debug":true,"NFd":33,"OomKillDisable":true,"NGoroutines":135,"SystemTime":"2017-08-24T17:44:34.077811894Z","LoggingDriver":"json-file","CgroupDriver":"cgroupfs","NEventsListener":0,"KernelVersion":"4.4.0-87-generic","OperatingSystem":"Ubuntu 16.04.3 LTS","OSType":"linux","Architecture":"x86_64","IndexServerAddress":"https://index.docker.io/v1/","RegistryConfig":{"AllowNondistributableArtifactsCIDRs":null,"AllowNondistributableArtifactsHostnames":null,"InsecureRegistryCIDRs":["127.0.0.0/8"],"IndexConfigs":{"docker.io":{"Name":"docker.io","Mirrors":null,"Secure":true,"Official":true}},"Mirrors":null},"NCPU":2,"MemTotal":2097356800,"GenericResources":null,"DockerRootDir":"/var/lib/docker","HttpProxy":"","HttpsProxy":"","NoProxy":"","Name":"system-sample","Labels":["provider=digitalocean"],"ExperimentalBuild":false,"ServerVersion":"17.06.1-ce","ClusterStore":"","ClusterAdvertise":"","Runtimes":{"runc":{"path":"docker-runc"}},"DefaultRuntime":"runc","Swarm":{"NodeID":"","NodeAddr":"","LocalNodeState":"inactive","ControlAvailable":false,"Error":"","RemoteManagers":null},"LiveRestoreEnabled":false,"Isolation":"","InitBinary":"docker-init","ContainerdCommit":{"ID":"6e23458c129b551d5c9871e5174f6b1b7f6d1170","Expected":"6e23458c129b551d5c9871e5174f6b1b7f6d1170"},"RuncCommit":{"ID":"810190ceaa507aa2727d7ae6f4790c76ec150bd2","Expected":"810190ceaa507aa2727d7ae6f4790c76ec150bd2"},"InitCommit":{"ID":"949e6fa","Expected":"949e6fa"},"SecurityOptions":["foo="],"Warnings":null,"ServerErrors":["an error happened"],"ClientInfo":{"Debug":false,"Warnings":null}}
|
{"ID":"EKHL:QDUU:QZ7U:MKGD:VDXK:S27Q:GIPU:24B7:R7VT:DGN6:QCSF:2UBX","Containers":0,"ContainersRunning":0,"ContainersPaused":0,"ContainersStopped":0,"Images":0,"Driver":"aufs","DriverStatus":[["Root Dir","/var/lib/docker/aufs"],["Backing Filesystem","extfs"],["Dirs","0"],["Dirperm1 Supported","true"]],"SystemStatus":null,"Plugins":{"Volume":["local"],"Network":["bridge","host","macvlan","null","overlay"],"Authorization":null,"Log":["awslogs","fluentd","gcplogs","gelf","journald","json-file","logentries","splunk","syslog"]},"MemoryLimit":true,"SwapLimit":true,"KernelMemory":true,"KernelMemoryTCP":false,"CpuCfsPeriod":true,"CpuCfsQuota":true,"CPUShares":true,"CPUSet":true,"IPv4Forwarding":true,"BridgeNfIptables":true,"BridgeNfIp6tables":true,"Debug":true,"NFd":33,"OomKillDisable":true,"NGoroutines":135,"SystemTime":"2017-08-24T17:44:34.077811894Z","LoggingDriver":"json-file","CgroupDriver":"cgroupfs","NEventsListener":0,"KernelVersion":"4.4.0-87-generic","OperatingSystem":"Ubuntu 16.04.3 LTS","OSType":"linux","Architecture":"x86_64","IndexServerAddress":"https://index.docker.io/v1/","RegistryConfig":{"AllowNondistributableArtifactsCIDRs":null,"AllowNondistributableArtifactsHostnames":null,"InsecureRegistryCIDRs":["127.0.0.0/8"],"IndexConfigs":{"docker.io":{"Name":"docker.io","Mirrors":null,"Secure":true,"Official":true}},"Mirrors":null},"NCPU":2,"MemTotal":2097356800,"GenericResources":null,"DockerRootDir":"/var/lib/docker","HttpProxy":"","HttpsProxy":"","NoProxy":"","Name":"system-sample","Labels":["provider=digitalocean"],"ExperimentalBuild":false,"ServerVersion":"17.06.1-ce","ClusterStore":"","ClusterAdvertise":"","Runtimes":{"runc":{"path":"docker-runc"}},"DefaultRuntime":"runc","Swarm":{"NodeID":"","NodeAddr":"","LocalNodeState":"inactive","ControlAvailable":false,"Error":"","RemoteManagers":null},"LiveRestoreEnabled":false,"Isolation":"","InitBinary":"docker-init","ContainerdCommit":{"ID":"6e23458c129b551d5c9871e5174f6b1b7f6d1170","Expected":"6e23458c129b551d5c9871e5174f6b1b7f6d1170"},"RuncCommit":{"ID":"810190ceaa507aa2727d7ae6f4790c76ec150bd2","Expected":"810190ceaa507aa2727d7ae6f4790c76ec150bd2"},"InitCommit":{"ID":"949e6fa","Expected":"949e6fa"},"SecurityOptions":["foo="],"Warnings":null,"ServerErrors":["an error happened"],"ClientInfo":{"Debug":false,"Plugins":[],"Warnings":null}}
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
{"ID":"EKHL:QDUU:QZ7U:MKGD:VDXK:S27Q:GIPU:24B7:R7VT:DGN6:QCSF:2UBX","Containers":0,"ContainersRunning":0,"ContainersPaused":0,"ContainersStopped":0,"Images":0,"Driver":"aufs","DriverStatus":[["Root Dir","/var/lib/docker/aufs"],["Backing Filesystem","extfs"],["Dirs","0"],["Dirperm1 Supported","true"]],"SystemStatus":null,"Plugins":{"Volume":["local"],"Network":["bridge","host","macvlan","null","overlay"],"Authorization":null,"Log":["awslogs","fluentd","gcplogs","gelf","journald","json-file","logentries","splunk","syslog"]},"MemoryLimit":true,"SwapLimit":true,"KernelMemory":true,"KernelMemoryTCP":false,"CpuCfsPeriod":true,"CpuCfsQuota":true,"CPUShares":true,"CPUSet":true,"IPv4Forwarding":true,"BridgeNfIptables":true,"BridgeNfIp6tables":true,"Debug":true,"NFd":33,"OomKillDisable":true,"NGoroutines":135,"SystemTime":"2017-08-24T17:44:34.077811894Z","LoggingDriver":"json-file","CgroupDriver":"cgroupfs","NEventsListener":0,"KernelVersion":"4.4.0-87-generic","OperatingSystem":"Ubuntu 16.04.3 LTS","OSType":"linux","Architecture":"x86_64","IndexServerAddress":"https://index.docker.io/v1/","RegistryConfig":{"AllowNondistributableArtifactsCIDRs":null,"AllowNondistributableArtifactsHostnames":null,"InsecureRegistryCIDRs":["127.0.0.0/8"],"IndexConfigs":{"docker.io":{"Name":"docker.io","Mirrors":null,"Secure":true,"Official":true}},"Mirrors":null},"NCPU":2,"MemTotal":2097356800,"GenericResources":null,"DockerRootDir":"/var/lib/docker","HttpProxy":"","HttpsProxy":"","NoProxy":"","Name":"system-sample","Labels":["provider=digitalocean"],"ExperimentalBuild":false,"ServerVersion":"17.06.1-ce","ClusterStore":"","ClusterAdvertise":"","Runtimes":{"runc":{"path":"docker-runc"}},"DefaultRuntime":"runc","Swarm":{"NodeID":"","NodeAddr":"","LocalNodeState":"inactive","ControlAvailable":false,"Error":"","RemoteManagers":null},"LiveRestoreEnabled":false,"Isolation":"","InitBinary":"docker-init","ContainerdCommit":{"ID":"6e23458c129b551d5c9871e5174f6b1b7f6d1170","Expected":"6e23458c129b551d5c9871e5174f6b1b7f6d1170"},"RuncCommit":{"ID":"810190ceaa507aa2727d7ae6f4790c76ec150bd2","Expected":"810190ceaa507aa2727d7ae6f4790c76ec150bd2"},"InitCommit":{"ID":"949e6fa","Expected":"949e6fa"},"SecurityOptions":["name=apparmor","name=seccomp,profile=default"],"Warnings":["WARNING: No memory limit support","WARNING: No swap limit support","WARNING: No kernel memory limit support","WARNING: No oom kill disable support","WARNING: No cpu cfs quota support","WARNING: No cpu cfs period support","WARNING: No cpu shares support","WARNING: No cpuset support","WARNING: IPv4 forwarding is disabled","WARNING: bridge-nf-call-iptables is disabled","WARNING: bridge-nf-call-ip6tables is disabled"],"ClientInfo":{"Debug":true,"Warnings":null}}
|
{"ID":"EKHL:QDUU:QZ7U:MKGD:VDXK:S27Q:GIPU:24B7:R7VT:DGN6:QCSF:2UBX","Containers":0,"ContainersRunning":0,"ContainersPaused":0,"ContainersStopped":0,"Images":0,"Driver":"aufs","DriverStatus":[["Root Dir","/var/lib/docker/aufs"],["Backing Filesystem","extfs"],["Dirs","0"],["Dirperm1 Supported","true"]],"SystemStatus":null,"Plugins":{"Volume":["local"],"Network":["bridge","host","macvlan","null","overlay"],"Authorization":null,"Log":["awslogs","fluentd","gcplogs","gelf","journald","json-file","logentries","splunk","syslog"]},"MemoryLimit":true,"SwapLimit":true,"KernelMemory":true,"KernelMemoryTCP":false,"CpuCfsPeriod":true,"CpuCfsQuota":true,"CPUShares":true,"CPUSet":true,"IPv4Forwarding":true,"BridgeNfIptables":true,"BridgeNfIp6tables":true,"Debug":true,"NFd":33,"OomKillDisable":true,"NGoroutines":135,"SystemTime":"2017-08-24T17:44:34.077811894Z","LoggingDriver":"json-file","CgroupDriver":"cgroupfs","NEventsListener":0,"KernelVersion":"4.4.0-87-generic","OperatingSystem":"Ubuntu 16.04.3 LTS","OSType":"linux","Architecture":"x86_64","IndexServerAddress":"https://index.docker.io/v1/","RegistryConfig":{"AllowNondistributableArtifactsCIDRs":null,"AllowNondistributableArtifactsHostnames":null,"InsecureRegistryCIDRs":["127.0.0.0/8"],"IndexConfigs":{"docker.io":{"Name":"docker.io","Mirrors":null,"Secure":true,"Official":true}},"Mirrors":null},"NCPU":2,"MemTotal":2097356800,"GenericResources":null,"DockerRootDir":"/var/lib/docker","HttpProxy":"","HttpsProxy":"","NoProxy":"","Name":"system-sample","Labels":["provider=digitalocean"],"ExperimentalBuild":false,"ServerVersion":"17.06.1-ce","ClusterStore":"","ClusterAdvertise":"","Runtimes":{"runc":{"path":"docker-runc"}},"DefaultRuntime":"runc","Swarm":{"NodeID":"","NodeAddr":"","LocalNodeState":"inactive","ControlAvailable":false,"Error":"","RemoteManagers":null},"LiveRestoreEnabled":false,"Isolation":"","InitBinary":"docker-init","ContainerdCommit":{"ID":"6e23458c129b551d5c9871e5174f6b1b7f6d1170","Expected":"6e23458c129b551d5c9871e5174f6b1b7f6d1170"},"RuncCommit":{"ID":"810190ceaa507aa2727d7ae6f4790c76ec150bd2","Expected":"810190ceaa507aa2727d7ae6f4790c76ec150bd2"},"InitCommit":{"ID":"949e6fa","Expected":"949e6fa"},"SecurityOptions":["name=apparmor","name=seccomp,profile=default"],"Warnings":["WARNING: No memory limit support","WARNING: No swap limit support","WARNING: No kernel memory limit support","WARNING: No oom kill disable support","WARNING: No cpu cfs quota support","WARNING: No cpu cfs period support","WARNING: No cpu shares support","WARNING: No cpuset support","WARNING: IPv4 forwarding is disabled","WARNING: bridge-nf-call-iptables is disabled","WARNING: bridge-nf-call-ip6tables is disabled"],"ClientInfo":{"Debug":true,"Plugins":[],"Warnings":null}}
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
{"ID":"EKHL:QDUU:QZ7U:MKGD:VDXK:S27Q:GIPU:24B7:R7VT:DGN6:QCSF:2UBX","Containers":0,"ContainersRunning":0,"ContainersPaused":0,"ContainersStopped":0,"Images":0,"Driver":"aufs","DriverStatus":[["Root Dir","/var/lib/docker/aufs"],["Backing Filesystem","extfs"],["Dirs","0"],["Dirperm1 Supported","true"]],"SystemStatus":null,"Plugins":{"Volume":["local"],"Network":["bridge","host","macvlan","null","overlay"],"Authorization":null,"Log":["awslogs","fluentd","gcplogs","gelf","journald","json-file","logentries","splunk","syslog"]},"MemoryLimit":false,"SwapLimit":false,"KernelMemory":false,"KernelMemoryTCP":false,"CpuCfsPeriod":false,"CpuCfsQuota":false,"CPUShares":false,"CPUSet":false,"IPv4Forwarding":false,"BridgeNfIptables":false,"BridgeNfIp6tables":false,"Debug":true,"NFd":33,"OomKillDisable":false,"NGoroutines":135,"SystemTime":"2017-08-24T17:44:34.077811894Z","LoggingDriver":"json-file","CgroupDriver":"cgroupfs","NEventsListener":0,"KernelVersion":"4.4.0-87-generic","OperatingSystem":"Ubuntu 16.04.3 LTS","OSType":"linux","Architecture":"x86_64","IndexServerAddress":"https://index.docker.io/v1/","RegistryConfig":{"AllowNondistributableArtifactsCIDRs":null,"AllowNondistributableArtifactsHostnames":null,"InsecureRegistryCIDRs":["127.0.0.0/8"],"IndexConfigs":{"docker.io":{"Name":"docker.io","Mirrors":null,"Secure":true,"Official":true}},"Mirrors":null},"NCPU":2,"MemTotal":2097356800,"GenericResources":null,"DockerRootDir":"/var/lib/docker","HttpProxy":"","HttpsProxy":"","NoProxy":"","Name":"system-sample","Labels":["provider=digitalocean"],"ExperimentalBuild":false,"ServerVersion":"17.06.1-ce","ClusterStore":"","ClusterAdvertise":"","Runtimes":{"runc":{"path":"docker-runc"}},"DefaultRuntime":"runc","Swarm":{"NodeID":"","NodeAddr":"","LocalNodeState":"inactive","ControlAvailable":false,"Error":"","RemoteManagers":null},"LiveRestoreEnabled":false,"Isolation":"","InitBinary":"docker-init","ContainerdCommit":{"ID":"6e23458c129b551d5c9871e5174f6b1b7f6d1170","Expected":"6e23458c129b551d5c9871e5174f6b1b7f6d1170"},"RuncCommit":{"ID":"810190ceaa507aa2727d7ae6f4790c76ec150bd2","Expected":"810190ceaa507aa2727d7ae6f4790c76ec150bd2"},"InitCommit":{"ID":"949e6fa","Expected":"949e6fa"},"SecurityOptions":["name=apparmor","name=seccomp,profile=default"],"Warnings":null,"ClientInfo":{"Debug":true,"Warnings":null}}
|
{"ID":"EKHL:QDUU:QZ7U:MKGD:VDXK:S27Q:GIPU:24B7:R7VT:DGN6:QCSF:2UBX","Containers":0,"ContainersRunning":0,"ContainersPaused":0,"ContainersStopped":0,"Images":0,"Driver":"aufs","DriverStatus":[["Root Dir","/var/lib/docker/aufs"],["Backing Filesystem","extfs"],["Dirs","0"],["Dirperm1 Supported","true"]],"SystemStatus":null,"Plugins":{"Volume":["local"],"Network":["bridge","host","macvlan","null","overlay"],"Authorization":null,"Log":["awslogs","fluentd","gcplogs","gelf","journald","json-file","logentries","splunk","syslog"]},"MemoryLimit":false,"SwapLimit":false,"KernelMemory":false,"KernelMemoryTCP":false,"CpuCfsPeriod":false,"CpuCfsQuota":false,"CPUShares":false,"CPUSet":false,"IPv4Forwarding":false,"BridgeNfIptables":false,"BridgeNfIp6tables":false,"Debug":true,"NFd":33,"OomKillDisable":false,"NGoroutines":135,"SystemTime":"2017-08-24T17:44:34.077811894Z","LoggingDriver":"json-file","CgroupDriver":"cgroupfs","NEventsListener":0,"KernelVersion":"4.4.0-87-generic","OperatingSystem":"Ubuntu 16.04.3 LTS","OSType":"linux","Architecture":"x86_64","IndexServerAddress":"https://index.docker.io/v1/","RegistryConfig":{"AllowNondistributableArtifactsCIDRs":null,"AllowNondistributableArtifactsHostnames":null,"InsecureRegistryCIDRs":["127.0.0.0/8"],"IndexConfigs":{"docker.io":{"Name":"docker.io","Mirrors":null,"Secure":true,"Official":true}},"Mirrors":null},"NCPU":2,"MemTotal":2097356800,"GenericResources":null,"DockerRootDir":"/var/lib/docker","HttpProxy":"","HttpsProxy":"","NoProxy":"","Name":"system-sample","Labels":["provider=digitalocean"],"ExperimentalBuild":false,"ServerVersion":"17.06.1-ce","ClusterStore":"","ClusterAdvertise":"","Runtimes":{"runc":{"path":"docker-runc"}},"DefaultRuntime":"runc","Swarm":{"NodeID":"","NodeAddr":"","LocalNodeState":"inactive","ControlAvailable":false,"Error":"","RemoteManagers":null},"LiveRestoreEnabled":false,"Isolation":"","InitBinary":"docker-init","ContainerdCommit":{"ID":"6e23458c129b551d5c9871e5174f6b1b7f6d1170","Expected":"6e23458c129b551d5c9871e5174f6b1b7f6d1170"},"RuncCommit":{"ID":"810190ceaa507aa2727d7ae6f4790c76ec150bd2","Expected":"810190ceaa507aa2727d7ae6f4790c76ec150bd2"},"InitCommit":{"ID":"949e6fa","Expected":"949e6fa"},"SecurityOptions":["name=apparmor","name=seccomp,profile=default"],"Warnings":null,"ClientInfo":{"Debug":true,"Plugins":[],"Warnings":null}}
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
{"ID":"EKHL:QDUU:QZ7U:MKGD:VDXK:S27Q:GIPU:24B7:R7VT:DGN6:QCSF:2UBX","Containers":0,"ContainersRunning":0,"ContainersPaused":0,"ContainersStopped":0,"Images":0,"Driver":"aufs","DriverStatus":[["Root Dir","/var/lib/docker/aufs"],["Backing Filesystem","extfs"],["Dirs","0"],["Dirperm1 Supported","true"]],"SystemStatus":null,"Plugins":{"Volume":["local"],"Network":["bridge","host","macvlan","null","overlay"],"Authorization":null,"Log":["awslogs","fluentd","gcplogs","gelf","journald","json-file","logentries","splunk","syslog"]},"MemoryLimit":true,"SwapLimit":true,"KernelMemory":true,"KernelMemoryTCP":false,"CpuCfsPeriod":true,"CpuCfsQuota":true,"CPUShares":true,"CPUSet":true,"IPv4Forwarding":true,"BridgeNfIptables":true,"BridgeNfIp6tables":true,"Debug":true,"NFd":33,"OomKillDisable":true,"NGoroutines":135,"SystemTime":"2017-08-24T17:44:34.077811894Z","LoggingDriver":"json-file","CgroupDriver":"cgroupfs","NEventsListener":0,"KernelVersion":"4.4.0-87-generic","OperatingSystem":"Ubuntu 16.04.3 LTS","OSType":"linux","Architecture":"x86_64","IndexServerAddress":"https://index.docker.io/v1/","RegistryConfig":{"AllowNondistributableArtifactsCIDRs":null,"AllowNondistributableArtifactsHostnames":null,"InsecureRegistryCIDRs":["127.0.0.0/8"],"IndexConfigs":{"docker.io":{"Name":"docker.io","Mirrors":null,"Secure":true,"Official":true}},"Mirrors":null},"NCPU":2,"MemTotal":2097356800,"GenericResources":null,"DockerRootDir":"/var/lib/docker","HttpProxy":"","HttpsProxy":"","NoProxy":"","Name":"system-sample","Labels":["provider=digitalocean"],"ExperimentalBuild":false,"ServerVersion":"17.06.1-ce","ClusterStore":"","ClusterAdvertise":"","Runtimes":{"runc":{"path":"docker-runc"}},"DefaultRuntime":"runc","Swarm":{"NodeID":"","NodeAddr":"","LocalNodeState":"inactive","ControlAvailable":false,"Error":"","RemoteManagers":null},"LiveRestoreEnabled":false,"Isolation":"","InitBinary":"docker-init","ContainerdCommit":{"ID":"6e23458c129b551d5c9871e5174f6b1b7f6d1170","Expected":"6e23458c129b551d5c9871e5174f6b1b7f6d1170"},"RuncCommit":{"ID":"810190ceaa507aa2727d7ae6f4790c76ec150bd2","Expected":"810190ceaa507aa2727d7ae6f4790c76ec150bd2"},"InitCommit":{"ID":"949e6fa","Expected":"949e6fa"},"SecurityOptions":["name=apparmor","name=seccomp,profile=default"],"Warnings":null,"ClientInfo":{"Debug":true,"Warnings":null}}
|
{"ID":"EKHL:QDUU:QZ7U:MKGD:VDXK:S27Q:GIPU:24B7:R7VT:DGN6:QCSF:2UBX","Containers":0,"ContainersRunning":0,"ContainersPaused":0,"ContainersStopped":0,"Images":0,"Driver":"aufs","DriverStatus":[["Root Dir","/var/lib/docker/aufs"],["Backing Filesystem","extfs"],["Dirs","0"],["Dirperm1 Supported","true"]],"SystemStatus":null,"Plugins":{"Volume":["local"],"Network":["bridge","host","macvlan","null","overlay"],"Authorization":null,"Log":["awslogs","fluentd","gcplogs","gelf","journald","json-file","logentries","splunk","syslog"]},"MemoryLimit":true,"SwapLimit":true,"KernelMemory":true,"KernelMemoryTCP":false,"CpuCfsPeriod":true,"CpuCfsQuota":true,"CPUShares":true,"CPUSet":true,"IPv4Forwarding":true,"BridgeNfIptables":true,"BridgeNfIp6tables":true,"Debug":true,"NFd":33,"OomKillDisable":true,"NGoroutines":135,"SystemTime":"2017-08-24T17:44:34.077811894Z","LoggingDriver":"json-file","CgroupDriver":"cgroupfs","NEventsListener":0,"KernelVersion":"4.4.0-87-generic","OperatingSystem":"Ubuntu 16.04.3 LTS","OSType":"linux","Architecture":"x86_64","IndexServerAddress":"https://index.docker.io/v1/","RegistryConfig":{"AllowNondistributableArtifactsCIDRs":null,"AllowNondistributableArtifactsHostnames":null,"InsecureRegistryCIDRs":["127.0.0.0/8"],"IndexConfigs":{"docker.io":{"Name":"docker.io","Mirrors":null,"Secure":true,"Official":true}},"Mirrors":null},"NCPU":2,"MemTotal":2097356800,"GenericResources":null,"DockerRootDir":"/var/lib/docker","HttpProxy":"","HttpsProxy":"","NoProxy":"","Name":"system-sample","Labels":["provider=digitalocean"],"ExperimentalBuild":false,"ServerVersion":"17.06.1-ce","ClusterStore":"","ClusterAdvertise":"","Runtimes":{"runc":{"path":"docker-runc"}},"DefaultRuntime":"runc","Swarm":{"NodeID":"","NodeAddr":"","LocalNodeState":"inactive","ControlAvailable":false,"Error":"","RemoteManagers":null},"LiveRestoreEnabled":false,"Isolation":"","InitBinary":"docker-init","ContainerdCommit":{"ID":"6e23458c129b551d5c9871e5174f6b1b7f6d1170","Expected":"6e23458c129b551d5c9871e5174f6b1b7f6d1170"},"RuncCommit":{"ID":"810190ceaa507aa2727d7ae6f4790c76ec150bd2","Expected":"810190ceaa507aa2727d7ae6f4790c76ec150bd2"},"InitCommit":{"ID":"949e6fa","Expected":"949e6fa"},"SecurityOptions":["name=apparmor","name=seccomp,profile=default"],"Warnings":null,"ClientInfo":{"Debug":true,"Plugins":[],"Warnings":null}}
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
WARNING: Plugin "/path/to/docker-badplugin" is not valid: something wrong
|
|
@ -0,0 +1,56 @@
|
||||||
|
Client:
|
||||||
|
Debug Mode: false
|
||||||
|
Plugins:
|
||||||
|
goodplugin: (0.1.0, ACME Corp) unit test is good
|
||||||
|
|
||||||
|
Server:
|
||||||
|
Containers: 0
|
||||||
|
Running: 0
|
||||||
|
Paused: 0
|
||||||
|
Stopped: 0
|
||||||
|
Images: 0
|
||||||
|
Server Version: 17.06.1-ce
|
||||||
|
Storage Driver: aufs
|
||||||
|
Root Dir: /var/lib/docker/aufs
|
||||||
|
Backing Filesystem: extfs
|
||||||
|
Dirs: 0
|
||||||
|
Dirperm1 Supported: true
|
||||||
|
Logging Driver: json-file
|
||||||
|
Cgroup Driver: cgroupfs
|
||||||
|
Plugins:
|
||||||
|
Volume: local
|
||||||
|
Network: bridge host macvlan null overlay
|
||||||
|
Log: awslogs fluentd gcplogs gelf journald json-file logentries splunk syslog
|
||||||
|
Swarm: inactive
|
||||||
|
Runtimes: runc
|
||||||
|
Default Runtime: runc
|
||||||
|
Init Binary: docker-init
|
||||||
|
containerd version: 6e23458c129b551d5c9871e5174f6b1b7f6d1170
|
||||||
|
runc version: 810190ceaa507aa2727d7ae6f4790c76ec150bd2
|
||||||
|
init version: 949e6fa
|
||||||
|
Security Options:
|
||||||
|
apparmor
|
||||||
|
seccomp
|
||||||
|
Profile: default
|
||||||
|
Kernel Version: 4.4.0-87-generic
|
||||||
|
Operating System: Ubuntu 16.04.3 LTS
|
||||||
|
OSType: linux
|
||||||
|
Architecture: x86_64
|
||||||
|
CPUs: 2
|
||||||
|
Total Memory: 1.953GiB
|
||||||
|
Name: system-sample
|
||||||
|
ID: EKHL:QDUU:QZ7U:MKGD:VDXK:S27Q:GIPU:24B7:R7VT:DGN6:QCSF:2UBX
|
||||||
|
Docker Root Dir: /var/lib/docker
|
||||||
|
Debug Mode: true
|
||||||
|
File Descriptors: 33
|
||||||
|
Goroutines: 135
|
||||||
|
System Time: 2017-08-24T17:44:34.077811894Z
|
||||||
|
EventsListeners: 0
|
||||||
|
Registry: https://index.docker.io/v1/
|
||||||
|
Labels:
|
||||||
|
provider=digitalocean
|
||||||
|
Experimental: false
|
||||||
|
Insecure Registries:
|
||||||
|
127.0.0.0/8
|
||||||
|
Live Restore Enabled: false
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
{"ID":"EKHL:QDUU:QZ7U:MKGD:VDXK:S27Q:GIPU:24B7:R7VT:DGN6:QCSF:2UBX","Containers":0,"ContainersRunning":0,"ContainersPaused":0,"ContainersStopped":0,"Images":0,"Driver":"aufs","DriverStatus":[["Root Dir","/var/lib/docker/aufs"],["Backing Filesystem","extfs"],["Dirs","0"],["Dirperm1 Supported","true"]],"SystemStatus":null,"Plugins":{"Volume":["local"],"Network":["bridge","host","macvlan","null","overlay"],"Authorization":null,"Log":["awslogs","fluentd","gcplogs","gelf","journald","json-file","logentries","splunk","syslog"]},"MemoryLimit":true,"SwapLimit":true,"KernelMemory":true,"KernelMemoryTCP":false,"CpuCfsPeriod":true,"CpuCfsQuota":true,"CPUShares":true,"CPUSet":true,"IPv4Forwarding":true,"BridgeNfIptables":true,"BridgeNfIp6tables":true,"Debug":true,"NFd":33,"OomKillDisable":true,"NGoroutines":135,"SystemTime":"2017-08-24T17:44:34.077811894Z","LoggingDriver":"json-file","CgroupDriver":"cgroupfs","NEventsListener":0,"KernelVersion":"4.4.0-87-generic","OperatingSystem":"Ubuntu 16.04.3 LTS","OSType":"linux","Architecture":"x86_64","IndexServerAddress":"https://index.docker.io/v1/","RegistryConfig":{"AllowNondistributableArtifactsCIDRs":null,"AllowNondistributableArtifactsHostnames":null,"InsecureRegistryCIDRs":["127.0.0.0/8"],"IndexConfigs":{"docker.io":{"Name":"docker.io","Mirrors":null,"Secure":true,"Official":true}},"Mirrors":null},"NCPU":2,"MemTotal":2097356800,"GenericResources":null,"DockerRootDir":"/var/lib/docker","HttpProxy":"","HttpsProxy":"","NoProxy":"","Name":"system-sample","Labels":["provider=digitalocean"],"ExperimentalBuild":false,"ServerVersion":"17.06.1-ce","ClusterStore":"","ClusterAdvertise":"","Runtimes":{"runc":{"path":"docker-runc"}},"DefaultRuntime":"runc","Swarm":{"NodeID":"","NodeAddr":"","LocalNodeState":"inactive","ControlAvailable":false,"Error":"","RemoteManagers":null},"LiveRestoreEnabled":false,"Isolation":"","InitBinary":"docker-init","ContainerdCommit":{"ID":"6e23458c129b551d5c9871e5174f6b1b7f6d1170","Expected":"6e23458c129b551d5c9871e5174f6b1b7f6d1170"},"RuncCommit":{"ID":"810190ceaa507aa2727d7ae6f4790c76ec150bd2","Expected":"810190ceaa507aa2727d7ae6f4790c76ec150bd2"},"InitCommit":{"ID":"949e6fa","Expected":"949e6fa"},"SecurityOptions":["name=apparmor","name=seccomp,profile=default"],"Warnings":null,"ClientInfo":{"Debug":false,"Plugins":[{"SchemaVersion":"0.1.0","Vendor":"ACME Corp","Version":"0.1.0","ShortDescription":"unit test is good","Name":"goodplugin","Path":"/path/to/docker-goodplugin"},{"Name":"badplugin","Path":"/path/to/docker-badplugin","Err":"something wrong"}],"Warnings":null}}
|
|
@ -1 +1 @@
|
||||||
{"ID":"EKHL:QDUU:QZ7U:MKGD:VDXK:S27Q:GIPU:24B7:R7VT:DGN6:QCSF:2UBX","Containers":0,"ContainersRunning":0,"ContainersPaused":0,"ContainersStopped":0,"Images":0,"Driver":"aufs","DriverStatus":[["Root Dir","/var/lib/docker/aufs"],["Backing Filesystem","extfs"],["Dirs","0"],["Dirperm1 Supported","true"]],"SystemStatus":null,"Plugins":{"Volume":["local"],"Network":["bridge","host","macvlan","null","overlay"],"Authorization":null,"Log":["awslogs","fluentd","gcplogs","gelf","journald","json-file","logentries","splunk","syslog"]},"MemoryLimit":true,"SwapLimit":true,"KernelMemory":true,"KernelMemoryTCP":false,"CpuCfsPeriod":true,"CpuCfsQuota":true,"CPUShares":true,"CPUSet":true,"IPv4Forwarding":true,"BridgeNfIptables":true,"BridgeNfIp6tables":true,"Debug":true,"NFd":33,"OomKillDisable":true,"NGoroutines":135,"SystemTime":"2017-08-24T17:44:34.077811894Z","LoggingDriver":"json-file","CgroupDriver":"cgroupfs","NEventsListener":0,"KernelVersion":"4.4.0-87-generic","OperatingSystem":"Ubuntu 16.04.3 LTS","OSType":"linux","Architecture":"x86_64","IndexServerAddress":"https://index.docker.io/v1/","RegistryConfig":{"AllowNondistributableArtifactsCIDRs":null,"AllowNondistributableArtifactsHostnames":null,"InsecureRegistryCIDRs":["127.0.0.0/8"],"IndexConfigs":{"docker.io":{"Name":"docker.io","Mirrors":null,"Secure":true,"Official":true}},"Mirrors":null},"NCPU":2,"MemTotal":2097356800,"GenericResources":null,"DockerRootDir":"/var/lib/docker","HttpProxy":"","HttpsProxy":"","NoProxy":"","Name":"system-sample","Labels":["provider=digitalocean"],"ExperimentalBuild":false,"ServerVersion":"17.06.1-ce","ClusterStore":"","ClusterAdvertise":"","Runtimes":{"runc":{"path":"docker-runc"}},"DefaultRuntime":"runc","Swarm":{"NodeID":"qo2dfdig9mmxqkawulggepdih","NodeAddr":"165.227.107.89","LocalNodeState":"active","ControlAvailable":true,"Error":"","RemoteManagers":[{"NodeID":"qo2dfdig9mmxqkawulggepdih","Addr":"165.227.107.89:2377"}],"Nodes":1,"Managers":1,"Cluster":{"ID":"9vs5ygs0gguyyec4iqf2314c0","Version":{"Index":11},"CreatedAt":"2017-08-24T17:34:19.278062352Z","UpdatedAt":"2017-08-24T17:34:42.398815481Z","Spec":{"Name":"default","Labels":null,"Orchestration":{"TaskHistoryRetentionLimit":5},"Raft":{"SnapshotInterval":10000,"KeepOldSnapshots":0,"LogEntriesForSlowFollowers":500,"ElectionTick":3,"HeartbeatTick":1},"Dispatcher":{"HeartbeatPeriod":5000000000},"CAConfig":{"NodeCertExpiry":7776000000000000},"TaskDefaults":{},"EncryptionConfig":{"AutoLockManagers":true}},"TLSInfo":{"TrustRoot":"\n-----BEGIN CERTIFICATE-----\nMIIBajCCARCgAwIBAgIUaFCW5xsq8eyiJ+Pmcv3MCflMLnMwCgYIKoZIzj0EAwIw\nEzERMA8GA1UEAxMIc3dhcm0tY2EwHhcNMTcwODI0MTcyOTAwWhcNMzcwODE5MTcy\nOTAwWjATMREwDwYDVQQDEwhzd2FybS1jYTBZMBMGByqGSM49AgEGCCqGSM49AwEH\nA0IABDy7NebyUJyUjWJDBUdnZoV6GBxEGKO4TZPNDwnxDxJcUdLVaB7WGa4/DLrW\nUfsVgh1JGik2VTiLuTMA1tLlNPOjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMB\nAf8EBTADAQH/MB0GA1UdDgQWBBQl16XFtaaXiUAwEuJptJlDjfKskDAKBggqhkjO\nPQQDAgNIADBFAiEAo9fTQNM5DP9bHVcTJYfl2Cay1bFu1E+lnpmN+EYJfeACIGKH\n1pCUkZ+D0IB6CiEZGWSHyLuXPM1rlP+I5KuS7sB8\n-----END CERTIFICATE-----\n","CertIssuerSubject":"MBMxETAPBgNVBAMTCHN3YXJtLWNh","CertIssuerPublicKey":"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEPLs15vJQnJSNYkMFR2dmhXoYHEQYo7hNk80PCfEPElxR0tVoHtYZrj8MutZR+xWCHUkaKTZVOIu5MwDW0uU08w=="},"RootRotationInProgress":false,"DefaultAddrPool":null,"SubnetSize":0,"DataPathPort":0}},"LiveRestoreEnabled":false,"Isolation":"","InitBinary":"docker-init","ContainerdCommit":{"ID":"6e23458c129b551d5c9871e5174f6b1b7f6d1170","Expected":"6e23458c129b551d5c9871e5174f6b1b7f6d1170"},"RuncCommit":{"ID":"810190ceaa507aa2727d7ae6f4790c76ec150bd2","Expected":"810190ceaa507aa2727d7ae6f4790c76ec150bd2"},"InitCommit":{"ID":"949e6fa","Expected":"949e6fa"},"SecurityOptions":["name=apparmor","name=seccomp,profile=default"],"Warnings":null,"ClientInfo":{"Debug":false,"Warnings":null}}
|
{"ID":"EKHL:QDUU:QZ7U:MKGD:VDXK:S27Q:GIPU:24B7:R7VT:DGN6:QCSF:2UBX","Containers":0,"ContainersRunning":0,"ContainersPaused":0,"ContainersStopped":0,"Images":0,"Driver":"aufs","DriverStatus":[["Root Dir","/var/lib/docker/aufs"],["Backing Filesystem","extfs"],["Dirs","0"],["Dirperm1 Supported","true"]],"SystemStatus":null,"Plugins":{"Volume":["local"],"Network":["bridge","host","macvlan","null","overlay"],"Authorization":null,"Log":["awslogs","fluentd","gcplogs","gelf","journald","json-file","logentries","splunk","syslog"]},"MemoryLimit":true,"SwapLimit":true,"KernelMemory":true,"KernelMemoryTCP":false,"CpuCfsPeriod":true,"CpuCfsQuota":true,"CPUShares":true,"CPUSet":true,"IPv4Forwarding":true,"BridgeNfIptables":true,"BridgeNfIp6tables":true,"Debug":true,"NFd":33,"OomKillDisable":true,"NGoroutines":135,"SystemTime":"2017-08-24T17:44:34.077811894Z","LoggingDriver":"json-file","CgroupDriver":"cgroupfs","NEventsListener":0,"KernelVersion":"4.4.0-87-generic","OperatingSystem":"Ubuntu 16.04.3 LTS","OSType":"linux","Architecture":"x86_64","IndexServerAddress":"https://index.docker.io/v1/","RegistryConfig":{"AllowNondistributableArtifactsCIDRs":null,"AllowNondistributableArtifactsHostnames":null,"InsecureRegistryCIDRs":["127.0.0.0/8"],"IndexConfigs":{"docker.io":{"Name":"docker.io","Mirrors":null,"Secure":true,"Official":true}},"Mirrors":null},"NCPU":2,"MemTotal":2097356800,"GenericResources":null,"DockerRootDir":"/var/lib/docker","HttpProxy":"","HttpsProxy":"","NoProxy":"","Name":"system-sample","Labels":["provider=digitalocean"],"ExperimentalBuild":false,"ServerVersion":"17.06.1-ce","ClusterStore":"","ClusterAdvertise":"","Runtimes":{"runc":{"path":"docker-runc"}},"DefaultRuntime":"runc","Swarm":{"NodeID":"qo2dfdig9mmxqkawulggepdih","NodeAddr":"165.227.107.89","LocalNodeState":"active","ControlAvailable":true,"Error":"","RemoteManagers":[{"NodeID":"qo2dfdig9mmxqkawulggepdih","Addr":"165.227.107.89:2377"}],"Nodes":1,"Managers":1,"Cluster":{"ID":"9vs5ygs0gguyyec4iqf2314c0","Version":{"Index":11},"CreatedAt":"2017-08-24T17:34:19.278062352Z","UpdatedAt":"2017-08-24T17:34:42.398815481Z","Spec":{"Name":"default","Labels":null,"Orchestration":{"TaskHistoryRetentionLimit":5},"Raft":{"SnapshotInterval":10000,"KeepOldSnapshots":0,"LogEntriesForSlowFollowers":500,"ElectionTick":3,"HeartbeatTick":1},"Dispatcher":{"HeartbeatPeriod":5000000000},"CAConfig":{"NodeCertExpiry":7776000000000000},"TaskDefaults":{},"EncryptionConfig":{"AutoLockManagers":true}},"TLSInfo":{"TrustRoot":"\n-----BEGIN CERTIFICATE-----\nMIIBajCCARCgAwIBAgIUaFCW5xsq8eyiJ+Pmcv3MCflMLnMwCgYIKoZIzj0EAwIw\nEzERMA8GA1UEAxMIc3dhcm0tY2EwHhcNMTcwODI0MTcyOTAwWhcNMzcwODE5MTcy\nOTAwWjATMREwDwYDVQQDEwhzd2FybS1jYTBZMBMGByqGSM49AgEGCCqGSM49AwEH\nA0IABDy7NebyUJyUjWJDBUdnZoV6GBxEGKO4TZPNDwnxDxJcUdLVaB7WGa4/DLrW\nUfsVgh1JGik2VTiLuTMA1tLlNPOjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMB\nAf8EBTADAQH/MB0GA1UdDgQWBBQl16XFtaaXiUAwEuJptJlDjfKskDAKBggqhkjO\nPQQDAgNIADBFAiEAo9fTQNM5DP9bHVcTJYfl2Cay1bFu1E+lnpmN+EYJfeACIGKH\n1pCUkZ+D0IB6CiEZGWSHyLuXPM1rlP+I5KuS7sB8\n-----END CERTIFICATE-----\n","CertIssuerSubject":"MBMxETAPBgNVBAMTCHN3YXJtLWNh","CertIssuerPublicKey":"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEPLs15vJQnJSNYkMFR2dmhXoYHEQYo7hNk80PCfEPElxR0tVoHtYZrj8MutZR+xWCHUkaKTZVOIu5MwDW0uU08w=="},"RootRotationInProgress":false,"DefaultAddrPool":null,"SubnetSize":0,"DataPathPort":0}},"LiveRestoreEnabled":false,"Isolation":"","InitBinary":"docker-init","ContainerdCommit":{"ID":"6e23458c129b551d5c9871e5174f6b1b7f6d1170","Expected":"6e23458c129b551d5c9871e5174f6b1b7f6d1170"},"RuncCommit":{"ID":"810190ceaa507aa2727d7ae6f4790c76ec150bd2","Expected":"810190ceaa507aa2727d7ae6f4790c76ec150bd2"},"InitCommit":{"ID":"949e6fa","Expected":"949e6fa"},"SecurityOptions":["name=apparmor","name=seccomp,profile=default"],"Warnings":null,"ClientInfo":{"Debug":false,"Plugins":[],"Warnings":null}}
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"github.com/docker/cli/cli"
|
"github.com/docker/cli/cli"
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
kubecontext "github.com/docker/cli/cli/context/kubernetes"
|
kubecontext "github.com/docker/cli/cli/context/kubernetes"
|
||||||
|
"github.com/docker/cli/cli/version"
|
||||||
"github.com/docker/cli/kubernetes"
|
"github.com/docker/cli/kubernetes"
|
||||||
"github.com/docker/cli/templates"
|
"github.com/docker/cli/templates"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
|
@ -135,13 +136,13 @@ func runVersion(dockerCli command.Cli, opts *versionOptions) error {
|
||||||
|
|
||||||
vd := versionInfo{
|
vd := versionInfo{
|
||||||
Client: clientVersion{
|
Client: clientVersion{
|
||||||
Platform: struct{ Name string }{cli.PlatformName},
|
Platform: struct{ Name string }{version.PlatformName},
|
||||||
Version: cli.Version,
|
Version: version.Version,
|
||||||
APIVersion: dockerCli.Client().ClientVersion(),
|
APIVersion: dockerCli.Client().ClientVersion(),
|
||||||
DefaultAPIVersion: dockerCli.DefaultVersion(),
|
DefaultAPIVersion: dockerCli.DefaultVersion(),
|
||||||
GoVersion: runtime.Version(),
|
GoVersion: runtime.Version(),
|
||||||
GitCommit: cli.GitCommit,
|
GitCommit: version.GitCommit,
|
||||||
BuildTime: reformatDate(cli.BuildTime),
|
BuildTime: reformatDate(version.BuildTime),
|
||||||
Os: runtime.GOOS,
|
Os: runtime.GOOS,
|
||||||
Arch: runtime.GOARCH,
|
Arch: runtime.GOARCH,
|
||||||
Experimental: dockerCli.ClientInfo().HasExperimental,
|
Experimental: dockerCli.ClientInfo().HasExperimental,
|
||||||
|
|
|
@ -46,6 +46,11 @@ func SetDir(dir string) {
|
||||||
configDir = dir
|
configDir = dir
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Path returns the path to a file relative to the config dir
|
||||||
|
func Path(p ...string) string {
|
||||||
|
return filepath.Join(append([]string{Dir()}, p...)...)
|
||||||
|
}
|
||||||
|
|
||||||
// LegacyLoadFromReader is a convenience function that creates a ConfigFile object from
|
// LegacyLoadFromReader is a convenience function that creates a ConfigFile object from
|
||||||
// a non-nested reader
|
// a non-nested reader
|
||||||
func LegacyLoadFromReader(configData io.Reader) (*configfile.ConfigFile, error) {
|
func LegacyLoadFromReader(configData io.Reader) (*configfile.ConfigFile, error) {
|
||||||
|
|
|
@ -548,3 +548,17 @@ func TestLoadDefaultConfigFile(t *testing.T) {
|
||||||
|
|
||||||
assert.Check(t, is.DeepEqual(expected, configFile))
|
assert.Check(t, is.DeepEqual(expected, configFile))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestConfigPath(t *testing.T) {
|
||||||
|
oldDir := Dir()
|
||||||
|
|
||||||
|
SetDir("dummy1")
|
||||||
|
f1 := Path("a", "b")
|
||||||
|
assert.Equal(t, f1, filepath.Join("dummy1", "a", "b"))
|
||||||
|
|
||||||
|
SetDir("dummy2")
|
||||||
|
f2 := Path("c", "d")
|
||||||
|
assert.Equal(t, f2, filepath.Join("dummy2", "c", "d"))
|
||||||
|
|
||||||
|
SetDir(oldDir)
|
||||||
|
}
|
||||||
|
|
|
@ -49,6 +49,7 @@ type ConfigFile struct {
|
||||||
StackOrchestrator string `json:"stackOrchestrator,omitempty"`
|
StackOrchestrator string `json:"stackOrchestrator,omitempty"`
|
||||||
Kubernetes *KubernetesConfig `json:"kubernetes,omitempty"`
|
Kubernetes *KubernetesConfig `json:"kubernetes,omitempty"`
|
||||||
CurrentContext string `json:"currentContext,omitempty"`
|
CurrentContext string `json:"currentContext,omitempty"`
|
||||||
|
CLIPluginsExtraDirs []string `json:"cliPluginsExtraDirs,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProxyConfig contains proxy configuration settings
|
// ProxyConfig contains proxy configuration settings
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package cli
|
package version
|
||||||
|
|
||||||
// Default build-time variable.
|
// Default build-time variable.
|
||||||
// These values are overridden via ldflags
|
// These values are overridden via ldflags
|
|
@ -7,11 +7,11 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/docker/cli/cli"
|
"github.com/docker/cli/cli"
|
||||||
|
pluginmanager "github.com/docker/cli/cli-plugins/manager"
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
"github.com/docker/cli/cli/command/commands"
|
"github.com/docker/cli/cli/command/commands"
|
||||||
cliconfig "github.com/docker/cli/cli/config"
|
|
||||||
"github.com/docker/cli/cli/debug"
|
|
||||||
cliflags "github.com/docker/cli/cli/flags"
|
cliflags "github.com/docker/cli/cli/flags"
|
||||||
|
"github.com/docker/cli/cli/version"
|
||||||
"github.com/docker/docker/api/types/versions"
|
"github.com/docker/docker/api/types/versions"
|
||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
@ -20,8 +20,11 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func newDockerCommand(dockerCli *command.DockerCli) *cobra.Command {
|
func newDockerCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||||
opts := cliflags.NewClientOptions()
|
var (
|
||||||
var flags *pflag.FlagSet
|
opts *cliflags.ClientOptions
|
||||||
|
flags *pflag.FlagSet
|
||||||
|
helpCmd *cobra.Command
|
||||||
|
)
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "docker [OPTIONS] COMMAND [ARG...]",
|
Use: "docker [OPTIONS] COMMAND [ARG...]",
|
||||||
|
@ -29,49 +32,59 @@ func newDockerCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||||
SilenceUsage: true,
|
SilenceUsage: true,
|
||||||
SilenceErrors: true,
|
SilenceErrors: true,
|
||||||
TraverseChildren: true,
|
TraverseChildren: true,
|
||||||
Args: noArgs,
|
FParseErrWhitelist: cobra.FParseErrWhitelist{
|
||||||
|
// UnknownFlags ignores any unknown
|
||||||
|
// --arguments on the top-level docker command
|
||||||
|
// only. This is necessary to allow passing
|
||||||
|
// --arguments to plugins otherwise
|
||||||
|
// e.g. `docker plugin --foo` is caught here
|
||||||
|
// in the monolithic CLI and `foo` is reported
|
||||||
|
// as an unknown argument.
|
||||||
|
UnknownFlags: true,
|
||||||
|
},
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return command.ShowHelp(dockerCli.Err())(cmd, args)
|
if len(args) == 0 {
|
||||||
|
return command.ShowHelp(dockerCli.Err())(cmd, args)
|
||||||
|
}
|
||||||
|
plugincmd, err := pluginmanager.PluginRunCommand(dockerCli, args[0], cmd)
|
||||||
|
if pluginmanager.IsNotFound(err) {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"docker: '%s' is not a docker command.\nSee 'docker --help'", args[0])
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return plugincmd.Run()
|
||||||
},
|
},
|
||||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||||
// flags must be the top-level command flags, not cmd.Flags()
|
// flags must be the top-level command flags, not cmd.Flags()
|
||||||
opts.Common.SetDefaultOptions(flags)
|
opts.Common.SetDefaultOptions(flags)
|
||||||
dockerPreRun(opts)
|
|
||||||
if err := dockerCli.Initialize(opts); err != nil {
|
if err := dockerCli.Initialize(opts); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return isSupported(cmd, dockerCli)
|
return isSupported(cmd, dockerCli)
|
||||||
},
|
},
|
||||||
Version: fmt.Sprintf("%s, build %s", cli.Version, cli.GitCommit),
|
Version: fmt.Sprintf("%s, build %s", version.Version, version.GitCommit),
|
||||||
DisableFlagsInUseLine: true,
|
DisableFlagsInUseLine: true,
|
||||||
}
|
}
|
||||||
cli.SetupRootCommand(cmd)
|
opts, flags, helpCmd = cli.SetupRootCommand(cmd)
|
||||||
|
|
||||||
flags = cmd.Flags()
|
|
||||||
flags.BoolP("version", "v", false, "Print version information and quit")
|
flags.BoolP("version", "v", false, "Print version information and quit")
|
||||||
flags.StringVar(&opts.ConfigDir, "config", cliconfig.Dir(), "Location of client config files")
|
|
||||||
opts.Common.InstallFlags(flags)
|
|
||||||
|
|
||||||
setFlagErrorFunc(dockerCli, cmd, flags, opts)
|
setFlagErrorFunc(dockerCli, cmd, flags, opts)
|
||||||
|
|
||||||
|
setupHelpCommand(dockerCli, cmd, helpCmd, flags, opts)
|
||||||
setHelpFunc(dockerCli, cmd, flags, opts)
|
setHelpFunc(dockerCli, cmd, flags, opts)
|
||||||
|
|
||||||
cmd.SetOutput(dockerCli.Out())
|
cmd.SetOutput(dockerCli.Out())
|
||||||
commands.AddCommands(cmd, dockerCli)
|
commands.AddCommands(cmd, dockerCli)
|
||||||
|
|
||||||
disableFlagsInUseLine(cmd)
|
cli.DisableFlagsInUseLine(cmd)
|
||||||
setValidateArgs(dockerCli, cmd, flags, opts)
|
setValidateArgs(dockerCli, cmd, flags, opts)
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func disableFlagsInUseLine(cmd *cobra.Command) {
|
|
||||||
visitAll(cmd, func(ccmd *cobra.Command) {
|
|
||||||
// do not add a `[flags]` to the end of the usage line.
|
|
||||||
ccmd.DisableFlagsInUseLine = true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func setFlagErrorFunc(dockerCli *command.DockerCli, cmd *cobra.Command, flags *pflag.FlagSet, opts *cliflags.ClientOptions) {
|
func setFlagErrorFunc(dockerCli *command.DockerCli, cmd *cobra.Command, flags *pflag.FlagSet, opts *cliflags.ClientOptions) {
|
||||||
// When invoking `docker stack --nonsense`, we need to make sure FlagErrorFunc return appropriate
|
// When invoking `docker stack --nonsense`, we need to make sure FlagErrorFunc return appropriate
|
||||||
// output if the feature is not supported.
|
// output if the feature is not supported.
|
||||||
|
@ -89,6 +102,51 @@ func setFlagErrorFunc(dockerCli *command.DockerCli, cmd *cobra.Command, flags *p
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setupHelpCommand(dockerCli *command.DockerCli, rootCmd, helpCmd *cobra.Command, flags *pflag.FlagSet, opts *cliflags.ClientOptions) {
|
||||||
|
origRun := helpCmd.Run
|
||||||
|
origRunE := helpCmd.RunE
|
||||||
|
|
||||||
|
helpCmd.Run = nil
|
||||||
|
helpCmd.RunE = func(c *cobra.Command, args []string) error {
|
||||||
|
// No Persistent* hooks are called for help, so we must initialize here.
|
||||||
|
if err := initializeDockerCli(dockerCli, flags, opts); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(args) > 0 {
|
||||||
|
helpcmd, err := pluginmanager.PluginRunCommand(dockerCli, args[0], rootCmd)
|
||||||
|
if err == nil {
|
||||||
|
err = helpcmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !pluginmanager.IsNotFound(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if origRunE != nil {
|
||||||
|
return origRunE(c, args)
|
||||||
|
}
|
||||||
|
origRun(c, args)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func tryRunPluginHelp(dockerCli command.Cli, ccmd *cobra.Command, cargs []string) error {
|
||||||
|
root := ccmd.Root()
|
||||||
|
|
||||||
|
cmd, _, err := root.Traverse(cargs)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
helpcmd, err := pluginmanager.PluginRunCommand(dockerCli, cmd.Name(), root)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return helpcmd.Run()
|
||||||
|
}
|
||||||
|
|
||||||
func setHelpFunc(dockerCli *command.DockerCli, cmd *cobra.Command, flags *pflag.FlagSet, opts *cliflags.ClientOptions) {
|
func setHelpFunc(dockerCli *command.DockerCli, cmd *cobra.Command, flags *pflag.FlagSet, opts *cliflags.ClientOptions) {
|
||||||
defaultHelpFunc := cmd.HelpFunc()
|
defaultHelpFunc := cmd.HelpFunc()
|
||||||
cmd.SetHelpFunc(func(ccmd *cobra.Command, args []string) {
|
cmd.SetHelpFunc(func(ccmd *cobra.Command, args []string) {
|
||||||
|
@ -96,6 +154,28 @@ func setHelpFunc(dockerCli *command.DockerCli, cmd *cobra.Command, flags *pflag.
|
||||||
ccmd.Println(err)
|
ccmd.Println(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add a stub entry for every plugin so they are
|
||||||
|
// included in the help output and so that
|
||||||
|
// `tryRunPluginHelp` can find them or if we fall
|
||||||
|
// through they will be included in the default help
|
||||||
|
// output.
|
||||||
|
if err := pluginmanager.AddPluginCommandStubs(dockerCli, ccmd.Root()); err != nil {
|
||||||
|
ccmd.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(args) >= 1 {
|
||||||
|
err := tryRunPluginHelp(dockerCli, ccmd, args)
|
||||||
|
if err == nil { // Successfully ran the plugin
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !pluginmanager.IsNotFound(err) {
|
||||||
|
ccmd.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err := isSupported(ccmd, dockerCli); err != nil {
|
if err := isSupported(ccmd, dockerCli); err != nil {
|
||||||
ccmd.Println(err)
|
ccmd.Println(err)
|
||||||
return
|
return
|
||||||
|
@ -104,6 +184,7 @@ func setHelpFunc(dockerCli *command.DockerCli, cmd *cobra.Command, flags *pflag.
|
||||||
ccmd.Println(err)
|
ccmd.Println(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultHelpFunc(ccmd, args)
|
defaultHelpFunc(ccmd, args)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -113,7 +194,7 @@ func setValidateArgs(dockerCli *command.DockerCli, cmd *cobra.Command, flags *pf
|
||||||
// As a result, here we replace the existing Args validation func to a wrapper,
|
// As a result, here we replace the existing Args validation func to a wrapper,
|
||||||
// where the wrapper will check to see if the feature is supported or not.
|
// where the wrapper will check to see if the feature is supported or not.
|
||||||
// The Args validation error will only be returned if the feature is supported.
|
// The Args validation error will only be returned if the feature is supported.
|
||||||
visitAll(cmd, func(ccmd *cobra.Command) {
|
cli.VisitAll(cmd, func(ccmd *cobra.Command) {
|
||||||
// if there is no tags for a command or any of its parent,
|
// if there is no tags for a command or any of its parent,
|
||||||
// there is no need to wrap the Args validation.
|
// there is no need to wrap the Args validation.
|
||||||
if !hasTags(ccmd) {
|
if !hasTags(ccmd) {
|
||||||
|
@ -144,28 +225,9 @@ func initializeDockerCli(dockerCli *command.DockerCli, flags *pflag.FlagSet, opt
|
||||||
// when using --help, PersistentPreRun is not called, so initialization is needed.
|
// when using --help, PersistentPreRun is not called, so initialization is needed.
|
||||||
// flags must be the top-level command flags, not cmd.Flags()
|
// flags must be the top-level command flags, not cmd.Flags()
|
||||||
opts.Common.SetDefaultOptions(flags)
|
opts.Common.SetDefaultOptions(flags)
|
||||||
dockerPreRun(opts)
|
|
||||||
return dockerCli.Initialize(opts)
|
return dockerCli.Initialize(opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
// visitAll will traverse all commands from the root.
|
|
||||||
// This is different from the VisitAll of cobra.Command where only parents
|
|
||||||
// are checked.
|
|
||||||
func visitAll(root *cobra.Command, fn func(*cobra.Command)) {
|
|
||||||
for _, cmd := range root.Commands() {
|
|
||||||
visitAll(cmd, fn)
|
|
||||||
}
|
|
||||||
fn(root)
|
|
||||||
}
|
|
||||||
|
|
||||||
func noArgs(cmd *cobra.Command, args []string) error {
|
|
||||||
if len(args) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return fmt.Errorf(
|
|
||||||
"docker: '%s' is not a docker command.\nSee 'docker --help'", args[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
dockerCli, err := command.NewDockerCli()
|
dockerCli, err := command.NewDockerCli()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -193,18 +255,6 @@ func main() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func dockerPreRun(opts *cliflags.ClientOptions) {
|
|
||||||
cliflags.SetLogLevel(opts.Common.LogLevel)
|
|
||||||
|
|
||||||
if opts.ConfigDir != "" {
|
|
||||||
cliconfig.SetDir(opts.ConfigDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
if opts.Common.Debug {
|
|
||||||
debug.Enable()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type versionDetails interface {
|
type versionDetails interface {
|
||||||
Client() client.APIClient
|
Client() client.APIClient
|
||||||
ClientInfo() command.ClientInfo
|
ClientInfo() command.ClientInfo
|
||||||
|
|
|
@ -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 e2e/cli-plugins/plugins/*
|
||||||
|
|
||||||
CMD ./scripts/test/e2e/entry
|
CMD ./scripts/test/e2e/entry
|
||||||
|
|
|
@ -0,0 +1,98 @@
|
||||||
|
---
|
||||||
|
description: "Writing Docker CLI Plugins"
|
||||||
|
keywords: "docker, cli plugin"
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- This file is maintained within the docker/cli GitHub
|
||||||
|
repository at https://github.com/docker/cli/. Make all
|
||||||
|
pull requests against that repo. If you see this file in
|
||||||
|
another repository, consider it read-only there, as it will
|
||||||
|
periodically be overwritten by the definitive file. Pull
|
||||||
|
requests which include edits to this file in other repositories
|
||||||
|
will be rejected.
|
||||||
|
-->
|
||||||
|
|
||||||
|
# Docker CLI Plugin Spec
|
||||||
|
|
||||||
|
The `docker` CLI supports adding additional top-level subcommands as
|
||||||
|
additional out-of-process commands which can be installed
|
||||||
|
independently. These plugins run on the client side and should not be
|
||||||
|
confused with "plugins" which run on the server.
|
||||||
|
|
||||||
|
This document contains information for authors of such plugins.
|
||||||
|
|
||||||
|
## Requirements for CLI Plugins
|
||||||
|
|
||||||
|
### Naming
|
||||||
|
|
||||||
|
A valid CLI plugin name consists only of lower case letters `a-z`
|
||||||
|
and the digits `0-9`. The leading character must be a letter. A valid
|
||||||
|
name therefore would match the regex `^[a-z][a-z0-9]*$`.
|
||||||
|
|
||||||
|
The binary implementing a plugin must be named `docker-$name` where
|
||||||
|
`$name` is the name of the plugin. On Windows a `.exe` suffix is
|
||||||
|
mandatory.
|
||||||
|
|
||||||
|
## Required sub-commands
|
||||||
|
|
||||||
|
A CLI plugin must support being invoked in at least these two ways:
|
||||||
|
|
||||||
|
* `docker-$name docker-cli-plugin-metadata` -- outputs metadata about
|
||||||
|
the plugin.
|
||||||
|
* `docker-$name [GLOBAL OPTIONS] $name [OPTIONS AND FURTHER SUB
|
||||||
|
COMMANDS]` -- the primary entry point to the plugin's functionality.
|
||||||
|
|
||||||
|
A plugin may implement other subcommands but these will never be
|
||||||
|
invoked by the current Docker CLI. However doing so is strongly
|
||||||
|
discouraged: new subcommands may be added in the future without
|
||||||
|
consideration for additional non-specified subcommands which may be
|
||||||
|
used by plugins in the field.
|
||||||
|
|
||||||
|
### The `docker-cli-plugin-metadata` subcommand
|
||||||
|
|
||||||
|
When invoked in this manner the plugin must produce a JSON object
|
||||||
|
(and nothing else) on its standard output and exit success (0).
|
||||||
|
|
||||||
|
The JSON object has the following defined keys:
|
||||||
|
* `SchemaVersion` (_string_) mandatory: must contain precisely "0.1.0".
|
||||||
|
* `Vendor` (_string_) mandatory: contains the name of the plugin vendor/author. May be truncated to 11 characters in some display contexts.
|
||||||
|
* `ShortDescription` (_string_) optional: a short description of the plugin, suitable for a single line help message.
|
||||||
|
* `Version` (_string_) optional: the version of the plugin, this is considered to be an opaque string by the core and therefore has no restrictions on its syntax.
|
||||||
|
* `URL` (_string_) optional: a pointer to the plugin's web page.
|
||||||
|
|
||||||
|
A binary which does not correctly output the metadata
|
||||||
|
(e.g. syntactically invalid, missing mandatory keys etc) is not
|
||||||
|
considered a valid CLI plugin and will not be run.
|
||||||
|
|
||||||
|
### The primary entry point subcommand
|
||||||
|
|
||||||
|
This is the entry point for actually running the plugin. It maybe have
|
||||||
|
options or further subcommands.
|
||||||
|
|
||||||
|
#### Required global options
|
||||||
|
|
||||||
|
A plugin is required to support all of the global options of the
|
||||||
|
top-level CLI, i.e. those listed by `man docker 1` with the exception
|
||||||
|
of `-v`.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Plugins distributed in packages for system wide installation on
|
||||||
|
Unix(-like) systems should be installed in either
|
||||||
|
`/usr/lib/docker/cli-plugins` or `/usr/libexec/docker/cli-plugins`
|
||||||
|
depending on which of `/usr/lib` and `/usr/libexec` is usual on that
|
||||||
|
system. System Administrators may also choose to manually install into
|
||||||
|
the `/usr/local/lib` or `/usr/local/libexec` equivalents but packages
|
||||||
|
should not do so.
|
||||||
|
|
||||||
|
Plugins distributed on Windows for system wide installation should be
|
||||||
|
installed in `%PROGRAMDATA%\Docker\cli-plugins`.
|
||||||
|
|
||||||
|
User's may on all systems install plugins into `~/.docker/cli-plugins`.
|
||||||
|
|
||||||
|
## Implementing a plugin in Go
|
||||||
|
|
||||||
|
When writing a plugin in Go the easiest way to meet the above
|
||||||
|
requirements is to simply call the
|
||||||
|
`github.com/docker/cli/cli-plugins/plugin.Run` method from your `main`
|
||||||
|
function to instantiate the plugin.
|
|
@ -0,0 +1,91 @@
|
||||||
|
package cliplugins
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gotest.tools/assert"
|
||||||
|
is "gotest.tools/assert/cmp"
|
||||||
|
"gotest.tools/icmd"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestGlobalHelp ensures correct behaviour when running `docker help`
|
||||||
|
func TestGlobalHelp(t *testing.T) {
|
||||||
|
run, cleanup := prepare(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
res := icmd.RunCmd(run("help"))
|
||||||
|
res.Assert(t, icmd.Expected{
|
||||||
|
ExitCode: 0,
|
||||||
|
})
|
||||||
|
assert.Assert(t, is.Equal(res.Stderr(), ""))
|
||||||
|
scanner := bufio.NewScanner(strings.NewReader(res.Stdout()))
|
||||||
|
|
||||||
|
// Instead of baking in the full current output of `docker
|
||||||
|
// help`, which can be expected to change regularly, bake in
|
||||||
|
// some checkpoints. Key things we are looking for:
|
||||||
|
//
|
||||||
|
// - The top-level description
|
||||||
|
// - Each of the main headings
|
||||||
|
// - Some builtin commands under the main headings
|
||||||
|
// - The `helloworld` plugin in the appropriate place
|
||||||
|
// - The `badmeta` plugin under the "Invalid Plugins" heading.
|
||||||
|
//
|
||||||
|
// Regexps are needed because the width depends on `unix.TIOCGWINSZ` or similar.
|
||||||
|
helloworldre := regexp.MustCompile(`^ helloworld\s+\(Docker Inc\.\)\s+A basic Hello World plugin for tests$`)
|
||||||
|
badmetare := regexp.MustCompile(`^ badmeta\s+invalid metadata: invalid character 'i' looking for beginning of object key string$`)
|
||||||
|
var helloworldcount, badmetacount int
|
||||||
|
for _, expected := range []*regexp.Regexp{
|
||||||
|
regexp.MustCompile(`^A self-sufficient runtime for containers$`),
|
||||||
|
regexp.MustCompile(`^Management Commands:$`),
|
||||||
|
regexp.MustCompile(`^ container\s+Manage containers$`),
|
||||||
|
regexp.MustCompile(`^Commands:$`),
|
||||||
|
regexp.MustCompile(`^ create\s+Create a new container$`),
|
||||||
|
helloworldre,
|
||||||
|
regexp.MustCompile(`^ ps\s+List containers$`),
|
||||||
|
regexp.MustCompile(`^Invalid Plugins:$`),
|
||||||
|
badmetare,
|
||||||
|
nil, // scan to end of input rather than stopping at badmetare
|
||||||
|
} {
|
||||||
|
var found bool
|
||||||
|
for scanner.Scan() {
|
||||||
|
text := scanner.Text()
|
||||||
|
if helloworldre.MatchString(text) {
|
||||||
|
helloworldcount++
|
||||||
|
}
|
||||||
|
if badmetare.MatchString(text) {
|
||||||
|
badmetacount++
|
||||||
|
}
|
||||||
|
|
||||||
|
if expected != nil && expected.MatchString(text) {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.Assert(t, expected == nil || found, "Did not find match for %q in `docker help` output", expected)
|
||||||
|
}
|
||||||
|
// We successfully scanned all the input
|
||||||
|
assert.Assert(t, !scanner.Scan())
|
||||||
|
assert.NilError(t, scanner.Err())
|
||||||
|
// Plugins should only be listed once.
|
||||||
|
assert.Assert(t, is.Equal(helloworldcount, 1))
|
||||||
|
assert.Assert(t, is.Equal(badmetacount, 1))
|
||||||
|
|
||||||
|
// Running with `--help` should produce the same.
|
||||||
|
res2 := icmd.RunCmd(run("--help"))
|
||||||
|
res2.Assert(t, icmd.Expected{
|
||||||
|
ExitCode: 0,
|
||||||
|
})
|
||||||
|
assert.Assert(t, is.Equal(res2.Stdout(), res.Stdout()))
|
||||||
|
assert.Assert(t, is.Equal(res2.Stderr(), ""))
|
||||||
|
|
||||||
|
// Running just `docker` (without `help` nor `--help`) should produce the same thing, except on Stderr.
|
||||||
|
res2 = icmd.RunCmd(run())
|
||||||
|
res2.Assert(t, icmd.Expected{
|
||||||
|
ExitCode: 0,
|
||||||
|
})
|
||||||
|
assert.Assert(t, is.Equal(res2.Stdout(), ""))
|
||||||
|
assert.Assert(t, is.Equal(res2.Stderr(), res.Stdout()))
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
// This is not a real plugin, but just returns malformated metadata
|
||||||
|
// from the subcommand and otherwise exits with failure.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/docker/cli/cli-plugins/manager"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if len(os.Args) == 2 && os.Args[1] == manager.MetadataSubcommandName {
|
||||||
|
fmt.Println(`{invalid-json}`)
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
|
@ -0,0 +1,196 @@
|
||||||
|
package cliplugins
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gotest.tools/assert"
|
||||||
|
is "gotest.tools/assert/cmp"
|
||||||
|
"gotest.tools/golden"
|
||||||
|
"gotest.tools/icmd"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestRunNonexisting ensures correct behaviour when running a nonexistent plugin.
|
||||||
|
func TestRunNonexisting(t *testing.T) {
|
||||||
|
run, cleanup := prepare(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
res := icmd.RunCmd(run("nonexistent"))
|
||||||
|
res.Assert(t, icmd.Expected{
|
||||||
|
ExitCode: 1,
|
||||||
|
})
|
||||||
|
assert.Assert(t, is.Equal(res.Stdout(), ""))
|
||||||
|
golden.Assert(t, res.Stderr(), "docker-nonexistent-err.golden")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHelpNonexisting ensures correct behaviour when invoking help on a nonexistent plugin.
|
||||||
|
func TestHelpNonexisting(t *testing.T) {
|
||||||
|
run, cleanup := prepare(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
res := icmd.RunCmd(run("help", "nonexistent"))
|
||||||
|
res.Assert(t, icmd.Expected{
|
||||||
|
ExitCode: 1,
|
||||||
|
})
|
||||||
|
assert.Assert(t, is.Equal(res.Stdout(), ""))
|
||||||
|
golden.Assert(t, res.Stderr(), "docker-help-nonexistent-err.golden")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestNonexistingHelp ensures correct behaviour when invoking a
|
||||||
|
// nonexistent plugin with `--help`.
|
||||||
|
func TestNonexistingHelp(t *testing.T) {
|
||||||
|
run, cleanup := prepare(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
res := icmd.RunCmd(run("nonexistent", "--help"))
|
||||||
|
res.Assert(t, icmd.Expected{
|
||||||
|
ExitCode: 0,
|
||||||
|
// This should actually be the whole docker help
|
||||||
|
// output, so spot check instead having of a golden
|
||||||
|
// with everything in, which will change too frequently.
|
||||||
|
Out: "Usage: docker [OPTIONS] COMMAND\n\nA self-sufficient runtime for containers",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRunBad ensures correct behaviour when running an existent but invalid plugin
|
||||||
|
func TestRunBad(t *testing.T) {
|
||||||
|
run, cleanup := prepare(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
res := icmd.RunCmd(run("badmeta"))
|
||||||
|
res.Assert(t, icmd.Expected{
|
||||||
|
ExitCode: 1,
|
||||||
|
})
|
||||||
|
assert.Assert(t, is.Equal(res.Stdout(), ""))
|
||||||
|
golden.Assert(t, res.Stderr(), "docker-badmeta-err.golden")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHelpBad ensures correct behaviour when invoking help on a existent but invalid plugin.
|
||||||
|
func TestHelpBad(t *testing.T) {
|
||||||
|
run, cleanup := prepare(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
res := icmd.RunCmd(run("help", "badmeta"))
|
||||||
|
res.Assert(t, icmd.Expected{
|
||||||
|
ExitCode: 1,
|
||||||
|
})
|
||||||
|
assert.Assert(t, is.Equal(res.Stdout(), ""))
|
||||||
|
golden.Assert(t, res.Stderr(), "docker-help-badmeta-err.golden")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestBadHelp ensures correct behaviour when invoking an
|
||||||
|
// existent but invalid plugin with `--help`.
|
||||||
|
func TestBadHelp(t *testing.T) {
|
||||||
|
run, cleanup := prepare(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
res := icmd.RunCmd(run("badmeta", "--help"))
|
||||||
|
res.Assert(t, icmd.Expected{
|
||||||
|
ExitCode: 0,
|
||||||
|
// This should be literally the whole docker help
|
||||||
|
// output, so spot check instead of a golden with
|
||||||
|
// everything in which will change all the time.
|
||||||
|
Out: "Usage: docker [OPTIONS] COMMAND\n\nA self-sufficient runtime for containers",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRunGood ensures correct behaviour when running a valid plugin
|
||||||
|
func TestRunGood(t *testing.T) {
|
||||||
|
run, cleanup := prepare(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
res := icmd.RunCmd(run("helloworld"))
|
||||||
|
res.Assert(t, icmd.Expected{
|
||||||
|
ExitCode: 0,
|
||||||
|
Out: "Hello World!",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHelpGood ensures correct behaviour when invoking help on a
|
||||||
|
// valid plugin. A global argument is included to ensure it does not
|
||||||
|
// interfere.
|
||||||
|
func TestHelpGood(t *testing.T) {
|
||||||
|
run, cleanup := prepare(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
res := icmd.RunCmd(run("-D", "help", "helloworld"))
|
||||||
|
res.Assert(t, icmd.Success)
|
||||||
|
golden.Assert(t, res.Stdout(), "docker-help-helloworld.golden")
|
||||||
|
assert.Assert(t, is.Equal(res.Stderr(), ""))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestGoodHelp ensures correct behaviour when calling a valid plugin
|
||||||
|
// with `--help`. A global argument is used to ensure it does not
|
||||||
|
// interfere.
|
||||||
|
func TestGoodHelp(t *testing.T) {
|
||||||
|
run, cleanup := prepare(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
res := icmd.RunCmd(run("-D", "helloworld", "--help"))
|
||||||
|
res.Assert(t, icmd.Success)
|
||||||
|
// This is the same golden file as `TestHelpGood`, above.
|
||||||
|
golden.Assert(t, res.Stdout(), "docker-help-helloworld.golden")
|
||||||
|
assert.Assert(t, is.Equal(res.Stderr(), ""))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRunGoodSubcommand ensures correct behaviour when running a valid plugin with a subcommand
|
||||||
|
func TestRunGoodSubcommand(t *testing.T) {
|
||||||
|
run, cleanup := prepare(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
res := icmd.RunCmd(run("helloworld", "goodbye"))
|
||||||
|
res.Assert(t, icmd.Expected{
|
||||||
|
ExitCode: 0,
|
||||||
|
Out: "Goodbye World!",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRunGoodArgument ensures correct behaviour when running a valid plugin with an `--argument`.
|
||||||
|
func TestRunGoodArgument(t *testing.T) {
|
||||||
|
run, cleanup := prepare(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
res := icmd.RunCmd(run("helloworld", "--who", "Cleveland"))
|
||||||
|
res.Assert(t, icmd.Expected{
|
||||||
|
ExitCode: 0,
|
||||||
|
Out: "Hello Cleveland!",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHelpGoodSubcommand ensures correct behaviour when invoking help on a
|
||||||
|
// valid plugin subcommand. A global argument is included to ensure it does not
|
||||||
|
// interfere.
|
||||||
|
func TestHelpGoodSubcommand(t *testing.T) {
|
||||||
|
run, cleanup := prepare(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
res := icmd.RunCmd(run("-D", "help", "helloworld", "goodbye"))
|
||||||
|
res.Assert(t, icmd.Success)
|
||||||
|
golden.Assert(t, res.Stdout(), "docker-help-helloworld-goodbye.golden")
|
||||||
|
assert.Assert(t, is.Equal(res.Stderr(), ""))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestGoodSubcommandHelp ensures correct behaviour when calling a valid plugin
|
||||||
|
// with a subcommand and `--help`. A global argument is used to ensure it does not
|
||||||
|
// interfere.
|
||||||
|
func TestGoodSubcommandHelp(t *testing.T) {
|
||||||
|
run, cleanup := prepare(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
res := icmd.RunCmd(run("-D", "helloworld", "goodbye", "--help"))
|
||||||
|
res.Assert(t, icmd.Success)
|
||||||
|
// This is the same golden file as `TestHelpGoodSubcommand`, above.
|
||||||
|
golden.Assert(t, res.Stdout(), "docker-help-helloworld-goodbye.golden")
|
||||||
|
assert.Assert(t, is.Equal(res.Stderr(), ""))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCliInitialized tests the code paths which ensure that the Cli
|
||||||
|
// object is initialized even if the plugin uses PersistentRunE
|
||||||
|
func TestCliInitialized(t *testing.T) {
|
||||||
|
run, cleanup := prepare(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
res := icmd.RunCmd(run("helloworld", "apiversion"))
|
||||||
|
res.Assert(t, icmd.Success)
|
||||||
|
assert.Assert(t, res.Stdout() != "")
|
||||||
|
assert.Assert(t, is.Equal(res.Stderr(), ""))
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
docker: 'badmeta' is not a docker command.
|
||||||
|
See 'docker --help'
|
|
@ -0,0 +1 @@
|
||||||
|
unknown help topic: badmeta
|
|
@ -0,0 +1,4 @@
|
||||||
|
|
||||||
|
Usage: docker helloworld goodbye
|
||||||
|
|
||||||
|
Say Goodbye instead of Hello
|
|
@ -0,0 +1,13 @@
|
||||||
|
|
||||||
|
Usage: docker helloworld [OPTIONS] COMMAND
|
||||||
|
|
||||||
|
A basic Hello World plugin for tests
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--who string Who are we addressing? (default "World")
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
apiversion Print the API version of the server
|
||||||
|
goodbye Say Goodbye instead of Hello
|
||||||
|
|
||||||
|
Run 'docker helloworld COMMAND --help' for more information on a command.
|
|
@ -0,0 +1 @@
|
||||||
|
unknown help topic: nonexistent
|
|
@ -0,0 +1,2 @@
|
||||||
|
docker: 'nonexistent' is not a docker command.
|
||||||
|
See 'docker --help'
|
|
@ -0,0 +1,24 @@
|
||||||
|
package cliplugins
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gotest.tools/fs"
|
||||||
|
"gotest.tools/icmd"
|
||||||
|
)
|
||||||
|
|
||||||
|
func prepare(t *testing.T) (func(args ...string) icmd.Cmd, func()) {
|
||||||
|
cfg := fs.NewDir(t, "plugin-test",
|
||||||
|
fs.WithFile("config.json", fmt.Sprintf(`{"cliPluginsExtraDirs": [%q]}`, os.Getenv("DOCKER_CLI_E2E_PLUGINS_EXTRA_DIRS"))),
|
||||||
|
)
|
||||||
|
run := func(args ...string) icmd.Cmd {
|
||||||
|
return icmd.Command("docker", append([]string{"--config", cfg.Path()}, args...)...)
|
||||||
|
}
|
||||||
|
cleanup := func() {
|
||||||
|
cfg.Remove()
|
||||||
|
}
|
||||||
|
return run, cleanup
|
||||||
|
|
||||||
|
}
|
|
@ -8,15 +8,15 @@ BUILDTIME=${BUILDTIME:-$(date --utc --rfc-3339 ns 2> /dev/null | sed -e 's/ /T/'
|
||||||
|
|
||||||
PLATFORM_LDFLAGS=
|
PLATFORM_LDFLAGS=
|
||||||
if test -n "${PLATFORM}"; then
|
if test -n "${PLATFORM}"; then
|
||||||
PLATFORM_LDFLAGS="-X \"github.com/docker/cli/cli.PlatformName=${PLATFORM}\""
|
PLATFORM_LDFLAGS="-X \"github.com/docker/cli/cli/version.PlatformName=${PLATFORM}\""
|
||||||
fi
|
fi
|
||||||
|
|
||||||
export LDFLAGS="\
|
export LDFLAGS="\
|
||||||
-w \
|
-w \
|
||||||
${PLATFORM_LDFLAGS} \
|
${PLATFORM_LDFLAGS} \
|
||||||
-X \"github.com/docker/cli/cli.GitCommit=${GITCOMMIT}\" \
|
-X \"github.com/docker/cli/cli/version.GitCommit=${GITCOMMIT}\" \
|
||||||
-X \"github.com/docker/cli/cli.BuildTime=${BUILDTIME}\" \
|
-X \"github.com/docker/cli/cli/version.BuildTime=${BUILDTIME}\" \
|
||||||
-X \"github.com/docker/cli/cli.Version=${VERSION}\" \
|
-X \"github.com/docker/cli/cli/version.Version=${VERSION}\" \
|
||||||
${LDFLAGS:-} \
|
${LDFLAGS:-} \
|
||||||
"
|
"
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
@ -69,6 +69,7 @@ function runtests {
|
||||||
TEST_SKIP_PLUGIN_TESTS="${SKIP_PLUGIN_TESTS-}" \
|
TEST_SKIP_PLUGIN_TESTS="${SKIP_PLUGIN_TESTS-}" \
|
||||||
GOPATH="$GOPATH" \
|
GOPATH="$GOPATH" \
|
||||||
PATH="$PWD/build/:/usr/bin" \
|
PATH="$PWD/build/:/usr/bin" \
|
||||||
|
DOCKER_CLI_E2E_PLUGINS_EXTRA_DIRS="$PWD/build/plugins-linux-amd64" \
|
||||||
"$(which go)" test -v ./e2e/... ${TESTFLAGS-}
|
"$(which go)" test -v ./e2e/... ${TESTFLAGS-}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue