17170: Improve error message for unrecognizable target UUID.
[arvados.git] / cmd / arvados-client / container_gateway.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: Apache-2.0
4
5 package main
6
7 import (
8         "context"
9         "flag"
10         "fmt"
11         "io"
12         "net/url"
13         "os"
14         "os/exec"
15         "strings"
16         "syscall"
17
18         "git.arvados.org/arvados.git/lib/controller/rpc"
19         "git.arvados.org/arvados.git/sdk/go/arvados"
20 )
21
22 // shellCommand connects the terminal to an interactive shell on a
23 // running container.
24 type shellCommand struct{}
25
26 func (shellCommand) RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
27         f := flag.NewFlagSet(prog, flag.ContinueOnError)
28         f.SetOutput(stderr)
29         f.Usage = func() {
30                 fmt.Fprint(stderr, prog+`: open an interactive shell on a running container.
31
32 Usage: `+prog+` [options] [username@]container-uuid [ssh-options] [remote-command [args...]]
33
34 Options:
35 `)
36                 f.PrintDefaults()
37         }
38         detachKeys := f.String("detach-keys", "ctrl-],ctrl-]", "set detach key sequence, as in docker-attach(1)")
39         err := f.Parse(args)
40         if err != nil {
41                 fmt.Fprintln(stderr, err)
42                 return 2
43         }
44
45         if f.NArg() < 1 {
46                 f.Usage()
47                 return 2
48         }
49         target := f.Args()[0]
50         if !strings.Contains(target, "@") {
51                 target = "root@" + target
52         }
53         sshargs := f.Args()[1:]
54
55         selfbin, err := os.Readlink("/proc/self/exe")
56         if err != nil {
57                 fmt.Fprintln(stderr, err)
58                 return 2
59         }
60         sshargs = append([]string{
61                 "ssh",
62                 "-o", "ProxyCommand " + selfbin + " connect-ssh -detach-keys=" + shellescape(*detachKeys) + " " + shellescape(target),
63                 "-o", "StrictHostKeyChecking no",
64                 target},
65                 sshargs...)
66         sshbin, err := exec.LookPath("ssh")
67         if err != nil {
68                 fmt.Fprintln(stderr, err)
69                 return 1
70         }
71         err = syscall.Exec(sshbin, sshargs, os.Environ())
72         fmt.Fprintf(stderr, "exec(%q) failed: %s\n", sshbin, err)
73         return 1
74 }
75
76 // connectSSHCommand connects stdin/stdout to a container's gateway
77 // server (see lib/crunchrun/ssh.go).
78 //
79 // It is intended to be invoked with OpenSSH client's ProxyCommand
80 // config.
81 type connectSSHCommand struct{}
82
83 func (connectSSHCommand) RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
84         f := flag.NewFlagSet(prog, flag.ContinueOnError)
85         f.SetOutput(stderr)
86         f.Usage = func() {
87                 fmt.Fprint(stderr, prog+`: connect to the gateway service for a running container.
88
89 Usage: `+prog+` [options] [username@]container-uuid
90
91 Options:
92 `)
93                 f.PrintDefaults()
94         }
95         detachKeys := f.String("detach-keys", "", "set detach key sequence, as in docker-attach(1)")
96         if err := f.Parse(args); err != nil {
97                 fmt.Fprintln(stderr, err)
98                 return 2
99         } else if f.NArg() != 1 {
100                 f.Usage()
101                 return 2
102         }
103         targetUUID := f.Args()[0]
104         loginUsername := "root"
105         if i := strings.Index(targetUUID, "@"); i >= 0 {
106                 loginUsername = targetUUID[:i]
107                 targetUUID = targetUUID[i+1:]
108         }
109         insecure := os.Getenv("ARVADOS_API_HOST_INSECURE")
110         rpcconn := rpc.NewConn("",
111                 &url.URL{
112                         Scheme: "https",
113                         Host:   os.Getenv("ARVADOS_API_HOST"),
114                 },
115                 insecure == "1" || insecure == "yes" || insecure == "true",
116                 func(context.Context) ([]string, error) {
117                         return []string{os.Getenv("ARVADOS_API_TOKEN")}, nil
118                 })
119         if strings.Contains(targetUUID, "-xvhdp-") {
120                 crs, err := rpcconn.ContainerRequestList(context.TODO(), arvados.ListOptions{Limit: -1, Filters: []arvados.Filter{{"uuid", "=", targetUUID}}})
121                 if err != nil {
122                         fmt.Fprintln(stderr, err)
123                         return 1
124                 }
125                 if len(crs.Items) < 1 {
126                         fmt.Fprintf(stderr, "container request %q not found\n", targetUUID)
127                         return 1
128                 }
129                 cr := crs.Items[0]
130                 if cr.ContainerUUID == "" {
131                         fmt.Fprintf(stderr, "no container assigned, container request state is %s\n", strings.ToLower(string(cr.State)))
132                         return 1
133                 }
134                 targetUUID = cr.ContainerUUID
135                 fmt.Fprintln(stderr, "connecting to container", targetUUID)
136         } else if !strings.Contains(targetUUID, "-dz642-") {
137                 fmt.Fprintf(stderr, "target UUID is not a container or container request UUID: %s\n", targetUUID)
138                 return 1
139         }
140         sshconn, err := rpcconn.ContainerSSH(context.TODO(), arvados.ContainerSSHOptions{
141                 UUID:          targetUUID,
142                 DetachKeys:    *detachKeys,
143                 LoginUsername: loginUsername,
144         })
145         if err != nil {
146                 fmt.Fprintln(stderr, "error setting up tunnel:", err)
147                 return 1
148         }
149         defer sshconn.Conn.Close()
150
151         ctx, cancel := context.WithCancel(context.Background())
152         go func() {
153                 defer cancel()
154                 _, err := io.Copy(stdout, sshconn.Conn)
155                 if err != nil && ctx.Err() == nil {
156                         fmt.Fprintf(stderr, "receive: %v\n", err)
157                 }
158         }()
159         go func() {
160                 defer cancel()
161                 _, err := io.Copy(sshconn.Conn, stdin)
162                 if err != nil && ctx.Err() == nil {
163                         fmt.Fprintf(stderr, "send: %v\n", err)
164                 }
165         }()
166         <-ctx.Done()
167         return 0
168 }
169
170 func shellescape(s string) string {
171         return "'" + strings.Replace(s, "'", "'\\''", -1) + "'"
172 }