瀏覽代碼

Merge pull request #206 from mavenugo/experimental

Moved all the service commands under experimental build tag
Jana Radhakrishnan 10 年之前
父節點
當前提交
9c480e81be

+ 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

+ 124 - 0
libnetwork/client/client_experimental_test.go

@@ -0,0 +1,124 @@
+// +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())
+	}
+}
+
+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())
+	}
+}
+
+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())
+	}
+}
+
+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())
+	}
+}
+*/

+ 8 - 104
libnetwork/client/client_test.go

@@ -27,10 +27,10 @@ func TestMain(m *testing.M) {
 var callbackFunc func(method, path string, data interface{}, headers map[string][]string) (io.ReadCloser, int, error)
 var mockNwJSON, mockNwListJSON, mockServiceJSON, mockServiceListJSON []byte
 var mockNwName = "test"
-var mockNwID = "23456789"
+var mockNwID = "2a3456789"
 var mockServiceName = "testSrv"
-var mockServiceID = "23456789"
-var mockContainerID = "23456789"
+var mockServiceID = "2a3456789"
+var mockContainerID = "2a3456789"
 
 func setupMockHTTPCallback() {
 	var list []networkResource
@@ -75,13 +75,15 @@ func setupMockHTTPCallback() {
 				rsp = string(mockServiceJSON)
 			}
 		case "POST":
+			var data []byte
 			if strings.HasSuffix(path, "networks") {
-				rsp = mockNwID
+				data, _ = json.Marshal(mockNwID)
 			} else if strings.HasSuffix(path, "endpoints") {
-				rsp = mockServiceID
+				data, _ = json.Marshal(mockServiceID)
 			} else if strings.HasSuffix(path, "containers") {
-				rsp = mockContainerID
+				data, _ = json.Marshal(mockContainerID)
 			}
+			rsp = string(data)
 		case "PUT":
 		case "DELETE":
 			rsp = ""
@@ -153,9 +155,6 @@ func TestClientNetworkLs(t *testing.T) {
 	if err != nil {
 		t.Fatal(err.Error())
 	}
-	if out.String() != string(mockNwListJSON) {
-		t.Fatal("Network List command fail to return the expected list")
-	}
 }
 
 func TestClientNetworkInfo(t *testing.T) {
@@ -166,9 +165,6 @@ func TestClientNetworkInfo(t *testing.T) {
 	if err != nil {
 		t.Fatal(err.Error())
 	}
-	if out.String() != string(mockNwJSON) {
-		t.Fatal("Network info command fail to return the expected object")
-	}
 }
 
 func TestClientNetworkInfoById(t *testing.T) {
@@ -179,98 +175,6 @@ func TestClientNetworkInfoById(t *testing.T) {
 	if err != nil {
 		t.Fatal(err.Error())
 	}
-	if out.String() != string(mockNwJSON) {
-		t.Fatal("Network info command fail to return the expected object")
-	}
-}
-
-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

+ 59 - 271
libnetwork/client/network.go

@@ -4,11 +4,11 @@ import (
 	"bytes"
 	"encoding/json"
 	"fmt"
-	"io"
 	"net/http"
-	"strings"
+	"text/tabwriter"
 
 	flag "github.com/docker/docker/pkg/mflag"
+	"github.com/docker/docker/pkg/stringid"
 )
 
 const (
@@ -26,12 +26,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"},
 	}
 )
 
@@ -66,9 +60,12 @@ func (cli *NetworkCli) CmdNetworkCreate(chain string, args ...string) error {
 	if err != nil {
 		return err
 	}
-	if _, err := io.Copy(cli.out, bytes.NewReader(obj)); err != nil {
+	var replyID string
+	err = json.Unmarshal(obj, &replyID)
+	if err != nil {
 		return err
 	}
+	fmt.Fprintf(cli.out, "%s\n", replyID)
 	return nil
 }
 
@@ -84,19 +81,20 @@ func (cli *NetworkCli) CmdNetworkRm(chain string, args ...string) error {
 	if err != nil {
 		return err
 	}
-	obj, _, err := readBody(cli.call("DELETE", "/networks/"+id, nil, nil))
+	_, _, err = readBody(cli.call("DELETE", "/networks/"+id, nil, nil))
 	if err != nil {
 		return err
 	}
-	if _, err := io.Copy(cli.out, bytes.NewReader(obj)); err != nil {
-		return err
-	}
 	return nil
 }
 
 // CmdNetworkLs handles Network List UI
 func (cli *NetworkCli) CmdNetworkLs(chain string, args ...string) error {
 	cmd := cli.Subcmd(chain, "ls", "", "Lists all the networks created by the user", false)
+	quiet := cmd.Bool([]string{"q", "-quiet"}, false, "Only display numeric IDs")
+	noTrunc := cmd.Bool([]string{"#notrunc", "-no-trunc"}, false, "Do not truncate the output")
+	nLatest := cmd.Bool([]string{"l", "-latest"}, false, "Show the latest network created")
+	last := cmd.Int([]string{"n"}, -1, "Show n last created networks")
 	err := cmd.ParseFlags(args, true)
 	if err != nil {
 		return err
@@ -105,9 +103,41 @@ func (cli *NetworkCli) CmdNetworkLs(chain string, args ...string) error {
 	if err != nil {
 		return err
 	}
-	if _, err := io.Copy(cli.out, bytes.NewReader(obj)); err != nil {
+	if *last == -1 && *nLatest {
+		*last = 1
+	}
+
+	var networkResources []networkResource
+	err = json.Unmarshal(obj, &networkResources)
+	if err != nil {
 		return err
 	}
+
+	wr := tabwriter.NewWriter(cli.out, 20, 1, 3, ' ', 0)
+
+	// unless quiet (-q) is specified, print field titles
+	if !*quiet {
+		fmt.Fprintln(wr, "NETWORK ID\tNAME\tTYPE")
+	}
+
+	for _, networkResource := range networkResources {
+		ID := networkResource.ID
+		netName := networkResource.Name
+		if !*noTrunc {
+			ID = stringid.TruncateID(ID)
+		}
+		if *quiet {
+			fmt.Fprintln(wr, ID)
+			continue
+		}
+		netType := networkResource.Type
+		fmt.Fprintf(wr, "%s\t%s\t%s\t",
+			ID,
+			netName,
+			netType)
+		fmt.Fprint(wr, "\n")
+	}
+	wr.Flush()
 	return nil
 }
 
@@ -129,9 +159,20 @@ func (cli *NetworkCli) CmdNetworkInfo(chain string, args ...string) error {
 	if err != nil {
 		return err
 	}
-	if _, err := io.Copy(cli.out, bytes.NewReader(obj)); err != nil {
+	networkResource := &networkResource{}
+	if err := json.NewDecoder(bytes.NewReader(obj)).Decode(networkResource); err != nil {
 		return err
 	}
+	fmt.Fprintf(cli.out, "Network Id: %s\n", networkResource.ID)
+	fmt.Fprintf(cli.out, "Name: %s\n", networkResource.Name)
+	fmt.Fprintf(cli.out, "Type: %s\n", networkResource.Type)
+	if networkResource.Endpoints != nil {
+		for _, endpointResource := range networkResource.Endpoints {
+			fmt.Fprintf(cli.out, "  Service Id: %s\n", endpointResource.ID)
+			fmt.Fprintf(cli.out, "\tName: %s\n", endpointResource.Name)
+		}
+	}
+
 	return nil
 }
 
@@ -184,249 +225,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 +232,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{}
+)

+ 317 - 0
libnetwork/client/service_experimental.go

@@ -0,0 +1,317 @@
+// +build experimental
+
+package client
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"text/tabwriter"
+
+	flag "github.com/docker/docker/pkg/mflag"
+	"github.com/docker/docker/pkg/stringid"
+)
+
+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
+	}
+
+	var replyID string
+	err = json.Unmarshal(obj, &replyID)
+	if err != nil {
+		return err
+	}
+
+	fmt.Fprintf(cli.out, "%s\n", replyID)
+	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
+	}
+
+	_, _, err = readBody(cli.call("DELETE", "/networks/"+networkID+"/endpoints/"+serviceID, nil, nil))
+	if 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)
+	quiet := cmd.Bool([]string{"q", "-quiet"}, false, "Only display numeric IDs")
+	noTrunc := cmd.Bool([]string{"#notrunc", "-no-trunc"}, false, "Do not truncate the output")
+	nLatest := cmd.Bool([]string{"l", "-latest"}, false, "Show the latest network created")
+	last := cmd.Int([]string{"n"}, -1, "Show n last created networks")
+	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 *last == -1 && *nLatest {
+		*last = 1
+	}
+
+	var endpointResources []endpointResource
+	err = json.Unmarshal(obj, &endpointResources)
+	if err != nil {
+		return err
+	}
+
+	wr := tabwriter.NewWriter(cli.out, 20, 1, 3, ' ', 0)
+	// unless quiet (-q) is specified, print field titles
+	if !*quiet {
+		fmt.Fprintln(wr, "NETWORK SERVICE ID\tNAME\tNETWORK")
+	}
+
+	for _, networkResource := range endpointResources {
+		ID := networkResource.ID
+		netName := networkResource.Name
+		if !*noTrunc {
+			ID = stringid.TruncateID(ID)
+		}
+		if *quiet {
+			fmt.Fprintln(wr, ID)
+			continue
+		}
+		network := networkResource.Network
+		fmt.Fprintf(wr, "%s\t%s\t%s",
+			ID,
+			netName,
+			network)
+		fmt.Fprint(wr, "\n")
+	}
+	wr.Flush()
+
+	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
+	}
+
+	endpointResource := &endpointResource{}
+	if err := json.NewDecoder(bytes.NewReader(obj)).Decode(endpointResource); err != nil {
+		return err
+	}
+	fmt.Fprintf(cli.out, "Service Id: %s\n", endpointResource.ID)
+	fmt.Fprintf(cli.out, "\tName: %s\n", endpointResource.Name)
+	fmt.Fprintf(cli.out, "\tNetwork: %s\n", endpointResource.Network)
+
+	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}
+
+	_, _, err = readBody(cli.call("POST", "/networks/"+networkID+"/endpoints/"+serviceID+"/containers", nc, nil))
+	if err != nil {
+		fmt.Fprintf(cli.err, "%s", err.Error())
+		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
+	}
+
+	_, _, 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
+	}
+	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
+}