diff --git a/api/client/cli.go b/api/client/cli.go index ca7000050d..6c3a8630d8 100644 --- a/api/client/cli.go +++ b/api/client/cli.go @@ -60,6 +60,21 @@ func (cli *DockerCli) Initialize() error { return cli.init() } +// Client returns the APIClient +func (cli *DockerCli) Client() client.APIClient { + return cli.client +} + +// Out returns the writer used for stdout +func (cli *DockerCli) Out() io.Writer { + return cli.out +} + +// Err returns the writer used for stderr +func (cli *DockerCli) Err() io.Writer { + return cli.err +} + // CheckTtyInput checks if we are trying to attach to a container tty // from a non-tty client input stream, and if so, returns an error. func (cli *DockerCli) CheckTtyInput(attachStdin, ttyMode bool) error { @@ -127,40 +142,13 @@ func NewDockerCli(in io.ReadCloser, out, err io.Writer, clientFlags *cliflags.Cl cli.init = func() error { clientFlags.PostParse() - configFile, e := cliconfig.Load(cliconfig.ConfigDir()) - if e != nil { - fmt.Fprintf(cli.err, "WARNING: Error loading config file:%v\n", e) - } - if !configFile.ContainsAuth() { - credentials.DetectDefaultStore(configFile) - } - cli.configFile = configFile + cli.configFile = LoadDefaultConfigFile(err) - host, err := getServerHost(clientFlags.Common.Hosts, clientFlags.Common.TLSOptions) + client, err := NewAPIClientFromFlags(clientFlags, cli.configFile) if err != nil { return err } - customHeaders := cli.configFile.HTTPHeaders - if customHeaders == nil { - customHeaders = map[string]string{} - } - customHeaders["User-Agent"] = clientUserAgent() - - verStr := api.DefaultVersion - if tmpStr := os.Getenv("DOCKER_API_VERSION"); tmpStr != "" { - verStr = tmpStr - } - - httpClient, err := newHTTPClient(host, clientFlags.Common.TLSOptions) - if err != nil { - return err - } - - client, err := client.NewClient(host, verStr, httpClient, customHeaders) - if err != nil { - return err - } cli.client = client if cli.in != nil { @@ -176,6 +164,45 @@ func NewDockerCli(in io.ReadCloser, out, err io.Writer, clientFlags *cliflags.Cl return cli } +// LoadDefaultConfigFile attempts to load the default config file and returns +// an initialized ConfigFile struct if none is found. +func LoadDefaultConfigFile(err io.Writer) *configfile.ConfigFile { + configFile, e := cliconfig.Load(cliconfig.ConfigDir()) + if e != nil { + fmt.Fprintf(err, "WARNING: Error loading config file:%v\n", e) + } + if !configFile.ContainsAuth() { + credentials.DetectDefaultStore(configFile) + } + return configFile +} + +// NewAPIClientFromFlags creates a new APIClient from command line flags +func NewAPIClientFromFlags(clientFlags *cliflags.ClientFlags, configFile *configfile.ConfigFile) (client.APIClient, error) { + host, err := getServerHost(clientFlags.Common.Hosts, clientFlags.Common.TLSOptions) + if err != nil { + return &client.Client{}, err + } + + customHeaders := configFile.HTTPHeaders + if customHeaders == nil { + customHeaders = map[string]string{} + } + customHeaders["User-Agent"] = clientUserAgent() + + verStr := api.DefaultVersion + if tmpStr := os.Getenv("DOCKER_API_VERSION"); tmpStr != "" { + verStr = tmpStr + } + + httpClient, err := newHTTPClient(host, clientFlags.Common.TLSOptions) + if err != nil { + return &client.Client{}, err + } + + return client.NewClient(host, verStr, httpClient, customHeaders) +} + func getServerHost(hosts []string, tlsOptions *tlsconfig.Options) (host string, err error) { switch len(hosts) { case 0: diff --git a/api/client/commands.go b/api/client/commands.go index 08346af7de..1031f92bb2 100644 --- a/api/client/commands.go +++ b/api/client/commands.go @@ -49,11 +49,6 @@ func (cli *DockerCli) Command(name string) func(...string) error { "unpause": cli.CmdUnpause, "update": cli.CmdUpdate, "version": cli.CmdVersion, - "volume": cli.CmdVolume, - "volume create": cli.CmdVolumeCreate, - "volume inspect": cli.CmdVolumeInspect, - "volume ls": cli.CmdVolumeLs, - "volume rm": cli.CmdVolumeRm, "wait": cli.CmdWait, }[name] } diff --git a/api/client/volume.go b/api/client/volume.go deleted file mode 100644 index 9681dc3c68..0000000000 --- a/api/client/volume.go +++ /dev/null @@ -1,181 +0,0 @@ -package client - -import ( - "fmt" - "sort" - "text/tabwriter" - - "golang.org/x/net/context" - - Cli "github.com/docker/docker/cli" - "github.com/docker/docker/opts" - flag "github.com/docker/docker/pkg/mflag" - runconfigopts "github.com/docker/docker/runconfig/opts" - "github.com/docker/engine-api/types" - "github.com/docker/engine-api/types/filters" -) - -// CmdVolume is the parent subcommand for all volume commands -// -// Usage: docker volume -func (cli *DockerCli) CmdVolume(args ...string) error { - description := Cli.DockerCommands["volume"].Description + "\n\nCommands:\n" - commands := [][]string{ - {"create", "Create a volume"}, - {"inspect", "Return low-level information on a volume"}, - {"ls", "List volumes"}, - {"rm", "Remove a volume"}, - } - - for _, cmd := range commands { - description += fmt.Sprintf(" %-25.25s%s\n", cmd[0], cmd[1]) - } - - description += "\nRun 'docker volume COMMAND --help' for more information on a command" - cmd := Cli.Subcmd("volume", []string{"[COMMAND]"}, description, false) - - cmd.Require(flag.Exact, 0) - err := cmd.ParseFlags(args, true) - cmd.Usage() - return err -} - -// CmdVolumeLs outputs a list of Docker volumes. -// -// Usage: docker volume ls [OPTIONS] -func (cli *DockerCli) CmdVolumeLs(args ...string) error { - cmd := Cli.Subcmd("volume ls", nil, "List volumes", true) - - quiet := cmd.Bool([]string{"q", "-quiet"}, false, "Only display volume names") - flFilter := opts.NewListOpts(nil) - cmd.Var(&flFilter, []string{"f", "-filter"}, "Provide filter values (i.e. 'dangling=true')") - - cmd.Require(flag.Exact, 0) - cmd.ParseFlags(args, true) - - volFilterArgs := filters.NewArgs() - for _, f := range flFilter.GetAll() { - var err error - volFilterArgs, err = filters.ParseFlag(f, volFilterArgs) - if err != nil { - return err - } - } - - volumes, err := cli.client.VolumeList(context.Background(), volFilterArgs) - if err != nil { - return err - } - - w := tabwriter.NewWriter(cli.out, 20, 1, 3, ' ', 0) - if !*quiet { - for _, warn := range volumes.Warnings { - fmt.Fprintln(cli.err, warn) - } - fmt.Fprintf(w, "DRIVER \tVOLUME NAME") - fmt.Fprintf(w, "\n") - } - - sort.Sort(byVolumeName(volumes.Volumes)) - for _, vol := range volumes.Volumes { - if *quiet { - fmt.Fprintln(w, vol.Name) - continue - } - fmt.Fprintf(w, "%s\t%s\n", vol.Driver, vol.Name) - } - w.Flush() - return nil -} - -type byVolumeName []*types.Volume - -func (r byVolumeName) Len() int { return len(r) } -func (r byVolumeName) Swap(i, j int) { r[i], r[j] = r[j], r[i] } -func (r byVolumeName) Less(i, j int) bool { - return r[i].Name < r[j].Name -} - -// CmdVolumeInspect displays low-level information on one or more volumes. -// -// Usage: docker volume inspect [OPTIONS] VOLUME [VOLUME...] -func (cli *DockerCli) CmdVolumeInspect(args ...string) error { - cmd := Cli.Subcmd("volume inspect", []string{"VOLUME [VOLUME...]"}, "Return low-level information on a volume", true) - tmplStr := cmd.String([]string{"f", "-format"}, "", "Format the output using the given go template") - - cmd.Require(flag.Min, 1) - cmd.ParseFlags(args, true) - - if err := cmd.Parse(args); err != nil { - return nil - } - - ctx := context.Background() - - inspectSearcher := func(name string) (interface{}, []byte, error) { - i, err := cli.client.VolumeInspect(ctx, name) - return i, nil, err - } - - return cli.inspectElements(*tmplStr, cmd.Args(), inspectSearcher) -} - -// CmdVolumeCreate creates a new volume. -// -// Usage: docker volume create [OPTIONS] -func (cli *DockerCli) CmdVolumeCreate(args ...string) error { - cmd := Cli.Subcmd("volume create", nil, "Create a volume", true) - flDriver := cmd.String([]string{"d", "-driver"}, "local", "Specify volume driver name") - flName := cmd.String([]string{"-name"}, "", "Specify volume name") - - flDriverOpts := opts.NewMapOpts(nil, nil) - cmd.Var(flDriverOpts, []string{"o", "-opt"}, "Set driver specific options") - - flLabels := opts.NewListOpts(nil) - cmd.Var(&flLabels, []string{"-label"}, "Set metadata for a volume") - - cmd.Require(flag.Exact, 0) - cmd.ParseFlags(args, true) - - volReq := types.VolumeCreateRequest{ - Driver: *flDriver, - DriverOpts: flDriverOpts.GetAll(), - Name: *flName, - Labels: runconfigopts.ConvertKVStringsToMap(flLabels.GetAll()), - } - - vol, err := cli.client.VolumeCreate(context.Background(), volReq) - if err != nil { - return err - } - - fmt.Fprintf(cli.out, "%s\n", vol.Name) - return nil -} - -// CmdVolumeRm removes one or more volumes. -// -// Usage: docker volume rm VOLUME [VOLUME...] -func (cli *DockerCli) CmdVolumeRm(args ...string) error { - cmd := Cli.Subcmd("volume rm", []string{"VOLUME [VOLUME...]"}, "Remove a volume", true) - cmd.Require(flag.Min, 1) - cmd.ParseFlags(args, true) - - var status = 0 - - ctx := context.Background() - - for _, name := range cmd.Args() { - if err := cli.client.VolumeRemove(ctx, name); err != nil { - fmt.Fprintf(cli.err, "%s\n", err) - status = 1 - continue - } - fmt.Fprintf(cli.out, "%s\n", name) - } - - if status != 0 { - return Cli.StatusError{StatusCode: status} - } - return nil -} diff --git a/api/client/volume/cmd.go b/api/client/volume/cmd.go new file mode 100644 index 0000000000..6c17a9cec8 --- /dev/null +++ b/api/client/volume/cmd.go @@ -0,0 +1,22 @@ +package volume + +import ( + "github.com/spf13/cobra" + + "github.com/docker/docker/api/client" +) + +// NewVolumeCommand returns a cobra command for `volume` subcommands +func NewVolumeCommand(dockerCli *client.DockerCli) *cobra.Command { + cmd := &cobra.Command{ + Use: "volume", + Short: "Manage Docker volumes", + } + cmd.AddCommand( + newCreateCommand(dockerCli), + newInspectCommand(dockerCli), + newListCommand(dockerCli), + newRemoveCommand(dockerCli), + ) + return cmd +} diff --git a/api/client/volume/create.go b/api/client/volume/create.go new file mode 100644 index 0000000000..4c6d0a56b5 --- /dev/null +++ b/api/client/volume/create.go @@ -0,0 +1,58 @@ +package volume + +import ( + "fmt" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/client" + "github.com/docker/docker/opts" + runconfigopts "github.com/docker/docker/runconfig/opts" + "github.com/docker/engine-api/types" + "github.com/spf13/cobra" +) + +type createOptions struct { + name string + driver string + driverOpts opts.MapOpts + labels []string +} + +func newCreateCommand(dockerCli *client.DockerCli) *cobra.Command { + var opts createOptions + + cmd := &cobra.Command{ + Use: "create", + Short: "Create a volume", + RunE: func(cmd *cobra.Command, args []string) error { + return runCreate(dockerCli, opts) + }, + } + flags := cmd.Flags() + flags.StringVarP(&opts.driver, "driver", "d", "local", "Specify volume driver name") + flags.StringVar(&opts.name, "name", "", "Specify volume name") + flags.VarP(&opts.driverOpts, "opt", "o", "Set driver specific options") + flags.StringSliceVar(&opts.labels, "label", []string{}, "Set metadata for a volume") + + return cmd +} + +func runCreate(dockerCli *client.DockerCli, opts createOptions) error { + client := dockerCli.Client() + + volReq := types.VolumeCreateRequest{ + Driver: opts.driver, + DriverOpts: opts.driverOpts.GetAll(), + Name: opts.name, + Labels: runconfigopts.ConvertKVStringsToMap(opts.labels), + } + + vol, err := client.VolumeCreate(context.Background(), volReq) + if err != nil { + return err + } + + fmt.Fprintf(dockerCli.Out(), "%s\n", vol.Name) + return nil +} diff --git a/api/client/volume/inspect.go b/api/client/volume/inspect.go new file mode 100644 index 0000000000..a47ee8bfe2 --- /dev/null +++ b/api/client/volume/inspect.go @@ -0,0 +1,46 @@ +package volume + +import ( + "golang.org/x/net/context" + + "github.com/docker/docker/api/client" + "github.com/docker/docker/api/client/inspect" + "github.com/docker/docker/cli" + "github.com/spf13/cobra" +) + +type inspectOptions struct { + format string + names []string +} + +func newInspectCommand(dockerCli *client.DockerCli) *cobra.Command { + var opts inspectOptions + + cmd := &cobra.Command{ + Use: "inspect [OPTIONS] VOLUME [VOLUME...]", + Short: "Return low-level information on a volume", + RunE: func(cmd *cobra.Command, args []string) error { + if err := cli.MinRequiredArgs(args, 1, cmd); err != nil { + return err + } + opts.names = args + return runInspect(dockerCli, opts) + }, + } + + cmd.Flags().StringVarP(&opts.format, "format", "f", "", "Format the output using the given go template") + + return cmd +} + +func runInspect(dockerCli *client.DockerCli, opts inspectOptions) error { + client := dockerCli.Client() + + getVolFunc := func(name string) (interface{}, []byte, error) { + i, err := client.VolumeInspect(context.Background(), name) + return i, nil, err + } + + return inspect.Inspect(dockerCli.Out(), opts.names, opts.format, getVolFunc) +} diff --git a/api/client/volume/list.go b/api/client/volume/list.go new file mode 100644 index 0000000000..541c2c1ed6 --- /dev/null +++ b/api/client/volume/list.go @@ -0,0 +1,84 @@ +package volume + +import ( + "fmt" + "sort" + "text/tabwriter" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/client" + "github.com/docker/engine-api/types" + "github.com/docker/engine-api/types/filters" + "github.com/spf13/cobra" +) + +type byVolumeName []*types.Volume + +func (r byVolumeName) Len() int { return len(r) } +func (r byVolumeName) Swap(i, j int) { r[i], r[j] = r[j], r[i] } +func (r byVolumeName) Less(i, j int) bool { + return r[i].Name < r[j].Name +} + +type listOptions struct { + quiet bool + filter []string +} + +func newListCommand(dockerCli *client.DockerCli) *cobra.Command { + var opts listOptions + + cmd := &cobra.Command{ + Use: "ls", + Aliases: []string{"list"}, + Short: "List volumes", + RunE: func(cmd *cobra.Command, args []string) error { + return runList(dockerCli, opts) + }, + } + + flags := cmd.Flags() + flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display volume names") + flags.StringSliceVarP(&opts.filter, "filter", "f", []string{}, "Provide filter values (i.e. 'dangling=true')") + + return cmd +} + +func runList(dockerCli *client.DockerCli, opts listOptions) error { + client := dockerCli.Client() + + volFilterArgs := filters.NewArgs() + for _, f := range opts.filter { + var err error + volFilterArgs, err = filters.ParseFlag(f, volFilterArgs) + if err != nil { + return err + } + } + + volumes, err := client.VolumeList(context.Background(), volFilterArgs) + if err != nil { + return err + } + + w := tabwriter.NewWriter(dockerCli.Out(), 20, 1, 3, ' ', 0) + if !opts.quiet { + for _, warn := range volumes.Warnings { + fmt.Fprintln(dockerCli.Err(), warn) + } + fmt.Fprintf(w, "DRIVER \tVOLUME NAME") + fmt.Fprintf(w, "\n") + } + + sort.Sort(byVolumeName(volumes.Volumes)) + for _, vol := range volumes.Volumes { + if opts.quiet { + fmt.Fprintln(w, vol.Name) + continue + } + fmt.Fprintf(w, "%s\t%s\n", vol.Driver, vol.Name) + } + w.Flush() + return nil +} diff --git a/api/client/volume/remove.go b/api/client/volume/remove.go new file mode 100644 index 0000000000..d49597cc9d --- /dev/null +++ b/api/client/volume/remove.go @@ -0,0 +1,44 @@ +package volume + +import ( + "fmt" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/client" + "github.com/docker/docker/cli" + "github.com/spf13/cobra" +) + +func newRemoveCommand(dockerCli *client.DockerCli) *cobra.Command { + return &cobra.Command{ + Use: "rm VOLUME [VOLUME]...", + Aliases: []string{"remove"}, + Short: "Remove a volume", + RunE: func(cmd *cobra.Command, args []string) error { + if err := cli.MinRequiredArgs(args, 1, cmd); err != nil { + return err + } + return runRemove(dockerCli, args) + }, + } +} + +func runRemove(dockerCli *client.DockerCli, volumes []string) error { + client := dockerCli.Client() + var status = 0 + + for _, name := range volumes { + if err := client.VolumeRemove(context.Background(), name); err != nil { + fmt.Fprintf(dockerCli.Err(), "%s\n", err) + status = 1 + continue + } + fmt.Fprintf(dockerCli.Err(), "%s\n", name) + } + + if status != 0 { + return cli.StatusError{StatusCode: status} + } + return nil +} diff --git a/cli/cobraadaptor/adaptor.go b/cli/cobraadaptor/adaptor.go new file mode 100644 index 0000000000..07ff8124b0 --- /dev/null +++ b/cli/cobraadaptor/adaptor.go @@ -0,0 +1,83 @@ +package cobraadaptor + +import ( + "github.com/docker/docker/api/client" + "github.com/docker/docker/api/client/volume" + "github.com/docker/docker/cli" + cliflags "github.com/docker/docker/cli/flags" + "github.com/docker/docker/pkg/term" + "github.com/spf13/cobra" +) + +// CobraAdaptor is an adaptor for supporting spf13/cobra commands in the +// docker/cli framework +type CobraAdaptor struct { + rootCmd *cobra.Command + dockerCli *client.DockerCli +} + +// NewCobraAdaptor returns a new handler +func NewCobraAdaptor(clientFlags *cliflags.ClientFlags) CobraAdaptor { + var rootCmd = &cobra.Command{ + Use: "docker", + } + rootCmd.SetUsageTemplate(usageTemplate) + + stdin, stdout, stderr := term.StdStreams() + dockerCli := client.NewDockerCli(stdin, stdout, stderr, clientFlags) + + rootCmd.AddCommand( + volume.NewVolumeCommand(dockerCli), + ) + return CobraAdaptor{ + rootCmd: rootCmd, + dockerCli: dockerCli, + } +} + +// Usage returns the list of commands and their short usage string for +// all top level cobra commands. +func (c CobraAdaptor) Usage() []cli.Command { + cmds := []cli.Command{} + for _, cmd := range c.rootCmd.Commands() { + cmds = append(cmds, cli.Command{Name: cmd.Use, Description: cmd.Short}) + } + return cmds +} + +func (c CobraAdaptor) run(cmd string, args []string) error { + c.dockerCli.Initialize() + // Prepend the command name to support normal cobra command delegation + c.rootCmd.SetArgs(append([]string{cmd}, args...)) + return c.rootCmd.Execute() +} + +// Command returns a cli command handler if one exists +func (c CobraAdaptor) Command(name string) func(...string) error { + for _, cmd := range c.rootCmd.Commands() { + if cmd.Name() == name { + return func(args ...string) error { + return c.run(name, args) + } + } + } + return nil +} + +var usageTemplate = `Usage: {{if .Runnable}}{{if .HasFlags}}{{appendIfNotPresent .UseLine "[OPTIONS]"}}{{else}}{{.UseLine}}{{end}}{{end}}{{if .HasSubCommands}}{{ .CommandPath}} COMMAND {{end}}{{if gt .Aliases 0}} + +Aliases: + {{.NameAndAliases}} +{{end}}{{if .HasExample}} + +Examples: +{{ .Example }}{{end}}{{ if .HasLocalFlags}} + +Options: +{{.LocalFlags.FlagUsages | trimRightSpace}}{{end}}{{ if .HasAvailableSubCommands}} + +Commands:{{range .Commands}}{{if .IsAvailableCommand}} + {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{ if .HasSubCommands }} + +Run '{{.CommandPath}} COMMAND --help' for more information on a command.{{end}} +` diff --git a/cli/required.go b/cli/required.go new file mode 100644 index 0000000000..6b83fadde1 --- /dev/null +++ b/cli/required.go @@ -0,0 +1,23 @@ +package cli + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +// MinRequiredArgs checks if the minimum number of args exists, and returns an +// error if they do not. +func MinRequiredArgs(args []string, min int, cmd *cobra.Command) error { + if len(args) >= min { + return nil + } + + return fmt.Errorf( + "\"%s\" requires at least %d argument(s).\n\nUsage: %s\n\n%s", + cmd.CommandPath(), + min, + cmd.UseLine(), + cmd.Short, + ) +} diff --git a/cmd/docker/docker.go b/cmd/docker/docker.go index 8397124932..45de2e3fca 100644 --- a/cmd/docker/docker.go +++ b/cmd/docker/docker.go @@ -8,6 +8,7 @@ import ( "github.com/Sirupsen/logrus" "github.com/docker/docker/api/client" "github.com/docker/docker/cli" + "github.com/docker/docker/cli/cobraadaptor" cliflags "github.com/docker/docker/cli/flags" "github.com/docker/docker/cliconfig" "github.com/docker/docker/dockerversion" @@ -31,6 +32,8 @@ func main() { flag.Merge(flag.CommandLine, clientFlags.FlagSet, commonFlags.FlagSet) + cobraAdaptor := cobraadaptor.NewCobraAdaptor(clientFlags) + flag.Usage = func() { fmt.Fprint(stdout, "Usage: docker [OPTIONS] COMMAND [arg...]\n docker [ --help | -v | --version ]\n\n") fmt.Fprint(stdout, "A self-sufficient runtime for containers.\n\nOptions:\n") @@ -40,8 +43,8 @@ func main() { help := "\nCommands:\n" - dockerCommands := sortCommands(cli.DockerCommandUsage) - for _, cmd := range dockerCommands { + dockerCommands := append(cli.DockerCommandUsage, cobraAdaptor.Usage()...) + for _, cmd := range sortCommands(dockerCommands) { help += fmt.Sprintf(" %-10.10s%s\n", cmd.Name, cmd.Description) } @@ -65,7 +68,7 @@ func main() { clientCli := client.NewDockerCli(stdin, stdout, stderr, clientFlags) - c := cli.New(clientCli, NewDaemonProxy()) + c := cli.New(clientCli, NewDaemonProxy(), cobraAdaptor) if err := c.Run(flag.Args()...); err != nil { if sterr, ok := err.(cli.StatusError); ok { if sterr.Status != "" { diff --git a/opts/opts.go b/opts/opts.go index 0b09981778..05d5497161 100644 --- a/opts/opts.go +++ b/opts/opts.go @@ -163,6 +163,11 @@ func (opts *MapOpts) String() string { return fmt.Sprintf("%v", map[string]string((opts.values))) } +// Type returns a string name for this Option type +func (opts *MapOpts) Type() string { + return "map" +} + // NewMapOpts creates a new MapOpts with the specified map of values and a validator. func NewMapOpts(values map[string]string, validator ValidatorFctType) *MapOpts { if values == nil {