2015-05-20 08:20:19 -04:00
|
|
|
package client
|
|
|
|
|
|
|
|
import (
|
2015-09-27 07:09:27 -04:00
|
|
|
"fmt"
|
2015-10-09 14:21:48 -04:00
|
|
|
"net"
|
2016-02-16 22:11:37 -05:00
|
|
|
"sort"
|
2015-10-09 14:21:48 -04:00
|
|
|
"strings"
|
2015-09-27 07:09:27 -04:00
|
|
|
"text/tabwriter"
|
|
|
|
|
2016-03-16 15:19:22 -04:00
|
|
|
"golang.org/x/net/context"
|
|
|
|
|
2015-09-27 07:09:27 -04:00
|
|
|
Cli "github.com/docker/docker/cli"
|
2015-10-09 14:21:48 -04:00
|
|
|
"github.com/docker/docker/opts"
|
2015-09-27 07:09:27 -04:00
|
|
|
flag "github.com/docker/docker/pkg/mflag"
|
|
|
|
"github.com/docker/docker/pkg/stringid"
|
2016-01-05 14:20:47 -05:00
|
|
|
runconfigopts "github.com/docker/docker/runconfig/opts"
|
2016-01-04 19:05:26 -05:00
|
|
|
"github.com/docker/engine-api/types"
|
|
|
|
"github.com/docker/engine-api/types/filters"
|
|
|
|
"github.com/docker/engine-api/types/network"
|
2015-05-20 08:20:19 -04:00
|
|
|
)
|
|
|
|
|
2015-09-27 07:09:27 -04:00
|
|
|
// CmdNetwork is the parent subcommand for all network commands
|
|
|
|
//
|
|
|
|
// Usage: docker network <COMMAND> [OPTIONS]
|
2015-05-20 08:20:19 -04:00
|
|
|
func (cli *DockerCli) CmdNetwork(args ...string) error {
|
2015-09-27 07:09:27 -04:00
|
|
|
cmd := Cli.Subcmd("network", []string{"COMMAND [OPTIONS]"}, networkUsage(), false)
|
|
|
|
cmd.Require(flag.Min, 1)
|
|
|
|
err := cmd.ParseFlags(args, true)
|
|
|
|
cmd.Usage()
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// CmdNetworkCreate creates a new network with a given name
|
|
|
|
//
|
|
|
|
// Usage: docker network create [OPTIONS] <NETWORK-NAME>
|
|
|
|
func (cli *DockerCli) CmdNetworkCreate(args ...string) error {
|
|
|
|
cmd := Cli.Subcmd("network create", []string{"NETWORK-NAME"}, "Creates a new network with a name specified by the user", false)
|
2015-10-09 14:21:48 -04:00
|
|
|
flDriver := cmd.String([]string{"d", "-driver"}, "bridge", "Driver to manage the Network")
|
2015-10-14 21:49:27 -04:00
|
|
|
flOpts := opts.NewMapOpts(nil, nil)
|
2015-10-09 14:21:48 -04:00
|
|
|
|
|
|
|
flIpamDriver := cmd.String([]string{"-ipam-driver"}, "default", "IP Address Management Driver")
|
|
|
|
flIpamSubnet := opts.NewListOpts(nil)
|
|
|
|
flIpamIPRange := opts.NewListOpts(nil)
|
|
|
|
flIpamGateway := opts.NewListOpts(nil)
|
|
|
|
flIpamAux := opts.NewMapOpts(nil, nil)
|
2015-10-23 15:28:39 -04:00
|
|
|
flIpamOpt := opts.NewMapOpts(nil, nil)
|
2015-10-09 14:21:48 -04:00
|
|
|
|
2015-10-14 21:49:27 -04:00
|
|
|
cmd.Var(&flIpamSubnet, []string{"-subnet"}, "subnet in CIDR format that represents a network segment")
|
2015-10-09 14:21:48 -04:00
|
|
|
cmd.Var(&flIpamIPRange, []string{"-ip-range"}, "allocate container ip from a sub-range")
|
|
|
|
cmd.Var(&flIpamGateway, []string{"-gateway"}, "ipv4 or ipv6 Gateway for the master subnet")
|
2015-10-14 21:49:27 -04:00
|
|
|
cmd.Var(flIpamAux, []string{"-aux-address"}, "auxiliary ipv4 or ipv6 addresses used by Network driver")
|
|
|
|
cmd.Var(flOpts, []string{"o", "-opt"}, "set driver specific options")
|
2015-10-23 15:28:39 -04:00
|
|
|
cmd.Var(flIpamOpt, []string{"-ipam-opt"}, "set IPAM driver specific options")
|
2015-10-09 14:21:48 -04:00
|
|
|
|
2015-12-27 21:15:50 -05:00
|
|
|
flInternal := cmd.Bool([]string{"-internal"}, false, "restricts external access to the network")
|
2016-02-11 20:42:15 -05:00
|
|
|
flIPv6 := cmd.Bool([]string{"-ipv6"}, false, "enable IPv6 networking")
|
2015-12-27 21:15:50 -05:00
|
|
|
|
2015-09-27 07:09:27 -04:00
|
|
|
cmd.Require(flag.Exact, 1)
|
|
|
|
err := cmd.ParseFlags(args, true)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2015-10-29 16:48:25 -04:00
|
|
|
// Set the default driver to "" if the user didn't set the value.
|
|
|
|
// That way we can know whether it was user input or not.
|
|
|
|
driver := *flDriver
|
|
|
|
if !cmd.IsSet("-driver") && !cmd.IsSet("d") {
|
|
|
|
driver = ""
|
|
|
|
}
|
|
|
|
|
2015-10-09 14:21:48 -04:00
|
|
|
ipamCfg, err := consolidateIpam(flIpamSubnet.GetAll(), flIpamIPRange.GetAll(), flIpamGateway.GetAll(), flIpamAux.GetAll())
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2015-09-27 07:09:27 -04:00
|
|
|
// Construct network create request body
|
2015-10-09 14:21:48 -04:00
|
|
|
nc := types.NetworkCreate{
|
|
|
|
Name: cmd.Arg(0),
|
2015-10-29 16:48:25 -04:00
|
|
|
Driver: driver,
|
2015-10-23 15:28:39 -04:00
|
|
|
IPAM: network.IPAM{Driver: *flIpamDriver, Config: ipamCfg, Options: flIpamOpt.GetAll()},
|
2015-10-14 21:49:27 -04:00
|
|
|
Options: flOpts.GetAll(),
|
2015-10-09 14:21:48 -04:00
|
|
|
CheckDuplicate: true,
|
2015-12-27 21:15:50 -05:00
|
|
|
Internal: *flInternal,
|
2015-12-10 09:02:50 -05:00
|
|
|
EnableIPv6: *flIPv6,
|
2015-10-09 14:21:48 -04:00
|
|
|
}
|
2015-12-04 11:33:00 -05:00
|
|
|
|
2016-03-16 15:19:22 -04:00
|
|
|
resp, err := cli.client.NetworkCreate(context.Background(), nc)
|
2015-09-27 07:09:27 -04:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
fmt.Fprintf(cli.out, "%s\n", resp.ID)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2015-10-29 12:10:20 -04:00
|
|
|
// CmdNetworkRm deletes one or more networks
|
2015-09-27 07:09:27 -04:00
|
|
|
//
|
2015-10-29 12:10:20 -04:00
|
|
|
// Usage: docker network rm NETWORK-NAME|NETWORK-ID [NETWORK-NAME|NETWORK-ID...]
|
2015-09-27 07:09:27 -04:00
|
|
|
func (cli *DockerCli) CmdNetworkRm(args ...string) error {
|
2015-10-29 12:10:20 -04:00
|
|
|
cmd := Cli.Subcmd("network rm", []string{"NETWORK [NETWORK...]"}, "Deletes one or more networks", false)
|
|
|
|
cmd.Require(flag.Min, 1)
|
2015-12-04 11:33:00 -05:00
|
|
|
if err := cmd.ParseFlags(args, true); err != nil {
|
2015-09-27 07:09:27 -04:00
|
|
|
return err
|
|
|
|
}
|
2015-10-29 12:10:20 -04:00
|
|
|
|
|
|
|
status := 0
|
|
|
|
for _, net := range cmd.Args() {
|
2016-03-16 15:19:22 -04:00
|
|
|
if err := cli.client.NetworkRemove(context.Background(), net); err != nil {
|
2015-10-29 12:10:20 -04:00
|
|
|
fmt.Fprintf(cli.err, "%s\n", err)
|
|
|
|
status = 1
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if status != 0 {
|
|
|
|
return Cli.StatusError{StatusCode: status}
|
2015-09-27 07:09:27 -04:00
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// CmdNetworkConnect connects a container to a network
|
|
|
|
//
|
2016-01-07 19:18:34 -05:00
|
|
|
// Usage: docker network connect [OPTIONS] <NETWORK> <CONTAINER>
|
2015-09-27 07:09:27 -04:00
|
|
|
func (cli *DockerCli) CmdNetworkConnect(args ...string) error {
|
|
|
|
cmd := Cli.Subcmd("network connect", []string{"NETWORK CONTAINER"}, "Connects a container to a network", false)
|
2016-01-07 19:18:34 -05:00
|
|
|
flIPAddress := cmd.String([]string{"-ip"}, "", "IP Address")
|
|
|
|
flIPv6Address := cmd.String([]string{"-ip6"}, "", "IPv6 Address")
|
2016-01-05 14:20:47 -05:00
|
|
|
flLinks := opts.NewListOpts(runconfigopts.ValidateLink)
|
|
|
|
cmd.Var(&flLinks, []string{"-link"}, "Add link to another container")
|
2016-01-08 08:45:56 -05:00
|
|
|
flAliases := opts.NewListOpts(nil)
|
|
|
|
cmd.Var(&flAliases, []string{"-alias"}, "Add network-scoped alias for the container")
|
2016-01-07 19:18:34 -05:00
|
|
|
cmd.Require(flag.Min, 2)
|
2015-12-04 11:33:00 -05:00
|
|
|
if err := cmd.ParseFlags(args, true); err != nil {
|
2015-09-27 07:09:27 -04:00
|
|
|
return err
|
|
|
|
}
|
2016-01-07 19:18:34 -05:00
|
|
|
epConfig := &network.EndpointSettings{
|
|
|
|
IPAMConfig: &network.EndpointIPAMConfig{
|
|
|
|
IPv4Address: *flIPAddress,
|
|
|
|
IPv6Address: *flIPv6Address,
|
|
|
|
},
|
2016-01-08 08:45:56 -05:00
|
|
|
Links: flLinks.GetAll(),
|
|
|
|
Aliases: flAliases.GetAll(),
|
2016-01-07 19:18:34 -05:00
|
|
|
}
|
2016-03-16 15:19:22 -04:00
|
|
|
return cli.client.NetworkConnect(context.Background(), cmd.Arg(0), cmd.Arg(1), epConfig)
|
2015-09-27 07:09:27 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// CmdNetworkDisconnect disconnects a container from a network
|
|
|
|
//
|
|
|
|
// Usage: docker network disconnect <NETWORK> <CONTAINER>
|
|
|
|
func (cli *DockerCli) CmdNetworkDisconnect(args ...string) error {
|
|
|
|
cmd := Cli.Subcmd("network disconnect", []string{"NETWORK CONTAINER"}, "Disconnects container from a network", false)
|
2016-01-12 23:56:36 -05:00
|
|
|
force := cmd.Bool([]string{"f", "-force"}, false, "Force the container to disconnect from a network")
|
2015-09-27 07:09:27 -04:00
|
|
|
cmd.Require(flag.Exact, 2)
|
2015-12-04 11:33:00 -05:00
|
|
|
if err := cmd.ParseFlags(args, true); err != nil {
|
2015-09-27 07:09:27 -04:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2016-03-16 15:19:22 -04:00
|
|
|
return cli.client.NetworkDisconnect(context.Background(), cmd.Arg(0), cmd.Arg(1), *force)
|
2015-09-27 07:09:27 -04:00
|
|
|
}
|
|
|
|
|
2015-12-13 11:00:39 -05:00
|
|
|
// CmdNetworkLs lists all the networks managed by docker daemon
|
2015-09-27 07:09:27 -04:00
|
|
|
//
|
|
|
|
// Usage: docker network ls [OPTIONS]
|
|
|
|
func (cli *DockerCli) CmdNetworkLs(args ...string) error {
|
2015-10-09 14:21:48 -04:00
|
|
|
cmd := Cli.Subcmd("network ls", nil, "Lists networks", true)
|
2015-09-27 07:09:27 -04:00
|
|
|
quiet := cmd.Bool([]string{"q", "-quiet"}, false, "Only display numeric IDs")
|
2015-10-09 14:21:48 -04:00
|
|
|
noTrunc := cmd.Bool([]string{"-no-trunc"}, false, "Do not truncate the output")
|
|
|
|
|
2015-11-10 03:57:06 -05:00
|
|
|
flFilter := opts.NewListOpts(nil)
|
|
|
|
cmd.Var(&flFilter, []string{"f", "-filter"}, "Filter output based on conditions provided")
|
|
|
|
|
2015-10-09 14:21:48 -04:00
|
|
|
cmd.Require(flag.Exact, 0)
|
2015-11-10 03:57:06 -05:00
|
|
|
err := cmd.ParseFlags(args, true)
|
|
|
|
if err != nil {
|
2015-09-27 07:09:27 -04:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2015-11-10 03:57:06 -05:00
|
|
|
// Consolidate all filter flags, and sanity check them early.
|
|
|
|
// They'll get process after get response from server.
|
|
|
|
netFilterArgs := filters.NewArgs()
|
|
|
|
for _, f := range flFilter.GetAll() {
|
|
|
|
if netFilterArgs, err = filters.ParseFlag(f, netFilterArgs); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
options := types.NetworkListOptions{
|
|
|
|
Filters: netFilterArgs,
|
|
|
|
}
|
|
|
|
|
2016-03-16 15:19:22 -04:00
|
|
|
networkResources, err := cli.client.NetworkList(context.Background(), options)
|
2015-09-27 07:09:27 -04:00
|
|
|
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\tDRIVER")
|
|
|
|
}
|
2016-02-16 22:11:37 -05:00
|
|
|
sort.Sort(byNetworkName(networkResources))
|
2015-09-27 07:09:27 -04:00
|
|
|
for _, networkResource := range networkResources {
|
|
|
|
ID := networkResource.ID
|
|
|
|
netName := networkResource.Name
|
|
|
|
if !*noTrunc {
|
|
|
|
ID = stringid.TruncateID(ID)
|
|
|
|
}
|
|
|
|
if *quiet {
|
|
|
|
fmt.Fprintln(wr, ID)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
driver := networkResource.Driver
|
|
|
|
fmt.Fprintf(wr, "%s\t%s\t%s\t",
|
|
|
|
ID,
|
|
|
|
netName,
|
|
|
|
driver)
|
|
|
|
fmt.Fprint(wr, "\n")
|
|
|
|
}
|
|
|
|
wr.Flush()
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2016-02-16 22:11:37 -05:00
|
|
|
type byNetworkName []types.NetworkResource
|
|
|
|
|
|
|
|
func (r byNetworkName) Len() int { return len(r) }
|
|
|
|
func (r byNetworkName) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
|
|
|
|
func (r byNetworkName) Less(i, j int) bool { return r[i].Name < r[j].Name }
|
|
|
|
|
2015-09-27 07:09:27 -04:00
|
|
|
// CmdNetworkInspect inspects the network object for more details
|
|
|
|
//
|
2015-10-23 16:48:22 -04:00
|
|
|
// Usage: docker network inspect [OPTIONS] <NETWORK> [NETWORK...]
|
2015-09-27 07:09:27 -04:00
|
|
|
func (cli *DockerCli) CmdNetworkInspect(args ...string) error {
|
2015-10-29 12:10:20 -04:00
|
|
|
cmd := Cli.Subcmd("network inspect", []string{"NETWORK [NETWORK...]"}, "Displays detailed information on one or more networks", false)
|
2015-12-02 16:32:10 -05:00
|
|
|
tmplStr := cmd.String([]string{"f", "-format"}, "", "Format the output using the given go template")
|
2015-10-21 02:57:29 -04:00
|
|
|
cmd.Require(flag.Min, 1)
|
2015-12-02 16:32:10 -05:00
|
|
|
|
|
|
|
if err := cmd.ParseFlags(args, true); err != nil {
|
2015-09-27 07:09:27 -04:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2015-12-07 22:04:38 -05:00
|
|
|
inspectSearcher := func(name string) (interface{}, []byte, error) {
|
2016-03-16 15:19:22 -04:00
|
|
|
i, err := cli.client.NetworkInspect(context.Background(), name)
|
2015-12-07 22:04:38 -05:00
|
|
|
return i, nil, err
|
2015-12-02 16:32:10 -05:00
|
|
|
}
|
|
|
|
|
2015-12-07 22:04:38 -05:00
|
|
|
return cli.inspectElements(*tmplStr, cmd.Args(), inspectSearcher)
|
2015-09-27 07:09:27 -04:00
|
|
|
}
|
|
|
|
|
2015-11-04 07:52:29 -05:00
|
|
|
// Consolidates the ipam configuration as a group from different related configurations
|
2015-10-09 14:21:48 -04:00
|
|
|
// user can configure network with multiple non-overlapping subnets and hence it is
|
2015-12-13 11:00:39 -05:00
|
|
|
// possible to correlate the various related parameters and consolidate them.
|
|
|
|
// consoidateIpam consolidates subnets, ip-ranges, gateways and auxiliary addresses into
|
2015-10-09 14:21:48 -04:00
|
|
|
// structured ipam data.
|
|
|
|
func consolidateIpam(subnets, ranges, gateways []string, auxaddrs map[string]string) ([]network.IPAMConfig, error) {
|
|
|
|
if len(subnets) < len(ranges) || len(subnets) < len(gateways) {
|
|
|
|
return nil, fmt.Errorf("every ip-range or gateway must have a corresponding subnet")
|
|
|
|
}
|
|
|
|
iData := map[string]*network.IPAMConfig{}
|
|
|
|
|
|
|
|
// Populate non-overlapping subnets into consolidation map
|
|
|
|
for _, s := range subnets {
|
|
|
|
for k := range iData {
|
|
|
|
ok1, err := subnetMatches(s, k)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
ok2, err := subnetMatches(k, s)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if ok1 || ok2 {
|
|
|
|
return nil, fmt.Errorf("multiple overlapping subnet configuration is not supported")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
iData[s] = &network.IPAMConfig{Subnet: s, AuxAddress: map[string]string{}}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Validate and add valid ip ranges
|
|
|
|
for _, r := range ranges {
|
|
|
|
match := false
|
|
|
|
for _, s := range subnets {
|
|
|
|
ok, err := subnetMatches(s, r)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if !ok {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if iData[s].IPRange != "" {
|
|
|
|
return nil, fmt.Errorf("cannot configure multiple ranges (%s, %s) on the same subnet (%s)", r, iData[s].IPRange, s)
|
|
|
|
}
|
|
|
|
d := iData[s]
|
|
|
|
d.IPRange = r
|
|
|
|
match = true
|
|
|
|
}
|
|
|
|
if !match {
|
|
|
|
return nil, fmt.Errorf("no matching subnet for range %s", r)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Validate and add valid gateways
|
|
|
|
for _, g := range gateways {
|
|
|
|
match := false
|
|
|
|
for _, s := range subnets {
|
|
|
|
ok, err := subnetMatches(s, g)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if !ok {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if iData[s].Gateway != "" {
|
|
|
|
return nil, fmt.Errorf("cannot configure multiple gateways (%s, %s) for the same subnet (%s)", g, iData[s].Gateway, s)
|
|
|
|
}
|
|
|
|
d := iData[s]
|
|
|
|
d.Gateway = g
|
|
|
|
match = true
|
|
|
|
}
|
|
|
|
if !match {
|
|
|
|
return nil, fmt.Errorf("no matching subnet for gateway %s", g)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Validate and add aux-addresses
|
|
|
|
for key, aa := range auxaddrs {
|
|
|
|
match := false
|
|
|
|
for _, s := range subnets {
|
|
|
|
ok, err := subnetMatches(s, aa)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if !ok {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
iData[s].AuxAddress[key] = aa
|
|
|
|
match = true
|
|
|
|
}
|
|
|
|
if !match {
|
|
|
|
return nil, fmt.Errorf("no matching subnet for aux-address %s", aa)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
idl := []network.IPAMConfig{}
|
|
|
|
for _, v := range iData {
|
|
|
|
idl = append(idl, *v)
|
|
|
|
}
|
|
|
|
return idl, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func subnetMatches(subnet, data string) (bool, error) {
|
|
|
|
var (
|
|
|
|
ip net.IP
|
|
|
|
)
|
|
|
|
|
|
|
|
_, s, err := net.ParseCIDR(subnet)
|
|
|
|
if err != nil {
|
|
|
|
return false, fmt.Errorf("Invalid subnet %s : %v", s, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if strings.Contains(data, "/") {
|
|
|
|
ip, _, err = net.ParseCIDR(data)
|
|
|
|
if err != nil {
|
|
|
|
return false, fmt.Errorf("Invalid cidr %s : %v", data, err)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
ip = net.ParseIP(data)
|
|
|
|
}
|
|
|
|
|
|
|
|
return s.Contains(ip), nil
|
|
|
|
}
|
|
|
|
|
2015-09-27 07:09:27 -04:00
|
|
|
func networkUsage() string {
|
|
|
|
networkCommands := map[string]string{
|
|
|
|
"create": "Create a network",
|
|
|
|
"connect": "Connect container to a network",
|
|
|
|
"disconnect": "Disconnect container from a network",
|
|
|
|
"inspect": "Display detailed network information",
|
|
|
|
"ls": "List all networks",
|
|
|
|
"rm": "Remove a network",
|
|
|
|
}
|
|
|
|
|
|
|
|
help := "Commands:\n"
|
|
|
|
|
|
|
|
for cmd, description := range networkCommands {
|
|
|
|
help += fmt.Sprintf(" %-25.25s%s\n", cmd, description)
|
|
|
|
}
|
|
|
|
|
|
|
|
help += fmt.Sprintf("\nRun 'docker network COMMAND --help' for more information on a command.")
|
|
|
|
return help
|
2015-05-20 08:20:19 -04:00
|
|
|
}
|