Skip to content

Commit

Permalink
Merge pull request docker#88 from andrewhsu/sync
Browse files Browse the repository at this point in the history
[master] sync to upstream cli master 8271c94
  • Loading branch information
thaJeztah committed Feb 11, 2019
2 parents 321d654 + 36118e0 commit b7bb77a
Show file tree
Hide file tree
Showing 365 changed files with 13,297 additions and 3,830 deletions.
1 change: 1 addition & 0 deletions .mailmap
Original file line number Diff line number Diff line change
Expand Up @@ -492,5 +492,6 @@ Zachary Jaffee <zjaffee@us.ibm.com> <zjaffee@apache.org>
ZhangHang <stevezhang2014@gmail.com>
Zhenkun Bi <bi.zhenkun@zte.com.cn>
Zhou Hao <zhouhao@cn.fujitsu.com>
Zhoulin Xie <zhoulin.xie@daocloud.io>
Zhu Kunjia <zhu.kunjia@zte.com.cn>
Zou Yu <zouyu7@huawei.com>
3 changes: 3 additions & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -205,9 +205,11 @@ Fabio Falci <fabiofalci@gmail.com>
Fabrizio Soppelsa <fsoppelsa@mirantis.com>
Felix Hupfeld <felix@quobyte.com>
Felix Rabe <felix@rabe.io>
Filip Jareš <filipjares@gmail.com>
Flavio Crisciani <flavio.crisciani@docker.com>
Florian Klein <florian.klein@free.fr>
Foysal Iqbal <foysal.iqbal.fb@gmail.com>
François Scala <francois.scala@swiss-as.com>
Fred Lifton <fred.lifton@docker.com>
Frederic Hemberger <mail@frederic-hemberger.de>
Frederick F. Kautz IV <fkautz@redhat.com>
Expand Down Expand Up @@ -683,6 +685,7 @@ Zhang Wentao <zhangwentao234@huawei.com>
ZhangHang <stevezhang2014@gmail.com>
zhenghenghuo <zhenghenghuo@zju.edu.cn>
Zhou Hao <zhouhao@cn.fujitsu.com>
Zhoulin Xie <zhoulin.xie@daocloud.io>
Zhu Guihua <zhugh.fnst@cn.fujitsu.com>
Álex González <agonzalezro@gmail.com>
Álvaro Lázaro <alvaro.lazaro.g@gmail.com>
Expand Down
18 changes: 15 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ clean: ## remove build artifacts
rm -rf ./build/* cli/winresources/rsrc_* ./man/man[1-9] docs/yaml/gen

.PHONY: test-unit
test-unit: ## run unit test
./scripts/test/unit $(shell go list ./... | grep -vE '/vendor/|/e2e/')
test-unit: ## run unit tests, to change the output format use: GOTESTSUM_FORMAT=(dots|short|standard-quiet|short-verbose|standard-verbose) make test-unit
gotestsum -- $(shell go list ./... | grep -vE '/vendor/|/e2e/')

.PHONY: test
test: test-unit ## run tests

.PHONY: test-coverage
test-coverage: ## run test coverage
./scripts/test/unit-with-coverage $(shell go list ./... | grep -vE '/vendor/|/e2e/')
gotestsum -- -coverprofile=coverage.txt $(shell go list ./... | grep -vE '/vendor/|/e2e/')

.PHONY: fmt
fmt:
Expand All @@ -34,6 +34,10 @@ binary: ## build executable for Linux
@echo "WARNING: binary creates a Linux executable. Use cross for macOS or Windows."
./scripts/build/binary

.PHONY: plugins
plugins: ## build example CLI plugins
./scripts/build/plugins

.PHONY: cross
cross: ## build executable for macOS and Windows
./scripts/build/cross
Expand All @@ -42,10 +46,18 @@ cross: ## build executable for macOS and Windows
binary-windows: ## build executable for Windows
./scripts/build/windows

.PHONY: plugins-windows
plugins-windows: ## build example CLI plugins for Windows
./scripts/build/plugins-windows

.PHONY: binary-osx
binary-osx: ## build executable for macOS
./scripts/build/osx

.PHONY: plugins-osx
plugins-osx: ## build example CLI plugins for macOS
./scripts/build/plugins-osx

.PHONY: dynbinary
dynbinary: ## build dynamically linked binary
./scripts/build/dynbinary
Expand Down
11 changes: 9 additions & 2 deletions circle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,16 @@ jobs:
- run:
name: "Unit Test with Coverage"
command: |
mkdir -p test-results/unit-tests
docker build -f dockerfiles/Dockerfile.dev --tag cli-builder:$CIRCLE_BUILD_NUM .
docker run --name \
docker run \
-e GOTESTSUM_JUNITFILE=/tmp/junit.xml \
--name \
test-$CIRCLE_BUILD_NUM cli-builder:$CIRCLE_BUILD_NUM \
make test-coverage
docker cp \
test-$CIRCLE_BUILD_NUM:/tmp/junit.xml \
./test-results/unit-tests/junit.xml
- run:
name: "Upload to Codecov"
command: |
Expand All @@ -70,6 +75,8 @@ jobs:
apk add -U bash curl
curl -s https://codecov.io/bash | bash || \
echo 'Codecov failed to upload'
- store_test_results:
path: test-results

validate:
working_directory: /work
Expand Down
59 changes: 59 additions & 0 deletions cli-plugins/examples/helloworld/main.go
Original file line number Diff line number Diff line change
@@ -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",
})
}
23 changes: 23 additions & 0 deletions cli-plugins/manager/candidate.go
Original file line number Diff line number Diff line change
@@ -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()
}
93 changes: 93 additions & 0 deletions cli-plugins/manager/candidate_test.go
Original file line number Diff line number Diff line change
@@ -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())
}
54 changes: 54 additions & 0 deletions cli-plugins/manager/cobra.go
Original file line number Diff line number Diff line change
@@ -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
}
43 changes: 43 additions & 0 deletions cli-plugins/manager/error.go
Original file line number Diff line number Diff line change
@@ -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 comments on commit b7bb77a

Please sign in to comment.