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