Merge component 'cli' from git@github.com:docker/cli master

This commit is contained in:
GordonTheTurtle
2018-06-29 16:41:45 +00:00
15 changed files with 309 additions and 103 deletions

View File

@ -5,6 +5,7 @@ import (
"io"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/client"
)
@ -15,6 +16,7 @@ type fakeClient struct {
pluginEnableFunc func(name string, options types.PluginEnableOptions) error
pluginRemoveFunc func(name string, options types.PluginRemoveOptions) error
pluginInstallFunc func(name string, options types.PluginInstallOptions) (io.ReadCloser, error)
pluginListFunc func(filter filters.Args) (types.PluginsListResponse, error)
}
func (c *fakeClient) PluginCreate(ctx context.Context, createContext io.Reader, createOptions types.PluginCreateOptions) error {
@ -51,3 +53,11 @@ func (c *fakeClient) PluginInstall(context context.Context, name string, install
}
return nil, nil
}
func (c *fakeClient) PluginList(context context.Context, filter filters.Args) (types.PluginsListResponse, error) {
if c.pluginListFunc != nil {
return c.pluginListFunc(filter)
}
return types.PluginsListResponse{}, nil
}

View File

@ -0,0 +1,150 @@
package plugin
import (
"fmt"
"io/ioutil"
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"gotest.tools/assert"
is "gotest.tools/assert/cmp"
"gotest.tools/golden"
)
func TestListErrors(t *testing.T) {
testCases := []struct {
description string
args []string
flags map[string]string
expectedError string
listFunc func(filter filters.Args) (types.PluginsListResponse, error)
}{
{
description: "too many arguments",
args: []string{"foo"},
expectedError: "accepts no arguments",
},
{
description: "error listing plugins",
args: []string{},
expectedError: "error listing plugins",
listFunc: func(filter filters.Args) (types.PluginsListResponse, error) {
return types.PluginsListResponse{}, fmt.Errorf("error listing plugins")
},
},
{
description: "invalid format",
args: []string{},
flags: map[string]string{
"format": "{{invalid format}}",
},
expectedError: "Template parsing error",
},
}
for _, tc := range testCases {
cli := test.NewFakeCli(&fakeClient{pluginListFunc: tc.listFunc})
cmd := newListCommand(cli)
cmd.SetArgs(tc.args)
for key, value := range tc.flags {
cmd.Flags().Set(key, value)
}
cmd.SetOutput(ioutil.Discard)
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
}
}
func TestList(t *testing.T) {
singlePluginListFunc := func(filter filters.Args) (types.PluginsListResponse, error) {
return types.PluginsListResponse{
{
ID: "id-foo",
Name: "name-foo",
Enabled: true,
Config: types.PluginConfig{
Description: "desc-bar",
},
},
}, nil
}
testCases := []struct {
description string
args []string
flags map[string]string
golden string
listFunc func(filter filters.Args) (types.PluginsListResponse, error)
}{
{
description: "list with no additional flags",
args: []string{},
golden: "plugin-list-without-format.golden",
listFunc: singlePluginListFunc,
},
{
description: "list with filters",
args: []string{},
flags: map[string]string{
"filter": "foo=bar",
},
golden: "plugin-list-without-format.golden",
listFunc: func(filter filters.Args) (types.PluginsListResponse, error) {
assert.Check(t, is.Equal("bar", filter.Get("foo")[0]))
return singlePluginListFunc(filter)
},
},
{
description: "list with quiet option",
args: []string{},
flags: map[string]string{
"quiet": "true",
},
golden: "plugin-list-with-quiet-option.golden",
listFunc: singlePluginListFunc,
},
{
description: "list with no-trunc option",
args: []string{},
flags: map[string]string{
"no-trunc": "true",
"format": "{{ .ID }}",
},
golden: "plugin-list-with-no-trunc-option.golden",
listFunc: func(filter filters.Args) (types.PluginsListResponse, error) {
return types.PluginsListResponse{
{
ID: "xyg4z2hiSLO5yTnBJfg4OYia9gKA6Qjd",
Name: "name-foo",
Enabled: true,
Config: types.PluginConfig{
Description: "desc-bar",
},
},
}, nil
},
},
{
description: "list with format",
args: []string{},
flags: map[string]string{
"format": "{{ .Name }}",
},
golden: "plugin-list-with-format.golden",
listFunc: singlePluginListFunc,
},
}
for _, tc := range testCases {
cli := test.NewFakeCli(&fakeClient{pluginListFunc: tc.listFunc})
cmd := newListCommand(cli)
cmd.SetArgs(tc.args)
for key, value := range tc.flags {
cmd.Flags().Set(key, value)
}
assert.NilError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), tc.golden)
}
}

