소스 검색

Service endpoint UI support

Signed-off-by: Madhu Venugopal <madhu@docker.com>
Madhu Venugopal 10 년 전
부모
커밋
0653afc5f5
4개의 변경된 파일436개의 추가작업 그리고 8개의 파일을 삭제
  1. 6 0
      libnetwork/client/client.go
  2. 129 5
      libnetwork/client/client_test.go
  3. 265 1
      libnetwork/client/network.go
  4. 36 2
      libnetwork/client/types.go

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

+ 129 - 5
libnetwork/client/client_test.go

@@ -3,6 +3,7 @@ package client
 import (
 	"bytes"
 	"encoding/json"
+	"fmt"
 	"io"
 	"os"
 	"strings"
@@ -24,9 +25,12 @@ func TestMain(m *testing.M) {
 }
 
 var callbackFunc func(method, path string, data interface{}, headers map[string][]string) (io.ReadCloser, int, error)
-var mockNwJSON, mockNwListJSON []byte
+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
@@ -34,19 +38,50 @@ func setupMockHTTPCallback() {
 	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, "networks?name=") {
+			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":
-			rsp = mockNwID
+			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 = ""
@@ -149,10 +184,99 @@ 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
 /*
-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
@@ -169,7 +293,7 @@ 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

+ 265 - 1
libnetwork/client/network.go

@@ -6,6 +6,7 @@ import (
 	"fmt"
 	"io"
 	"net/http"
+	"strings"
 
 	flag "github.com/docker/docker/pkg/mflag"
 )
@@ -25,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"},
 	}
 )
 
@@ -177,13 +184,270 @@ 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"
 
 	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
+}