package client import ( "bytes" "encoding/json" "fmt" "net/http" "strings" "text/tabwriter" flag "github.com/docker/docker/pkg/mflag" "github.com/docker/docker/pkg/stringid" ) var ( serviceCommands = []command{ {"publish", "Publish a service"}, {"unpublish", "Remove a service"}, {"attach", "Attach a backend (container) to the service"}, {"detach", "Detach the backend from the service"}, {"ls", "Lists all services"}, {"info", "Display information about a service"}, } ) func lookupServiceID(cli *NetworkCli, nwName, svNameID string) (string, error) { // Sanity Check obj, _, err := readBody(cli.call("GET", fmt.Sprintf("/networks?name=%s", nwName), nil, nil)) if err != nil { return "", err } var nwList []networkResource if err = json.Unmarshal(obj, &nwList); err != nil { return "", err } if len(nwList) == 0 { return "", fmt.Errorf("Network %s does not exist", nwName) } if nwName == "" { obj, _, err := readBody(cli.call("GET", "/networks/"+nwList[0].ID, nil, nil)) if err != nil { return "", err } networkResource := &networkResource{} if err := json.NewDecoder(bytes.NewReader(obj)).Decode(networkResource); err != nil { return "", err } nwName = networkResource.Name } // Query service by name obj, statusCode, err := readBody(cli.call("GET", fmt.Sprintf("/services?name=%s", svNameID), nil, nil)) if err != nil { return "", err } if statusCode != http.StatusOK { return "", fmt.Errorf("name query failed for %s due to: (%d) %s", svNameID, statusCode, string(obj)) } var list []*serviceResource if err = json.Unmarshal(obj, &list); err != nil { return "", err } for _, sr := range list { if sr.Network == nwName { return sr.ID, nil } } // Query service by Partial-id (this covers full id as well) obj, statusCode, err = readBody(cli.call("GET", fmt.Sprintf("/services?partial-id=%s", svNameID), nil, nil)) if err != nil { return "", err } if statusCode != http.StatusOK { return "", fmt.Errorf("partial-id match query failed for %s due to: (%d) %s", svNameID, statusCode, string(obj)) } if err = json.Unmarshal(obj, &list); err != nil { return "", err } for _, sr := range list { if sr.Network == nwName { return sr.ID, nil } } return "", fmt.Errorf("Service %s not found on network %s", svNameID, nwName) } func lookupContainerID(cli *NetworkCli, cnNameID string) (string, error) { // Container is a Docker resource, ask docker about it. // In case of connecton error, we assume we are running in dnet and return whatever was passed to us obj, _, err := readBody(cli.call("GET", fmt.Sprintf("/containers/%s/json", cnNameID), nil, nil)) if err != nil { // We are probably running outside of docker return cnNameID, nil } var x map[string]interface{} err = json.Unmarshal(obj, &x) if err != nil { return "", err } if iid, ok := x["Id"]; ok { if id, ok := iid.(string); ok { return id, nil } return "", fmt.Errorf("Unexpected data type for container ID in json response") } return "", fmt.Errorf("Cannot find container ID in json response") } func lookupSandboxID(cli *NetworkCli, containerID string) (string, error) { obj, _, err := readBody(cli.call("GET", fmt.Sprintf("/sandboxes?partial-container-id=%s", containerID), nil, nil)) if err != nil { return "", err } var sandboxList []sandboxResource err = json.Unmarshal(obj, &sandboxList) if err != nil { return "", err } if len(sandboxList) == 0 { return "", fmt.Errorf("cannot find sandbox for container: %s", containerID) } return sandboxList[0].ID, nil } // CmdService handles the service UI func (cli *NetworkCli) CmdService(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 } // Parse service name for "SERVICE[.NETWORK]" format func parseServiceName(name string) (string, string) { s := strings.Split(name, ".") var sName, nName string if len(s) > 1 { nName = s[len(s)-1] sName = strings.Join(s[:len(s)-1], ".") } else { sName = s[0] } return sName, nName } // CmdServicePublish handles service create UI func (cli *NetworkCli) CmdServicePublish(chain string, args ...string) error { cmd := cli.Subcmd(chain, "publish", "SERVICE[.NETWORK]", "Publish a new service on a network", false) cmd.Require(flag.Exact, 1) err := cmd.ParseFlags(args, true) if err != nil { return err } sn, nn := parseServiceName(cmd.Arg(0)) sc := serviceCreate{Name: sn, Network: nn} obj, _, err := readBody(cli.call("POST", "/services", sc, 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 } // CmdServiceUnpublish handles service delete UI func (cli *NetworkCli) CmdServiceUnpublish(chain string, args ...string) error { cmd := cli.Subcmd(chain, "unpublish", "SERVICE[.NETWORK]", "Removes a service", false) cmd.Require(flag.Exact, 1) err := cmd.ParseFlags(args, true) if err != nil { return err } sn, nn := parseServiceName(cmd.Arg(0)) serviceID, err := lookupServiceID(cli, nn, sn) if err != nil { return err } _, _, err = readBody(cli.call("DELETE", "/services/"+serviceID, nil, nil)) return err } // CmdServiceLs handles service list UI func (cli *NetworkCli) CmdServiceLs(chain string, args ...string) error { cmd := cli.Subcmd(chain, "ls", "SERVICE", "Lists all the services on a network", false) flNetwork := cmd.String([]string{"net", "-network"}, "", "Only show the services that are published on the specified network") quiet := cmd.Bool([]string{"q", "-quiet"}, false, "Only display numeric IDs") noTrunc := cmd.Bool([]string{"#notrunc", "-no-trunc"}, false, "Do not truncate the output") err := cmd.ParseFlags(args, true) if err != nil { return err } var obj []byte if *flNetwork == "" { obj, _, err = readBody(cli.call("GET", "/services", nil, nil)) } else { obj, _, err = readBody(cli.call("GET", "/services?network="+*flNetwork, nil, nil)) } if err != nil { return err } var serviceResources []serviceResource err = json.Unmarshal(obj, &serviceResources) if err != nil { fmt.Println(err) return err } wr := tabwriter.NewWriter(cli.out, 20, 1, 3, ' ', 0) // unless quiet (-q) is specified, print field titles if !*quiet { fmt.Fprintln(wr, "SERVICE ID\tNAME\tNETWORK\tCONTAINER") } for _, sr := range serviceResources { ID := sr.ID bkID, err := getBackendID(cli, ID) if err != nil { return err } if !*noTrunc { ID = stringid.TruncateID(ID) bkID = stringid.TruncateID(bkID) } if !*quiet { fmt.Fprintf(wr, "%s\t%s\t%s\t%s\n", ID, sr.Name, sr.Network, bkID) } else { fmt.Fprintln(wr, ID) } } wr.Flush() return nil } func getBackendID(cli *NetworkCli, servID string) (string, error) { var ( obj []byte err error bk string ) if obj, _, err = readBody(cli.call("GET", "/services/"+servID+"/backend", nil, nil)); err == nil { var sr sandboxResource if err := json.NewDecoder(bytes.NewReader(obj)).Decode(&sr); err == nil { bk = sr.ContainerID } else { // Only print a message, don't make the caller cli fail for this fmt.Fprintf(cli.out, "Failed to retrieve backend list for service %s (%v)\n", servID, err) } } return bk, err } // CmdServiceInfo handles service info UI func (cli *NetworkCli) CmdServiceInfo(chain string, args ...string) error { cmd := cli.Subcmd(chain, "info", "SERVICE[.NETWORK]", "Displays detailed information about a service", false) cmd.Require(flag.Min, 1) err := cmd.ParseFlags(args, true) if err != nil { return err } sn, nn := parseServiceName(cmd.Arg(0)) serviceID, err := lookupServiceID(cli, nn, sn) if err != nil { return err } obj, _, err := readBody(cli.call("GET", "/services/"+serviceID, nil, nil)) if err != nil { return err } sr := &serviceResource{} if err := json.NewDecoder(bytes.NewReader(obj)).Decode(sr); err != nil { return err } fmt.Fprintf(cli.out, "Service Id: %s\n", sr.ID) fmt.Fprintf(cli.out, "\tName: %s\n", sr.Name) fmt.Fprintf(cli.out, "\tNetwork: %s\n", sr.Network) return nil } // CmdServiceAttach handles service attach UI func (cli *NetworkCli) CmdServiceAttach(chain string, args ...string) error { cmd := cli.Subcmd(chain, "attach", "CONTAINER SERVICE[.NETWORK]", "Sets a container as a service backend", false) cmd.Require(flag.Min, 2) err := cmd.ParseFlags(args, true) if err != nil { return err } containerID, err := lookupContainerID(cli, cmd.Arg(0)) if err != nil { return err } sandboxID, err := lookupSandboxID(cli, containerID) if err != nil { return err } sn, nn := parseServiceName(cmd.Arg(1)) serviceID, err := lookupServiceID(cli, nn, sn) if err != nil { return err } nc := serviceAttach{SandboxID: sandboxID} _, _, err = readBody(cli.call("POST", "/services/"+serviceID+"/backend", nc, nil)) return err } // CmdServiceDetach handles service detach UI func (cli *NetworkCli) CmdServiceDetach(chain string, args ...string) error { cmd := cli.Subcmd(chain, "detach", "CONTAINER SERVICE", "Removes a container from service backend", false) cmd.Require(flag.Min, 2) err := cmd.ParseFlags(args, true) if err != nil { return err } sn, nn := parseServiceName(cmd.Arg(1)) containerID, err := lookupContainerID(cli, cmd.Arg(0)) if err != nil { return err } sandboxID, err := lookupSandboxID(cli, containerID) if err != nil { return err } serviceID, err := lookupServiceID(cli, nn, sn) if err != nil { return err } _, _, err = readBody(cli.call("DELETE", "/services/"+serviceID+"/backend/"+sandboxID, nil, nil)) if 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.name, cmd.description) } help += fmt.Sprintf("\nRun '%s service COMMAND --help' for more information on a command.", chain) return help }