diff --git a/docker/docker.go b/docker/docker.go index 73caf65123..66311efac1 100644 --- a/docker/docker.go +++ b/docker/docker.go @@ -1,14 +1,12 @@ package main import ( + "github.com/dotcloud/docker/rcli" "io" - "encoding/json" "log" "os" - "net" - "fmt" "syscall" -"unsafe" + "unsafe" ) @@ -172,17 +170,10 @@ func main() { } defer Restore(0, oldState) } - cmd, err := json.Marshal(os.Args[1:]) + conn, err := rcli.CallTCP(os.Getenv("DOCKER"), os.Args[1:]...) if err != nil { Fatal(err) } - conn, err := net.Dial("tcp", os.Getenv("DOCKER")) - if err != nil { - Fatal(err) - } - if _, err := fmt.Fprintln(conn, string(cmd)); err != nil { - Fatal(err) - } go func() { if _, err := io.Copy(os.Stdout, conn); err != nil { Fatal(err) diff --git a/dockerd/dockerd.go b/dockerd/dockerd.go index c330dc4b8f..fe011b1752 100644 --- a/dockerd/dockerd.go +++ b/dockerd/dockerd.go @@ -1,64 +1,56 @@ package main import ( + "github.com/dotcloud/docker/rcli" + "github.com/dotcloud/docker/fake" + "github.com/dotcloud/docker/future" "bufio" "errors" "log" "io" "io/ioutil" - "net" - "net/url" - "net/http" "os/exec" "flag" - "reflect" "fmt" "github.com/kr/pty" - "path" "strings" - "time" - "math/rand" - "crypto/sha256" "bytes" "text/tabwriter" "sort" "os" - "archive/tar" - "encoding/json" + "time" ) -func (docker *Docker) CmdHelp(stdin io.ReadCloser, stdout io.Writer, args ...string) error { - if len(args) == 0 { - fmt.Fprintf(stdout, "Usage: docker COMMAND [arg...]\n\nA self-sufficient runtime for linux containers.\n\nCommands:\n") - for _, cmd := range [][]interface{}{ - {"run", "Run a command in a container"}, - {"list", "Display a list of containers"}, - {"get", "Download a tarball and create a container from it"}, - {"put", "Upload a tarball and create a container from it"}, - {"rm", "Remove containers"}, - {"wait", "Wait for the state of a container to change"}, - {"stop", "Stop a running container"}, - {"logs", "Fetch the logs of a container"}, - {"diff", "Inspect changes on a container's filesystem"}, - {"fork", "Duplicate a container"}, - {"attach", "Attach to the standard inputs and outputs of a running container"}, - {"info", "Display system-wide information"}, - {"web", "Generate a web UI"}, - } { - fmt.Fprintf(stdout, " %-10.10s%s\n", cmd...) - } - } else { - if method := docker.getMethod(args[0]); method == nil { - return errors.New("No such command: " + args[0]) - } else { - method(stdin, stdout, "--help") - } - } - return nil + +func (docker *Docker) Name() string { + return "docker" } +func (docker *Docker) Help() string { + help := "Usage: docker COMMAND [arg...]\n\nA self-sufficient runtime for linux containers.\n\nCommands:\n" + for _, cmd := range [][]interface{}{ + {"run", "Run a command in a container"}, + {"list", "Display a list of containers"}, + {"pull", "Download a tarball and create a container from it"}, + {"put", "Upload a tarball and create a container from it"}, + {"rm", "Remove containers"}, + {"wait", "Wait for the state of a container to change"}, + {"stop", "Stop a running container"}, + {"logs", "Fetch the logs of a container"}, + {"diff", "Inspect changes on a container's filesystem"}, + {"commit", "Save the state of a container"}, + {"attach", "Attach to the standard inputs and outputs of a running container"}, + {"info", "Display system-wide information"}, + {"web", "Generate a web UI"}, + } { + help += fmt.Sprintf(" %-10.10s%s\n", cmd...) + } + return help +} + + func (docker *Docker) CmdList(stdin io.ReadCloser, stdout io.Writer, args ...string) error { - flags := Subcmd(stdout, "list", "[OPTIONS] [NAME]", "List containers") + flags := rcli.Subcmd(stdout, "list", "[OPTIONS] [NAME]", "List containers") limit := flags.Int("l", 0, "Only show the N most recent versions of each name") quiet := flags.Bool("q", false, "only show numeric IDs") flags.Parse(args) @@ -91,7 +83,7 @@ func (docker *Docker) CmdList(stdin io.ReadCloser, stdout io.Writer, args ...str for idx, field := range []string{ /* NAME */ container.Name, /* ID */ container.Id, - /* CREATED */ humanDuration(time.Now().Sub(container.Created)) + " ago", + /* CREATED */ future.HumanDuration(time.Now().Sub(container.Created)) + " ago", /* SOURCE */ container.Source, /* SIZE */ fmt.Sprintf("%.1fM", float32(container.Size) / 1024 / 1024), /* CHANGES */ fmt.Sprintf("%.1fM", float32(container.BytesChanged) / 1024 / 1024), @@ -130,7 +122,7 @@ func (docker *Docker) findContainer(name string) (*Container, bool) { func (docker *Docker) CmdRm(stdin io.ReadCloser, stdout io.Writer, args ...string) error { - flags := Subcmd(stdout, "rm", "[OPTIONS] CONTAINER", "Remove a container") + flags := rcli.Subcmd(stdout, "rm", "[OPTIONS] CONTAINER", "Remove a container") if err := flags.Parse(args); err != nil { return nil } @@ -142,7 +134,7 @@ func (docker *Docker) CmdRm(stdin io.ReadCloser, stdout io.Writer, args ...strin return nil } -func (docker *Docker) CmdGet(stdin io.ReadCloser, stdout io.Writer, args ...string) error { +func (docker *Docker) CmdPull(stdin io.ReadCloser, stdout io.Writer, args ...string) error { if len(args) < 1 { return errors.New("Not enough arguments") } @@ -162,8 +154,8 @@ func (docker *Docker) CmdPut(stdin io.ReadCloser, stdout io.Writer, args ...stri return nil } -func (docker *Docker) CmdFork(stdin io.ReadCloser, stdout io.Writer, args ...string) error { - flags := Subcmd(stdout, +func (docker *Docker) CmdCommit(stdin io.ReadCloser, stdout io.Writer, args ...string) error { + flags := rcli.Subcmd(stdout, "fork", "[OPTIONS] CONTAINER [DEST]", "Duplicate a container") // FIXME "-r" to reset changes in the new container @@ -187,7 +179,7 @@ func (docker *Docker) CmdFork(stdin io.ReadCloser, stdout io.Writer, args ...str } func (docker *Docker) CmdTar(stdin io.ReadCloser, stdout io.Writer, args ...string) error { - flags := Subcmd(stdout, + flags := rcli.Subcmd(stdout, "tar", "CONTAINER", "Stream the contents of a container as a tar archive") if err := flags.Parse(args); err != nil { @@ -196,13 +188,13 @@ func (docker *Docker) CmdTar(stdin io.ReadCloser, stdout io.Writer, args ...stri name := flags.Arg(0) if _, exists := docker.findContainer(name); exists { // Stream the entire contents of the container (basically a volatile snapshot) - return WriteFakeTar(stdout) + return fake.WriteFakeTar(stdout) } return errors.New("No such container: " + name) } func (docker *Docker) CmdDiff(stdin io.ReadCloser, stdout io.Writer, args ...string) error { - flags := Subcmd(stdout, + flags := rcli.Subcmd(stdout, "diff", "CONTAINER [OPTIONS]", "Inspect changes on a container's filesystem") fl_diff := flags.Bool("d", true, "Show changes in diff format") @@ -238,7 +230,7 @@ index 2dae694..e43caca 100644 --- a/dockerd/dockerd.go +++ b/dockerd/dockerd.go @@ -158,6 +158,7 @@ func (docker *Docker) CmdDiff(stdin io.ReadCloser, stdout io.Writer, args ...str - flags := Subcmd(stdout, + flags := rcli.Subcmd(stdout, "diff", "CONTAINER [OPTIONS]", "Inspect changes on a container's filesystem") + fl_diff := flags.Bool("d", true, "Show changes in diff format") @@ -290,12 +282,11 @@ func (c *ByDate) Del(id string) { func (docker *Docker) addContainer(name string, source string, size uint) *Container { - // Generate a fake random size if size == 0 { - size = uint(rand.Int31n(142 * 1024 * 1024)) + size = fake.RandomContainerSize() } c := &Container{ - Id: randomId(), + Id: fake.RandomId(), Name: name, Created: time.Now(), Source: source, @@ -330,7 +321,7 @@ func (docker *Docker) rm(id string) (*Container, error) { func (docker *Docker) CmdLogs(stdin io.ReadCloser, stdout io.Writer, args ...string) error { - flags := Subcmd(stdout, "logs", "[OPTIONS] CONTAINER", "Fetch the logs of a container") + flags := rcli.Subcmd(stdout, "logs", "[OPTIONS] CONTAINER", "Fetch the logs of a container") if err := flags.Parse(args); err != nil { return nil } @@ -349,7 +340,7 @@ func (docker *Docker) CmdLogs(stdin io.ReadCloser, stdout io.Writer, args ...str } func (docker *Docker) CmdRun(stdin io.ReadCloser, stdout io.Writer, args ...string) error { - flags := Subcmd(stdout, "run", "[OPTIONS] CONTAINER COMMAND [ARG...]", "Run a command in a container") + flags := rcli.Subcmd(stdout, "run", "[OPTIONS] CONTAINER COMMAND [ARG...]", "Run a command in a container") fl_attach := flags.Bool("a", false, "Attach stdin and stdout") if err := flags.Parse(args); err != nil { return nil @@ -396,60 +387,21 @@ func startCommand(cmd *exec.Cmd, interactive bool) (io.WriteCloser, io.ReadClose return stdin, stdout, nil } -func (docker *Docker) ListenAndServeTCP(addr string) error { - listener, err := net.Listen("tcp", addr) - if err != nil { - return err - } - defer listener.Close() - for { - if conn, err := listener.Accept(); err != nil { - return err - } else { - go func() { - if err := docker.serve(conn); err != nil { - log.Printf("Error: " + err.Error() + "\n") - fmt.Fprintf(conn, "Error: " + err.Error() + "\n") - } - conn.Close() - }() - } - } - return nil -} - -func (docker *Docker) ListenAndServeHTTP(addr string) error { - return http.ListenAndServe(addr, docker) -} - func main() { - rand.Seed(time.Now().UTC().UnixNano()) + fake.Seed() flag.Parse() docker := New() go func() { - if err := docker.ListenAndServeHTTP(":8080"); err != nil { + if err := rcli.ListenAndServeHTTP(":8080", docker); err != nil { log.Fatal(err) } }() - if err := docker.ListenAndServeTCP(":4242"); err != nil { + if err := rcli.ListenAndServeTCP(":4242", docker); err != nil { log.Fatal(err) } } -func (docker *Docker) serve(conn net.Conn) error { - r := bufio.NewReader(conn) - var args []string - if line, err := r.ReadString('\n'); err != nil { - return err - } else if err := json.Unmarshal([]byte(line), &args); err != nil { - return err - } else { - return docker.Call(ioutil.NopCloser(r), conn, args...) - } - return nil -} - func New() *Docker { return &Docker{ containersByName: make(map[string]*ByDate), @@ -457,43 +409,6 @@ func New() *Docker { } } -type AutoFlush struct { - http.ResponseWriter -} - -func (w *AutoFlush) Write(data []byte) (int, error) { - ret, err := w.ResponseWriter.Write(data) - if flusher, ok := w.ResponseWriter.(http.Flusher); ok { - flusher.Flush() - } - return ret, err -} - -func (docker *Docker) ServeHTTP(w http.ResponseWriter, r *http.Request) { - cmd, args := URLToCall(r.URL) - if err := docker.Call(r.Body, &AutoFlush{w}, append([]string{cmd}, args...)...); err != nil { - fmt.Fprintf(w, "Error: " + err.Error() + "\n") - } -} - -func (docker *Docker) Call(stdin io.ReadCloser, stdout io.Writer, args ...string) error { - flags := flag.NewFlagSet("docker", flag.ContinueOnError) - flags.SetOutput(stdout) - flags.Usage = func() { docker.CmdHelp(stdin, stdout) } - if err := flags.Parse(args); err != nil { - return err - } - cmd := flags.Arg(0) - log.Printf("%s\n", strings.Join(append(append([]string{"docker"}, cmd), args[1:]...), " ")) - if cmd == "" { - cmd = "help" - } - method := docker.getMethod(cmd) - if method != nil { - return method(stdin, stdout, args[1:]...) - } - return errors.New("No such command: " + cmd) -} func (docker *Docker) CmdMirror(stdin io.ReadCloser, stdout io.Writer, args ...string) error { _, err := io.Copy(stdout, stdin) @@ -517,7 +432,7 @@ func (docker *Docker) CmdDebug(stdin io.ReadCloser, stdout io.Writer, args ...st } func (docker *Docker) CmdWeb(stdin io.ReadCloser, stdout io.Writer, args ...string) error { - flags := Subcmd(stdout, "web", "[OPTIONS]", "A web UI for docker") + flags := rcli.Subcmd(stdout, "web", "[OPTIONS]", "A web UI for docker") showurl := flags.Bool("u", false, "Return the URL of the web UI") if err := flags.Parse(args); err != nil { return nil @@ -534,25 +449,6 @@ func (docker *Docker) CmdWeb(stdin io.ReadCloser, stdout io.Writer, args ...stri return nil } -func (docker *Docker) getMethod(name string) Cmd { - methodName := "Cmd"+strings.ToUpper(name[:1])+strings.ToLower(name[1:]) - method, exists := reflect.TypeOf(docker).MethodByName(methodName) - if !exists { - return nil - } - return func(stdin io.ReadCloser, stdout io.Writer, args ...string) error { - ret := method.Func.CallSlice([]reflect.Value{ - reflect.ValueOf(docker), - reflect.ValueOf(stdin), - reflect.ValueOf(stdout), - reflect.ValueOf(args), - })[0].Interface() - if ret == nil { - return nil - } - return ret.(error) - } -} func Go(f func() error) chan error { ch := make(chan error) @@ -596,8 +492,8 @@ func (c *Container) Run(command string, args []string, stdin io.ReadCloser, stdo cmd := exec.Command(c.Cmd, c.Args...) cmd_stdin, cmd_stdout, err := startCommand(cmd, true) // ADD FAKE RANDOM CHANGES - c.FilesChanged = uint(rand.Int31n(42)) - c.BytesChanged = uint(rand.Int31n(24 * 1024 * 1024)) + c.FilesChanged = fake.RandomFilesChanged() + c.BytesChanged = fake.RandomBytesChanged() if err != nil { return err } @@ -643,92 +539,3 @@ func (c *Container) CmdString() string { return strings.Join(append([]string{c.Cmd}, c.Args...), " ") } -type Cmd func(io.ReadCloser, io.Writer, ...string) error -type CmdMethod func(*Docker, io.ReadCloser, io.Writer, ...string) error - -// Use this key to encode an RPC call into an URL, -// eg. domain.tld/path/to/method?q=get_user&q=gordon -const ARG_URL_KEY = "q" - -func URLToCall(u *url.URL) (method string, args []string) { - return path.Base(u.Path), u.Query()[ARG_URL_KEY] -} - - -func randomBytes() io.Reader { - return bytes.NewBuffer([]byte(fmt.Sprintf("%x", rand.Int()))) -} - -func ComputeId(content io.Reader) (string, error) { - h := sha256.New() - if _, err := io.Copy(h, content); err != nil { - return "", err - } - return fmt.Sprintf("%x", h.Sum(nil)[:8]), nil -} - -func randomId() string { - id, _ := ComputeId(randomBytes()) // can't fail - return id -} - - -func humanDuration(d time.Duration) string { - if seconds := int(d.Seconds()); seconds < 1 { - return "Less than a second" - } else if seconds < 60 { - return fmt.Sprintf("%d seconds", seconds) - } else if minutes := int(d.Minutes()); minutes == 1 { - return "About a minute" - } else if minutes < 60 { - return fmt.Sprintf("%d minutes", minutes) - } else if hours := int(d.Hours()); hours == 1{ - return "About an hour" - } else if hours < 48 { - return fmt.Sprintf("%d hours", hours) - } else if hours < 24 * 7 * 2 { - return fmt.Sprintf("%d days", hours / 24) - } else if hours < 24 * 30 * 3 { - return fmt.Sprintf("%d weeks", hours / 24 / 7) - } else if hours < 24 * 365 * 2 { - return fmt.Sprintf("%d months", hours / 24 / 30) - } - return fmt.Sprintf("%d years", d.Hours() / 24 / 365) -} - -func Subcmd(output io.Writer, name, signature, description string) *flag.FlagSet { - flags := flag.NewFlagSet(name, flag.ContinueOnError) - flags.SetOutput(output) - flags.Usage = func() { - fmt.Fprintf(output, "\nUsage: docker %s %s\n\n%s\n\n", name, signature, description) - flags.PrintDefaults() - } - return flags -} - - -func WriteFakeTar(dst io.Writer) error { - if data, err := FakeTar(); err != nil { - return err - } else if _, err := io.Copy(dst, data); err != nil { - return err - } - return nil -} - -func FakeTar() (io.Reader, error) { - content := []byte("Hello world!\n") - buf := new(bytes.Buffer) - tw := tar.NewWriter(buf) - for _, name := range []string {"/etc/postgres/postgres.conf", "/etc/passwd", "/var/log/postgres", "/var/log/postgres/postgres.conf"} { - hdr := new(tar.Header) - hdr.Size = int64(len(content)) - hdr.Name = name - if err := tw.WriteHeader(hdr); err != nil { - return nil, err - } - tw.Write([]byte(content)) - } - tw.Close() - return buf, nil -} diff --git a/fake/fake.go b/fake/fake.go new file mode 100644 index 0000000000..df2731a8a3 --- /dev/null +++ b/fake/fake.go @@ -0,0 +1,65 @@ +package fake + +import ( + "github.com/dotcloud/docker/future" + "bytes" + "math/rand" + "time" + "io" + "archive/tar" + "fmt" +) + +func Seed() { + rand.Seed(time.Now().UTC().UnixNano()) +} + +func randomBytes() io.Reader { + return bytes.NewBuffer([]byte(fmt.Sprintf("%x", rand.Int()))) +} + +func FakeTar() (io.Reader, error) { + content := []byte("Hello world!\n") + buf := new(bytes.Buffer) + tw := tar.NewWriter(buf) + for _, name := range []string {"/etc/postgres/postgres.conf", "/etc/passwd", "/var/log/postgres", "/var/log/postgres/postgres.conf"} { + hdr := new(tar.Header) + hdr.Size = int64(len(content)) + hdr.Name = name + if err := tw.WriteHeader(hdr); err != nil { + return nil, err + } + tw.Write([]byte(content)) + } + tw.Close() + return buf, nil +} + + +func WriteFakeTar(dst io.Writer) error { + if data, err := FakeTar(); err != nil { + return err + } else if _, err := io.Copy(dst, data); err != nil { + return err + } + return nil +} + + +func RandomId() string { + id, _ := future.ComputeId(randomBytes()) // can't fail + return id +} + + +func RandomBytesChanged() uint { + return uint(rand.Int31n(24 * 1024 * 1024)) +} + +func RandomFilesChanged() uint { + return uint(rand.Int31n(42)) +} + +func RandomContainerSize() uint { + return uint(rand.Int31n(142 * 1024 * 1024)) +} diff --git a/future/future.go b/future/future.go new file mode 100644 index 0000000000..3b33e7754d --- /dev/null +++ b/future/future.go @@ -0,0 +1,39 @@ +package future + +import ( + "crypto/sha256" + "io" + "fmt" + "time" +) + +func ComputeId(content io.Reader) (string, error) { + h := sha256.New() + if _, err := io.Copy(h, content); err != nil { + return "", err + } + return fmt.Sprintf("%x", h.Sum(nil)[:8]), nil +} + +func HumanDuration(d time.Duration) string { + if seconds := int(d.Seconds()); seconds < 1 { + return "Less than a second" + } else if seconds < 60 { + return fmt.Sprintf("%d seconds", seconds) + } else if minutes := int(d.Minutes()); minutes == 1 { + return "About a minute" + } else if minutes < 60 { + return fmt.Sprintf("%d minutes", minutes) + } else if hours := int(d.Hours()); hours == 1{ + return "About an hour" + } else if hours < 48 { + return fmt.Sprintf("%d hours", hours) + } else if hours < 24 * 7 * 2 { + return fmt.Sprintf("%d days", hours / 24) + } else if hours < 24 * 30 * 3 { + return fmt.Sprintf("%d weeks", hours / 24 / 7) + } else if hours < 24 * 365 * 2 { + return fmt.Sprintf("%d months", hours / 24 / 30) + } + return fmt.Sprintf("%d years", d.Hours() / 24 / 365) +} diff --git a/rcli/http.go b/rcli/http.go new file mode 100644 index 0000000000..e6cb5657d9 --- /dev/null +++ b/rcli/http.go @@ -0,0 +1,41 @@ +package rcli + +import ( + "net/http" + "net/url" + "path" + "fmt" +) + + +// Use this key to encode an RPC call into an URL, +// eg. domain.tld/path/to/method?q=get_user&q=gordon +const ARG_URL_KEY = "q" + +func URLToCall(u *url.URL) (method string, args []string) { + return path.Base(u.Path), u.Query()[ARG_URL_KEY] +} + + +func ListenAndServeHTTP(addr string, service Service) error { + return http.ListenAndServe(addr, http.HandlerFunc( + func (w http.ResponseWriter, r *http.Request) { + cmd, args := URLToCall(r.URL) + if err := call(service, r.Body, &AutoFlush{w}, append([]string{cmd}, args...)...); err != nil { + fmt.Fprintf(w, "Error: " + err.Error() + "\n") + } + })) +} + + +type AutoFlush struct { + http.ResponseWriter +} + +func (w *AutoFlush) Write(data []byte) (int, error) { + ret, err := w.ResponseWriter.Write(data) + if flusher, ok := w.ResponseWriter.(http.Flusher); ok { + flusher.Flush() + } + return ret, err +} diff --git a/rcli/tcp.go b/rcli/tcp.go new file mode 100644 index 0000000000..572dabb56c --- /dev/null +++ b/rcli/tcp.go @@ -0,0 +1,62 @@ +package rcli + +import ( + "io" + "io/ioutil" + "net" + "log" + "fmt" + "encoding/json" + "bufio" +) + +func CallTCP(addr string, args ...string) (io.ReadWriteCloser, error) { + cmd, err := json.Marshal(args) + if err != nil { + return nil, err + } + conn, err := net.Dial("tcp", addr) + if err != nil { + return nil, err + } + if _, err := fmt.Fprintln(conn, string(cmd)); err != nil { + return nil, err + } + return conn, nil +} + +func ListenAndServeTCP(addr string, service Service) error { + listener, err := net.Listen("tcp", addr) + if err != nil { + return err + } + defer listener.Close() + for { + if conn, err := listener.Accept(); err != nil { + return err + } else { + go func() { + if err := Serve(conn, service); err != nil { + log.Printf("Error: " + err.Error() + "\n") + fmt.Fprintf(conn, "Error: " + err.Error() + "\n") + } + conn.Close() + }() + } + } + return nil +} + +func Serve(conn io.ReadWriter, service Service) error { + r := bufio.NewReader(conn) + var args []string + if line, err := r.ReadString('\n'); err != nil { + return err + } else if err := json.Unmarshal([]byte(line), &args); err != nil { + return err + } else { + return call(service, ioutil.NopCloser(r), conn, args...) + } + return nil +} + diff --git a/rcli/types.go b/rcli/types.go new file mode 100644 index 0000000000..b36aa75e8c --- /dev/null +++ b/rcli/types.go @@ -0,0 +1,84 @@ +package rcli + +import ( + "fmt" + "io" + "reflect" + "flag" + "log" + "strings" + "errors" +) + +type Service interface { + Name() string + Help() string +} + +type Cmd func(io.ReadCloser, io.Writer, ...string) error +type CmdMethod func(Service, io.ReadCloser, io.Writer, ...string) error + + +func call(service Service, stdin io.ReadCloser, stdout io.Writer, args ...string) error { + flags := flag.NewFlagSet("main", flag.ContinueOnError) + flags.SetOutput(stdout) + flags.Usage = func() { stdout.Write([]byte(service.Help())) } + if err := flags.Parse(args); err != nil { + return err + } + cmd := flags.Arg(0) + log.Printf("%s\n", strings.Join(append(append([]string{service.Name()}, cmd), flags.Args()[1:]...), " ")) + if cmd == "" { + cmd = "help" + } + method := getMethod(service, cmd) + if method != nil { + return method(stdin, stdout, args[1:]...) + } + return errors.New("No such command: " + cmd) +} + +func getMethod(service Service, name string) Cmd { + if name == "help" { + return func(stdin io.ReadCloser, stdout io.Writer, args ...string) error { + if len(args) == 0 { + stdout.Write([]byte(service.Help())) + } else { + if method := getMethod(service, args[0]); method == nil { + return errors.New("No such command: " + args[0]) + } else { + method(stdin, stdout, "--help") + } + } + return nil + } + } + methodName := "Cmd"+strings.ToUpper(name[:1])+strings.ToLower(name[1:]) + method, exists := reflect.TypeOf(service).MethodByName(methodName) + if !exists { + return nil + } + return func(stdin io.ReadCloser, stdout io.Writer, args ...string) error { + ret := method.Func.CallSlice([]reflect.Value{ + reflect.ValueOf(service), + reflect.ValueOf(stdin), + reflect.ValueOf(stdout), + reflect.ValueOf(args), + })[0].Interface() + if ret == nil { + return nil + } + return ret.(error) + } +} + +func Subcmd(output io.Writer, name, signature, description string) *flag.FlagSet { + flags := flag.NewFlagSet(name, flag.ContinueOnError) + flags.SetOutput(output) + flags.Usage = func() { + fmt.Fprintf(output, "\nUsage: docker %s %s\n\n%s\n\n", name, signature, description) + flags.PrintDefaults() + } + return flags +} +