6dbc241bca3cb7252f8488a8c061bb43a94aa3c1
[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         "bytes"
9         "context"
10         "flag"
11         "fmt"
12         "io"
13         "net/url"
14         "os"
15         "os/exec"
16         "strings"
17         "syscall"
18
19         "git.arvados.org/arvados.git/lib/controller/rpc"
20         "git.arvados.org/arvados.git/sdk/go/arvados"
21 )
22
23 // shellCommand connects the terminal to an interactive shell on a
24 // running container.
25 type shellCommand struct{}
26
27 func (shellCommand) RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
28         f := flag.NewFlagSet(prog, flag.ContinueOnError)
29         f.SetOutput(stderr)
30         f.Usage = func() {
31                 fmt.Fprint(stderr, prog+`: open an interactive shell on a running container.
32
33 Usage: `+prog+` [options] [username@]container-uuid [ssh-options] [remote-command [args...]]
34
35 Options:
36 `)
37                 f.PrintDefaults()
38         }
39         detachKeys := f.String("detach-keys", "ctrl-],ctrl-]", "set detach key sequence, as in docker-attach(1)")
40         err := f.Parse(args)
41         if err != nil {
42                 fmt.Fprintln(stderr, err)
43                 return 2
44         }
45
46         if f.NArg() < 1 {
47                 f.Usage()
48                 return 2
49         }
50         target := f.Args()[0]
51         if !strings.Contains(target, "@") {
52                 target = "root@" + target
53         }
54         sshargs := f.Args()[1:]
55
56         // Try setting up a tunnel, and exit right away if it
57         // fails. This tunnel won't get used -- we'll set up a new
58         // tunnel when running as SSH client's ProxyCommand child --
59         // but in most cases where the real tunnel setup would fail,
60         // we catch the problem earlier here. This makes it less
61         // likely that an error message about tunnel setup will get
62         // hidden behind noisy errors from SSH client like this:
63         //
64         // [useful tunnel setup error message here]
65         // kex_exchange_identification: Connection closed by remote host
66         // Connection closed by UNKNOWN port 65535
67         // exit status 255
68         exitcode := connectSSHCommand{}.RunCommand(
69                 "arvados-client connect-ssh",
70                 []string{"-detach-keys=" + *detachKeys, "-probe-only=true", target},
71                 &bytes.Buffer{}, &bytes.Buffer{}, stderr)
72         if exitcode != 0 {
73                 return exitcode
74         }
75
76         selfbin, err := os.Readlink("/proc/self/exe")
77         if err != nil {
78                 fmt.Fprintln(stderr, err)
79                 return 2
80         }
81         sshargs = append([]string{
82                 "ssh",
83                 "-o", "ProxyCommand " + selfbin + " connect-ssh -detach-keys=" + shellescape(*detachKeys) + " " + shellescape(target),
84                 "-o", "StrictHostKeyChecking no",
85                 target},
86                 sshargs...)
87         sshbin, err := exec.LookPath("ssh")
88         if err != nil {
89                 fmt.Fprintln(stderr, err)
90                 return 1
91         }
92         err = syscall.Exec(sshbin, sshargs, os.Environ())
93         fmt.Fprintf(stderr, "exec(%q) failed: %s\n", sshbin, err)
94         return 1
95 }
96
97 // connectSSHCommand connects stdin/stdout to a container's gateway
98 // server (see lib/crunchrun/ssh.go).
99 //
100 // It is intended to be invoked with OpenSSH client's ProxyCommand
101 // config.
102 type connectSSHCommand struct{}
103
104 func (connectSSHCommand) RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
105         f := flag.NewFlagSet(prog, flag.ContinueOnError)
106         f.SetOutput(stderr)
107         f.Usage = func() {
108                 fmt.Fprint(stderr, prog+`: connect to the gateway service for a running container.
109
110 Usage: `+prog+` [options] [username@]container-uuid
111
112 Options:
113 `)
114                 f.PrintDefaults()
115         }
116         probeOnly := f.Bool("probe-only", false, "do not transfer IO, just exit 0 immediately if tunnel setup succeeds")
117         detachKeys := f.String("detach-keys", "", "set detach key sequence, as in docker-attach(1)")
118         if err := f.Parse(args); err != nil {
119                 fmt.Fprintln(stderr, err)
120                 return 2
121         } else if f.NArg() != 1 {
122                 f.Usage()
123                 return 2
124         }
125         targetUUID := f.Args()[0]
126         loginUsername := "root"
127         if i := strings.Index(targetUUID, "@"); i >= 0 {
128                 loginUsername = targetUUID[:i]
129                 targetUUID = targetUUID[i+1:]
130         }
131         if os.Getenv("ARVADOS_API_HOST") == "" || os.Getenv("ARVADOS_API_TOKEN") == "" {
132                 fmt.Fprintln(stderr, "fatal: ARVADOS_API_HOST and ARVADOS_API_TOKEN environment variables are not set")
133                 return 1
134         }
135         insecure := os.Getenv("ARVADOS_API_HOST_INSECURE")
136         rpcconn := rpc.NewConn("",
137                 &url.URL{
138                         Scheme: "https",
139                         Host:   os.Getenv("ARVADOS_API_HOST"),
140                 },
141                 insecure == "1" || insecure == "yes" || insecure == "true",
142                 func(context.Context) ([]string, error) {
143                         return []string{os.Getenv("ARVADOS_API_TOKEN")}, nil
144                 })
145         if strings.Contains(targetUUID, "-xvhdp-") {
146                 crs, err := rpcconn.ContainerRequestList(context.TODO(), arvados.ListOptions{Limit: -1, Filters: []arvados.Filter{{"uuid", "=", targetUUID}}})
147                 if err != nil {
148                         fmt.Fprintln(stderr, err)
149                         return 1
150                 }
151                 if len(crs.Items) < 1 {
152                         fmt.Fprintf(stderr, "container request %q not found\n", targetUUID)
153                         return 1
154                 }
155                 cr := crs.Items[0]
156                 if cr.ContainerUUID == "" {
157                         fmt.Fprintf(stderr, "no container assigned, container request state is %s\n", strings.ToLower(string(cr.State)))
158                         return 1
159                 }
160                 targetUUID = cr.ContainerUUID
161                 fmt.Fprintln(stderr, "connecting to container", targetUUID)
162         } else if !strings.Contains(targetUUID, "-dz642-") {
163                 fmt.Fprintf(stderr, "target UUID is not a container or container request UUID: %s\n", targetUUID)
164                 return 1
165         }
166         sshconn, err := rpcconn.ContainerSSH(context.TODO(), arvados.ContainerSSHOptions{
167                 UUID:          targetUUID,
168                 DetachKeys:    *detachKeys,
169                 LoginUsername: loginUsername,
170         })
171         if err != nil {
172                 fmt.Fprintln(stderr, "error setting up tunnel:", err)
173                 return 1
174         }
175         defer sshconn.Conn.Close()
176
177         if *probeOnly {
178                 return 0
179         }
180
181         ctx, cancel := context.WithCancel(context.Background())
182         go func() {
183                 defer cancel()
184                 _, err := io.Copy(stdout, sshconn.Conn)
185                 if err != nil && ctx.Err() == nil {
186                         fmt.Fprintf(stderr, "receive: %v\n", err)
187                 }
188         }()
189         go func() {
190                 defer cancel()
191                 _, err := io.Copy(sshconn.Conn, stdin)
192                 if err != nil && ctx.Err() == nil {
193                         fmt.Fprintf(stderr, "send: %v\n", err)
194                 }
195         }()
196         <-ctx.Done()
197         return 0
198 }
199
200 func shellescape(s string) string {
201         return "'" + strings.Replace(s, "'", "'\\''", -1) + "'"
202 }