diff --git a/client/client.go b/client/client.go index 64c6f7d..4bc86da 100644 --- a/client/client.go +++ b/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 { diff --git a/client/client_test.go b/client/client_test.go index 582c9b9..6a8c675 100644 --- a/client/client_test.go +++ b/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 diff --git a/client/network.go b/client/network.go index 9725827..2b57bed 100644 --- a/client/network.go +++ b/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 +} diff --git a/client/types.go b/client/types.go index 27d17b9..972ed43 100644 --- a/client/types.go +++ b/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 +}