Refactor stack command

- Define command and subcommands only once
- Use annotations for k8s or swarm specific flags or subcommands

Signed-off-by: Vincent Demeester <vincent@sbr.pm>
This commit is contained in:
Vincent Demeester 2017-12-04 12:30:39 +01:00 committed by Silvin Lubecki
parent 0508c09494
commit dedd0db51a
49 changed files with 780 additions and 591 deletions

View File

@ -135,9 +135,11 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions) error {
if err != nil { if err != nil {
return errors.Wrap(err, "Experimental field") return errors.Wrap(err, "Experimental field")
} }
orchestrator := GetOrchestrator(cli.configFile.Orchestrator)
cli.clientInfo = ClientInfo{ cli.clientInfo = ClientInfo{
DefaultVersion: cli.client.ClientVersion(), DefaultVersion: cli.client.ClientVersion(),
HasExperimental: hasExperimental, HasExperimental: hasExperimental,
HasKubernetes: orchestrator == OrchestratorKubernetes,
} }
cli.initializeFromClient() cli.initializeFromClient()
return nil return nil
@ -202,6 +204,7 @@ type ServerInfo struct {
// ClientInfo stores details about the supported features of the client // ClientInfo stores details about the supported features of the client
type ClientInfo struct { type ClientInfo struct {
HasExperimental bool HasExperimental bool
HasKubernetes bool
DefaultVersion string DefaultVersion string
} }

View File

@ -3,17 +3,15 @@ package command
import ( import (
"os" "os"
"strings" "strings"
cliconfig "github.com/docker/cli/cli/config"
) )
// Orchestrator type acts as an enum describing supported orchestrators. // Orchestrator type acts as an enum describing supported orchestrators.
type Orchestrator string type Orchestrator string
const ( const (
// Kubernetes orchestrator // OrchestratorKubernetes orchestrator
OrchestratorKubernetes = Orchestrator("kubernetes") OrchestratorKubernetes = Orchestrator("kubernetes")
// Swarm orchestrator // OrchestratorSwarm orchestrator
OrchestratorSwarm = Orchestrator("swarm") OrchestratorSwarm = Orchestrator("swarm")
orchestratorUnset = Orchestrator("unset") orchestratorUnset = Orchestrator("unset")
@ -34,18 +32,16 @@ func normalize(flag string) Orchestrator {
// GetOrchestrator checks DOCKER_ORCHESTRATOR environment variable and configuration file // GetOrchestrator checks DOCKER_ORCHESTRATOR environment variable and configuration file
// orchestrator value and returns user defined Orchestrator. // orchestrator value and returns user defined Orchestrator.
func GetOrchestrator(dockerCli Cli) Orchestrator { func GetOrchestrator(orchestrator string) Orchestrator {
// Check environment variable // Check environment variable
env := os.Getenv(dockerOrchestrator) env := os.Getenv(dockerOrchestrator)
if o := normalize(env); o != orchestratorUnset { if o := normalize(env); o != orchestratorUnset {
return o return o
} }
// Check config file // Check specified orchestrator
if configFile := cliconfig.LoadDefaultConfigFile(dockerCli.Err()); configFile != nil { if o := normalize(orchestrator); o != orchestratorUnset {
if o := normalize(configFile.Orchestrator); o != orchestratorUnset {
return o return o
} }
}
// Nothing set, use default orchestrator // Nothing set, use default orchestrator
return defaultOrchestrator return defaultOrchestrator

View File

@ -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
}

View File

@ -3,8 +3,6 @@ package stack
import ( import (
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
"github.com/docker/cli/cli/command" "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" "github.com/spf13/cobra"
) )
@ -17,26 +15,24 @@ func NewStackCommand(dockerCli command.Cli) *cobra.Command {
RunE: command.ShowHelp(dockerCli.Err()), RunE: command.ShowHelp(dockerCli.Err()),
Annotations: map[string]string{"version": "1.25"}, Annotations: map[string]string{"version": "1.25"},
} }
switch command.GetOrchestrator(dockerCli) { cmd.AddCommand(
case command.OrchestratorKubernetes: newDeployCommand(dockerCli),
kubernetes.AddStackCommands(cmd, dockerCli) newListCommand(dockerCli),
case command.OrchestratorSwarm: newPsCommand(dockerCli),
swarm.AddStackCommands(cmd, 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 return cmd
} }
// NewTopLevelDeployCommand returns a command for `docker deploy` // NewTopLevelDeployCommand returns a command for `docker deploy`
func NewTopLevelDeployCommand(dockerCli command.Cli) *cobra.Command { func NewTopLevelDeployCommand(dockerCli command.Cli) *cobra.Command {
var cmd *cobra.Command cmd := newDeployCommand(dockerCli)
switch command.GetOrchestrator(dockerCli) {
case command.OrchestratorKubernetes:
cmd = kubernetes.NewTopLevelDeployCommand(dockerCli)
case command.OrchestratorSwarm:
cmd = swarm.NewTopLevelDeployCommand(dockerCli)
default:
cmd = swarm.NewTopLevelDeployCommand(dockerCli)
}
// Remove the aliases at the top level // Remove the aliases at the top level
cmd.Aliases = []string{} cmd.Aliases = []string{}
cmd.Annotations = map[string]string{"experimental": "", "version": "1.25"} cmd.Annotations = map[string]string{"experimental": "", "version": "1.25"}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -5,51 +5,26 @@ import (
"io/ioutil" "io/ioutil"
"path" "path"
"github.com/docker/cli/cli" "github.com/docker/cli/cli/command/stack/options"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/stack/common"
composeTypes "github.com/docker/cli/cli/compose/types" composeTypes "github.com/docker/cli/cli/compose/types"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/cobra"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
corev1 "k8s.io/client-go/kubernetes/typed/core/v1" corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
) )
type deployOptions struct { // RunDeploy is the kubernetes implementation of docker stack deploy
composefile string func RunDeploy(dockerCli *KubeCli, opts options.Deploy) error {
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 {
cmdOut := dockerCli.Out() cmdOut := dockerCli.Out()
// Check arguments // Check arguments
if opts.composefile == "" { if opts.Composefile == "" {
return errors.Errorf("Please specify a Compose file (with --compose-file).") return errors.Errorf("Please specify a Compose file (with --compose-file).")
} }
// Initialize clients // Initialize clients
stacks, err := kubeCli.Stacks() stacks, err := dockerCli.stacks()
if err != nil { if err != nil {
return err return err
} }
composeClient, err := kubeCli.ComposeClient() composeClient, err := dockerCli.composeClient()
if err != nil { if err != nil {
return err return err
} }
@ -62,7 +37,7 @@ func runDeploy(dockerCli command.Cli, kubeCli *kubeCli, opts deployOptions) erro
} }
// Parse the compose file // Parse the compose file
stack, cfg, err := LoadStack(opts.stack, opts.composefile) stack, cfg, err := LoadStack(opts.Namespace, opts.Composefile)
if err != nil { if err != nil {
return err return err
} }

View File

@ -3,42 +3,19 @@ package kubernetes
import ( import (
"sort" "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/formatter"
"github.com/spf13/cobra" "github.com/docker/cli/cli/command/stack/options"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"vbom.ml/util/sortorder" "vbom.ml/util/sortorder"
) )
type listOptions struct { // RunList is the kubernetes implementation of docker stack ls
format string func RunList(dockerCli *KubeCli, opts options.List) error {
} stacks, err := getStacks(dockerCli)
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)
if err != nil { if err != nil {
return err return err
} }
format := opts.format format := opts.Format
if len(format) == 0 { if len(format) == 0 {
format = formatter.TableFormatKey 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) 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 (n byName) Less(i, j int) bool { return sortorder.NaturalLess(n[i].Name, n[j].Name) }
func getStacks(kubeCli *kubeCli) ([]*formatter.Stack, error) { func getStacks(kubeCli *KubeCli) ([]*formatter.Stack, error) {
stackSvc, err := kubeCli.Stacks() stackSvc, err := kubeCli.stacks()
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -4,13 +4,11 @@ import (
"fmt" "fmt"
"sort" "sort"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/formatter" "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/cli/command/task"
"github.com/docker/cli/opts"
"github.com/docker/docker/api/types/swarm" "github.com/docker/docker/api/types/swarm"
"github.com/spf13/cobra"
apiv1 "k8s.io/api/core/v1" apiv1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/fields"
@ -18,46 +16,16 @@ import (
"k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api"
) )
type psOptions struct { // RunPS is the kubernetes implementation of docker stack ps
filter opts.FilterOpt func RunPS(dockerCli *KubeCli, options options.PS) error {
noTrunc bool namespace := options.Namespace
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
// Initialize clients // Initialize clients
client, err := kubeCli.ComposeClient() client, err := dockerCli.composeClient()
if err != nil { if err != nil {
return err return err
} }
stacks, err := kubeCli.Stacks() stacks, err := dockerCli.stacks()
if err != nil { if err != nil {
return err 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) return fmt.Errorf("nothing found in stack: %s", namespace)
} }
format := options.format format := options.Format
if len(format) == 0 { 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)) tasks := make([]swarm.Task, len(pods))
for i, pod := range pods { for i, pod := range pods {
tasks[i] = podToTask(pod) 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) type idResolver func(name string) (string, error)

View File

@ -3,38 +3,17 @@ package kubernetes
import ( import (
"fmt" "fmt"
"github.com/docker/cli/cli" "github.com/docker/cli/cli/command/stack/options"
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
) )
type removeOptions struct { // RunRemove is the kubernetes implementation of docker stack remove
stacks []string func RunRemove(dockerCli *KubeCli, opts options.Remove) error {
} stacks, err := dockerCli.stacks()
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()
if err != nil { if err != nil {
return err return err
} }
for _, stack := range opts.stacks { for _, stack := range opts.Namespaces {
fmt.Fprintf(dockerCli.Out(), "Removing stack: %s\n", stack) fmt.Fprintf(dockerCli.Out(), "Removing stack: %s\n", stack)
err := stacks.Delete(stack, &metav1.DeleteOptions{}) err := stacks.Delete(stack, &metav1.DeleteOptions{})
if err != nil { if err != nil {

View File

@ -3,62 +3,36 @@ package kubernetes
import ( import (
"fmt" "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/formatter"
"github.com/docker/cli/cli/command/stack/options"
"github.com/docker/cli/kubernetes/labels" "github.com/docker/cli/kubernetes/labels"
"github.com/spf13/cobra"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
) )
type servicesOptions struct { // RunServices is the kubernetes implementation of docker stack services
quiet bool func RunServices(dockerCli *KubeCli, opts options.Services) error {
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 {
// Initialize clients // Initialize clients
client, err := kubeCli.ComposeClient() client, err := dockerCli.composeClient()
if err != nil { if err != nil {
return nil return nil
} }
stacks, err := kubeCli.Stacks() stacks, err := dockerCli.stacks()
if err != nil { if err != nil {
return err return err
} }
replicas := client.ReplicaSets() replicas := client.ReplicaSets()
if _, err := stacks.Get(options.namespace, metav1.GetOptions{}); err != nil { if _, err := stacks.Get(opts.Namespace, metav1.GetOptions{}); err != nil {
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 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 { if err != nil {
return err 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 { if err != nil {
return err return err
} }
@ -69,13 +43,13 @@ func runServices(dockerCli command.Cli, kubeCli *kubeCli, options servicesOption
return err return err
} }
if options.quiet { if opts.Quiet {
info = map[string]formatter.ServiceListInfo{} info = map[string]formatter.ServiceListInfo{}
} }
format := options.format format := opts.Format
if len(format) == 0 { if len(format) == 0 {
if len(dockerCli.ConfigFile().ServicesFormat) > 0 && !options.quiet { if len(dockerCli.ConfigFile().ServicesFormat) > 0 && !opts.Quiet {
format = dockerCli.ConfigFile().ServicesFormat format = dockerCli.ConfigFile().ServicesFormat
} else { } else {
format = formatter.TableFormatKey format = formatter.TableFormatKey
@ -84,7 +58,7 @@ func runServices(dockerCli command.Cli, kubeCli *kubeCli, options servicesOption
servicesCtx := formatter.Context{ servicesCtx := formatter.Context{
Output: dockerCli.Out(), Output: dockerCli.Out(),
Format: formatter.NewServiceListFormat(format, options.quiet), Format: formatter.NewServiceListFormat(format, opts.Quiet),
} }
return formatter.ServiceListWrite(servicesCtx, services, info) return formatter.ServiceListWrite(servicesCtx, services, info)
} }

35
cli/command/stack/list.go Normal file
View File

@ -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
}

View File

@ -1,4 +1,4 @@
package swarm package stack
import ( import (
"io/ioutil" "io/ioutil"

View File

@ -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
}

41
cli/command/stack/ps.go Normal file
View File

@ -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
}

View File

@ -1,4 +1,4 @@
package swarm package stack
import ( import (
"io/ioutil" "io/ioutil"

View File

@ -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
}

View File

@ -1,4 +1,4 @@
package swarm package stack
import ( import (
"errors" "errors"

View File

@ -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
}

View File

@ -1,4 +1,4 @@
package swarm package stack
import ( import (
"io/ioutil" "io/ioutil"

View File

@ -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)
}

View File

@ -3,60 +3,25 @@ package swarm
import ( import (
"fmt" "fmt"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command" "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/cli/cli/compose/convert"
"github.com/docker/docker/api/types/swarm" "github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/api/types/versions" "github.com/docker/docker/api/types/versions"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/cobra"
"golang.org/x/net/context" "golang.org/x/net/context"
) )
// Resolve image constants
const ( const (
defaultNetworkDriver = "overlay" defaultNetworkDriver = "overlay"
resolveImageAlways = "always" ResolveImageAlways = "always"
resolveImageChanged = "changed" ResolveImageChanged = "changed"
resolveImageNever = "never" ResolveImageNever = "never"
) )
type deployOptions struct { // RunDeploy is the swarm implementation of docker stack deploy
bundlefile string func RunDeploy(dockerCli command.Cli, opts options.Deploy) error {
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 {
ctx := context.Background() ctx := context.Background()
if err := validateResolveImageFlag(dockerCli, &opts); err != nil { if err := validateResolveImageFlag(dockerCli, &opts); err != nil {
@ -64,11 +29,11 @@ func runDeploy(dockerCli command.Cli, opts deployOptions) error {
} }
switch { 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).") 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.") 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) return deployBundle(ctx, dockerCli, opts)
default: default:
return deployCompose(ctx, dockerCli, opts) 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 // validateResolveImageFlag validates the opts.resolveImage command line option
// and also turns image resolution off if the version is older than 1.30 // and also turns image resolution off if the version is older than 1.30
func validateResolveImageFlag(dockerCli command.Cli, opts *deployOptions) error { func validateResolveImageFlag(dockerCli command.Cli, opts *options.Deploy) error {
if opts.resolveImage != resolveImageAlways && opts.resolveImage != resolveImageChanged && opts.resolveImage != resolveImageNever { if opts.ResolveImage != ResolveImageAlways && opts.ResolveImage != ResolveImageChanged && opts.ResolveImage != ResolveImageNever {
return errors.Errorf("Invalid option %s for flag --resolve-image", opts.resolveImage) return errors.Errorf("Invalid option %s for flag --resolve-image", opts.ResolveImage)
} }
// client side image resolution should not be done when the supported // client side image resolution should not be done when the supported
// server version is older than 1.30 // server version is older than 1.30
if versions.LessThan(dockerCli.Client().ClientVersion(), "1.30") { if versions.LessThan(dockerCli.Client().ClientVersion(), "1.30") {
opts.resolveImage = resolveImageNever opts.ResolveImage = ResolveImageNever
} }
return nil return nil
} }

View File

@ -1,17 +1,23 @@
package swarm package swarm
import ( import (
"fmt"
"io"
"os"
"golang.org/x/net/context" "golang.org/x/net/context"
"github.com/docker/cli/cli/command" "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/cli/cli/compose/convert"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/swarm" "github.com/docker/docker/api/types/swarm"
"github.com/pkg/errors"
) )
func deployBundle(ctx context.Context, dockerCli command.Cli, opts deployOptions) error { func deployBundle(ctx context.Context, dockerCli command.Cli, opts options.Deploy) error {
bundle, err := common.LoadBundlefile(dockerCli.Err(), opts.namespace, opts.bundlefile) bundle, err := loadBundlefile(dockerCli.Err(), opts.Namespace, opts.Bundlefile)
if err != nil { if err != nil {
return err return err
} }
@ -20,9 +26,9 @@ func deployBundle(ctx context.Context, dockerCli command.Cli, opts deployOptions
return err return err
} }
namespace := convert.NewNamespace(opts.namespace) namespace := convert.NewNamespace(opts.Namespace)
if opts.prune { if opts.Prune {
services := map[string]struct{}{} services := map[string]struct{}{}
for service := range bundle.Services { for service := range bundle.Services {
services[service] = struct{}{} 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 { if err := createNetworks(ctx, dockerCli, namespace, networks); err != nil {
return err 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
} }

View File

@ -1,4 +1,4 @@
package common package swarm
import ( import (
"bytes" "bytes"
@ -32,7 +32,7 @@ func TestLoadBundlefileErrors(t *testing.T) {
} }
for _, tc := range testCases { 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) assert.Error(t, err, tc.expectedError)
} }
} }
@ -42,7 +42,7 @@ func TestLoadBundlefile(t *testing.T) {
namespace := "" namespace := ""
path := filepath.Join("testdata", "bundlefile_with_two_services.dab") 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.NoError(t, err)
assert.Equal(t, len(bundleFile.Services), 2) assert.Equal(t, len(bundleFile.Services), 2)

View File

@ -10,6 +10,7 @@ import (
"strings" "strings"
"github.com/docker/cli/cli/command" "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/convert"
"github.com/docker/cli/cli/compose/loader" "github.com/docker/cli/cli/compose/loader"
composetypes "github.com/docker/cli/cli/compose/types" composetypes "github.com/docker/cli/cli/compose/types"
@ -22,8 +23,8 @@ import (
"golang.org/x/net/context" "golang.org/x/net/context"
) )
func deployCompose(ctx context.Context, dockerCli command.Cli, opts deployOptions) error { func deployCompose(ctx context.Context, dockerCli command.Cli, opts options.Deploy) error {
configDetails, err := getConfigDetails(opts.composefile, dockerCli.In()) configDetails, err := getConfigDetails(opts.Composefile, dockerCli.In())
if err != nil { if err != nil {
return err return err
} }
@ -54,9 +55,9 @@ func deployCompose(ctx context.Context, dockerCli command.Cli, opts deployOption
return err return err
} }
namespace := convert.NewNamespace(opts.namespace) namespace := convert.NewNamespace(opts.Namespace)
if opts.prune { if opts.Prune {
services := map[string]struct{}{} services := map[string]struct{}{}
for _, service := range config.Services { for _, service := range config.Services {
services[service.Name] = struct{}{} services[service.Name] = struct{}{}
@ -93,7 +94,7 @@ func deployCompose(ctx context.Context, dockerCli command.Cli, opts deployOption
if err != nil { if err != nil {
return err 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{} { func getServicesDeclaredNetworks(serviceConfigs []composetypes.ServiceConfig) map[string]struct{} {
@ -339,7 +340,7 @@ func deployServices(
updateOpts := types.ServiceUpdateOptions{EncodedRegistryAuth: encodedAuth} updateOpts := types.ServiceUpdateOptions{EncodedRegistryAuth: encodedAuth}
switch { 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 // image should be updated by the server using QueryRegistry
updateOpts.QueryRegistry = true updateOpts.QueryRegistry = true
case image == service.Spec.Labels[convert.LabelImage]: case image == service.Spec.Labels[convert.LabelImage]:
@ -369,7 +370,7 @@ func deployServices(
createOpts := types.ServiceCreateOptions{EncodedRegistryAuth: encodedAuth} createOpts := types.ServiceCreateOptions{EncodedRegistryAuth: encodedAuth}
// query registry if flag disabling it was not set // query registry if flag disabling it was not set
if resolveImage == resolveImageAlways || resolveImage == resolveImageChanged { if resolveImage == ResolveImageAlways || resolveImage == ResolveImageChanged {
createOpts.QueryRegistry = true createOpts.QueryRegistry = true
} }

View File

@ -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.NoError(t, err)
assert.Equal(t, testcase.expectedQueryRegistry, receivedOptions.QueryRegistry) assert.Equal(t, testcase.expectedQueryRegistry, receivedOptions.QueryRegistry)
assert.Equal(t, testcase.expectedImage, receivedService.TaskTemplate.ContainerSpec.Image) assert.Equal(t, testcase.expectedImage, receivedService.TaskTemplate.ContainerSpec.Image)

View File

@ -3,41 +3,19 @@ package swarm
import ( import (
"sort" "sort"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/formatter" "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/cli/cli/compose/convert"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/client" "github.com/docker/docker/client"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/cobra"
"golang.org/x/net/context" "golang.org/x/net/context"
"vbom.ml/util/sortorder" "vbom.ml/util/sortorder"
) )
type listOptions struct { // RunList is the swarm implementation of docker stack ls
format string func RunList(dockerCli command.Cli, opts options.List) error {
}
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 {
client := dockerCli.Client() client := dockerCli.Client()
ctx := context.Background() ctx := context.Background()
@ -45,7 +23,7 @@ func runList(dockerCli command.Cli, opts listOptions) error {
if err != nil { if err != nil {
return err return err
} }
format := opts.format format := opts.Format
if len(format) == 0 { if len(format) == 0 {
format = formatter.TableFormatKey format = formatter.TableFormatKey
} }

View File

@ -3,53 +3,21 @@ package swarm
import ( import (
"fmt" "fmt"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/idresolver" "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/cli/command/task"
"github.com/docker/cli/opts"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/spf13/cobra"
"golang.org/x/net/context" "golang.org/x/net/context"
) )
type psOptions struct { // RunPS is the swarm implementation of docker stack ps
filter opts.FilterOpt func RunPS(dockerCli command.Cli, opts options.PS) error {
noTrunc bool namespace := opts.Namespace
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
client := dockerCli.Client() client := dockerCli.Client()
ctx := context.Background() ctx := context.Background()
filter := getStackFilterFromOpt(options.namespace, options.filter) filter := getStackFilterFromOpt(opts.Namespace, opts.Filter)
tasks, err := client.TaskList(ctx, types.TaskListOptions{Filters: filter}) tasks, err := client.TaskList(ctx, types.TaskListOptions{Filters: filter})
if err != nil { if err != nil {
@ -60,10 +28,10 @@ func runPS(dockerCli command.Cli, options psOptions) error {
return fmt.Errorf("nothing found in stack: %s", namespace) return fmt.Errorf("nothing found in stack: %s", namespace)
} }
format := options.format format := opts.Format
if len(format) == 0 { 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)
} }

View File

@ -5,38 +5,18 @@ import (
"sort" "sort"
"strings" "strings"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command" "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"
"github.com/docker/docker/api/types/swarm" "github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/api/types/versions" "github.com/docker/docker/api/types/versions"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/cobra"
"golang.org/x/net/context" "golang.org/x/net/context"
) )
type removeOptions struct { // RunRemove is the swarm implementation of docker stack remove
namespaces []string func RunRemove(dockerCli command.Cli, opts options.Remove) error {
} namespaces := opts.Namespaces
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
client := dockerCli.Client() client := dockerCli.Client()
ctx := context.Background() ctx := context.Background()

View File

@ -3,49 +3,21 @@ package swarm
import ( import (
"fmt" "fmt"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/cli/command/service" "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"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
"github.com/spf13/cobra"
"golang.org/x/net/context" "golang.org/x/net/context"
) )
type servicesOptions struct { // RunServices is the swarm implementation of docker stack services
quiet bool func RunServices(dockerCli command.Cli, opts options.Services) error {
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 {
ctx := context.Background() ctx := context.Background()
client := dockerCli.Client() client := dockerCli.Client()
filter := getStackFilterFromOpt(options.namespace, options.filter) filter := getStackFilterFromOpt(opts.Namespace, opts.Filter)
services, err := client.ServiceList(ctx, types.ServiceListOptions{Filters: filter}) services, err := client.ServiceList(ctx, types.ServiceListOptions{Filters: filter})
if err != nil { if err != nil {
return err 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 no services in this stack, print message and exit 0
if len(services) == 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 return nil
} }
info := map[string]formatter.ServiceListInfo{} info := map[string]formatter.ServiceListInfo{}
if !options.quiet { if !opts.Quiet {
taskFilter := filters.NewArgs() taskFilter := filters.NewArgs()
for _, service := range services { for _, service := range services {
taskFilter.Add("service", service.ID) taskFilter.Add("service", service.ID)
@ -77,9 +49,9 @@ func runServices(dockerCli command.Cli, options servicesOptions) error {
info = service.GetServicesStatus(services, nodes, tasks) info = service.GetServicesStatus(services, nodes, tasks)
} }
format := options.format format := opts.Format
if len(format) == 0 { if len(format) == 0 {
if len(dockerCli.ConfigFile().ServicesFormat) > 0 && !options.quiet { if len(dockerCli.ConfigFile().ServicesFormat) > 0 && !opts.Quiet {
format = dockerCli.ConfigFile().ServicesFormat format = dockerCli.ConfigFile().ServicesFormat
} else { } else {
format = formatter.TableFormatKey format = formatter.TableFormatKey
@ -88,7 +60,7 @@ func runServices(dockerCli command.Cli, options servicesOptions) error {
servicesCtx := formatter.Context{ servicesCtx := formatter.Context{
Output: dockerCli.Out(), Output: dockerCli.Out(),
Format: formatter.NewServiceListFormat(format, options.quiet), Format: formatter.NewServiceListFormat(format, opts.Quiet),
} }
return formatter.ServiceListWrite(servicesCtx, services, info) return formatter.ServiceListWrite(servicesCtx, services, info)
} }

View File

@ -137,8 +137,12 @@ func runVersion(dockerCli *command.DockerCli, opts *versionOptions) error {
BuildTime: cli.BuildTime, BuildTime: cli.BuildTime,
Os: runtime.GOOS, Os: runtime.GOOS,
Arch: runtime.GOARCH, Arch: runtime.GOARCH,
<<<<<<< HEAD
Experimental: dockerCli.ClientInfo().HasExperimental, Experimental: dockerCli.ClientInfo().HasExperimental,
Orchestrator: string(command.GetOrchestrator(dockerCli)), Orchestrator: string(command.GetOrchestrator(dockerCli)),
=======
Orchestrator: string(command.GetOrchestrator(dockerCli.ConfigFile().Orchestrator)),
>>>>>>> Refactor stack command
}, },
} }
vd.Client.Platform.Name = cli.PlatformName vd.Client.Platform.Name = cli.PlatformName

View File

@ -195,6 +195,7 @@ type versionDetails interface {
Client() client.APIClient Client() client.APIClient
ClientInfo() command.ClientInfo ClientInfo() command.ClientInfo
ServerInfo() command.ServerInfo ServerInfo() command.ServerInfo
ClientInfo() command.ClientInfo
} }
func hideUnsupportedFeatures(cmd *cobra.Command, details versionDetails) { func hideUnsupportedFeatures(cmd *cobra.Command, details versionDetails) {
@ -202,6 +203,7 @@ func hideUnsupportedFeatures(cmd *cobra.Command, details versionDetails) {
osType := details.ServerInfo().OSType osType := details.ServerInfo().OSType
hasExperimental := details.ServerInfo().HasExperimental hasExperimental := details.ServerInfo().HasExperimental
hasExperimentalCLI := details.ClientInfo().HasExperimental hasExperimentalCLI := details.ClientInfo().HasExperimental
hasKubernetes := details.ClientInfo().HasKubernetes
cmd.Flags().VisitAll(func(f *pflag.Flag) { cmd.Flags().VisitAll(func(f *pflag.Flag) {
// hide experimental flags // hide experimental flags
@ -215,6 +217,15 @@ func hideUnsupportedFeatures(cmd *cobra.Command, details versionDetails) {
f.Hidden = true 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 // hide flags not supported by the server
if !isOSTypeSupported(f, osType) || !isVersionSupported(f, clientVersion) { 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 // hide subcommands not supported by the server
if subcmdVersion, ok := subcmd.Annotations["version"]; ok && versions.LessThan(clientVersion, subcmdVersion) { if subcmdVersion, ok := subcmd.Annotations["version"]; ok && versions.LessThan(clientVersion, subcmdVersion) {
subcmd.Hidden = true subcmd.Hidden = true
@ -243,23 +264,22 @@ func hideUnsupportedFeatures(cmd *cobra.Command, details versionDetails) {
} }
func isSupported(cmd *cobra.Command, details versionDetails) error { 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() clientVersion := details.Client().ClientVersion()
osType := details.ServerInfo().OSType osType := details.ServerInfo().OSType
hasExperimental := details.ServerInfo().HasExperimental 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())
}
}
errs := []string{} errs := []string{}
@ -279,12 +299,45 @@ func isSupported(cmd *cobra.Command, details versionDetails) error {
if _, ok := f.Annotations["experimentalCLI"]; ok && !hasExperimentalCLI { if _, ok := f.Annotations["experimentalCLI"]; ok && !hasExperimentalCLI {
errs = append(errs, fmt.Sprintf("\"--%s\" is only supported when experimental cli features are enabled", f.Name)) 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 { if len(errs) > 0 {
return errors.New(strings.Join(errs, "\n")) 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 return nil
} }