cmd/gomote: consistently support a direct-to-buildlet mode, bypassing coordinator

For testing new builder images, this adds support to gomote to connect
to a buildlet directly, without going via the coordinator. If the
builder name contains a '@' character, the name is expected to be of
the form <build-config-name>@ip[:port].

Also update the gomote run -path docs while I'm here, since I always
forget the magic expansions (which at least are documentd in the
buildlet godoc). Copy them to the flags doc.

Change-Id: I50227855897106e1cc60c1881700866d5fddf2a5
Reviewed-on: https://go-review.googlesource.com/38775
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
diff --git a/cmd/gomote/destroy.go b/cmd/gomote/destroy.go
index 9fbc016..77d7110 100644
--- a/cmd/gomote/destroy.go
+++ b/cmd/gomote/destroy.go
@@ -23,7 +23,7 @@
 		fs.Usage()
 	}
 	name := fs.Arg(0)
-	bc, err := namedClient(name)
+	bc, _, err := clientAndConf(name)
 	if err != nil {
 		return err
 	}
diff --git a/cmd/gomote/get.go b/cmd/gomote/get.go
index b514fda..576b618 100644
--- a/cmd/gomote/get.go
+++ b/cmd/gomote/get.go
@@ -29,7 +29,7 @@
 	}
 
 	name := fs.Arg(0)
-	bc, err := namedClient(name)
+	bc, _, err := clientAndConf(name)
 	if err != nil {
 		return err
 	}
diff --git a/cmd/gomote/gomote.go b/cmd/gomote/gomote.go
index 567abc3..c7996a6 100644
--- a/cmd/gomote/gomote.go
+++ b/cmd/gomote/gomote.go
@@ -6,6 +6,8 @@
 The gomote command is a client for the Go builder infrastructure.
 It's a remote control for remote Go builder machines.
 
+See https://golang.org/wiki/Gomote
+
 Usage:
 
   gomote [global-flags] cmd [cmd-flags]
@@ -15,8 +17,68 @@
   user-username-openbsd-amd64-60-0
   $ gomote push user-username-openbsd-amd64-60-0
   $ gomote run user-username-openbsd-amd64-60-0 go/src/make.bash
+  $ gomote run user-username-openbsd-amd64-60-0 go/bin/go test -v -short os
 
-TODO: document more, and figure out the CLI interface more.
+To list the subcommands, run "gomote" without arguments:
+
+  Commands:
+
+    create     create a buildlet
+    destroy    destroy a buildlet
+    gettar     extract a tar.gz from a buildlet
+    list       list buildlets
+    ls         list the contents of a directory on a buildlet
+    ping       test whether a buildlet is alive and reachable
+    push       sync the repo of your pwd to the buildlet
+    put        put files on a buildlet
+    put14      put Go 1.4 in place
+    puttar     extract a tar.gz to a buildlet
+    rm         delete files or directories
+    run        run a command on a buildlet
+
+To list all the builder types available, run "create" with no arguments:
+
+  $ gomote create
+  (tons of builder types)
+
+The "gomote run" command has many of its own flags:
+
+  $ gomote run -h
+  create usage: gomote run [run-opts] <instance> <cmd> [args...]
+    -builderenv string
+          Optional alternate builder to act like. Must share the same
+          underlying buildlet host type, or it's an error. For
+          instance, linux-amd64-race or linux-386-387 are compatible
+          with linux-amd64, but openbsd-amd64 and openbsd-386 are
+          different hosts.
+    -debug
+          write debug info about the command's execution before it begins
+    -dir string
+          Directory to run from. Defaults to the directory of the
+          command, or the work directory if -system is true.
+    -e value
+          Environment variable KEY=value. The -e flag may be repeated
+          multiple times to add multiple things to the environment.
+    -path string
+          Comma-separated list of ExecOpts.Path elements. The special
+          string 'EMPTY' means to run without any $PATH. The empty
+          string (default) does not modify the $PATH. Otherwise, the
+          following expansions apply: the string '$PATH' expands to
+          the current PATH element(s), the substring '$WORKDIR'
+          expands to the buildlet's temp workdir.
+    -system
+          run inside the system, and not inside the workdir; this is implicit if cmd starts with '/'
+
+Debugging buildlets directly
+
+Using "gomote create" contacts the build coordinator
+(farmer.golang.org) and requests that it create the buildlet on your
+behalf. All subsequent commands (such as "gomote run" or "gomote ls")
+then proxy your request via the coordinator.  To access a buildlet
+directly (for example, when working on the buildlet code), you can
+skip the "gomote create" step and use the special builder name
+"<build-config-name>@ip[:port>", such as "windows-amd64-gce@10.1.5.3".
+
 */
 package main
 
diff --git a/cmd/gomote/list.go b/cmd/gomote/list.go
index 83b1cd1..3b00aec 100644
--- a/cmd/gomote/list.go
+++ b/cmd/gomote/list.go
@@ -43,7 +43,36 @@
 	return nil
 }
 
