diff --git a/cli/command/cli.go b/cli/command/cli.go index 12c24ca0fe..d4d9fd8198 100644 --- a/cli/command/cli.go +++ b/cli/command/cli.go @@ -135,9 +135,11 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions) error { if err != nil { return errors.Wrap(err, "Experimental field") } + orchestrator := GetOrchestrator(cli.configFile.Orchestrator) cli.clientInfo = ClientInfo{ DefaultVersion: cli.client.ClientVersion(), HasExperimental: hasExperimental, + HasKubernetes: orchestrator == OrchestratorKubernetes, } cli.initializeFromClient() return nil @@ -202,6 +204,7 @@ type ServerInfo struct { // ClientInfo stores details about the supported features of the client type ClientInfo struct { HasExperimental bool + HasKubernetes bool DefaultVersion string } diff --git a/cli/command/orchestrator.go b/cli/command/orchestrator.go index 7297383201..5c7dd08a3b 100644 --- a/cli/command/orchestrator.go +++ b/cli/command/orchestrator.go @@ -3,17 +3,15 @@ package command import ( "os" "strings" - - cliconfig "github.com/docker/cli/cli/config" ) // Orchestrator type acts as an enum describing supported orchestrators. type Orchestrator string const ( - // Kubernetes orchestrator + // OrchestratorKubernetes orchestrator OrchestratorKubernetes = Orchestrator("kubernetes") - // Swarm orchestrator + // OrchestratorSwarm orchestrator OrchestratorSwarm = Orchestrator("swarm") orchestratorUnset = Orchestrator("unset") @@ -34,17 +32,15 @@ func normalize(flag string) Orchestrator { // GetOrchestrator checks DOCKER_ORCHESTRATOR environment variable and configuration file // orchestrator value and returns user defined Orchestrator. -func GetOrchestrator(dockerCli Cli) Orchestrator { +func GetOrchestrator(orchestrator string) Orchestrator { // Check environment variable env := os.Getenv(dockerOrchestrator) if o := normalize(env); o != orchestratorUnset { return o } - // Check config file - if configFile := cliconfig.LoadDefaultConfigFile(dockerCli.Err()); configFile != nil { - if o := normalize(configFile.Orchestrator); o != orchestratorUnset { - return o - } + // Check specified orchestrator + if o := normalize(orchestrator); o != orchestratorUnset { + return o } // Nothing set, use default orchestrator diff --git a/cli/command/stack/client_test.go b/cli/command/stack/client_test.go new file mode 100644 index 0000000000..bcb92db6c9 --- /dev/null +++ b/cli/command/stack/client_test.go @@ -0,0 +1,239 @@ +package stack + +import ( + "strings" + + "github.com/docker/cli/cli/compose/convert" + "github.com/docker/docker/api" + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/client" + "golang.org/x/net/context" +) + +type fakeClient struct { + client.Client + + version string + + services []string + networks []string + secrets []string + configs []string + + removedServices []string + removedNetworks []string + removedSecrets []string + removedConfigs []string + + serviceListFunc func(options types.ServiceListOptions) ([]swarm.Service, error) + networkListFunc func(options types.NetworkListOptions) ([]types.NetworkResource, error) + secretListFunc func(options types.SecretListOptions) ([]swarm.Secret, error) + configListFunc func(options types.ConfigListOptions) ([]swarm.Config, error) + nodeListFunc func(options types.NodeListOptions) ([]swarm.Node, error) + taskListFunc func(options types.TaskListOptions) ([]swarm.Task, error) + nodeInspectWithRaw func(ref string) (swarm.Node, []byte, error) + + serviceUpdateFunc func(serviceID string, version swarm.Version, service swarm.ServiceSpec, options types.ServiceUpdateOptions) (types.ServiceUpdateResponse, error) + + serviceRemoveFunc func(serviceID string) error + networkRemoveFunc func(networkID string) error + secretRemoveFunc func(secretID string) error + configRemoveFunc func(configID string) error +} + +func (cli *fakeClient) ServerVersion(ctx context.Context) (types.Version, error) { + return types.Version{ + Version: "docker-dev", + APIVersion: api.DefaultVersion, + }, nil +} + +func (cli *fakeClient) ClientVersion() string { + return cli.version +} + +func (cli *fakeClient) ServiceList(ctx context.Context, options types.ServiceListOptions) ([]swarm.Service, error) { + if cli.serviceListFunc != nil { + return cli.serviceListFunc(options) + } + + namespace := namespaceFromFilters(options.Filters) + servicesList := []swarm.Service{} + for _, name := range cli.services { + if belongToNamespace(name, namespace) { + servicesList = append(servicesList, serviceFromName(name)) + } + } + return servicesList, nil +} + +func (cli *fakeClient) NetworkList(ctx context.Context, options types.NetworkListOptions) ([]types.NetworkResource, error) { + if cli.networkListFunc != nil { + return cli.networkListFunc(options) + } + + namespace := namespaceFromFilters(options.Filters) + networksList := []types.NetworkResource{} + for _, name := range cli.networks { + if belongToNamespace(name, namespace) { + networksList = append(networksList, networkFromName(name)) + } + } + return networksList, nil +} + +func (cli *fakeClient) SecretList(ctx context.Context, options types.SecretListOptions) ([]swarm.Secret, error) { + if cli.secretListFunc != nil { + return cli.secretListFunc(options) + } + + namespace := namespaceFromFilters(options.Filters) + secretsList := []swarm.Secret{} + for _, name := range cli.secrets { + if belongToNamespace(name, namespace) { + secretsList = append(secretsList, secretFromName(name)) + } + } + return secretsList, nil +} + +func (cli *fakeClient) ConfigList(ctx context.Context, options types.ConfigListOptions) ([]swarm.Config, error) { + if cli.configListFunc != nil { + return cli.configListFunc(options) + } + + namespace := namespaceFromFilters(options.Filters) + configsList := []swarm.Config{} + for _, name := range cli.configs { + if belongToNamespace(name, namespace) { + configsList = append(configsList, configFromName(name)) + } + } + return configsList, nil +} + +func (cli *fakeClient) TaskList(ctx context.Context, options types.TaskListOptions) ([]swarm.Task, error) { + if cli.taskListFunc != nil { + return cli.taskListFunc(options) + } + return []swarm.Task{}, nil +} + +func (cli *fakeClient) NodeList(ctx context.Context, options types.NodeListOptions) ([]swarm.Node, error) { + if cli.nodeListFunc != nil { + return cli.nodeListFunc(options) + } + return []swarm.Node{}, nil +} + +func (cli *fakeClient) NodeInspectWithRaw(ctx context.Context, ref string) (swarm.Node, []byte, error) { + if cli.nodeInspectWithRaw != nil { + return cli.nodeInspectWithRaw(ref) + } + return swarm.Node{}, nil, nil +} + +func (cli *fakeClient) ServiceUpdate(ctx context.Context, serviceID string, version swarm.Version, service swarm.ServiceSpec, options types.ServiceUpdateOptions) (types.ServiceUpdateResponse, error) { + if cli.serviceUpdateFunc != nil { + return cli.serviceUpdateFunc(serviceID, version, service, options) + } + + return types.ServiceUpdateResponse{}, nil +} + +func (cli *fakeClient) ServiceRemove(ctx context.Context, serviceID string) error { + if cli.serviceRemoveFunc != nil { + return cli.serviceRemoveFunc(serviceID) + } + + cli.removedServices = append(cli.removedServices, serviceID) + return nil +} + +func (cli *fakeClient) NetworkRemove(ctx context.Context, networkID string) error { + if cli.networkRemoveFunc != nil { + return cli.networkRemoveFunc(networkID) + } + + cli.removedNetworks = append(cli.removedNetworks, networkID) + return nil +} + +func (cli *fakeClient) SecretRemove(ctx context.Context, secretID string) error { + if cli.secretRemoveFunc != nil { + return cli.secretRemoveFunc(secretID) + } + + cli.removedSecrets = append(cli.removedSecrets, secretID) + return nil +} + +func (cli *fakeClient) ConfigRemove(ctx context.Context, configID string) error { + if cli.configRemoveFunc != nil { + return cli.configRemoveFunc(configID) + } + + cli.removedConfigs = append(cli.removedConfigs, configID) + return nil +} + +func serviceFromName(name string) swarm.Service { + return swarm.Service{ + ID: "ID-" + name, + Spec: swarm.ServiceSpec{ + Annotations: swarm.Annotations{Name: name}, + }, + } +} + +func networkFromName(name string) types.NetworkResource { + return types.NetworkResource{ + ID: "ID-" + name, + Name: name, + } +} + +func secretFromName(name string) swarm.Secret { + return swarm.Secret{ + ID: "ID-" + name, + Spec: swarm.SecretSpec{ + Annotations: swarm.Annotations{Name: name}, + }, + } +} + +func configFromName(name string) swarm.Config { + return swarm.Config{ + ID: "ID-" + name, + Spec: swarm.ConfigSpec{ + Annotations: swarm.Annotations{Name: name}, + }, + } +} + +func namespaceFromFilters(filters filters.Args) string { + label := filters.Get("label")[0] + return strings.TrimPrefix(label, convert.LabelNamespace+"=") +} + +func belongToNamespace(id, namespace string) bool { + return strings.HasPrefix(id, namespace+"_") +} + +func objectName(namespace, name string) string { + return namespace + "_" + name +} + +func objectID(name string) string { + return "ID-" + name +} + +func buildObjectIDs(objectNames []string) []string { + IDs := make([]string, len(objectNames)) + for i, name := range objectNames { + IDs[i] = objectID(name) + } + return IDs +} diff --git a/cli/command/stack/cmd.go b/cli/command/stack/cmd.go index a4b9f08141..7186c28f66 100644 --- a/cli/command/stack/cmd.go +++ b/cli/command/stack/cmd.go @@ -3,8 +3,6 @@ package stack import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/stack/kubernetes" - "github.com/docker/cli/cli/command/stack/swarm" "github.com/spf13/cobra" ) @@ -17,26 +15,24 @@ func NewStackCommand(dockerCli command.Cli) *cobra.Command { RunE: command.ShowHelp(dockerCli.Err()), Annotations: map[string]string{"version": "1.25"}, } - switch command.GetOrchestrator(dockerCli) { - case command.OrchestratorKubernetes: - kubernetes.AddStackCommands(cmd, dockerCli) - case command.OrchestratorSwarm: - swarm.AddStackCommands(cmd, dockerCli) - } + cmd.AddCommand( + newDeployCommand(dockerCli), + newListCommand(dockerCli), + newPsCommand(dockerCli), + newRemoveCommand(dockerCli), + newServicesCommand(dockerCli), + ) + flags := cmd.PersistentFlags() + flags.String("namespace", "default", "Kubernetes namespace to use") + flags.SetAnnotation("namespace", "kubernetes", nil) + flags.String("kubeconfig", "", "Kubernetes config file") + flags.SetAnnotation("kubeconfig", "kubernetes", nil) return cmd } // NewTopLevelDeployCommand returns a command for `docker deploy` func NewTopLevelDeployCommand(dockerCli command.Cli) *cobra.Command { - var cmd *cobra.Command - switch command.GetOrchestrator(dockerCli) { - case command.OrchestratorKubernetes: - cmd = kubernetes.NewTopLevelDeployCommand(dockerCli) - case command.OrchestratorSwarm: - cmd = swarm.NewTopLevelDeployCommand(dockerCli) - default: - cmd = swarm.NewTopLevelDeployCommand(dockerCli) - } + cmd := newDeployCommand(dockerCli) // Remove the aliases at the top level cmd.Aliases = []string{} cmd.Annotations = map[string]string{"experimental": "", "version": "1.25"} diff --git a/cli/command/stack/common/opts.go b/cli/command/stack/common/opts.go deleted file mode 100644 index 094dab63fd..0000000000 --- a/cli/command/stack/common/opts.go +++ /dev/null @@ -1,55 +0,0 @@ -package common - -import ( - "fmt" - "io" - "os" - - "github.com/docker/cli/cli/command/bundlefile" - "github.com/pkg/errors" - "github.com/spf13/pflag" -) - -// AddComposefileFlag adds compose-file file to the specified flagset -func AddComposefileFlag(opt *string, flags *pflag.FlagSet) { - flags.StringVarP(opt, "compose-file", "c", "", "Path to a Compose file") - flags.SetAnnotation("compose-file", "version", []string{"1.25"}) -} - -// AddBundlefileFlag adds bundle-file file to the specified flagset -func AddBundlefileFlag(opt *string, flags *pflag.FlagSet) { - flags.StringVar(opt, "bundle-file", "", "Path to a Distributed Application Bundle file") - flags.SetAnnotation("bundle-file", "experimental", nil) -} - -// AddRegistryAuthFlag adds with-registry-auth file to the specified flagset -func AddRegistryAuthFlag(opt *bool, flags *pflag.FlagSet) { - flags.BoolVar(opt, "with-registry-auth", false, "Send registry authentication details to Swarm agents") -} - -// LoadBundlefile loads a bundle-file from the specified path -func LoadBundlefile(stderr io.Writer, namespace string, path string) (*bundlefile.Bundlefile, error) { - defaultPath := fmt.Sprintf("%s.dab", namespace) - - if path == "" { - path = defaultPath - } - if _, err := os.Stat(path); err != nil { - return nil, errors.Errorf( - "Bundle %s not found. Specify the path with --file", - path) - } - - fmt.Fprintf(stderr, "Loading bundle from %s\n", path) - reader, err := os.Open(path) - if err != nil { - return nil, err - } - defer reader.Close() - - bundle, err := bundlefile.LoadFile(reader) - if err != nil { - return nil, errors.Errorf("Error reading %s: %v\n", path, err) - } - return bundle, err -} diff --git a/cli/command/stack/deploy.go b/cli/command/stack/deploy.go new file mode 100644 index 0000000000..f9866716a8 --- /dev/null +++ b/cli/command/stack/deploy.go @@ -0,0 +1,49 @@ +package stack + +import ( + "github.com/docker/cli/cli" + "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/command/stack/kubernetes" + "github.com/docker/cli/cli/command/stack/options" + "github.com/docker/cli/cli/command/stack/swarm" + "github.com/spf13/cobra" +) + +func newDeployCommand(dockerCli command.Cli) *cobra.Command { + var opts options.Deploy + + cmd := &cobra.Command{ + Use: "deploy [OPTIONS] STACK", + Aliases: []string{"up"}, + Short: "Deploy a new stack or update an existing stack", + Args: cli.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + opts.Namespace = args[0] + if dockerCli.ClientInfo().HasKubernetes { + kli, err := kubernetes.WrapCli(dockerCli, cmd) + if err != nil { + return err + } + return kubernetes.RunDeploy(kli, opts) + } + return swarm.RunDeploy(dockerCli, opts) + }, + } + + flags := cmd.Flags() + flags.StringVar(&opts.Bundlefile, "bundle-file", "", "Path to a Distributed Application Bundle file") + flags.SetAnnotation("bundle-file", "experimental", nil) + flags.SetAnnotation("bundle-file", "swarm", nil) + flags.StringVarP(&opts.Composefile, "compose-file", "c", "", "Path to a Compose file") + flags.SetAnnotation("compose-file", "version", []string{"1.25"}) + flags.BoolVar(&opts.SendRegistryAuth, "with-registry-auth", false, "Send registry authentication details to Swarm agents") + flags.SetAnnotation("with-registry-auth", "swarm", nil) + flags.BoolVar(&opts.Prune, "prune", false, "Prune services that are no longer referenced") + flags.SetAnnotation("prune", "version", []string{"1.27"}) + flags.SetAnnotation("prune", "swarm", nil) + flags.StringVar(&opts.ResolveImage, "resolve-image", swarm.ResolveImageAlways, + `Query the registry to resolve image digest and supported platforms ("`+swarm.ResolveImageAlways+`"|"`+swarm.ResolveImageChanged+`"|"`+swarm.ResolveImageNever+`")`) + flags.SetAnnotation("resolve-image", "version", []string{"1.30"}) + flags.SetAnnotation("resolve-image", "swarm", nil) + return cmd +} diff --git a/cli/command/stack/kubernetes/cli.go b/cli/command/stack/kubernetes/cli.go new file mode 100644 index 0000000000..74c77891c1 --- /dev/null +++ b/cli/command/stack/kubernetes/cli.go @@ -0,0 +1,75 @@ +package kubernetes + +import ( + "os" + "path/filepath" + + "github.com/docker/cli/cli/command" + composev1beta1 "github.com/docker/cli/kubernetes/client/clientset_generated/clientset/typed/compose/v1beta1" + "github.com/docker/docker/pkg/homedir" + "github.com/spf13/cobra" + restclient "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" +) + +// KubeCli holds kubernetes specifics (client, namespace) with the command.Cli +type KubeCli struct { + command.Cli + kubeConfig *restclient.Config + kubeNamespace string +} + +// WrapCli wraps command.Cli with kubernetes specifiecs +func WrapCli(dockerCli command.Cli, cmd *cobra.Command) (*KubeCli, error) { + var err error + cli := &KubeCli{ + Cli: dockerCli, + kubeNamespace: "default", + } + if cmd.PersistentFlags().Changed("namespace") { + cli.kubeNamespace, err = cmd.PersistentFlags().GetString("namespace") + if err != nil { + return nil, err + } + } + kubeConfig := "" + if cmd.PersistentFlags().Changed("kubeconfig") { + kubeConfig, err = cmd.PersistentFlags().GetString("namespace") + if err != nil { + return nil, err + } + } + if kubeConfig == "" { + if config := os.Getenv("KUBECONFIG"); config != "" { + kubeConfig = config + } else { + kubeConfig = filepath.Join(homedir.Get(), ".kube/config") + } + } + + config, err := clientcmd.BuildConfigFromFlags("", kubeConfig) + if err != nil { + return nil, err + } + cli.kubeConfig = config + + return cli, nil +} + +func (c *KubeCli) composeClient() (*Factory, error) { + return NewFactory(c.kubeNamespace, c.kubeConfig) +} + +func (c *KubeCli) stacks() (composev1beta1.StackInterface, error) { + err := APIPresent(c.kubeConfig) + if err != nil { + return nil, err + } + + clientSet, err := composev1beta1.NewForConfig(c.kubeConfig) + if err != nil { + return nil, err + } + + return clientSet.Stacks(c.kubeNamespace), nil +} diff --git a/cli/command/stack/kubernetes/cmd.go b/cli/command/stack/kubernetes/cmd.go deleted file mode 100644 index 55406e2ba3..0000000000 --- a/cli/command/stack/kubernetes/cmd.go +++ /dev/null @@ -1,107 +0,0 @@ -package kubernetes - -import ( - "os" - "path/filepath" - - "github.com/docker/cli/cli/command" - composev1beta1 "github.com/docker/cli/kubernetes/client/clientset_generated/clientset/typed/compose/v1beta1" - "github.com/docker/docker/pkg/homedir" - "github.com/spf13/cobra" - "github.com/spf13/pflag" - restclient "k8s.io/client-go/rest" - "k8s.io/client-go/tools/clientcmd" -) - -// AddStackCommands adds `stack` subcommands -func AddStackCommands(root *cobra.Command, dockerCli command.Cli) { - var kubeCli kubeCli - configureCommand(root, &kubeCli) - root.AddCommand( - newDeployCommand(dockerCli, &kubeCli), - newListCommand(dockerCli, &kubeCli), - newRemoveCommand(dockerCli, &kubeCli), - newServicesCommand(dockerCli, &kubeCli), - newPsCommand(dockerCli, &kubeCli), - ) -} - -// NewTopLevelDeployCommand returns a command for `docker deploy` -func NewTopLevelDeployCommand(dockerCli command.Cli) *cobra.Command { - var kubeCli kubeCli - cmd := newDeployCommand(dockerCli, &kubeCli) - configureCommand(cmd, &kubeCli) - return cmd -} - -func configureCommand(root *cobra.Command, kubeCli *kubeCli) { - var ( - kubeOpts kubeOptions - ) - kubeOpts.installFlags(root.PersistentFlags()) - preRunE := root.PersistentPreRunE - root.PersistentPreRunE = func(cmd *cobra.Command, args []string) error { - if preRunE != nil { - if err := preRunE(cmd, args); err != nil { - return err - } - } - kubeCli.kubeNamespace = kubeOpts.namespace - if kubeCli.kubeNamespace == "" { - kubeCli.kubeNamespace = "default" - } - // Read kube config flag and environment variable - if kubeOpts.kubeconfig == "" { - if config := os.Getenv("KUBECONFIG"); config != "" { - kubeOpts.kubeconfig = config - } else { - kubeOpts.kubeconfig = filepath.Join(homedir.Get(), ".kube/config") - } - } - config, err := clientcmd.BuildConfigFromFlags("", kubeOpts.kubeconfig) - if err != nil { - return err - } - kubeCli.kubeConfig = config - return nil - } -} - -// KubeOptions are options specific to kubernetes -type kubeOptions struct { - namespace string - kubeconfig string -} - -// InstallFlags adds flags for the common options on the FlagSet -func (opts *kubeOptions) installFlags(flags *pflag.FlagSet) { - flags.StringVar(&opts.namespace, "namespace", "default", "Kubernetes namespace to use") - flags.StringVar(&opts.kubeconfig, "kubeconfig", "", "Kubernetes config file") -} - -type kubeCli struct { - kubeConfig *restclient.Config - kubeNamespace string -} - -func (c *kubeCli) ComposeClient() (*Factory, error) { - return NewFactory(c.kubeNamespace, c.kubeConfig) -} - -func (c *kubeCli) KubeConfig() *restclient.Config { - return c.kubeConfig -} - -func (c *kubeCli) Stacks() (composev1beta1.StackInterface, error) { - err := APIPresent(c.kubeConfig) - if err != nil { - return nil, err - } - - clientSet, err := composev1beta1.NewForConfig(c.kubeConfig) - if err != nil { - return nil, err - } - - return clientSet.Stacks(c.kubeNamespace), nil -} diff --git a/cli/command/stack/kubernetes/deploy.go b/cli/command/stack/kubernetes/deploy.go index 6bea9ac3c7..e350d353d6 100644 --- a/cli/command/stack/kubernetes/deploy.go +++ b/cli/command/stack/kubernetes/deploy.go @@ -5,51 +5,26 @@ import ( "io/ioutil" "path" - "github.com/docker/cli/cli" - "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/stack/common" + "github.com/docker/cli/cli/command/stack/options" composeTypes "github.com/docker/cli/cli/compose/types" "github.com/pkg/errors" - "github.com/spf13/cobra" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" corev1 "k8s.io/client-go/kubernetes/typed/core/v1" ) -type deployOptions struct { - composefile string - stack string -} - -func newDeployCommand(dockerCli command.Cli, kubeCli *kubeCli) *cobra.Command { - var opts deployOptions - cmd := &cobra.Command{ - Use: "deploy [OPTIONS] STACK", - Aliases: []string{"up"}, - Short: "Deploy a new stack or update an existing stack", - Args: cli.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.stack = args[0] - return runDeploy(dockerCli, kubeCli, opts) - }, - } - flags := cmd.Flags() - common.AddComposefileFlag(&opts.composefile, flags) - // FIXME(vdemeester) other flags ? (bundlefile, registry-auth, prune, resolve-image) ? - return cmd -} - -func runDeploy(dockerCli command.Cli, kubeCli *kubeCli, opts deployOptions) error { +// RunDeploy is the kubernetes implementation of docker stack deploy +func RunDeploy(dockerCli *KubeCli, opts options.Deploy) error { cmdOut := dockerCli.Out() // Check arguments - if opts.composefile == "" { + if opts.Composefile == "" { return errors.Errorf("Please specify a Compose file (with --compose-file).") } // Initialize clients - stacks, err := kubeCli.Stacks() + stacks, err := dockerCli.stacks() if err != nil { return err } - composeClient, err := kubeCli.ComposeClient() + composeClient, err := dockerCli.composeClient() if err != nil { return err } @@ -62,7 +37,7 @@ func runDeploy(dockerCli command.Cli, kubeCli *kubeCli, opts deployOptions) erro } // Parse the compose file - stack, cfg, err := LoadStack(opts.stack, opts.composefile) + stack, cfg, err := LoadStack(opts.Namespace, opts.Composefile) if err != nil { return err } diff --git a/cli/command/stack/kubernetes/list.go b/cli/command/stack/kubernetes/list.go index ad490e504a..0fae8b981a 100644 --- a/cli/command/stack/kubernetes/list.go +++ b/cli/command/stack/kubernetes/list.go @@ -3,42 +3,19 @@ package kubernetes import ( "sort" - "github.com/docker/cli/cli" - "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/formatter" - "github.com/spf13/cobra" + "github.com/docker/cli/cli/command/stack/options" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "vbom.ml/util/sortorder" ) -type listOptions struct { - format string -} - -func newListCommand(dockerCli command.Cli, kubeCli *kubeCli) *cobra.Command { - opts := listOptions{} - cmd := &cobra.Command{ - Use: "ls", - Aliases: []string{"list"}, - Short: "List stacks", - Args: cli.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - return runList(dockerCli, kubeCli, opts) - }, - } - - flags := cmd.Flags() - flags.StringVar(&opts.format, "format", "", "Pretty-print stacks using a Go template") - - return cmd -} - -func runList(dockerCli command.Cli, kubeCli *kubeCli, opts listOptions) error { - stacks, err := getStacks(kubeCli) +// RunList is the kubernetes implementation of docker stack ls +func RunList(dockerCli *KubeCli, opts options.List) error { + stacks, err := getStacks(dockerCli) if err != nil { return err } - format := opts.format + format := opts.Format if len(format) == 0 { format = formatter.TableFormatKey } @@ -56,8 +33,8 @@ func (n byName) Len() int { return len(n) } func (n byName) Swap(i, j int) { n[i], n[j] = n[j], n[i] } func (n byName) Less(i, j int) bool { return sortorder.NaturalLess(n[i].Name, n[j].Name) } -func getStacks(kubeCli *kubeCli) ([]*formatter.Stack, error) { - stackSvc, err := kubeCli.Stacks() +func getStacks(kubeCli *KubeCli) ([]*formatter.Stack, error) { + stackSvc, err := kubeCli.stacks() if err != nil { return nil, err } diff --git a/cli/command/stack/kubernetes/ps.go b/cli/command/stack/kubernetes/ps.go index a2e3efe728..d33acabcbe 100644 --- a/cli/command/stack/kubernetes/ps.go +++ b/cli/command/stack/kubernetes/ps.go @@ -4,13 +4,11 @@ import ( "fmt" "sort" - "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/formatter" + "github.com/docker/cli/cli/command/stack/options" "github.com/docker/cli/cli/command/task" - "github.com/docker/cli/opts" "github.com/docker/docker/api/types/swarm" - "github.com/spf13/cobra" apiv1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/fields" @@ -18,46 +16,16 @@ import ( "k8s.io/kubernetes/pkg/api" ) -type psOptions struct { - filter opts.FilterOpt - noTrunc bool - namespace string - noResolve bool - quiet bool - format string -} - -func newPsCommand(dockerCli command.Cli, kubeCli *kubeCli) *cobra.Command { - options := psOptions{filter: opts.NewFilterOpt()} - - cmd := &cobra.Command{ - Use: "ps [OPTIONS] STACK", - Short: "List the tasks in the stack", - Args: cli.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - options.namespace = args[0] - return runPS(dockerCli, kubeCli, options) - }, - } - flags := cmd.Flags() - flags.BoolVar(&options.noTrunc, "no-trunc", false, "Do not truncate output") - flags.BoolVar(&options.noResolve, "no-resolve", false, "Do not map IDs to Names") - flags.VarP(&options.filter, "filter", "f", "Filter output based on conditions provided") - flags.BoolVarP(&options.quiet, "quiet", "q", false, "Only display task IDs") - flags.StringVar(&options.format, "format", "", "Pretty-print tasks using a Go template") - - return cmd -} - -func runPS(dockerCli command.Cli, kubeCli *kubeCli, options psOptions) error { - namespace := options.namespace +// RunPS is the kubernetes implementation of docker stack ps +func RunPS(dockerCli *KubeCli, options options.PS) error { + namespace := options.Namespace // Initialize clients - client, err := kubeCli.ComposeClient() + client, err := dockerCli.composeClient() if err != nil { return err } - stacks, err := kubeCli.Stacks() + stacks, err := dockerCli.stacks() if err != nil { return err } @@ -77,17 +45,17 @@ func runPS(dockerCli command.Cli, kubeCli *kubeCli, options psOptions) error { return fmt.Errorf("nothing found in stack: %s", namespace) } - format := options.format + format := options.Format if len(format) == 0 { - format = task.DefaultFormat(dockerCli.ConfigFile(), options.quiet) + format = task.DefaultFormat(dockerCli.ConfigFile(), options.Quiet) } - nodeResolver := makeNodeResolver(options.noResolve, client.Nodes()) + nodeResolver := makeNodeResolver(options.NoResolve, client.Nodes()) tasks := make([]swarm.Task, len(pods)) for i, pod := range pods { tasks[i] = podToTask(pod) } - return print(dockerCli, tasks, pods, nodeResolver, !options.noTrunc, options.quiet, format) + return print(dockerCli, tasks, pods, nodeResolver, !options.NoTrunc, options.Quiet, format) } type idResolver func(name string) (string, error) diff --git a/cli/command/stack/kubernetes/remove.go b/cli/command/stack/kubernetes/remove.go index 05e6b3543a..955bb30195 100644 --- a/cli/command/stack/kubernetes/remove.go +++ b/cli/command/stack/kubernetes/remove.go @@ -3,38 +3,17 @@ package kubernetes import ( "fmt" - "github.com/docker/cli/cli" - "github.com/docker/cli/cli/command" - "github.com/spf13/cobra" + "github.com/docker/cli/cli/command/stack/options" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -type removeOptions struct { - stacks []string -} - -func newRemoveCommand(dockerCli command.Cli, kubeCli *kubeCli) *cobra.Command { - var opts removeOptions - - cmd := &cobra.Command{ - Use: "rm STACK [STACK...]", - Aliases: []string{"remove", "down"}, - Short: "Remove one or more stacks", - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.stacks = args - return runRemove(dockerCli, kubeCli, opts) - }, - } - return cmd -} - -func runRemove(dockerCli command.Cli, kubeCli *kubeCli, opts removeOptions) error { - stacks, err := kubeCli.Stacks() +// RunRemove is the kubernetes implementation of docker stack remove +func RunRemove(dockerCli *KubeCli, opts options.Remove) error { + stacks, err := dockerCli.stacks() if err != nil { return err } - for _, stack := range opts.stacks { + for _, stack := range opts.Namespaces { fmt.Fprintf(dockerCli.Out(), "Removing stack: %s\n", stack) err := stacks.Delete(stack, &metav1.DeleteOptions{}) if err != nil { diff --git a/cli/command/stack/kubernetes/services.go b/cli/command/stack/kubernetes/services.go index 9cc82f1cb2..ee8cecacb4 100644 --- a/cli/command/stack/kubernetes/services.go +++ b/cli/command/stack/kubernetes/services.go @@ -3,62 +3,36 @@ package kubernetes import ( "fmt" - "github.com/docker/cli/cli" - "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/formatter" + "github.com/docker/cli/cli/command/stack/options" "github.com/docker/cli/kubernetes/labels" - "github.com/spf13/cobra" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -type servicesOptions struct { - quiet bool - format string - namespace string -} - -func newServicesCommand(dockerCli command.Cli, kubeCli *kubeCli) *cobra.Command { - var options servicesOptions - - cmd := &cobra.Command{ - Use: "services [OPTIONS] STACK", - Short: "List the services in the stack", - Args: cli.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - options.namespace = args[0] - return runServices(dockerCli, kubeCli, options) - }, - } - flags := cmd.Flags() - flags.BoolVarP(&options.quiet, "quiet", "q", false, "Only display IDs") - flags.StringVar(&options.format, "format", "", "Pretty-print services using a Go template") - - return cmd -} - -func runServices(dockerCli command.Cli, kubeCli *kubeCli, options servicesOptions) error { +// RunServices is the kubernetes implementation of docker stack services +func RunServices(dockerCli *KubeCli, opts options.Services) error { // Initialize clients - client, err := kubeCli.ComposeClient() + client, err := dockerCli.composeClient() if err != nil { return nil } - stacks, err := kubeCli.Stacks() + stacks, err := dockerCli.stacks() if err != nil { return err } replicas := client.ReplicaSets() - if _, err := stacks.Get(options.namespace, metav1.GetOptions{}); err != nil { - fmt.Fprintf(dockerCli.Err(), "Nothing found in stack: %s\n", options.namespace) + if _, err := stacks.Get(opts.Namespace, metav1.GetOptions{}); err != nil { + fmt.Fprintf(dockerCli.Err(), "Nothing found in stack: %s\n", opts.Namespace) return nil } - replicasList, err := replicas.List(metav1.ListOptions{LabelSelector: labels.SelectorForStack(options.namespace)}) + replicasList, err := replicas.List(metav1.ListOptions{LabelSelector: labels.SelectorForStack(opts.Namespace)}) if err != nil { return err } - servicesList, err := client.Services().List(metav1.ListOptions{LabelSelector: labels.SelectorForStack(options.namespace)}) + servicesList, err := client.Services().List(metav1.ListOptions{LabelSelector: labels.SelectorForStack(opts.Namespace)}) if err != nil { return err } @@ -69,13 +43,13 @@ func runServices(dockerCli command.Cli, kubeCli *kubeCli, options servicesOption return err } - if options.quiet { + if opts.Quiet { info = map[string]formatter.ServiceListInfo{} } - format := options.format + format := opts.Format if len(format) == 0 { - if len(dockerCli.ConfigFile().ServicesFormat) > 0 && !options.quiet { + if len(dockerCli.ConfigFile().ServicesFormat) > 0 && !opts.Quiet { format = dockerCli.ConfigFile().ServicesFormat } else { format = formatter.TableFormatKey @@ -84,7 +58,7 @@ func runServices(dockerCli command.Cli, kubeCli *kubeCli, options servicesOption servicesCtx := formatter.Context{ Output: dockerCli.Out(), - Format: formatter.NewServiceListFormat(format, options.quiet), + Format: formatter.NewServiceListFormat(format, opts.Quiet), } return formatter.ServiceListWrite(servicesCtx, services, info) } diff --git a/cli/command/stack/list.go b/cli/command/stack/list.go new file mode 100644 index 0000000000..4db83765fe --- /dev/null +++ b/cli/command/stack/list.go @@ -0,0 +1,35 @@ +package stack + +import ( + "github.com/docker/cli/cli" + "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/command/stack/kubernetes" + "github.com/docker/cli/cli/command/stack/options" + "github.com/docker/cli/cli/command/stack/swarm" + "github.com/spf13/cobra" +) + +func newListCommand(dockerCli command.Cli) *cobra.Command { + opts := options.List{} + + cmd := &cobra.Command{ + Use: "ls", + Aliases: []string{"list"}, + Short: "List stacks", + Args: cli.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + if dockerCli.ClientInfo().HasKubernetes { + kli, err := kubernetes.WrapCli(dockerCli, cmd) + if err != nil { + return err + } + return kubernetes.RunList(kli, opts) + } + return swarm.RunList(dockerCli, opts) + }, + } + + flags := cmd.Flags() + flags.StringVar(&opts.Format, "format", "", "Pretty-print stacks using a Go template") + return cmd +} diff --git a/cli/command/stack/swarm/list_test.go b/cli/command/stack/list_test.go similarity index 99% rename from cli/command/stack/swarm/list_test.go rename to cli/command/stack/list_test.go index 590daba6ed..90d32e0f99 100644 --- a/cli/command/stack/swarm/list_test.go +++ b/cli/command/stack/list_test.go @@ -1,4 +1,4 @@ -package swarm +package stack import ( "io/ioutil" diff --git a/cli/command/stack/options/opts.go b/cli/command/stack/options/opts.go new file mode 100644 index 0000000000..cbf5398dbf --- /dev/null +++ b/cli/command/stack/options/opts.go @@ -0,0 +1,41 @@ +package options + +import "github.com/docker/cli/opts" + +// Deploy holds docker stack deploy options +type Deploy struct { + Bundlefile string + Composefile string + Namespace string + ResolveImage string + SendRegistryAuth bool + Prune bool +} + +// List holds docker stack ls options +type List struct { + Format string +} + +// PS holds docker stack ps options +type PS struct { + Filter opts.FilterOpt + NoTrunc bool + Namespace string + NoResolve bool + Quiet bool + Format string +} + +// Remove holds docker stack remove options +type Remove struct { + Namespaces []string +} + +// Services holds docker stack services options +type Services struct { + Quiet bool + Format string + Filter opts.FilterOpt + Namespace string +} diff --git a/cli/command/stack/ps.go b/cli/command/stack/ps.go new file mode 100644 index 0000000000..cd8c32a28e --- /dev/null +++ b/cli/command/stack/ps.go @@ -0,0 +1,41 @@ +package stack + +import ( + "github.com/docker/cli/cli" + "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/command/stack/kubernetes" + "github.com/docker/cli/cli/command/stack/options" + "github.com/docker/cli/cli/command/stack/swarm" + cliopts "github.com/docker/cli/opts" + "github.com/spf13/cobra" +) + +func newPsCommand(dockerCli command.Cli) *cobra.Command { + opts := options.PS{Filter: cliopts.NewFilterOpt()} + + cmd := &cobra.Command{ + Use: "ps [OPTIONS] STACK", + Short: "List the tasks in the stack", + Args: cli.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + opts.Namespace = args[0] + if dockerCli.ClientInfo().HasKubernetes { + kli, err := kubernetes.WrapCli(dockerCli, cmd) + if err != nil { + return err + } + return kubernetes.RunPS(kli, opts) + } + return swarm.RunPS(dockerCli, opts) + }, + } + flags := cmd.Flags() + flags.BoolVar(&opts.NoTrunc, "no-trunc", false, "Do not truncate output") + flags.BoolVar(&opts.NoResolve, "no-resolve", false, "Do not map IDs to Names") + flags.VarP(&opts.Filter, "filter", "f", "Filter output based on conditions provided") + flags.SetAnnotation("filter", "swarm", nil) + flags.BoolVarP(&opts.Quiet, "quiet", "q", false, "Only display task IDs") + flags.StringVar(&opts.Format, "format", "", "Pretty-print tasks using a Go template") + + return cmd +} diff --git a/cli/command/stack/swarm/ps_test.go b/cli/command/stack/ps_test.go similarity index 99% rename from cli/command/stack/swarm/ps_test.go rename to cli/command/stack/ps_test.go index 293d70e286..fb7055f09c 100644 --- a/cli/command/stack/swarm/ps_test.go +++ b/cli/command/stack/ps_test.go @@ -1,4 +1,4 @@ -package swarm +package stack import ( "io/ioutil" diff --git a/cli/command/stack/remove.go b/cli/command/stack/remove.go new file mode 100644 index 0000000000..8324ba09a2 --- /dev/null +++ b/cli/command/stack/remove.go @@ -0,0 +1,33 @@ +package stack + +import ( + "github.com/docker/cli/cli" + "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/command/stack/kubernetes" + "github.com/docker/cli/cli/command/stack/options" + "github.com/docker/cli/cli/command/stack/swarm" + "github.com/spf13/cobra" +) + +func newRemoveCommand(dockerCli command.Cli) *cobra.Command { + var opts options.Remove + + cmd := &cobra.Command{ + Use: "rm STACK [STACK...]", + Aliases: []string{"remove", "down"}, + Short: "Remove one or more stacks", + Args: cli.RequiresMinArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + opts.Namespaces = args + if dockerCli.ClientInfo().HasKubernetes { + kli, err := kubernetes.WrapCli(dockerCli, cmd) + if err != nil { + return err + } + return kubernetes.RunRemove(kli, opts) + } + return swarm.RunRemove(dockerCli, opts) + }, + } + return cmd +} diff --git a/cli/command/stack/swarm/remove_test.go b/cli/command/stack/remove_test.go similarity index 99% rename from cli/command/stack/swarm/remove_test.go rename to cli/command/stack/remove_test.go index b2b9b4a523..0ce5cdacfc 100644 --- a/cli/command/stack/swarm/remove_test.go +++ b/cli/command/stack/remove_test.go @@ -1,4 +1,4 @@ -package swarm +package stack import ( "errors" diff --git a/cli/command/stack/services.go b/cli/command/stack/services.go new file mode 100644 index 0000000000..b3331c3df4 --- /dev/null +++ b/cli/command/stack/services.go @@ -0,0 +1,39 @@ +package stack + +import ( + "github.com/docker/cli/cli" + "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/command/stack/kubernetes" + "github.com/docker/cli/cli/command/stack/options" + "github.com/docker/cli/cli/command/stack/swarm" + cliopts "github.com/docker/cli/opts" + "github.com/spf13/cobra" +) + +func newServicesCommand(dockerCli command.Cli) *cobra.Command { + opts := options.Services{Filter: cliopts.NewFilterOpt()} + + cmd := &cobra.Command{ + Use: "services [OPTIONS] STACK", + Short: "List the services in the stack", + Args: cli.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + opts.Namespace = args[0] + if dockerCli.ClientInfo().HasKubernetes { + kli, err := kubernetes.WrapCli(dockerCli, cmd) + if err != nil { + return err + } + return kubernetes.RunServices(kli, opts) + } + return swarm.RunServices(dockerCli, opts) + }, + } + flags := cmd.Flags() + flags.BoolVarP(&opts.Quiet, "quiet", "q", false, "Only display IDs") + flags.StringVar(&opts.Format, "format", "", "Pretty-print services using a Go template") + flags.VarP(&opts.Filter, "filter", "f", "Filter output based on conditions provided") + flags.SetAnnotation("filter", "swarm", nil) + + return cmd +} diff --git a/cli/command/stack/swarm/services_test.go b/cli/command/stack/services_test.go similarity index 99% rename from cli/command/stack/swarm/services_test.go rename to cli/command/stack/services_test.go index 5ffb5c878a..2f6d6e0e00 100644 --- a/cli/command/stack/swarm/services_test.go +++ b/cli/command/stack/services_test.go @@ -1,4 +1,4 @@ -package swarm +package stack import ( "io/ioutil" diff --git a/cli/command/stack/swarm/cmd.go b/cli/command/stack/swarm/cmd.go deleted file mode 100644 index 25a68f7fdc..0000000000 --- a/cli/command/stack/swarm/cmd.go +++ /dev/null @@ -1,22 +0,0 @@ -package swarm - -import ( - "github.com/docker/cli/cli/command" - "github.com/spf13/cobra" -) - -// AddStackCommands adds `stack` subcommands -func AddStackCommands(root *cobra.Command, dockerCli command.Cli) { - root.AddCommand( - newDeployCommand(dockerCli), - newListCommand(dockerCli), - newRemoveCommand(dockerCli), - newServicesCommand(dockerCli), - newPsCommand(dockerCli), - ) -} - -// NewTopLevelDeployCommand returns a command for `docker deploy` -func NewTopLevelDeployCommand(dockerCli command.Cli) *cobra.Command { - return newDeployCommand(dockerCli) -} diff --git a/cli/command/stack/swarm/deploy.go b/cli/command/stack/swarm/deploy.go index 1ce98a6556..2337873719 100644 --- a/cli/command/stack/swarm/deploy.go +++ b/cli/command/stack/swarm/deploy.go @@ -3,60 +3,25 @@ package swarm import ( "fmt" - "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/stack/common" + "github.com/docker/cli/cli/command/stack/options" "github.com/docker/cli/cli/compose/convert" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/api/types/versions" "github.com/pkg/errors" - "github.com/spf13/cobra" "golang.org/x/net/context" ) +// Resolve image constants const ( defaultNetworkDriver = "overlay" - resolveImageAlways = "always" - resolveImageChanged = "changed" - resolveImageNever = "never" + ResolveImageAlways = "always" + ResolveImageChanged = "changed" + ResolveImageNever = "never" ) -type deployOptions struct { - bundlefile string - composefile string - namespace string - resolveImage string - sendRegistryAuth bool - prune bool -} - -func newDeployCommand(dockerCli command.Cli) *cobra.Command { - var opts deployOptions - - cmd := &cobra.Command{ - Use: "deploy [OPTIONS] STACK", - Aliases: []string{"up"}, - Short: "Deploy a new stack or update an existing stack", - Args: cli.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.namespace = args[0] - return runDeploy(dockerCli, opts) - }, - } - - flags := cmd.Flags() - common.AddBundlefileFlag(&opts.bundlefile, flags) - common.AddComposefileFlag(&opts.composefile, flags) - common.AddRegistryAuthFlag(&opts.sendRegistryAuth, flags) - flags.BoolVar(&opts.prune, "prune", false, "Prune services that are no longer referenced") - flags.SetAnnotation("prune", "version", []string{"1.27"}) - flags.StringVar(&opts.resolveImage, "resolve-image", resolveImageAlways, - `Query the registry to resolve image digest and supported platforms ("`+resolveImageAlways+`"|"`+resolveImageChanged+`"|"`+resolveImageNever+`")`) - flags.SetAnnotation("resolve-image", "version", []string{"1.30"}) - return cmd -} - -func runDeploy(dockerCli command.Cli, opts deployOptions) error { +// RunDeploy is the swarm implementation of docker stack deploy +func RunDeploy(dockerCli command.Cli, opts options.Deploy) error { ctx := context.Background() if err := validateResolveImageFlag(dockerCli, &opts); err != nil { @@ -64,11 +29,11 @@ func runDeploy(dockerCli command.Cli, opts deployOptions) error { } switch { - case opts.bundlefile == "" && opts.composefile == "": + case opts.Bundlefile == "" && opts.Composefile == "": return errors.Errorf("Please specify either a bundle file (with --bundle-file) or a Compose file (with --compose-file).") - case opts.bundlefile != "" && opts.composefile != "": + case opts.Bundlefile != "" && opts.Composefile != "": return errors.Errorf("You cannot specify both a bundle file and a Compose file.") - case opts.bundlefile != "": + case opts.Bundlefile != "": return deployBundle(ctx, dockerCli, opts) default: return deployCompose(ctx, dockerCli, opts) @@ -77,14 +42,14 @@ func runDeploy(dockerCli command.Cli, opts deployOptions) error { // validateResolveImageFlag validates the opts.resolveImage command line option // and also turns image resolution off if the version is older than 1.30 -func validateResolveImageFlag(dockerCli command.Cli, opts *deployOptions) error { - if opts.resolveImage != resolveImageAlways && opts.resolveImage != resolveImageChanged && opts.resolveImage != resolveImageNever { - return errors.Errorf("Invalid option %s for flag --resolve-image", opts.resolveImage) +func validateResolveImageFlag(dockerCli command.Cli, opts *options.Deploy) error { + if opts.ResolveImage != ResolveImageAlways && opts.ResolveImage != ResolveImageChanged && opts.ResolveImage != ResolveImageNever { + return errors.Errorf("Invalid option %s for flag --resolve-image", opts.ResolveImage) } // client side image resolution should not be done when the supported // server version is older than 1.30 if versions.LessThan(dockerCli.Client().ClientVersion(), "1.30") { - opts.resolveImage = resolveImageNever + opts.ResolveImage = ResolveImageNever } return nil } diff --git a/cli/command/stack/swarm/deploy_bundlefile.go b/cli/command/stack/swarm/deploy_bundlefile.go index 70b982138f..b3819ef0f4 100644 --- a/cli/command/stack/swarm/deploy_bundlefile.go +++ b/cli/command/stack/swarm/deploy_bundlefile.go @@ -1,17 +1,23 @@ package swarm import ( + "fmt" + "io" + "os" + "golang.org/x/net/context" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/stack/common" + "github.com/docker/cli/cli/command/bundlefile" + "github.com/docker/cli/cli/command/stack/options" "github.com/docker/cli/cli/compose/convert" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/swarm" + "github.com/pkg/errors" ) -func deployBundle(ctx context.Context, dockerCli command.Cli, opts deployOptions) error { - bundle, err := common.LoadBundlefile(dockerCli.Err(), opts.namespace, opts.bundlefile) +func deployBundle(ctx context.Context, dockerCli command.Cli, opts options.Deploy) error { + bundle, err := loadBundlefile(dockerCli.Err(), opts.Namespace, opts.Bundlefile) if err != nil { return err } @@ -20,9 +26,9 @@ func deployBundle(ctx context.Context, dockerCli command.Cli, opts deployOptions return err } - namespace := convert.NewNamespace(opts.namespace) + namespace := convert.NewNamespace(opts.Namespace) - if opts.prune { + if opts.Prune { services := map[string]struct{}{} for service := range bundle.Services { services[service] = struct{}{} @@ -88,5 +94,31 @@ func deployBundle(ctx context.Context, dockerCli command.Cli, opts deployOptions if err := createNetworks(ctx, dockerCli, namespace, networks); err != nil { return err } - return deployServices(ctx, dockerCli, services, namespace, opts.sendRegistryAuth, opts.resolveImage) + return deployServices(ctx, dockerCli, services, namespace, opts.SendRegistryAuth, opts.ResolveImage) +} + +func loadBundlefile(stderr io.Writer, namespace string, path string) (*bundlefile.Bundlefile, error) { + defaultPath := fmt.Sprintf("%s.dab", namespace) + + if path == "" { + path = defaultPath + } + if _, err := os.Stat(path); err != nil { + return nil, errors.Errorf( + "Bundle %s not found. Specify the path with --file", + path) + } + + fmt.Fprintf(stderr, "Loading bundle from %s\n", path) + reader, err := os.Open(path) + if err != nil { + return nil, err + } + defer reader.Close() + + bundle, err := bundlefile.LoadFile(reader) + if err != nil { + return nil, errors.Errorf("Error reading %s: %v\n", path, err) + } + return bundle, err } diff --git a/cli/command/stack/common/opts_test.go b/cli/command/stack/swarm/deploy_bundlefile_test.go similarity index 87% rename from cli/command/stack/common/opts_test.go rename to cli/command/stack/swarm/deploy_bundlefile_test.go index b3874f497e..c259c60d2a 100644 --- a/cli/command/stack/common/opts_test.go +++ b/cli/command/stack/swarm/deploy_bundlefile_test.go @@ -1,4 +1,4 @@ -package common +package swarm import ( "bytes" @@ -32,7 +32,7 @@ func TestLoadBundlefileErrors(t *testing.T) { } for _, tc := range testCases { - _, err := LoadBundlefile(&bytes.Buffer{}, tc.namespace, tc.path) + _, err := loadBundlefile(&bytes.Buffer{}, tc.namespace, tc.path) assert.Error(t, err, tc.expectedError) } } @@ -42,7 +42,7 @@ func TestLoadBundlefile(t *testing.T) { namespace := "" path := filepath.Join("testdata", "bundlefile_with_two_services.dab") - bundleFile, err := LoadBundlefile(buf, namespace, path) + bundleFile, err := loadBundlefile(buf, namespace, path) assert.NoError(t, err) assert.Equal(t, len(bundleFile.Services), 2) diff --git a/cli/command/stack/swarm/deploy_composefile.go b/cli/command/stack/swarm/deploy_composefile.go index 6d4c96521e..95700b8b90 100644 --- a/cli/command/stack/swarm/deploy_composefile.go +++ b/cli/command/stack/swarm/deploy_composefile.go @@ -10,6 +10,7 @@ import ( "strings" "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/command/stack/options" "github.com/docker/cli/cli/compose/convert" "github.com/docker/cli/cli/compose/loader" composetypes "github.com/docker/cli/cli/compose/types" @@ -22,8 +23,8 @@ import ( "golang.org/x/net/context" ) -func deployCompose(ctx context.Context, dockerCli command.Cli, opts deployOptions) error { - configDetails, err := getConfigDetails(opts.composefile, dockerCli.In()) +func deployCompose(ctx context.Context, dockerCli command.Cli, opts options.Deploy) error { + configDetails, err := getConfigDetails(opts.Composefile, dockerCli.In()) if err != nil { return err } @@ -54,9 +55,9 @@ func deployCompose(ctx context.Context, dockerCli command.Cli, opts deployOption return err } - namespace := convert.NewNamespace(opts.namespace) + namespace := convert.NewNamespace(opts.Namespace) - if opts.prune { + if opts.Prune { services := map[string]struct{}{} for _, service := range config.Services { services[service.Name] = struct{}{} @@ -93,7 +94,7 @@ func deployCompose(ctx context.Context, dockerCli command.Cli, opts deployOption if err != nil { return err } - return deployServices(ctx, dockerCli, services, namespace, opts.sendRegistryAuth, opts.resolveImage) + return deployServices(ctx, dockerCli, services, namespace, opts.SendRegistryAuth, opts.ResolveImage) } func getServicesDeclaredNetworks(serviceConfigs []composetypes.ServiceConfig) map[string]struct{} { @@ -339,7 +340,7 @@ func deployServices( updateOpts := types.ServiceUpdateOptions{EncodedRegistryAuth: encodedAuth} switch { - case resolveImage == resolveImageAlways || (resolveImage == resolveImageChanged && image != service.Spec.Labels[convert.LabelImage]): + case resolveImage == ResolveImageAlways || (resolveImage == ResolveImageChanged && image != service.Spec.Labels[convert.LabelImage]): // image should be updated by the server using QueryRegistry updateOpts.QueryRegistry = true case image == service.Spec.Labels[convert.LabelImage]: @@ -369,7 +370,7 @@ func deployServices( createOpts := types.ServiceCreateOptions{EncodedRegistryAuth: encodedAuth} // query registry if flag disabling it was not set - if resolveImage == resolveImageAlways || resolveImage == resolveImageChanged { + if resolveImage == ResolveImageAlways || resolveImage == ResolveImageChanged { createOpts.QueryRegistry = true } diff --git a/cli/command/stack/swarm/deploy_test.go b/cli/command/stack/swarm/deploy_test.go index f4792a1128..73c91a0392 100644 --- a/cli/command/stack/swarm/deploy_test.go +++ b/cli/command/stack/swarm/deploy_test.go @@ -92,7 +92,7 @@ func TestServiceUpdateResolveImageChanged(t *testing.T) { }, }, } - err := deployServices(ctx, client, spec, namespace, false, resolveImageChanged) + err := deployServices(ctx, client, spec, namespace, false, ResolveImageChanged) assert.NoError(t, err) assert.Equal(t, testcase.expectedQueryRegistry, receivedOptions.QueryRegistry) assert.Equal(t, testcase.expectedImage, receivedService.TaskTemplate.ContainerSpec.Image) diff --git a/cli/command/stack/swarm/list.go b/cli/command/stack/swarm/list.go index 216159a432..3bcdab7232 100644 --- a/cli/command/stack/swarm/list.go +++ b/cli/command/stack/swarm/list.go @@ -3,41 +3,19 @@ package swarm import ( "sort" - "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/formatter" + "github.com/docker/cli/cli/command/stack/options" "github.com/docker/cli/cli/compose/convert" "github.com/docker/docker/api/types" "github.com/docker/docker/client" "github.com/pkg/errors" - "github.com/spf13/cobra" "golang.org/x/net/context" "vbom.ml/util/sortorder" ) -type listOptions struct { - format string -} - -func newListCommand(dockerCli command.Cli) *cobra.Command { - opts := listOptions{} - - cmd := &cobra.Command{ - Use: "ls", - Aliases: []string{"list"}, - Short: "List stacks", - Args: cli.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - return runList(dockerCli, opts) - }, - } - - flags := cmd.Flags() - flags.StringVar(&opts.format, "format", "", "Pretty-print stacks using a Go template") - return cmd -} - -func runList(dockerCli command.Cli, opts listOptions) error { +// RunList is the swarm implementation of docker stack ls +func RunList(dockerCli command.Cli, opts options.List) error { client := dockerCli.Client() ctx := context.Background() @@ -45,7 +23,7 @@ func runList(dockerCli command.Cli, opts listOptions) error { if err != nil { return err } - format := opts.format + format := opts.Format if len(format) == 0 { format = formatter.TableFormatKey } diff --git a/cli/command/stack/swarm/ps.go b/cli/command/stack/swarm/ps.go index 9995dde155..721f85d9c3 100644 --- a/cli/command/stack/swarm/ps.go +++ b/cli/command/stack/swarm/ps.go @@ -3,53 +3,21 @@ package swarm import ( "fmt" - "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/idresolver" + "github.com/docker/cli/cli/command/stack/options" "github.com/docker/cli/cli/command/task" - "github.com/docker/cli/opts" "github.com/docker/docker/api/types" - "github.com/spf13/cobra" "golang.org/x/net/context" ) -type psOptions struct { - filter opts.FilterOpt - noTrunc bool - namespace string - noResolve bool - quiet bool - format string -} - -func newPsCommand(dockerCli command.Cli) *cobra.Command { - options := psOptions{filter: opts.NewFilterOpt()} - - cmd := &cobra.Command{ - Use: "ps [OPTIONS] STACK", - Short: "List the tasks in the stack", - Args: cli.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - options.namespace = args[0] - return runPS(dockerCli, options) - }, - } - flags := cmd.Flags() - flags.BoolVar(&options.noTrunc, "no-trunc", false, "Do not truncate output") - flags.BoolVar(&options.noResolve, "no-resolve", false, "Do not map IDs to Names") - flags.VarP(&options.filter, "filter", "f", "Filter output based on conditions provided") - flags.BoolVarP(&options.quiet, "quiet", "q", false, "Only display task IDs") - flags.StringVar(&options.format, "format", "", "Pretty-print tasks using a Go template") - - return cmd -} - -func runPS(dockerCli command.Cli, options psOptions) error { - namespace := options.namespace +// RunPS is the swarm implementation of docker stack ps +func RunPS(dockerCli command.Cli, opts options.PS) error { + namespace := opts.Namespace client := dockerCli.Client() ctx := context.Background() - filter := getStackFilterFromOpt(options.namespace, options.filter) + filter := getStackFilterFromOpt(opts.Namespace, opts.Filter) tasks, err := client.TaskList(ctx, types.TaskListOptions{Filters: filter}) if err != nil { @@ -60,10 +28,10 @@ func runPS(dockerCli command.Cli, options psOptions) error { return fmt.Errorf("nothing found in stack: %s", namespace) } - format := options.format + format := opts.Format if len(format) == 0 { - format = task.DefaultFormat(dockerCli.ConfigFile(), options.quiet) + format = task.DefaultFormat(dockerCli.ConfigFile(), opts.Quiet) } - return task.Print(ctx, dockerCli, tasks, idresolver.New(client, options.noResolve), !options.noTrunc, options.quiet, format) + return task.Print(ctx, dockerCli, tasks, idresolver.New(client, opts.NoResolve), !opts.NoTrunc, opts.Quiet, format) } diff --git a/cli/command/stack/swarm/remove.go b/cli/command/stack/swarm/remove.go index 9e164141f0..4b5941c252 100644 --- a/cli/command/stack/swarm/remove.go +++ b/cli/command/stack/swarm/remove.go @@ -5,38 +5,18 @@ import ( "sort" "strings" - "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/command/stack/options" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/api/types/versions" "github.com/pkg/errors" - "github.com/spf13/cobra" "golang.org/x/net/context" ) -type removeOptions struct { - namespaces []string -} - -func newRemoveCommand(dockerCli command.Cli) *cobra.Command { - var opts removeOptions - - cmd := &cobra.Command{ - Use: "rm STACK [STACK...]", - Aliases: []string{"remove", "down"}, - Short: "Remove one or more stacks", - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.namespaces = args - return runRemove(dockerCli, opts) - }, - } - return cmd -} - -func runRemove(dockerCli command.Cli, opts removeOptions) error { - namespaces := opts.namespaces +// RunRemove is the swarm implementation of docker stack remove +func RunRemove(dockerCli command.Cli, opts options.Remove) error { + namespaces := opts.Namespaces client := dockerCli.Client() ctx := context.Background() diff --git a/cli/command/stack/swarm/services.go b/cli/command/stack/swarm/services.go index 2e2c7dde44..aceb82dff2 100644 --- a/cli/command/stack/swarm/services.go +++ b/cli/command/stack/swarm/services.go @@ -3,49 +3,21 @@ package swarm import ( "fmt" - "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/cli/command/service" - "github.com/docker/cli/opts" + "github.com/docker/cli/cli/command/stack/options" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/filters" - "github.com/spf13/cobra" "golang.org/x/net/context" ) -type servicesOptions struct { - quiet bool - format string - filter opts.FilterOpt - namespace string -} - -func newServicesCommand(dockerCli command.Cli) *cobra.Command { - options := servicesOptions{filter: opts.NewFilterOpt()} - - cmd := &cobra.Command{ - Use: "services [OPTIONS] STACK", - Short: "List the services in the stack", - Args: cli.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - options.namespace = args[0] - return runServices(dockerCli, options) - }, - } - flags := cmd.Flags() - flags.BoolVarP(&options.quiet, "quiet", "q", false, "Only display IDs") - flags.StringVar(&options.format, "format", "", "Pretty-print services using a Go template") - flags.VarP(&options.filter, "filter", "f", "Filter output based on conditions provided") - - return cmd -} - -func runServices(dockerCli command.Cli, options servicesOptions) error { +// RunServices is the swarm implementation of docker stack services +func RunServices(dockerCli command.Cli, opts options.Services) error { ctx := context.Background() client := dockerCli.Client() - filter := getStackFilterFromOpt(options.namespace, options.filter) + filter := getStackFilterFromOpt(opts.Namespace, opts.Filter) services, err := client.ServiceList(ctx, types.ServiceListOptions{Filters: filter}) if err != nil { return err @@ -53,12 +25,12 @@ func runServices(dockerCli command.Cli, options servicesOptions) error { // if no services in this stack, print message and exit 0 if len(services) == 0 { - fmt.Fprintf(dockerCli.Err(), "Nothing found in stack: %s\n", options.namespace) + fmt.Fprintf(dockerCli.Err(), "Nothing found in stack: %s\n", opts.Namespace) return nil } info := map[string]formatter.ServiceListInfo{} - if !options.quiet { + if !opts.Quiet { taskFilter := filters.NewArgs() for _, service := range services { taskFilter.Add("service", service.ID) @@ -77,9 +49,9 @@ func runServices(dockerCli command.Cli, options servicesOptions) error { info = service.GetServicesStatus(services, nodes, tasks) } - format := options.format + format := opts.Format if len(format) == 0 { - if len(dockerCli.ConfigFile().ServicesFormat) > 0 && !options.quiet { + if len(dockerCli.ConfigFile().ServicesFormat) > 0 && !opts.Quiet { format = dockerCli.ConfigFile().ServicesFormat } else { format = formatter.TableFormatKey @@ -88,7 +60,7 @@ func runServices(dockerCli command.Cli, options servicesOptions) error { servicesCtx := formatter.Context{ Output: dockerCli.Out(), - Format: formatter.NewServiceListFormat(format, options.quiet), + Format: formatter.NewServiceListFormat(format, opts.Quiet), } return formatter.ServiceListWrite(servicesCtx, services, info) } diff --git a/cli/command/stack/common/testdata/bundlefile_with_two_services.dab b/cli/command/stack/swarm/testdata/bundlefile_with_two_services.dab similarity index 100% rename from cli/command/stack/common/testdata/bundlefile_with_two_services.dab rename to cli/command/stack/swarm/testdata/bundlefile_with_two_services.dab diff --git a/cli/command/stack/swarm/testdata/stack-list-sort-natural.golden b/cli/command/stack/testdata/stack-list-sort-natural.golden similarity index 100% rename from cli/command/stack/swarm/testdata/stack-list-sort-natural.golden rename to cli/command/stack/testdata/stack-list-sort-natural.golden diff --git a/cli/command/stack/swarm/testdata/stack-list-sort.golden b/cli/command/stack/testdata/stack-list-sort.golden similarity index 100% rename from cli/command/stack/swarm/testdata/stack-list-sort.golden rename to cli/command/stack/testdata/stack-list-sort.golden diff --git a/cli/command/stack/swarm/testdata/stack-list-with-format.golden b/cli/command/stack/testdata/stack-list-with-format.golden similarity index 100% rename from cli/command/stack/swarm/testdata/stack-list-with-format.golden rename to cli/command/stack/testdata/stack-list-with-format.golden diff --git a/cli/command/stack/swarm/testdata/stack-list-without-format.golden b/cli/command/stack/testdata/stack-list-without-format.golden similarity index 100% rename from cli/command/stack/swarm/testdata/stack-list-without-format.golden rename to cli/command/stack/testdata/stack-list-without-format.golden diff --git a/cli/command/stack/swarm/testdata/stack-ps-with-config-format.golden b/cli/command/stack/testdata/stack-ps-with-config-format.golden similarity index 100% rename from cli/command/stack/swarm/testdata/stack-ps-with-config-format.golden rename to cli/command/stack/testdata/stack-ps-with-config-format.golden diff --git a/cli/command/stack/swarm/testdata/stack-ps-with-format.golden b/cli/command/stack/testdata/stack-ps-with-format.golden similarity index 100% rename from cli/command/stack/swarm/testdata/stack-ps-with-format.golden rename to cli/command/stack/testdata/stack-ps-with-format.golden diff --git a/cli/command/stack/swarm/testdata/stack-ps-with-no-resolve-option.golden b/cli/command/stack/testdata/stack-ps-with-no-resolve-option.golden similarity index 100% rename from cli/command/stack/swarm/testdata/stack-ps-with-no-resolve-option.golden rename to cli/command/stack/testdata/stack-ps-with-no-resolve-option.golden diff --git a/cli/command/stack/swarm/testdata/stack-ps-with-no-trunc-option.golden b/cli/command/stack/testdata/stack-ps-with-no-trunc-option.golden similarity index 100% rename from cli/command/stack/swarm/testdata/stack-ps-with-no-trunc-option.golden rename to cli/command/stack/testdata/stack-ps-with-no-trunc-option.golden diff --git a/cli/command/stack/swarm/testdata/stack-ps-with-quiet-option.golden b/cli/command/stack/testdata/stack-ps-with-quiet-option.golden similarity index 100% rename from cli/command/stack/swarm/testdata/stack-ps-with-quiet-option.golden rename to cli/command/stack/testdata/stack-ps-with-quiet-option.golden diff --git a/cli/command/stack/swarm/testdata/stack-ps-without-format.golden b/cli/command/stack/testdata/stack-ps-without-format.golden similarity index 100% rename from cli/command/stack/swarm/testdata/stack-ps-without-format.golden rename to cli/command/stack/testdata/stack-ps-without-format.golden diff --git a/cli/command/stack/swarm/testdata/stack-services-with-config-format.golden b/cli/command/stack/testdata/stack-services-with-config-format.golden similarity index 100% rename from cli/command/stack/swarm/testdata/stack-services-with-config-format.golden rename to cli/command/stack/testdata/stack-services-with-config-format.golden diff --git a/cli/command/stack/swarm/testdata/stack-services-with-format.golden b/cli/command/stack/testdata/stack-services-with-format.golden similarity index 100% rename from cli/command/stack/swarm/testdata/stack-services-with-format.golden rename to cli/command/stack/testdata/stack-services-with-format.golden diff --git a/cli/command/stack/swarm/testdata/stack-services-with-quiet-option.golden b/cli/command/stack/testdata/stack-services-with-quiet-option.golden similarity index 100% rename from cli/command/stack/swarm/testdata/stack-services-with-quiet-option.golden rename to cli/command/stack/testdata/stack-services-with-quiet-option.golden diff --git a/cli/command/stack/swarm/testdata/stack-services-without-format.golden b/cli/command/stack/testdata/stack-services-without-format.golden similarity index 100% rename from cli/command/stack/swarm/testdata/stack-services-without-format.golden rename to cli/command/stack/testdata/stack-services-without-format.golden diff --git a/cli/command/system/version.go b/cli/command/system/version.go index f6a725ae23..2e676a0c14 100644 --- a/cli/command/system/version.go +++ b/cli/command/system/version.go @@ -137,8 +137,12 @@ func runVersion(dockerCli *command.DockerCli, opts *versionOptions) error { BuildTime: cli.BuildTime, Os: runtime.GOOS, Arch: runtime.GOARCH, +<<<<<<< HEAD Experimental: dockerCli.ClientInfo().HasExperimental, Orchestrator: string(command.GetOrchestrator(dockerCli)), +======= + Orchestrator: string(command.GetOrchestrator(dockerCli.ConfigFile().Orchestrator)), +>>>>>>> Refactor stack command }, } vd.Client.Platform.Name = cli.PlatformName diff --git a/cmd/docker/docker.go b/cmd/docker/docker.go index 6c3cba61fc..c96c3b674b 100644 --- a/cmd/docker/docker.go +++ b/cmd/docker/docker.go @@ -195,6 +195,7 @@ type versionDetails interface { Client() client.APIClient ClientInfo() command.ClientInfo ServerInfo() command.ServerInfo + ClientInfo() command.ClientInfo } func hideUnsupportedFeatures(cmd *cobra.Command, details versionDetails) { @@ -202,6 +203,7 @@ func hideUnsupportedFeatures(cmd *cobra.Command, details versionDetails) { osType := details.ServerInfo().OSType hasExperimental := details.ServerInfo().HasExperimental hasExperimentalCLI := details.ClientInfo().HasExperimental + hasKubernetes := details.ClientInfo().HasKubernetes cmd.Flags().VisitAll(func(f *pflag.Flag) { // hide experimental flags @@ -215,6 +217,15 @@ func hideUnsupportedFeatures(cmd *cobra.Command, details versionDetails) { f.Hidden = true } } + if !hasKubernetes { + if _, ok := f.Annotations["kubernetes"]; ok { + f.Hidden = true + } + } else { + if _, ok := f.Annotations["swarm"]; ok { + f.Hidden = true + } + } // hide flags not supported by the server if !isOSTypeSupported(f, osType) || !isVersionSupported(f, clientVersion) { @@ -235,6 +246,16 @@ func hideUnsupportedFeatures(cmd *cobra.Command, details versionDetails) { } } + if !hasKubernetes { + if _, ok := subcmd.Annotations["kubernetes"]; ok { + subcmd.Hidden = true + } + } else { + if _, ok := subcmd.Annotations["swarm"]; ok { + subcmd.Hidden = true + } + } + // hide subcommands not supported by the server if subcmdVersion, ok := subcmd.Annotations["version"]; ok && versions.LessThan(clientVersion, subcmdVersion) { subcmd.Hidden = true @@ -243,23 +264,22 @@ func hideUnsupportedFeatures(cmd *cobra.Command, details versionDetails) { } func isSupported(cmd *cobra.Command, details versionDetails) error { + if err := areSubcommandsSupported(cmd, details); err != nil { + return err + } + + if err := areFlagsSupported(cmd, details); err != nil { + return err + } + + return nil +} + +func areFlagsSupported(cmd *cobra.Command, details versionDetails) error { clientVersion := details.Client().ClientVersion() osType := details.ServerInfo().OSType hasExperimental := details.ServerInfo().HasExperimental - hasExperimentalCLI := details.ClientInfo().HasExperimental - - // Check recursively so that, e.g., `docker stack ls` returns the same output as `docker stack` - for curr := cmd; curr != nil; curr = curr.Parent() { - if cmdVersion, ok := curr.Annotations["version"]; ok && versions.LessThan(clientVersion, cmdVersion) { - return fmt.Errorf("%s requires API version %s, but the Docker daemon API version is %s", cmd.CommandPath(), cmdVersion, clientVersion) - } - if _, ok := curr.Annotations["experimental"]; ok && !hasExperimental { - return fmt.Errorf("%s is only supported on a Docker daemon with experimental features enabled", cmd.CommandPath()) - } - if _, ok := curr.Annotations["experimentalCLI"]; ok && !hasExperimentalCLI { - return fmt.Errorf("%s is only supported when experimental cli features are enabled", cmd.CommandPath()) - } - } + hasKubernetes := details.ClientInfo().HasKubernetes errs := []string{} @@ -279,12 +299,45 @@ func isSupported(cmd *cobra.Command, details versionDetails) error { if _, ok := f.Annotations["experimentalCLI"]; ok && !hasExperimentalCLI { errs = append(errs, fmt.Sprintf("\"--%s\" is only supported when experimental cli features are enabled", f.Name)) } + if _, ok := f.Annotations["kubernetes"]; ok && !hasKubernetes { + errs = append(errs, fmt.Sprintf("\"--%s\" is only supported on a Docker cli with kubernetes features enabled", f.Name)) + } + if _, ok := f.Annotations["swarm"]; ok && hasKubernetes { + errs = append(errs, fmt.Sprintf("\"--%s\" is only supported on a Docker cli with swarm features enabled", f.Name)) + } } }) if len(errs) > 0 { return errors.New(strings.Join(errs, "\n")) } + return nil +} +// Check recursively so that, e.g., `docker stack ls` returns the same output as `docker stack` +func areSubcommandsSupported(cmd *cobra.Command, details versionDetails) error { + clientVersion := details.Client().ClientVersion() + hasExperimental := details.ServerInfo().HasExperimental + hasExperimentalCLI := details.ClientInfo().HasExperimental + hasKubernetes := details.ClientInfo().HasKubernetes + + // Check recursively so that, e.g., `docker stack ls` returns the same output as `docker stack` + for curr := cmd; curr != nil; curr = curr.Parent() { + if cmdVersion, ok := curr.Annotations["version"]; ok && versions.LessThan(clientVersion, cmdVersion) { + return fmt.Errorf("%s requires API version %s, but the Docker daemon API version is %s", cmd.CommandPath(), cmdVersion, clientVersion) + } + if _, ok := curr.Annotations["experimental"]; ok && !hasExperimental { + return fmt.Errorf("%s is only supported on a Docker daemon with experimental features enabled", cmd.CommandPath()) + } + if _, ok := curr.Annotations["experimentalCLI"]; ok && !hasExperimentalCLI { + return fmt.Errorf("%s is only supported when experimental cli features are enabled", cmd.CommandPath()) + } + if _, ok := curr.Annotations["kubernetes"]; ok && !hasKubernetes { + return fmt.Errorf("%s is only supported on a Docker cli with kubernetes features enabled", cmd.CommandPath()) + } + if _, ok := curr.Annotations["swarm"]; ok && hasKubernetes { + return fmt.Errorf("%s is only supported on a Docker cli with swarm features enabled", cmd.CommandPath()) + } + } return nil }