View File

@ -0,0 +1 @@
name-foo

View File

@ -0,0 +1 @@
xyg4z2hiSLO5yTnBJfg4OYia9gKA6Qjd

View File

@ -0,0 +1 @@
id-foo

View File

@ -0,0 +1,2 @@
ID NAME DESCRIPTION ENABLED
id-foo name-foo desc-bar true

View File

@ -8,13 +8,72 @@ import (
"strings"
"github.com/docker/cli/cli/compose/loader"
"github.com/docker/cli/cli/compose/schema"
composeTypes "github.com/docker/cli/cli/compose/types"
composetypes "github.com/docker/cli/cli/compose/types"
"github.com/docker/cli/kubernetes/compose/v1beta1"
"github.com/docker/cli/kubernetes/compose/v1beta2"
"github.com/pkg/errors"
yaml "gopkg.in/yaml.v2"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// NewStackConverter returns a converter from types.Config (compose) to the specified
// stack version or error out if the version is not supported or existent.
func NewStackConverter(version string) (StackConverter, error) {
switch version {
case "v1beta1":
return stackV1Beta1Converter{}, nil
case "v1beta2":
return stackV1Beta2Converter{}, nil
default:
return nil, errors.Errorf("stack version %s unsupported", version)
}
}
// StackConverter converts a compose types.Config to a Stack
type StackConverter interface {
FromCompose(stderr io.Writer, name string, cfg *composetypes.Config) (Stack, error)
}
type stackV1Beta1Converter struct{}
func (s stackV1Beta1Converter) FromCompose(stderr io.Writer, name string, cfg *composetypes.Config) (Stack, error) {
cfg.Version = v1beta1.MaxComposeVersion
st, err := fromCompose(stderr, name, cfg)
if err != nil {
return Stack{}, err
}
res, err := yaml.Marshal(cfg)
if err != nil {
return Stack{}, err
}
// reload the result to check that it produced a valid 3.5 compose file
resparsedConfig, err := loader.ParseYAML(res)
if err != nil {
return Stack{}, err
}
if err = schema.Validate(resparsedConfig, v1beta1.MaxComposeVersion); err != nil {
return Stack{}, errors.Wrapf(err, "the compose yaml file is invalid with v%s", v1beta1.MaxComposeVersion)
}
st.ComposeFile = string(res)
return st, nil
}
type stackV1Beta2Converter struct{}
func (s stackV1Beta2Converter) FromCompose(stderr io.Writer, name string, cfg *composetypes.Config) (Stack, error) {
return fromCompose(stderr, name, cfg)
}
func fromCompose(stderr io.Writer, name string, cfg *composetypes.Config) (Stack, error) {
return Stack{
Name: name,
Spec: fromComposeConfig(stderr, cfg),
}, nil
}
func loadStackData(composefile string) (*composetypes.Config, error) {
parsed, err := loader.ParseYAML([]byte(composefile))
if err != nil {
@ -30,44 +89,44 @@ func loadStackData(composefile string) (*composetypes.Config, error) {
}
// Conversions from internal stack to different stack compose component versions.
func stackFromV1beta1(in *v1beta1.Stack) (stack, error) {
func stackFromV1beta1(in *v1beta1.Stack) (Stack, error) {
cfg, err := loadStackData(in.Spec.ComposeFile)
if err != nil {
return stack{}, err
return Stack{}, err
}
return stack{
name: in.ObjectMeta.Name,
namespace: in.ObjectMeta.Namespace,
composeFile: in.Spec.ComposeFile,
spec: fromComposeConfig(ioutil.Discard, cfg),
return Stack{
Name: in.ObjectMeta.Name,
Namespace: in.ObjectMeta.Namespace,
ComposeFile: in.Spec.ComposeFile,
Spec: fromComposeConfig(ioutil.Discard, cfg),
}, nil
}
func stackToV1beta1(s stack) *v1beta1.Stack {
func stackToV1beta1(s Stack) *v1beta1.Stack {
return &v1beta1.Stack{
ObjectMeta: metav1.ObjectMeta{
Name: s.name,
Name: s.Name,
},
Spec: v1beta1.StackSpec{
ComposeFile: s.composeFile,
ComposeFile: s.ComposeFile,
},
}
}
func stackFromV1beta2(in *v1beta2.Stack) stack {
return stack{
name: in.ObjectMeta.Name,
namespace: in.ObjectMeta.Namespace,
spec: in.Spec,
func stackFromV1beta2(in *v1beta2.Stack) Stack {
return Stack{
Name: in.ObjectMeta.Name,
Namespace: in.ObjectMeta.Namespace,
Spec: in.Spec,
}
}
func stackToV1beta2(s stack) *v1beta2.Stack {
func stackToV1beta2(s Stack) *v1beta2.Stack {
return &v1beta2.Stack{
ObjectMeta: metav1.ObjectMeta{
Name: s.name,
Name: s.Name,
},
Spec: s.spec,
Spec: s.Spec,
}
}

View File

@ -0,0 +1,18 @@
package kubernetes
import (
"testing"
"gotest.tools/assert"
is "gotest.tools/assert/cmp"
)
func TestNewStackConverter(t *testing.T) {
_, err := NewStackConverter("v1alpha1")
assert.Check(t, is.ErrorContains(err, "stack version v1alpha1 unsupported"))
_, err = NewStackConverter("v1beta1")
assert.NilError(t, err)
_, err = NewStackConverter("v1beta2")
assert.NilError(t, err)
}

View File

@ -75,13 +75,13 @@ func RunDeploy(dockerCli *KubeCli, opts options.Deploy, cfg *composetypes.Config
}
}()
err = watcher.Watch(stack.name, stack.getServices(), statusUpdates)
err = watcher.Watch(stack.Name, stack.getServices(), statusUpdates)
close(statusUpdates)
<-displayDone
if err != nil {
return err
}
fmt.Fprintf(cmdOut, "\nStack %s is stable and running\n\n", stack.name)
fmt.Fprintf(cmdOut, "\nStack %s is stable and running\n\n", stack.Name)
return nil
}

View File

@ -48,10 +48,10 @@ func getStacks(kubeCli *KubeCli, opts options.List) ([]*formatter.Stack, error)
var formattedStacks []*formatter.Stack
for _, stack := range stacks {
formattedStacks = append(formattedStacks, &formatter.Stack{
Name: stack.name,
Name: stack.Name,
Services: len(stack.getServices()),
Orchestrator: "Kubernetes",
Namespace: stack.namespace,
Namespace: stack.Namespace,
})
}
return formattedStacks, nil

View File

@ -12,18 +12,18 @@ import (
corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
)
// stack is the main type used by stack commands so they remain independent from kubernetes compose component version.
type stack struct {
name string
namespace string
composeFile string
spec *v1beta2.StackSpec
// Stack is the main type used by stack commands so they remain independent from kubernetes compose component version.
type Stack struct {
Name string
Namespace string
ComposeFile string
Spec *v1beta2.StackSpec
}
// getServices returns all the stack service names, sorted lexicographically
func (s *stack) getServices() []string {
services := make([]string, len(s.spec.Services))
for i, service := range s.spec.Services {
func (s *Stack) getServices() []string {
services := make([]string, len(s.Spec.Services))
for i, service := range s.Spec.Services {
services[i] = service.Name
}
sort.Strings(services)
@ -31,8 +31,8 @@ func (s *stack) getServices() []string {
}
// createFileBasedConfigMaps creates a Kubernetes ConfigMap for each Compose global file-based config.
func (s *stack) createFileBasedConfigMaps(configMaps corev1.ConfigMapInterface) error {
for name, config := range s.spec.Configs {
func (s *Stack) createFileBasedConfigMaps(configMaps corev1.ConfigMapInterface) error {
for name, config := range s.Spec.Configs {
if config.File == "" {
continue
}
@ -43,7 +43,7 @@ func (s *stack) createFileBasedConfigMaps(configMaps corev1.ConfigMapInterface)
return err
}
if _, err := configMaps.Create(toConfigMap(s.name, name, fileName, content)); err != nil {
if _, err := configMaps.Create(toConfigMap(s.Name, name, fileName, content)); err != nil {
return err
}
}
@ -71,8 +71,8 @@ func toConfigMap(stackName, name, key string, content []byte) *apiv1.ConfigMap {
}
// createFileBasedSecrets creates a Kubernetes Secret for each Compose global file-based secret.
func (s *stack) createFileBasedSecrets(secrets corev1.SecretInterface) error {
for name, secret := range s.spec.Secrets {
func (s *Stack) createFileBasedSecrets(secrets corev1.SecretInterface) error {
for name, secret := range s.Spec.Secrets {
if secret.File == "" {
continue
}
@ -83,7 +83,7 @@ func (s *stack) createFileBasedSecrets(secrets corev1.SecretInterface) error {
return err
}
if _, err := secrets.Create(toSecret(s.name, name, fileName, content)); err != nil {
if _, err := secrets.Create(toSecret(s.Name, name, fileName, content)); err != nil {
return err
}
}

View File

@ -2,17 +2,10 @@ package kubernetes
import (
"fmt"
"io"
"github.com/docker/cli/cli/compose/loader"
"github.com/docker/cli/cli/compose/schema"
composetypes "github.com/docker/cli/cli/compose/types"
composev1beta1 "github.com/docker/cli/kubernetes/client/clientset/typed/compose/v1beta1"
composev1beta2 "github.com/docker/cli/kubernetes/client/clientset/typed/compose/v1beta2"
v1beta1types "github.com/docker/cli/kubernetes/compose/v1beta1"
"github.com/docker/cli/kubernetes/labels"
"github.com/pkg/errors"
yaml "gopkg.in/yaml.v2"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
"k8s.io/client-go/rest"
@ -20,16 +13,17 @@ import (
// StackClient talks to a kubernetes compose component.
type StackClient interface {
CreateOrUpdate(s stack) error
StackConverter
CreateOrUpdate(s Stack) error
Delete(name string) error
Get(name string) (stack, error)
List(opts metav1.ListOptions) ([]stack, error)
IsColliding(servicesClient corev1.ServiceInterface, s stack) error
FromCompose(stderr io.Writer, name string, cfg *composetypes.Config) (stack, error)
Get(name string) (Stack, error)
List(opts metav1.ListOptions) ([]Stack, error)
IsColliding(servicesClient corev1.ServiceInterface, s Stack) error
}
// stackV1Beta1 implements stackClient interface and talks to compose component v1beta1.
type stackV1Beta1 struct {
stackV1Beta1Converter
stacks composev1beta1.StackInterface
}
@ -41,10 +35,10 @@ func newStackV1Beta1(config *rest.Config, namespace string) (*stackV1Beta1, erro
return &stackV1Beta1{stacks: client.Stacks(namespace)}, nil
}
func (s *stackV1Beta1) CreateOrUpdate(internalStack stack) error {
func (s *stackV1Beta1) CreateOrUpdate(internalStack Stack) error {
// If it already exists, update the stack
if stackBeta1, err := s.stacks.Get(internalStack.name, metav1.GetOptions{}); err == nil {
stackBeta1.Spec.ComposeFile = internalStack.composeFile
if stackBeta1, err := s.stacks.Get(internalStack.Name, metav1.GetOptions{}); err == nil {
stackBeta1.Spec.ComposeFile = internalStack.ComposeFile
_, err := s.stacks.Update(stackBeta1)
return err
}
@ -57,20 +51,20 @@ func (s *stackV1Beta1) Delete(name string) error {
return s.stacks.Delete(name, &metav1.DeleteOptions{})
}
func (s *stackV1Beta1) Get(name string) (stack, error) {
func (s *stackV1Beta1) Get(name string) (Stack, error) {
stackBeta1, err := s.stacks.Get(name, metav1.GetOptions{})
if err != nil {
return stack{}, err
return Stack{}, err
}
return stackFromV1beta1(stackBeta1)
}
func (s *stackV1Beta1) List(opts metav1.ListOptions) ([]stack, error) {
func (s *stackV1Beta1) List(opts metav1.ListOptions) ([]Stack, error) {
list, err := s.stacks.List(opts)
if err != nil {
return nil, err
}
stacks := make([]stack, len(list.Items))
stacks := make([]Stack, len(list.Items))
for i := range list.Items {
stack, err := stackFromV1beta1(&list.Items[i])
if err != nil {
@ -82,9 +76,9 @@ func (s *stackV1Beta1) List(opts metav1.ListOptions) ([]stack, error) {
}
// IsColliding verifies that services defined in the stack collides with already deployed services
func (s *stackV1Beta1) IsColliding(servicesClient corev1.ServiceInterface, st stack) error {
func (s *stackV1Beta1) IsColliding(servicesClient corev1.ServiceInterface, st Stack) error {
for _, srv := range st.getServices() {
if err := verify(servicesClient, st.name, srv); err != nil {
if err := verify(servicesClient, st.Name, srv); err != nil {
return err
}
}
@ -108,31 +102,9 @@ func verify(services corev1.ServiceInterface, stackName string, service string)
return nil
}
func (s *stackV1Beta1) FromCompose(stderr io.Writer, name string, cfg *composetypes.Config) (stack, error) {
cfg.Version = v1beta1types.MaxComposeVersion
st, err := fromCompose(stderr, name, cfg)
if err != nil {
return stack{}, err
}
res, err := yaml.Marshal(cfg)
if err != nil {
return stack{}, err
}
// reload the result to check that it produced a valid 3.5 compose file
resparsedConfig, err := loader.ParseYAML(res)
if err != nil {
return stack{}, err
}
if err = schema.Validate(resparsedConfig, v1beta1types.MaxComposeVersion); err != nil {
return stack{}, errors.Wrapf(err, "the compose yaml file is invalid with v%s", v1beta1types.MaxComposeVersion)
}
st.composeFile = string(res)
return st, nil
}
// stackV1Beta2 implements stackClient interface and talks to compose component v1beta2.
type stackV1Beta2 struct {
stackV1Beta2Converter
stacks composev1beta2.StackInterface
}
@ -144,10 +116,10 @@ func newStackV1Beta2(config *rest.Config, namespace string) (*stackV1Beta2, erro
return &stackV1Beta2{stacks: client.Stacks(namespace)}, nil
}
func (s *stackV1Beta2) CreateOrUpdate(internalStack stack) error {
func (s *stackV1Beta2) CreateOrUpdate(internalStack Stack) error {
// If it already exists, update the stack
if stackBeta2, err := s.stacks.Get(internalStack.name, metav1.GetOptions{}); err == nil {
stackBeta2.Spec = internalStack.spec
if stackBeta2, err := s.stacks.Get(internalStack.Name, metav1.GetOptions{}); err == nil {
stackBeta2.Spec = internalStack.Spec
_, err := s.stacks.Update(stackBeta2)
return err
}
@ -160,20 +132,20 @@ func (s *stackV1Beta2) Delete(name string) error {
return s.stacks.Delete(name, &metav1.DeleteOptions{})
}
func (s *stackV1Beta2) Get(name string) (stack, error) {
func (s *stackV1Beta2) Get(name string) (Stack, error) {
stackBeta2, err := s.stacks.Get(name, metav1.GetOptions{})
if err != nil {
return stack{}, err
return Stack{}, err
}
return stackFromV1beta2(stackBeta2), nil
}
func (s *stackV1Beta2) List(opts metav1.ListOptions) ([]stack, error) {
func (s *stackV1Beta2) List(opts metav1.ListOptions) ([]Stack, error) {
list, err := s.stacks.List(opts)
if err != nil {
return nil, err
}
stacks := make([]stack, len(list.Items))
stacks := make([]Stack, len(list.Items))
for i := range list.Items {
stacks[i] = stackFromV1beta2(&list.Items[i])
}
@ -181,17 +153,6 @@ func (s *stackV1Beta2) List(opts metav1.ListOptions) ([]stack, error) {
}
// IsColliding is handle server side with the compose api v1beta2, so nothing to do here
func (s *stackV1Beta2) IsColliding(servicesClient corev1.ServiceInterface, st stack) error {
func (s *stackV1Beta2) IsColliding(servicesClient corev1.ServiceInterface, st Stack) error {
return nil
}
func (s *stackV1Beta2) FromCompose(stderr io.Writer, name string, cfg *composetypes.Config) (stack, error) {
return fromCompose(stderr, name, cfg)
}
func fromCompose(stderr io.Writer, name string, cfg *composetypes.Config) (stack, error) {
return stack{
name: name,
spec: fromComposeConfig(stderr, cfg),
}, nil
}

View File

@ -25,7 +25,7 @@ func TestFromCompose(t *testing.T) {
},
})
assert.NilError(t, err)
assert.Equal(t, "foo", s.name)
assert.Equal(t, "foo", s.Name)
assert.Equal(t, string(`version: "3.5"
services:
bar:
@ -36,7 +36,7 @@ networks: {}
volumes: {}
secrets: {}
configs: {}
`), s.composeFile)
`), s.ComposeFile)
}
func TestFromComposeUnsupportedVersion(t *testing.T) {

View File

@ -2222,6 +2222,7 @@ _docker_daemon() {
--cpu-rt-period
--cpu-rt-runtime
--data-root
--default-address-pool
--default-gateway
--default-gateway-v6
--default-runtime
@ -4618,6 +4619,7 @@ _docker_system_events() {
enable
exec_create
exec_detach
exec_die
exec_start
export
health_status

View File

@ -1432,11 +1432,12 @@ The list of currently supported options that can be reconfigured is this:
specified at container creation. It defaults to "default" which is
the runtime shipped with the official docker packages.
- `runtimes`: it updates the list of available OCI runtimes that can
be used to run containers
be used to run containers.
- `authorization-plugin`: specifies the authorization plugins to use.
- `allow-nondistributable-artifacts`: Replaces the set of registries to which the daemon will push nondistributable artifacts with a new set of registries.
- `insecure-registries`: it replaces the daemon insecure registries with a new set of insecure registries. If some existing insecure registries in daemon's configuration are not in newly reloaded insecure resgitries, these existing ones will be removed from daemon's config.
- `registry-mirrors`: it replaces the daemon registry mirrors with a new set of registry mirrors. If some existing registry mirrors in daemon's configuration are not in newly reloaded registry mirrors, these existing ones will be removed from daemon's config.
- `shutdown-timeout`: it replaces the daemon's existing configuration timeout with a new timeout for shutting down all containers.
Updating and reloading the cluster configurations such as `--cluster-store`,
`--cluster-advertise` and `--cluster-store-opts` will take effect only if