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