Przeglądaj źródła

Merge pull request #196 from mavenugo/api-changes

Modified Client to make use of the corrected REST API & service endpoint support
Jana Radhakrishnan 10 lat temu
rodzic
commit
0d62997f3b

+ 1 - 1
libnetwork/api/api.go

@@ -89,7 +89,7 @@ func (h *httpHandler) initRouter() {
 			{"/networks/" + nwID, nil, procGetNetwork},
 			{"/networks/" + nwID + "/endpoints", []string{"name", epName}, procGetEndpoints},
 			{"/networks/" + nwID + "/endpoints", []string{"partial-id", epPID}, procGetEndpoints},
-
+			{"/networks/" + nwID + "/endpoints", nil, procGetEndpoints},
 			{"/networks/" + nwID + "/endpoints/" + epID, nil, procGetEndpoint},
 		},
 		"POST": {

+ 2 - 2
libnetwork/api/api_test.go

@@ -1391,8 +1391,8 @@ func TestEndToEnd(t *testing.T) {
 		t.Fatal(err)
 	}
 	handleRequest(rsp, req)
-	if rsp.statusCode != http.StatusNotFound {
-		t.Fatalf("Expected StatusNotFound. Got (%d): %s", rsp.statusCode, rsp.body)
+	if rsp.statusCode != http.StatusOK {
+		t.Fatalf("Expected StatusOK. Got (%d): %s", rsp.statusCode, rsp.body)
 	}
 
 	req, err = http.NewRequest("GET", "/networks/"+nid+"/endpoints?name=bla", nil)

+ 6 - 0
libnetwork/client/client.go

@@ -49,6 +49,12 @@ func (cli *NetworkCli) getMethod(args ...string) (func(string, ...string) error,
 // Cmd is borrowed from Docker UI and acts as the entry point for network UI commands.
 // network UI commands are designed to be invoked from multiple parent chains
 func (cli *NetworkCli) Cmd(chain string, args ...string) error {
+	if len(args) > 2 {
+		method, exists := cli.getMethod(args[:3]...)
+		if exists {
+			return method(chain+" "+args[0]+" "+args[1], args[3:]...)
+		}
+	}
 	if len(args) > 1 {
 		method, exists := cli.getMethod(args[:2]...)
 		if exists {

+ 196 - 42
libnetwork/client/client_test.go

@@ -2,7 +2,11 @@ package client
 
 import (
 	"bytes"
+	"encoding/json"
+	"fmt"
 	"io"
+	"os"
+	"strings"
 	"testing"
 
 	_ "github.com/docker/libnetwork/netutils"
@@ -15,12 +19,80 @@ type nopCloser struct {
 
 func (nopCloser) Close() error { return nil }
 
+func TestMain(m *testing.M) {
+	setupMockHTTPCallback()
+	os.Exit(m.Run())
+}
+
+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 mockServiceName = "testSrv"
+var mockServiceID = "23456789"
+var mockContainerID = "23456789"
+
+func setupMockHTTPCallback() {
+	var list []networkResource
+	nw := networkResource{Name: mockNwName, ID: mockNwID}
+	mockNwJSON, _ = json.Marshal(nw)
+	list = append(list, nw)
+	mockNwListJSON, _ = json.Marshal(list)
+
+	var srvList []endpointResource
+	ep := endpointResource{Name: mockServiceName, ID: mockServiceID, Network: mockNwName}
+	mockServiceJSON, _ = json.Marshal(ep)
+	srvList = append(srvList, ep)
+	mockServiceListJSON, _ = json.Marshal(srvList)
+
+	callbackFunc = func(method, path string, data interface{}, headers map[string][]string) (io.ReadCloser, int, error) {
+		var rsp string
+		switch method {
+		case "GET":
+			if strings.Contains(path, fmt.Sprintf("networks?name=%s", mockNwName)) {
+				rsp = string(mockNwListJSON)
+			} else if strings.Contains(path, "networks?name=") {
+				rsp = "[]"
+			} else if strings.Contains(path, fmt.Sprintf("networks?partial-id=%s", mockNwID)) {
+				rsp = string(mockNwListJSON)
+			} else if strings.Contains(path, "networks?partial-id=") {
+				rsp = "[]"
+			} else if strings.HasSuffix(path, "networks") {
+				rsp = string(mockNwListJSON)
+			} else if strings.HasSuffix(path, "networks/"+mockNwID) {
+				rsp = string(mockNwJSON)
+			} else if strings.Contains(path, fmt.Sprintf("endpoints?name=%s", mockServiceName)) {
+				rsp = string(mockServiceListJSON)
+			} else if strings.Contains(path, "endpoints?name=") {
+				rsp = "[]"
+			} else if strings.Contains(path, fmt.Sprintf("endpoints?partial-id=%s", mockServiceID)) {
+				rsp = string(mockServiceListJSON)
+			} else if strings.Contains(path, "endpoints?partial-id=") {
+				rsp = "[]"
+			} else if strings.HasSuffix(path, "endpoints") {
+				rsp = string(mockServiceListJSON)
+			} else if strings.HasSuffix(path, "endpoints/"+mockServiceID) {
+				rsp = string(mockServiceJSON)
+			}
+		case "POST":
+			if strings.HasSuffix(path, "networks") {
+				rsp = mockNwID
+			} else if strings.HasSuffix(path, "endpoints") {
+				rsp = mockServiceID
+			} else if strings.HasSuffix(path, "containers") {
+				rsp = mockContainerID
+			}
+		case "PUT":
+		case "DELETE":
+			rsp = ""
+		}
+		return nopCloser{bytes.NewBufferString(rsp)}, 200, nil
+	}
+}
+
 func TestClientDummyCommand(t *testing.T) {
 	var out, errOut bytes.Buffer
-	cFunc := func(method, path string, data interface{}, headers map[string][]string) (io.ReadCloser, int, error) {
-		return nopCloser{bytes.NewBufferString("")}, 200, nil
-	}
-	cli := NewNetworkCli(&out, &errOut, cFunc)
+	cli := NewNetworkCli(&out, &errOut, callbackFunc)
 
 	err := cli.Cmd("docker", "dummy")
 	if err == nil {
@@ -30,10 +102,7 @@ func TestClientDummyCommand(t *testing.T) {
 
 func TestClientNetworkInvalidCommand(t *testing.T) {
 	var out, errOut bytes.Buffer
-	cFunc := func(method, path string, data interface{}, headers map[string][]string) (io.ReadCloser, int, error) {
-		return nopCloser{bytes.NewBufferString("")}, 200, nil
-	}
-	cli := NewNetworkCli(&out, &errOut, cFunc)
+	cli := NewNetworkCli(&out, &errOut, callbackFunc)
 
 	err := cli.Cmd("docker", "network", "invalid")
 	if err == nil {
@@ -43,12 +112,9 @@ func TestClientNetworkInvalidCommand(t *testing.T) {
 
 func TestClientNetworkCreate(t *testing.T) {
 	var out, errOut bytes.Buffer
-	cFunc := func(method, path string, data interface{}, headers map[string][]string) (io.ReadCloser, int, error) {
-		return nopCloser{bytes.NewBufferString("")}, 200, nil
-	}
-	cli := NewNetworkCli(&out, &errOut, cFunc)
+	cli := NewNetworkCli(&out, &errOut, callbackFunc)
 
-	err := cli.Cmd("docker", "network", "create", "test")
+	err := cli.Cmd("docker", "network", "create", mockNwName)
 	if err != nil {
 		t.Fatal(err.Error())
 	}
@@ -56,17 +122,14 @@ func TestClientNetworkCreate(t *testing.T) {
 
 func TestClientNetworkCreateWithDriver(t *testing.T) {
 	var out, errOut bytes.Buffer
-	cFunc := func(method, path string, data interface{}, headers map[string][]string) (io.ReadCloser, int, error) {
-		return nopCloser{bytes.NewBufferString("")}, 200, nil
-	}
-	cli := NewNetworkCli(&out, &errOut, cFunc)
+	cli := NewNetworkCli(&out, &errOut, callbackFunc)
 
-	err := cli.Cmd("docker", "network", "create", "-f=dummy", "test")
+	err := cli.Cmd("docker", "network", "create", "-f=dummy", mockNwName)
 	if err == nil {
 		t.Fatalf("Passing incorrect flags to the create command must fail")
 	}
 
-	err = cli.Cmd("docker", "network", "create", "-d=dummy", "test")
+	err = cli.Cmd("docker", "network", "create", "-d=dummy", mockNwName)
 	if err != nil {
 		t.Fatalf(err.Error())
 	}
@@ -74,12 +137,9 @@ func TestClientNetworkCreateWithDriver(t *testing.T) {
 
 func TestClientNetworkRm(t *testing.T) {
 	var out, errOut bytes.Buffer
-	cFunc := func(method, path string, data interface{}, headers map[string][]string) (io.ReadCloser, int, error) {
-		return nopCloser{bytes.NewBufferString("")}, 200, nil
-	}
-	cli := NewNetworkCli(&out, &errOut, cFunc)
+	cli := NewNetworkCli(&out, &errOut, callbackFunc)
 
-	err := cli.Cmd("docker", "network", "rm", "test")
+	err := cli.Cmd("docker", "network", "rm", mockNwName)
 	if err != nil {
 		t.Fatal(err.Error())
 	}
@@ -87,47 +147,141 @@ func TestClientNetworkRm(t *testing.T) {
 
 func TestClientNetworkLs(t *testing.T) {
 	var out, errOut bytes.Buffer
-	networks := "db,web,test"
-	cFunc := func(method, path string, data interface{}, headers map[string][]string) (io.ReadCloser, int, error) {
-		return nopCloser{bytes.NewBufferString(networks)}, 200, nil
-	}
-	cli := NewNetworkCli(&out, &errOut, cFunc)
+	cli := NewNetworkCli(&out, &errOut, callbackFunc)
 
 	err := cli.Cmd("docker", "network", "ls")
 	if err != nil {
 		t.Fatal(err.Error())
 	}
-	if out.String() != networks {
-		t.Fatal("Network List command fail to return the intended list")
+	if out.String() != string(mockNwListJSON) {
+		t.Fatal("Network List command fail to return the expected list")
 	}
 }
 
 func TestClientNetworkInfo(t *testing.T) {
 	var out, errOut bytes.Buffer
-	info := "dummy info"
-	cFunc := func(method, path string, data interface{}, headers map[string][]string) (io.ReadCloser, int, error) {
-		return nopCloser{bytes.NewBufferString(info)}, 200, nil
+	cli := NewNetworkCli(&out, &errOut, callbackFunc)
+
+	err := cli.Cmd("docker", "network", "info", mockNwName)
+	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) {
+	var out, errOut bytes.Buffer
+	cli := NewNetworkCli(&out, &errOut, callbackFunc)
+
+	err := cli.Cmd("docker", "network", "info", mockNwID)
+	if err != nil {
+		t.Fatal(err.Error())
+	}
+	if out.String() != string(mockNwJSON) {
+		t.Fatal("Network info command fail to return the expected object")
 	}
-	cli := NewNetworkCli(&out, &errOut, cFunc)
+}
+
+func TestClientNetworkServiceInvalidCommand(t *testing.T) {
+	var out, errOut bytes.Buffer
+	cli := NewNetworkCli(&out, &errOut, callbackFunc)
 
-	err := cli.Cmd("docker", "network", "info", "test")
+	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())
 	}
-	if out.String() != info {
-		t.Fatal("Network List command fail to return the intended list")
+}
+
+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 TestClientNetworkCreateHelp(t *testing.T) {
+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, cFunc)
+	cli := NewNetworkCli(&out, &errOut, callbackFunc)
 
 	err := cli.Cmd("docker", "network", "create", "--help")
 	if err != nil {
@@ -139,12 +293,12 @@ func TestClientNetworkCreateHelp(t *testing.T) {
 // 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 TestClientNetworkCreateMissingArgument(t *testing.T) {
+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, cFunc)
+	cli := NewNetworkCli(&out, &errOut, callbackFunc)
 
 	err := cli.Cmd("docker", "network", "create")
 	if err != nil {

+ 330 - 9
libnetwork/client/network.go

@@ -2,8 +2,11 @@ package client
 
 import (
 	"bytes"
+	"encoding/json"
 	"fmt"
 	"io"
+	"net/http"
+	"strings"
 
 	flag "github.com/docker/docker/pkg/mflag"
 )
@@ -23,6 +26,12 @@ 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"},
 	}
 )
 
@@ -33,7 +42,7 @@ func (cli *NetworkCli) CmdNetwork(chain string, args ...string) error {
 	err := cmd.ParseFlags(args, true)
 	if err == nil {
 		cmd.Usage()
-		return fmt.Errorf("Invalid command : %v", args)
+		return fmt.Errorf("invalid command : %v", args)
 	}
 	return err
 }
@@ -55,7 +64,6 @@ func (cli *NetworkCli) CmdNetworkCreate(chain string, args ...string) error {
 
 	obj, _, err := readBody(cli.call("POST", "/networks", 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 {
@@ -66,15 +74,18 @@ func (cli *NetworkCli) CmdNetworkCreate(chain string, args ...string) error {
 
 // CmdNetworkRm handles Network Delete UI
 func (cli *NetworkCli) CmdNetworkRm(chain string, args ...string) error {
-	cmd := cli.Subcmd(chain, "rm", "NETWORK-NAME", "Deletes a network", false)
+	cmd := cli.Subcmd(chain, "rm", "NETWORK", "Deletes a network", false)
 	cmd.Require(flag.Min, 1)
 	err := cmd.ParseFlags(args, true)
 	if err != nil {
 		return err
 	}
-	obj, _, err := readBody(cli.call("DELETE", "/networks/name/"+cmd.Arg(0), nil, nil))
+	id, err := lookupNetworkID(cli, cmd.Arg(0))
+	if err != nil {
+		return err
+	}
+	obj, _, err := readBody(cli.call("DELETE", "/networks/"+id, 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 {
@@ -92,7 +103,6 @@ func (cli *NetworkCli) CmdNetworkLs(chain string, args ...string) error {
 	}
 	obj, _, err := readBody(cli.call("GET", "/networks", 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 {
@@ -103,13 +113,310 @@ func (cli *NetworkCli) CmdNetworkLs(chain string, args ...string) error {
 
 // CmdNetworkInfo handles Network Info UI
 func (cli *NetworkCli) CmdNetworkInfo(chain string, args ...string) error {
-	cmd := cli.Subcmd(chain, "info", "NETWORK-NAME", "Displays detailed information on a network", false)
+	cmd := cli.Subcmd(chain, "info", "NETWORK", "Displays detailed information on a network", false)
+	cmd.Require(flag.Min, 1)
+	err := cmd.ParseFlags(args, true)
+	if err != nil {
+		return err
+	}
+
+	id, err := lookupNetworkID(cli, cmd.Arg(0))
+	if err != nil {
+		return err
+	}
+
+	obj, _, err := readBody(cli.call("GET", "/networks/"+id, nil, nil))
+	if err != nil {
+		return err
+	}
+	if _, err := io.Copy(cli.out, bytes.NewReader(obj)); err != nil {
+		return err
+	}
+	return nil
+}
+
+// Helper function to predict if a string is a name or id or partial-id
+// This provides a best-effort mechanism to identify a id with the help of GET Filter APIs
+// Being a UI, its most likely that name will be used by the user, which is used to lookup
+// the corresponding ID. If ID is not found, this function will assume that the passed string
+// is an ID by itself.
+
+func lookupNetworkID(cli *NetworkCli, nameID string) (string, error) {
+	obj, statusCode, err := readBody(cli.call("GET", "/networks?name="+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", "/networks?partial-id="+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 Networks matching the partial identifier (%s). Please use full identifier", nameID)
+	}
+	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
 	}
-	obj, _, err := readBody(cli.call("GET", "/networks/name/"+cmd.Arg(0), nil, nil))
+
+	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
@@ -124,9 +431,23 @@ func networkUsage(chain string) string {
 	help := "Commands:\n"
 
 	for _, cmd := range networkCommands {
-		help += fmt.Sprintf("    %-10.10s%s\n", cmd.name, cmd.description)
+		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)
+		}
+	}
+
+	help += fmt.Sprintf("\nRun '%s service COMMAND --help' for more information on a command.", chain)
+	return help
+}

+ 36 - 2
libnetwork/client/types.go

@@ -1,6 +1,6 @@
 package client
 
-import "github.com/docker/libnetwork/sandbox"
+import "github.com/docker/libnetwork/types"
 
 /***********
  Resources
@@ -19,7 +19,6 @@ type endpointResource struct {
 	Name    string
 	ID      string
 	Network string
-	Info    sandbox.Info
 }
 
 /***********
@@ -32,3 +31,38 @@ type networkCreate struct {
 	NetworkType string
 	Options     map[string]interface{}
 }
+
+// endpointCreate represents the body of the "create endpoint" http request message
+type endpointCreate struct {
+	Name         string
+	NetworkID    string
+	ExposedPorts []types.TransportPort
+	PortMapping  []types.PortBinding
+}
+
+// endpointJoin represents the expected body of the "join endpoint" or "leave endpoint" http request messages
+type endpointJoin struct {
+	ContainerID       string
+	HostName          string
+	DomainName        string
+	HostsPath         string
+	ResolvConfPath    string
+	DNS               []string
+	ExtraHosts        []endpointExtraHost
+	ParentUpdates     []endpointParentUpdate
+	UseDefaultSandbox bool
+}
+
+// EndpointExtraHost represents the extra host object
+type endpointExtraHost struct {
+	Name    string
+	Address string
+}
+
+// EndpointParentUpdate is the object carrying the information about the
+// endpoint parent that needs to be updated
+type endpointParentUpdate struct {
+	EndpointID string
+	Name       string
+	Address    string
+}

+ 2 - 2
libnetwork/cmd/dnet/dnet.go

@@ -158,7 +158,7 @@ func (d *dnetConnection) httpCall(method, path string, data interface{}, headers
 		statusCode = resp.StatusCode
 	}
 	if err != nil {
-		return nil, statusCode, fmt.Errorf("An error occurred trying to connect: %v", err)
+		return nil, statusCode, fmt.Errorf("error when trying to connect: %v", err)
 	}
 
 	if statusCode < 200 || statusCode >= 400 {
@@ -166,7 +166,7 @@ func (d *dnetConnection) httpCall(method, path string, data interface{}, headers
 		if err != nil {
 			return nil, statusCode, err
 		}
-		return nil, statusCode, fmt.Errorf("Error response from daemon: %s", bytes.TrimSpace(body))
+		return nil, statusCode, fmt.Errorf("error : %s", bytes.TrimSpace(body))
 	}
 
 	return resp.Body, statusCode, nil