+// clientAndConfig returns a buildlet.Client and its build config for
+// a named remote buildlet (a buildlet connection owned by the build
+// coordinator).
+//
+// As a special case, if name contains '@', the name is expected to be
+// of the form <build-config-name>@ip[:port]. For example,
+// "windows-amd64-race@10.0.0.1".
 func clientAndConf(name string) (bc *buildlet.Client, conf dashboard.BuildConfig, err error) {
+	var ok bool
+
+	if strings.Contains(name, "@") {
+		f := strings.SplitN(name, "@", 2)
+		if len(f) != 2 {
+			err = fmt.Errorf("unsupported name %q; for @ form expect <build-config-name>@host[:port]")
+			return
+		}
+		builderType := f[0]
+		conf, ok = dashboard.Builders[builderType]
+		if !ok {
+			err = fmt.Errorf("unknown builder type %q", name, builderType)
+			return
+		}
+		ipPort := f[1]
+		if !strings.Contains(ipPort, ":") {
+			ipPort += ":80"
+		}
+		bc = buildlet.NewClient(ipPort, buildlet.NoKeyPair)
+		return
+	}
+
 	cc, err := buildlet.NewCoordinatorClientFromFlags()
 	if err != nil {
 		return
@@ -53,7 +82,6 @@
 	if err != nil {
 		return
 	}
-	var ok bool
 	for _, rb := range rbs {
 		if rb.Name == name {
 			conf, ok = dashboard.Builders[rb.BuilderType]
@@ -69,17 +97,9 @@
 		return
 	}
 
-	bc, err = namedClient(name)
-	return
-}
-
-func namedClient(name string) (*buildlet.Client, error) {
-	if strings.Contains(name, ":") {
-		return buildlet.NewClient(name, buildlet.NoKeyPair), nil
-	}
-	cc, err := buildlet.NewCoordinatorClientFromFlags()
+	bc, err = cc.NamedBuildlet(name)
 	if err != nil {
-		return nil, err
+		return
 	}
-	return cc.NamedBuildlet(name)
+	return bc, conf, nil
 }
diff --git a/cmd/gomote/ls.go b/cmd/gomote/ls.go
index ca237be..8386d62 100644
--- a/cmd/gomote/ls.go
+++ b/cmd/gomote/ls.go
@@ -35,7 +35,7 @@
 		dir = fs.Arg(1)
 	}
 	name := fs.Arg(0)
-	bc, err := namedClient(name)
+	bc, _, err := clientAndConf(name)
 	if err != nil {
 		return err
 	}
diff --git a/cmd/gomote/ping.go b/cmd/gomote/ping.go
index 4de73a8..4c9e0cc 100644
--- a/cmd/gomote/ping.go
+++ b/cmd/gomote/ping.go
@@ -23,7 +23,7 @@
 		fs.Usage()
 	}
 	name := fs.Arg(0)
-	bc, err := namedClient(name)
+	bc, _, err := clientAndConf(name)
 	if err != nil {
 		return err
 	}
diff --git a/cmd/gomote/put.go b/cmd/gomote/put.go
index b4461d3..fcaacb8 100644
--- a/cmd/gomote/put.go
+++ b/cmd/gomote/put.go
@@ -46,7 +46,7 @@
 	}
 
 	name := fs.Arg(0)
-	bc, err := namedClient(name)
+	bc, _, err := clientAndConf(name)
 	if err != nil {
 		return err
 	}
@@ -125,7 +125,7 @@
 		fs.Usage()
 	}
 
-	bc, err := namedClient(fs.Arg(0))
+	bc, _, err := clientAndConf(fs.Arg(0))
 	if err != nil {
 		return err
 	}
diff --git a/cmd/gomote/rm.go b/cmd/gomote/rm.go
index c8a206c..93e4d0d 100644
--- a/cmd/gomote/rm.go
+++ b/cmd/gomote/rm.go
@@ -25,7 +25,7 @@
 	}
 	name := fs.Arg(0)
 	args = fs.Args()[1:]
-	bc, err := namedClient(name)
+	bc, _, err := clientAndConf(name)
 	if err != nil {
 		return err
 	}
diff --git a/cmd/gomote/run.go b/cmd/gomote/run.go
index 197d877..d110dc1 100644
--- a/cmd/gomote/run.go
+++ b/cmd/gomote/run.go
@@ -29,7 +29,8 @@
 	var env stringSlice
 	fs.Var(&env, "e", "Environment variable KEY=value. The -e flag may be repeated multiple times to add multiple things to the environment.")
 	var path string
-	fs.StringVar(&path, "path", "", "Comma-separated list of ExecOpts.Path elements. The special string 'EMPTY' means to run without any $PATH. The empty string (default) does not modify the $PATH.")
+	fs.StringVar(&path, "path", "", "Comma-separated list of ExecOpts.Path elements. The special string 'EMPTY' means to run without any $PATH. The empty string (default) does not modify the $PATH. Otherwise, the following expansions apply: the string '$PATH' expands to the current PATH element(s), the substring '$WORKDIR' expands to the buildlet's temp workdir.")
+
 	var dir string
 	fs.StringVar(&dir, "dir", "", "Directory to run from. Defaults to the directory of the command, or the work directory if -system is true.")
 	var builderEnv string