Parcourir la source

Moved all the service commands under experimental build tag

In order to support the docker experimental feature build, moving the
service commands under experimental tag. Please refer to :
https://github.com/docker/docker/pull/13338/ for more information

Signed-off-by: Madhu Venugopal <madhu@docker.com>
Madhu Venugopal il y a 10 ans
Parent
commit
f1712c0bf6

+ 1 - 1
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

+ 133 - 0
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())
+	}
+}
+*/

+ 0 - 89
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
 /*

+ 3 - 263
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)
 	}
 
-	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)
-		}
+	for _, cmd := range serviceCommands {
+		help += fmt.Sprintf("    %-25.25s%s\n", "service "+cmd.name, cmd.description)
 	}
 
-	help += fmt.Sprintf("\nRun '%s service COMMAND --help' for more information on a command.", chain)
+	help += fmt.Sprintf("\nRun '%s network COMMAND --help' for more information on a command.", chain)
 	return help
 }

+ 7 - 0
libnetwork/client/service.go

@@ -0,0 +1,7 @@
+// +build !experimental
+
+package client
+
+var (
+	serviceCommands = []command{}
+)

+ 278 - 0
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
+}