1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: Apache-2.0
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"
25 // shellCommand connects the terminal to an interactive shell on a
27 type shellCommand struct{}
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 {
34 } else if f.NArg() < 1 {
35 fmt.Fprintf(stderr, "missing required argument: container-uuid (try -help)\n")
39 if !strings.Contains(target, "@") {
40 target = "root@" + target
42 sshargs := f.Args()[1:]
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:
52 // [useful tunnel setup error message here]
53 // kex_exchange_identification: Connection closed by remote host
54 // Connection closed by UNKNOWN port 65535
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)
68 target = strings.Trim(probetarget.String(), "\n")
70 selfbin, err := os.Readlink("/proc/self/exe")
72 fmt.Fprintln(stderr, err)
75 sshargs = append([]string{
77 "-o", "ProxyCommand " + selfbin + " connect-ssh -detach-keys=" + shellescape(*detachKeys) + " " + shellescape(target),
78 "-o", "StrictHostKeyChecking no",
81 sshbin, err := exec.LookPath("ssh")
83 fmt.Fprintln(stderr, err)
86 err = syscall.Exec(sshbin, sshargs, os.Environ())
87 fmt.Fprintf(stderr, "exec(%q) failed: %s\n", sshbin, err)
91 // connectSSHCommand connects stdin/stdout to a container's gateway
92 // server (see lib/crunchrun/ssh.go).
94 // It is intended to be invoked with OpenSSH client's ProxyCommand
96 type connectSSHCommand struct{}
98 func (connectSSHCommand) RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
99 f := flag.NewFlagSet(prog, flag.ContinueOnError)
102 _, prog := filepath.Split(prog)
103 fmt.Fprint(stderr, prog+`: connect to the gateway service for a running container.
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.
108 Usage: `+prog+` [options] [username@]container-uuid
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 {
118 } else if f.NArg() != 1 {
119 fmt.Fprintf(stderr, "missing required argument: [username@]container-uuid\n")
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:]
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")
132 insecure := os.Getenv("ARVADOS_API_HOST_INSECURE")
133 rpcconn := rpc.NewConn("",
136 Host: os.Getenv("ARVADOS_API_HOST"),
138 insecure == "1" || insecure == "yes" || insecure == "true",
139 func(context.Context) ([]string, error) {
140 return []string{os.Getenv("ARVADOS_API_TOKEN")}, nil
142 if strings.Contains(targetUUID, "-xvhdp-") {
143 crs, err := rpcconn.ContainerRequestList(context.TODO(), arvados.ListOptions{Limit: -1, Filters: []arvados.Filter{{"uuid", "=", targetUUID}}})
145 fmt.Fprintln(stderr, err)
148 if len(crs.Items) < 1 {
149 fmt.Fprintf(stderr, "container request %q not found\n", targetUUID)
153 if cr.ContainerUUID == "" {
154 fmt.Fprintf(stderr, "no container assigned, container request state is %s\n", strings.ToLower(string(cr.State)))
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)
163 sshconn, err := rpcconn.ContainerSSH(context.TODO(), arvados.ContainerSSHOptions{
165 DetachKeys: *detachKeys,
166 LoginUsername: loginUsername,
169 fmt.Fprintln(stderr, "error setting up tunnel:", err)
172 defer sshconn.Conn.Close()
175 fmt.Fprintln(stdout, targetUUID)
179 ctx, cancel := context.WithCancel(context.Background())
182 _, err := io.Copy(stdout, sshconn.Conn)
183 if err != nil && ctx.Err() == nil {
184 fmt.Fprintf(stderr, "receive: %v\n", err)
189 _, err := io.Copy(sshconn.Conn, stdin)
190 if err != nil && ctx.Err() == nil {
191 fmt.Fprintf(stderr, "send: %v\n", err)
198 func shellescape(s string) string {
199 return "'" + strings.Replace(s, "'", "'\\''", -1) + "'"