17840: Deduplicate flag-parsing code.
[arvados.git] / cmd / arvados-client / container_gateway.go
index 46fa6932b33e67938145274af46fa7f07b5ad309..aca6c5b797fa4ec3b036ee8300ae3f4fcbe5e885 100644 (file)
@@ -5,6 +5,7 @@
 package main
 
 import (
+       "bytes"
        "context"
        "flag"
        "fmt"
@@ -12,9 +13,11 @@ import (
        "net/url"
        "os"
        "os/exec"
+       "path/filepath"
        "strings"
        "syscall"
 
+       "git.arvados.org/arvados.git/lib/cmd"
        "git.arvados.org/arvados.git/lib/controller/rpc"
        "git.arvados.org/arvados.git/sdk/go/arvados"
 )
@@ -25,57 +28,64 @@ type shellCommand struct{}
 
 func (shellCommand) RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
        f := flag.NewFlagSet(prog, flag.ContinueOnError)
-       f.SetOutput(stderr)
-       f.Usage = func() {
-               fmt.Print(stderr, prog+`: open an interactive shell on a running container.
-
-Usage: `+prog+` [options] container-uuid [ssh-options] [remote-command [args...]]
-
-Options:
-`)
-               f.PrintDefaults()
-       }
        detachKeys := f.String("detach-keys", "ctrl-],ctrl-]", "set detach key sequence, as in docker-attach(1)")
-       err := f.Parse(args)
-       if err != nil {
-               fmt.Println(stderr, err)
-               f.Usage()
+       if ok, code := cmd.ParseFlags(f, prog, args, "[username@]container-uuid [ssh-options] [remote-command [args...]]", stderr); !ok {
+               return code
+       } else if f.NArg() < 1 {
+               fmt.Fprintf(stderr, "missing required argument: container-uuid (try -help)\n")
                return 2
        }
-
-       if f.NArg() < 1 {
-               f.Usage()
-               return 2
+       target := f.Args()[0]
+       if !strings.Contains(target, "@") {
+               target = "root@" + target
        }
-       targetUUID := f.Args()[0]
        sshargs := f.Args()[1:]
 
+       // Try setting up a tunnel, and exit right away if it
+       // fails. This tunnel won't get used -- we'll set up a new
+       // tunnel when running as SSH client's ProxyCommand child --
+       // but in most cases where the real tunnel setup would fail,
+       // we catch the problem earlier here. This makes it less
+       // likely that an error message about tunnel setup will get
+       // hidden behind noisy errors from SSH client like this:
+       //
+       // [useful tunnel setup error message here]
+       // kex_exchange_identification: Connection closed by remote host
+       // Connection closed by UNKNOWN port 65535
+       // exit status 255
+       //
+       // In case our target is a container request, the probe also
+       // resolves it to a container, so we don't connect to two
+       // different containers in a race.
+       var probetarget bytes.Buffer
+       exitcode := connectSSHCommand{}.RunCommand(
+               "arvados-client connect-ssh",
+               []string{"-detach-keys=" + *detachKeys, "-probe-only=true", target},
+               &bytes.Buffer{}, &probetarget, stderr)
+       if exitcode != 0 {
+               return exitcode
+       }
+       target = strings.Trim(probetarget.String(), "\n")
+
        selfbin, err := os.Readlink("/proc/self/exe")
        if err != nil {
                fmt.Fprintln(stderr, err)
                return 2
        }
        sshargs = append([]string{
-               "-o", "ProxyCommand " + selfbin + " connect-ssh -detach-keys='" + strings.Replace(*detachKeys, "'", "'\\''", -1) + "' " + targetUUID,
+               "ssh",
+               "-o", "ProxyCommand " + selfbin + " connect-ssh -detach-keys=" + shellescape(*detachKeys) + " " + shellescape(target),
                "-o", "StrictHostKeyChecking no",
-               "root@" + targetUUID},
+               target},
                sshargs...)
-       cmd := exec.Command("ssh", sshargs...)
-       cmd.Stdin = stdin
-       cmd.Stdout = stdout
-       cmd.Stderr = stderr
-       err = cmd.Run()
-       if err == nil {
-               return 0
-       } else if exiterr, ok := err.(*exec.ExitError); !ok {
-               fmt.Fprintln(stderr, err)
-               return 1
-       } else if status, ok := exiterr.Sys().(syscall.WaitStatus); !ok {
+       sshbin, err := exec.LookPath("ssh")
+       if err != nil {
                fmt.Fprintln(stderr, err)
                return 1
-       } else {
-               return status.ExitStatus()
        }
+       err = syscall.Exec(sshbin, sshargs, os.Environ())
+       fmt.Fprintf(stderr, "exec(%q) failed: %s\n", sshbin, err)
+       return 1
 }
 
 // connectSSHCommand connects stdin/stdout to a container's gateway
@@ -89,24 +99,36 @@ func (connectSSHCommand) RunCommand(prog string, args []string, stdin io.Reader,
        f := flag.NewFlagSet(prog, flag.ContinueOnError)
        f.SetOutput(stderr)
        f.Usage = func() {
+               _, prog := filepath.Split(prog)
                fmt.Fprint(stderr, prog+`: connect to the gateway service for a running container.
 
-Usage: `+prog+` [options] container-uuid
+NOTE: You almost certainly don't want to use this command directly. It
+is meant to be used internally. Use "arvados-client shell" instead.
+
+Usage: `+prog+` [options] [username@]container-uuid
 
 Options:
 `)
                f.PrintDefaults()
        }
+       probeOnly := f.Bool("probe-only", false, "do not transfer IO, just setup tunnel, print target UUID, and exit")
        detachKeys := f.String("detach-keys", "", "set detach key sequence, as in docker-attach(1)")
-       if err := f.Parse(args); err != nil {
-               fmt.Fprintln(stderr, err)
-               f.Usage()
-               return 2
+       if ok, code := cmd.ParseFlags(f, prog, args, "[username@]container-uuid", stderr); !ok {
+               return code
        } else if f.NArg() != 1 {
-               f.Usage()
+               fmt.Fprintf(stderr, "missing required argument: [username@]container-uuid\n")
                return 2
        }
        targetUUID := f.Args()[0]
+       loginUsername := "root"
+       if i := strings.Index(targetUUID, "@"); i >= 0 {
+               loginUsername = targetUUID[:i]
+               targetUUID = targetUUID[i+1:]
+       }
+       if os.Getenv("ARVADOS_API_HOST") == "" || os.Getenv("ARVADOS_API_TOKEN") == "" {
+               fmt.Fprintln(stderr, "fatal: ARVADOS_API_HOST and ARVADOS_API_TOKEN environment variables are not set")
+               return 1
+       }
        insecure := os.Getenv("ARVADOS_API_HOST_INSECURE")
        rpcconn := rpc.NewConn("",
                &url.URL{
@@ -117,28 +139,43 @@ Options:
                func(context.Context) ([]string, error) {
                        return []string{os.Getenv("ARVADOS_API_TOKEN")}, nil
                })
-       // if strings.Contains(targetUUID, "-xvhdp-") {
-       //      cr, err := rpcconn.ContainerRequestGet(context.TODO(), arvados.GetOptions{UUID: targetUUID})
-       //      if err != nil {
-       //              fmt.Fprintln(stderr, err)
-       //              return 1
-       //      }
-       //      if cr.ContainerUUID == "" {
-       //              fmt.Fprintf(stderr, "no container assigned, container request state is %s\n", strings.ToLower(cr.State))
-       //              return 1
-       //      }
-       //      targetUUID = cr.ContainerUUID
-       // }
+       if strings.Contains(targetUUID, "-xvhdp-") {
+               crs, err := rpcconn.ContainerRequestList(context.TODO(), arvados.ListOptions{Limit: -1, Filters: []arvados.Filter{{"uuid", "=", targetUUID}}})
+               if err != nil {
+                       fmt.Fprintln(stderr, err)
+                       return 1
+               }
+               if len(crs.Items) < 1 {
+                       fmt.Fprintf(stderr, "container request %q not found\n", targetUUID)
+                       return 1
+               }
+               cr := crs.Items[0]
+               if cr.ContainerUUID == "" {
+                       fmt.Fprintf(stderr, "no container assigned, container request state is %s\n", strings.ToLower(string(cr.State)))
+                       return 1
+               }
+               targetUUID = cr.ContainerUUID
+               fmt.Fprintln(stderr, "connecting to container", targetUUID)
+       } else if !strings.Contains(targetUUID, "-dz642-") {
+               fmt.Fprintf(stderr, "target UUID is not a container or container request UUID: %s\n", targetUUID)
+               return 1
+       }
        sshconn, err := rpcconn.ContainerSSH(context.TODO(), arvados.ContainerSSHOptions{
-               UUID:       targetUUID,
-               DetachKeys: *detachKeys,
+               UUID:          targetUUID,
+               DetachKeys:    *detachKeys,
+               LoginUsername: loginUsername,
        })
        if err != nil {
-               fmt.Fprintln(stderr, err)
+               fmt.Fprintln(stderr, "error setting up tunnel:", err)
                return 1
        }
        defer sshconn.Conn.Close()
 
+       if *probeOnly {
+               fmt.Fprintln(stdout, targetUUID)
+               return 0
+       }
+
        ctx, cancel := context.WithCancel(context.Background())
        go func() {
                defer cancel()
@@ -157,3 +194,7 @@ Options:
        <-ctx.Done()
        return 0
 }
+
+func shellescape(s string) string {
+       return "'" + strings.Replace(s, "'", "'\\''", -1) + "'"
+}