diff --git a/libnetwork/Makefile b/libnetwork/Makefile index 892d827dbf..59c181ecdc 100644 --- a/libnetwork/Makefile +++ b/libnetwork/Makefile @@ -22,7 +22,7 @@ build: ${build_image}.created ${docker} make build-local build-local: - $(shell which godep) go build ./... + $(shell which godep) go build -tags experimental ./... check: ${build_image}.created ${docker} make check-local diff --git a/libnetwork/client/client_experimental_test.go b/libnetwork/client/client_experimental_test.go new file mode 100644 index 0000000000..ad83570cd3 --- /dev/null +++ b/libnetwork/client/client_experimental_test.go @@ -0,0 +1,133 @@ +// +build experimental + +package client + +import ( + "bytes" + "testing" + + _ "github.com/docker/libnetwork/netutils" +) + +func TestClientNetworkServiceInvalidCommand(t *testing.T) { + var out, errOut bytes.Buffer + cli := NewNetworkCli(&out, &errOut, callbackFunc) + + err := cli.Cmd("docker", "network", "service", "invalid") + if err == nil { + t.Fatalf("Passing invalid commands must fail") + } +} + +func TestClientNetworkServiceCreate(t *testing.T) { + var out, errOut bytes.Buffer + cli := NewNetworkCli(&out, &errOut, callbackFunc) + + err := cli.Cmd("docker", "network", "service", "create", mockServiceName, mockNwName) + if err != nil { + t.Fatal(err.Error()) + } +} + +func TestClientNetworkServiceRm(t *testing.T) { + var out, errOut bytes.Buffer + cli := NewNetworkCli(&out, &errOut, callbackFunc) + + err := cli.Cmd("docker", "network", "service", "rm", mockServiceName, mockNwName) + if err != nil { + t.Fatal(err.Error()) + } +} + +func TestClientNetworkServiceLs(t *testing.T) { + var out, errOut bytes.Buffer + cli := NewNetworkCli(&out, &errOut, callbackFunc) + + err := cli.Cmd("docker", "network", "service", "ls", mockNwName) + if err != nil { + t.Fatal(err.Error()) + } + if out.String() != string(mockServiceListJSON) { + t.Fatal("Network service ls command fail to return the expected list") + } +} + +func TestClientNetworkServiceInfo(t *testing.T) { + var out, errOut bytes.Buffer + cli := NewNetworkCli(&out, &errOut, callbackFunc) + + err := cli.Cmd("docker", "network", "service", "info", mockServiceName, mockNwName) + if err != nil { + t.Fatal(err.Error()) + } + if out.String() != string(mockServiceJSON) { + t.Fatal("Network info command fail to return the expected object") + } +} + +func TestClientNetworkServiceInfoById(t *testing.T) { + var out, errOut bytes.Buffer + cli := NewNetworkCli(&out, &errOut, callbackFunc) + + err := cli.Cmd("docker", "network", "service", "info", mockServiceID, mockNwID) + if err != nil { + t.Fatal(err.Error()) + } + if out.String() != string(mockServiceJSON) { + t.Fatal("Network info command fail to return the expected object") + } +} + +func TestClientNetworkServiceJoin(t *testing.T) { + var out, errOut bytes.Buffer + cli := NewNetworkCli(&out, &errOut, callbackFunc) + + err := cli.Cmd("docker", "network", "service", "join", mockContainerID, mockServiceName, mockNwName) + if err != nil { + t.Fatal(err.Error()) + } +} + +func TestClientNetworkServiceLeave(t *testing.T) { + var out, errOut bytes.Buffer + cli := NewNetworkCli(&out, &errOut, callbackFunc) + + err := cli.Cmd("docker", "network", "service", "leave", mockContainerID, mockServiceName, mockNwName) + if err != nil { + t.Fatal(err.Error()) + } +} + +// Docker Flag processing in flag.go uses os.Exit() frequently, even for --help +// TODO : Handle the --help test-case in the IT when CLI is available +/* +func TestClientNetworkServiceCreateHelp(t *testing.T) { + var out, errOut bytes.Buffer + cFunc := func(method, path string, data interface{}, headers map[string][]string) (io.ReadCloser, int, error) { + return nil, 0, nil + } + cli := NewNetworkCli(&out, &errOut, callbackFunc) + + err := cli.Cmd("docker", "network", "create", "--help") + if err != nil { + t.Fatalf(err.Error()) + } +} +*/ + +// Docker flag processing in flag.go uses os.Exit(1) for incorrect parameter case. +// TODO : Handle the missing argument case in the IT when CLI is available +/* +func TestClientNetworkServiceCreateMissingArgument(t *testing.T) { + var out, errOut bytes.Buffer + cFunc := func(method, path string, data interface{}, headers map[string][]string) (io.ReadCloser, int, error) { + return nil, 0, nil + } + cli := NewNetworkCli(&out, &errOut, callbackFunc) + + err := cli.Cmd("docker", "network", "create") + if err != nil { + t.Fatal(err.Error()) + } +} +*/ diff --git a/libnetwork/client/client_test.go b/libnetwork/client/client_test.go index 6a8c675de2..85b55144af 100644 --- a/libnetwork/client/client_test.go +++ b/libnetwork/client/client_test.go @@ -184,95 +184,6 @@ func TestClientNetworkInfoById(t *testing.T) { } } -func TestClientNetworkServiceInvalidCommand(t *testing.T) { - var out, errOut bytes.Buffer - cli := NewNetworkCli(&out, &errOut, callbackFunc) - - err := cli.Cmd("docker", "network", "service", "invalid") - if err == nil { - t.Fatalf("Passing invalid commands must fail") - } -} - -func TestClientNetworkServiceCreate(t *testing.T) { - var out, errOut bytes.Buffer - cli := NewNetworkCli(&out, &errOut, callbackFunc) - - err := cli.Cmd("docker", "network", "service", "create", mockServiceName, mockNwName) - if err != nil { - t.Fatal(err.Error()) - } -} - -func TestClientNetworkServiceRm(t *testing.T) { - var out, errOut bytes.Buffer - cli := NewNetworkCli(&out, &errOut, callbackFunc) - - err := cli.Cmd("docker", "network", "service", "rm", mockServiceName, mockNwName) - if err != nil { - t.Fatal(err.Error()) - } -} - -func TestClientNetworkServiceLs(t *testing.T) { - var out, errOut bytes.Buffer - cli := NewNetworkCli(&out, &errOut, callbackFunc) - - err := cli.Cmd("docker", "network", "service", "ls", mockNwName) - if err != nil { - t.Fatal(err.Error()) - } - if out.String() != string(mockServiceListJSON) { - t.Fatal("Network service ls command fail to return the expected list") - } -} - -func TestClientNetworkServiceInfo(t *testing.T) { - var out, errOut bytes.Buffer - cli := NewNetworkCli(&out, &errOut, callbackFunc) - - err := cli.Cmd("docker", "network", "service", "info", mockServiceName, mockNwName) - if err != nil { - t.Fatal(err.Error()) - } - if out.String() != string(mockServiceJSON) { - t.Fatal("Network info command fail to return the expected object") - } -} - -func TestClientNetworkServiceInfoById(t *testing.T) { - var out, errOut bytes.Buffer - cli := NewNetworkCli(&out, &errOut, callbackFunc) - - err := cli.Cmd("docker", "network", "service", "info", mockServiceID, mockNwID) - if err != nil { - t.Fatal(err.Error()) - } - if out.String() != string(mockServiceJSON) { - t.Fatal("Network info command fail to return the expected object") - } -} - -func TestClientNetworkServiceJoin(t *testing.T) { - var out, errOut bytes.Buffer - cli := NewNetworkCli(&out, &errOut, callbackFunc) - - err := cli.Cmd("docker", "network", "service", "join", mockContainerID, mockServiceName, mockNwName) - if err != nil { - t.Fatal(err.Error()) - } -} - -func TestClientNetworkServiceLeave(t *testing.T) { - var out, errOut bytes.Buffer - cli := NewNetworkCli(&out, &errOut, callbackFunc) - - err := cli.Cmd("docker", "network", "service", "leave", mockContainerID, mockServiceName, mockNwName) - if err != nil { - t.Fatal(err.Error()) - } -} - // Docker Flag processing in flag.go uses os.Exit() frequently, even for --help // TODO : Handle the --help test-case in the IT when CLI is available /* diff --git a/libnetwork/client/network.go b/libnetwork/client/network.go index 2b57bede4d..64e4de2d62 100644 --- a/libnetwork/client/network.go +++ b/libnetwork/client/network.go @@ -6,7 +6,6 @@ import ( "fmt" "io" "net/http" - "strings" flag "github.com/docker/docker/pkg/mflag" ) @@ -26,12 +25,6 @@ var ( {"rm", "Remove a network"}, {"ls", "List all networks"}, {"info", "Display information of a network"}, - {"service create", "Create a service endpoint"}, - {"service rm", "Remove a service endpoint"}, - {"service join", "Join a container to a service endpoint"}, - {"service leave", "Leave a container from a service endpoint"}, - {"service ls", "Lists all service endpoints on a network"}, - {"service info", "Display information of a service endpoint"}, } ) @@ -184,249 +177,6 @@ func lookupNetworkID(cli *NetworkCli, nameID string) (string, error) { return list[0].ID, nil } -func lookupServiceID(cli *NetworkCli, networkID string, nameID string) (string, error) { - obj, statusCode, err := readBody(cli.call("GET", fmt.Sprintf("/networks/%s/endpoints?name=%s", networkID, nameID), nil, nil)) - if err != nil { - return "", err - } - - if statusCode != http.StatusOK { - return "", fmt.Errorf("name query failed for %s due to : statuscode(%d) %v", nameID, statusCode, string(obj)) - } - - var list []*networkResource - err = json.Unmarshal(obj, &list) - if err != nil { - return "", err - } - if len(list) > 0 { - // name query filter will always return a single-element collection - return list[0].ID, nil - } - - // Check for Partial-id - obj, statusCode, err = readBody(cli.call("GET", fmt.Sprintf("/networks/%s/endpoints?partial-id=%s", networkID, nameID), nil, nil)) - if err != nil { - return "", err - } - - if statusCode != http.StatusOK { - return "", fmt.Errorf("partial-id match query failed for %s due to : statuscode(%d) %v", nameID, statusCode, string(obj)) - } - - err = json.Unmarshal(obj, &list) - if err != nil { - return "", err - } - if len(list) == 0 { - return "", fmt.Errorf("resource not found %s", nameID) - } - if len(list) > 1 { - return "", fmt.Errorf("multiple services matching the partial identifier (%s). Please use full identifier", nameID) - } - return list[0].ID, nil -} - -func lookupContainerID(cli *NetworkCli, nameID string) (string, error) { - // TODO : containerID to sandbox-key ? - return nameID, nil -} - -// CmdNetworkService handles the network service UI -func (cli *NetworkCli) CmdNetworkService(chain string, args ...string) error { - cmd := cli.Subcmd(chain, "service", "COMMAND [OPTIONS] [arg...]", serviceUsage(chain), false) - cmd.Require(flag.Min, 1) - err := cmd.ParseFlags(args, true) - if err == nil { - cmd.Usage() - return fmt.Errorf("Invalid command : %v", args) - } - return err -} - -// CmdNetworkServiceCreate handles service create UI -func (cli *NetworkCli) CmdNetworkServiceCreate(chain string, args ...string) error { - cmd := cli.Subcmd(chain, "create", "SERVICE NETWORK", "Creates a new service on a network", false) - cmd.Require(flag.Min, 2) - err := cmd.ParseFlags(args, true) - if err != nil { - return err - } - - networkID, err := lookupNetworkID(cli, cmd.Arg(1)) - if err != nil { - return err - } - - ec := endpointCreate{Name: cmd.Arg(0), NetworkID: networkID} - - obj, _, err := readBody(cli.call("POST", "/networks/"+networkID+"/endpoints", ec, nil)) - if err != nil { - return err - } - if _, err := io.Copy(cli.out, bytes.NewReader(obj)); err != nil { - return err - } - return nil -} - -// CmdNetworkServiceRm handles service delete UI -func (cli *NetworkCli) CmdNetworkServiceRm(chain string, args ...string) error { - cmd := cli.Subcmd(chain, "rm", "SERVICE NETWORK", "Deletes a service", false) - cmd.Require(flag.Min, 2) - err := cmd.ParseFlags(args, true) - if err != nil { - return err - } - - networkID, err := lookupNetworkID(cli, cmd.Arg(1)) - if err != nil { - return err - } - - serviceID, err := lookupServiceID(cli, networkID, cmd.Arg(0)) - if err != nil { - return err - } - - obj, _, err := readBody(cli.call("DELETE", "/networks/"+networkID+"/endpoints/"+serviceID, nil, nil)) - if err != nil { - return err - } - if _, err := io.Copy(cli.out, bytes.NewReader(obj)); err != nil { - return err - } - return nil -} - -// CmdNetworkServiceLs handles service list UI -func (cli *NetworkCli) CmdNetworkServiceLs(chain string, args ...string) error { - cmd := cli.Subcmd(chain, "ls", "NETWORK", "Lists all the services on a network", false) - err := cmd.ParseFlags(args, true) - if err != nil { - return err - } - - cmd.Require(flag.Min, 1) - - networkID, err := lookupNetworkID(cli, cmd.Arg(0)) - if err != nil { - return err - } - - obj, _, err := readBody(cli.call("GET", "/networks/"+networkID+"/endpoints", nil, nil)) - if err != nil { - fmt.Fprintf(cli.err, "%s", err.Error()) - return err - } - if _, err := io.Copy(cli.out, bytes.NewReader(obj)); err != nil { - return err - } - return nil -} - -// CmdNetworkServiceInfo handles service info UI -func (cli *NetworkCli) CmdNetworkServiceInfo(chain string, args ...string) error { - cmd := cli.Subcmd(chain, "info", "SERVICE NETWORK", "Displays detailed information on a service", false) - cmd.Require(flag.Min, 2) - err := cmd.ParseFlags(args, true) - if err != nil { - return err - } - - networkID, err := lookupNetworkID(cli, cmd.Arg(1)) - if err != nil { - return err - } - - serviceID, err := lookupServiceID(cli, networkID, cmd.Arg(0)) - if err != nil { - return err - } - - obj, _, err := readBody(cli.call("GET", "/networks/"+networkID+"/endpoints/"+serviceID, nil, nil)) - if err != nil { - fmt.Fprintf(cli.err, "%s", err.Error()) - return err - } - if _, err := io.Copy(cli.out, bytes.NewReader(obj)); err != nil { - return err - } - return nil -} - -// CmdNetworkServiceJoin handles service join UI -func (cli *NetworkCli) CmdNetworkServiceJoin(chain string, args ...string) error { - cmd := cli.Subcmd(chain, "join", "CONTAINER SERVICE NETWORK", "Sets a container as a service backend", false) - cmd.Require(flag.Min, 3) - err := cmd.ParseFlags(args, true) - if err != nil { - return err - } - - containerID, err := lookupContainerID(cli, cmd.Arg(0)) - if err != nil { - return err - } - - networkID, err := lookupNetworkID(cli, cmd.Arg(2)) - if err != nil { - return err - } - - serviceID, err := lookupServiceID(cli, networkID, cmd.Arg(1)) - if err != nil { - return err - } - - nc := endpointJoin{ContainerID: containerID} - - obj, _, err := readBody(cli.call("POST", "/networks/"+networkID+"/endpoints/"+serviceID+"/containers", nc, nil)) - if err != nil { - fmt.Fprintf(cli.err, "%s", err.Error()) - return err - } - if _, err := io.Copy(cli.out, bytes.NewReader(obj)); err != nil { - return err - } - return nil -} - -// CmdNetworkServiceLeave handles service leave UI -func (cli *NetworkCli) CmdNetworkServiceLeave(chain string, args ...string) error { - cmd := cli.Subcmd(chain, "leave", "CONTAINER SERVICE NETWORK", "Removes a container from service backend", false) - cmd.Require(flag.Min, 3) - err := cmd.ParseFlags(args, true) - if err != nil { - return err - } - - containerID, err := lookupContainerID(cli, cmd.Arg(0)) - if err != nil { - return err - } - - networkID, err := lookupNetworkID(cli, cmd.Arg(2)) - if err != nil { - return err - } - - serviceID, err := lookupServiceID(cli, networkID, cmd.Arg(1)) - if err != nil { - return err - } - - obj, _, err := readBody(cli.call("DELETE", "/networks/"+networkID+"/endpoints/"+serviceID+"/containers/"+containerID, nil, nil)) - if err != nil { - fmt.Fprintf(cli.err, "%s", err.Error()) - return err - } - if _, err := io.Copy(cli.out, bytes.NewReader(obj)); err != nil { - return err - } - return nil -} - func networkUsage(chain string) string { help := "Commands:\n" @@ -434,20 +184,10 @@ func networkUsage(chain string) string { help += fmt.Sprintf(" %-25.25s%s\n", cmd.name, cmd.description) } + for _, cmd := range serviceCommands { + help += fmt.Sprintf(" %-25.25s%s\n", "service "+cmd.name, cmd.description) + } + help += fmt.Sprintf("\nRun '%s network COMMAND --help' for more information on a command.", chain) return help } - -func serviceUsage(chain string) string { - help := "Commands:\n" - - for _, cmd := range networkCommands { - if strings.HasPrefix(cmd.name, "service ") { - command := strings.SplitAfter(cmd.name, "service ") - help += fmt.Sprintf(" %-10.10s%s\n", command[1], cmd.description) - } - } - - help += fmt.Sprintf("\nRun '%s service COMMAND --help' for more information on a command.", chain) - return help -} diff --git a/libnetwork/client/service.go b/libnetwork/client/service.go new file mode 100644 index 0000000000..afdbb7f84b --- /dev/null +++ b/libnetwork/client/service.go @@ -0,0 +1,7 @@ +// +build !experimental + +package client + +var ( + serviceCommands = []command{} +) diff --git a/libnetwork/client/service_experimental.go b/libnetwork/client/service_experimental.go new file mode 100644 index 0000000000..16ded97c6a --- /dev/null +++ b/libnetwork/client/service_experimental.go @@ -0,0 +1,278 @@ +// +build experimental + +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + + flag "github.com/docker/docker/pkg/mflag" +) + +var ( + serviceCommands = []command{ + {"create", "Create a service endpoint"}, + {"rm", "Remove a service endpoint"}, + {"join", "Join a container to a service endpoint"}, + {"leave", "Leave a container from a service endpoint"}, + {"ls", "Lists all service endpoints on a network"}, + {"info", "Display information of a service endpoint"}, + } +) + +func lookupServiceID(cli *NetworkCli, networkID string, nameID string) (string, error) { + obj, statusCode, err := readBody(cli.call("GET", fmt.Sprintf("/networks/%s/endpoints?name=%s", networkID, nameID), nil, nil)) + if err != nil { + return "", err + } + + if statusCode != http.StatusOK { + return "", fmt.Errorf("name query failed for %s due to : statuscode(%d) %v", nameID, statusCode, string(obj)) + } + + var list []*networkResource + err = json.Unmarshal(obj, &list) + if err != nil { + return "", err + } + if len(list) > 0 { + // name query filter will always return a single-element collection + return list[0].ID, nil + } + + // Check for Partial-id + obj, statusCode, err = readBody(cli.call("GET", fmt.Sprintf("/networks/%s/endpoints?partial-id=%s", networkID, nameID), nil, nil)) + if err != nil { + return "", err + } + + if statusCode != http.StatusOK { + return "", fmt.Errorf("partial-id match query failed for %s due to : statuscode(%d) %v", nameID, statusCode, string(obj)) + } + + err = json.Unmarshal(obj, &list) + if err != nil { + return "", err + } + if len(list) == 0 { + return "", fmt.Errorf("resource not found %s", nameID) + } + if len(list) > 1 { + return "", fmt.Errorf("multiple services matching the partial identifier (%s). Please use full identifier", nameID) + } + return list[0].ID, nil +} + +func lookupContainerID(cli *NetworkCli, nameID string) (string, error) { + // TODO : containerID to sandbox-key ? + return nameID, nil +} + +// CmdNetworkService handles the network service UI +func (cli *NetworkCli) CmdNetworkService(chain string, args ...string) error { + cmd := cli.Subcmd(chain, "service", "COMMAND [OPTIONS] [arg...]", serviceUsage(chain), false) + cmd.Require(flag.Min, 1) + err := cmd.ParseFlags(args, true) + if err == nil { + cmd.Usage() + return fmt.Errorf("Invalid command : %v", args) + } + return err +} + +// CmdNetworkServiceCreate handles service create UI +func (cli *NetworkCli) CmdNetworkServiceCreate(chain string, args ...string) error { + cmd := cli.Subcmd(chain, "create", "SERVICE NETWORK", "Creates a new service on a network", false) + cmd.Require(flag.Min, 2) + err := cmd.ParseFlags(args, true) + if err != nil { + return err + } + + networkID, err := lookupNetworkID(cli, cmd.Arg(1)) + if err != nil { + return err + } + + ec := endpointCreate{Name: cmd.Arg(0), NetworkID: networkID} + + obj, _, err := readBody(cli.call("POST", "/networks/"+networkID+"/endpoints", ec, nil)) + if err != nil { + return err + } + if _, err := io.Copy(cli.out, bytes.NewReader(obj)); err != nil { + return err + } + return nil +} + +// CmdNetworkServiceRm handles service delete UI +func (cli *NetworkCli) CmdNetworkServiceRm(chain string, args ...string) error { + cmd := cli.Subcmd(chain, "rm", "SERVICE NETWORK", "Deletes a service", false) + cmd.Require(flag.Min, 2) + err := cmd.ParseFlags(args, true) + if err != nil { + return err + } + + networkID, err := lookupNetworkID(cli, cmd.Arg(1)) + if err != nil { + return err + } + + serviceID, err := lookupServiceID(cli, networkID, cmd.Arg(0)) + if err != nil { + return err + } + + obj, _, err := readBody(cli.call("DELETE", "/networks/"+networkID+"/endpoints/"+serviceID, nil, nil)) + if err != nil { + return err + } + if _, err := io.Copy(cli.out, bytes.NewReader(obj)); err != nil { + return err + } + return nil +} + +// CmdNetworkServiceLs handles service list UI +func (cli *NetworkCli) CmdNetworkServiceLs(chain string, args ...string) error { + cmd := cli.Subcmd(chain, "ls", "NETWORK", "Lists all the services on a network", false) + err := cmd.ParseFlags(args, true) + if err != nil { + return err + } + + cmd.Require(flag.Min, 1) + + networkID, err := lookupNetworkID(cli, cmd.Arg(0)) + if err != nil { + return err + } + + obj, _, err := readBody(cli.call("GET", "/networks/"+networkID+"/endpoints", nil, nil)) + if err != nil { + fmt.Fprintf(cli.err, "%s", err.Error()) + return err + } + if _, err := io.Copy(cli.out, bytes.NewReader(obj)); err != nil { + return err + } + return nil +} + +// CmdNetworkServiceInfo handles service info UI +func (cli *NetworkCli) CmdNetworkServiceInfo(chain string, args ...string) error { + cmd := cli.Subcmd(chain, "info", "SERVICE NETWORK", "Displays detailed information on a service", false) + cmd.Require(flag.Min, 2) + err := cmd.ParseFlags(args, true) + if err != nil { + return err + } + + networkID, err := lookupNetworkID(cli, cmd.Arg(1)) + if err != nil { + return err + } + + serviceID, err := lookupServiceID(cli, networkID, cmd.Arg(0)) + if err != nil { + return err + } + + obj, _, err := readBody(cli.call("GET", "/networks/"+networkID+"/endpoints/"+serviceID, nil, nil)) + if err != nil { + fmt.Fprintf(cli.err, "%s", err.Error()) + return err + } + if _, err := io.Copy(cli.out, bytes.NewReader(obj)); err != nil { + return err + } + return nil +} + +// CmdNetworkServiceJoin handles service join UI +func (cli *NetworkCli) CmdNetworkServiceJoin(chain string, args ...string) error { + cmd := cli.Subcmd(chain, "join", "CONTAINER SERVICE NETWORK", "Sets a container as a service backend", false) + cmd.Require(flag.Min, 3) + err := cmd.ParseFlags(args, true) + if err != nil { + return err + } + + containerID, err := lookupContainerID(cli, cmd.Arg(0)) + if err != nil { + return err + } + + networkID, err := lookupNetworkID(cli, cmd.Arg(2)) + if err != nil { + return err + } + + serviceID, err := lookupServiceID(cli, networkID, cmd.Arg(1)) + if err != nil { + return err + } + + nc := endpointJoin{ContainerID: containerID} + + obj, _, err := readBody(cli.call("POST", "/networks/"+networkID+"/endpoints/"+serviceID+"/containers", nc, nil)) + if err != nil { + fmt.Fprintf(cli.err, "%s", err.Error()) + return err + } + if _, err := io.Copy(cli.out, bytes.NewReader(obj)); err != nil { + return err + } + return nil +} + +// CmdNetworkServiceLeave handles service leave UI +func (cli *NetworkCli) CmdNetworkServiceLeave(chain string, args ...string) error { + cmd := cli.Subcmd(chain, "leave", "CONTAINER SERVICE NETWORK", "Removes a container from service backend", false) + cmd.Require(flag.Min, 3) + err := cmd.ParseFlags(args, true) + if err != nil { + return err + } + + containerID, err := lookupContainerID(cli, cmd.Arg(0)) + if err != nil { + return err + } + + networkID, err := lookupNetworkID(cli, cmd.Arg(2)) + if err != nil { + return err + } + + serviceID, err := lookupServiceID(cli, networkID, cmd.Arg(1)) + if err != nil { + return err + } + + obj, _, err := readBody(cli.call("DELETE", "/networks/"+networkID+"/endpoints/"+serviceID+"/containers/"+containerID, nil, nil)) + if err != nil { + fmt.Fprintf(cli.err, "%s", err.Error()) + return err + } + if _, err := io.Copy(cli.out, bytes.NewReader(obj)); err != nil { + return err + } + return nil +} + +func serviceUsage(chain string) string { + help := "Commands:\n" + + for _, cmd := range serviceCommands { + help += fmt.Sprintf(" %-10.10s%s\n", cmd, cmd.description) + } + + help += fmt.Sprintf("\nRun '%s service COMMAND --help' for more information on a command.", chain) + return help +}