1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
25 "git.arvados.org/arvados.git/lib/controller/router"
26 "git.arvados.org/arvados.git/lib/controller/rpc"
27 "git.arvados.org/arvados.git/lib/crunchrun"
28 "git.arvados.org/arvados.git/lib/ctrlctx"
29 "git.arvados.org/arvados.git/sdk/go/arvados"
30 "git.arvados.org/arvados.git/sdk/go/arvadosclient"
31 "git.arvados.org/arvados.git/sdk/go/arvadostest"
32 "git.arvados.org/arvados.git/sdk/go/auth"
33 "git.arvados.org/arvados.git/sdk/go/ctxlog"
34 "git.arvados.org/arvados.git/sdk/go/httpserver"
35 "git.arvados.org/arvados.git/sdk/go/keepclient"
36 "golang.org/x/crypto/ssh"
37 check "gopkg.in/check.v1"
40 var _ = check.Suite(&ContainerGatewaySuite{})
42 type ContainerGatewaySuite struct {
50 func (s *ContainerGatewaySuite) SetUpTest(c *check.C) {
51 s.localdbSuite.SetUpTest(c)
53 cr, err := s.localdb.ContainerRequestCreate(s.userctx, arvados.CreateOptions{
54 Attrs: map[string]interface{}{
55 "command": []string{"echo", time.Now().Format(time.RFC3339Nano)},
56 "container_count_max": 1,
57 "container_image": "arvados/apitestfixture:latest",
59 "environment": map[string]string{},
60 "output_path": "/out",
62 "state": arvados.ContainerRequestStateCommitted,
63 "mounts": map[string]interface{}{
64 "/out": map[string]interface{}{
69 "runtime_constraints": map[string]interface{}{
73 c.Assert(err, check.IsNil)
75 s.ctrUUID = cr.ContainerUUID
77 h := hmac.New(sha256.New, []byte(s.cluster.SystemRootToken))
78 fmt.Fprint(h, s.ctrUUID)
79 authKey := fmt.Sprintf("%x", h.Sum(nil))
81 rtr := router.New(s.localdb, router.Config{})
82 s.srv = httptest.NewUnstartedServer(httpserver.AddRequestIDs(httpserver.LogRequests(rtr)))
84 // the test setup doesn't use lib/service so
85 // service.URLFromContext() returns nothing -- instead, this
86 // is how we advertise our internal URL and enable
87 // proxy-to-other-controller mode,
88 forceInternalURLForTest = &arvados.URL{Scheme: "https", Host: s.srv.Listener.Addr().String()}
89 ac := &arvados.Client{
90 APIHost: s.srv.Listener.Addr().String(),
91 AuthToken: arvadostest.Dispatch1Token,
94 s.gw = &crunchrun.Gateway{
95 ContainerUUID: s.ctrUUID,
97 Address: "localhost:0",
98 Log: ctxlog.TestLogger(c),
99 Target: crunchrun.GatewayTargetStub{},
102 c.Assert(s.gw.Start(), check.IsNil)
104 rootctx := ctrlctx.NewWithToken(s.ctx, s.cluster, s.cluster.SystemRootToken)
105 _, err = s.localdb.ContainerUpdate(rootctx, arvados.UpdateOptions{
107 Attrs: map[string]interface{}{
108 "state": arvados.ContainerStateLocked}})
109 c.Assert(err, check.IsNil)
110 _, err = s.localdb.ContainerUpdate(rootctx, arvados.UpdateOptions{
112 Attrs: map[string]interface{}{
113 "state": arvados.ContainerStateRunning,
114 "gateway_address": s.gw.Address}})
115 c.Assert(err, check.IsNil)
117 s.cluster.Containers.ShellAccess.Admin = true
118 s.cluster.Containers.ShellAccess.User = true
119 _, err = s.db.Exec(`update containers set interactive_session_started=$1 where uuid=$2`, false, s.ctrUUID)
120 c.Check(err, check.IsNil)
123 func (s *ContainerGatewaySuite) TearDownTest(c *check.C) {
125 s.localdbSuite.TearDownTest(c)
128 func (s *ContainerGatewaySuite) TestConfig(c *check.C) {
129 for _, trial := range []struct {
135 {true, true, arvadostest.ActiveTokenV2, 0},
136 {true, false, arvadostest.ActiveTokenV2, 503},
137 {false, true, arvadostest.ActiveTokenV2, 0},
138 {false, false, arvadostest.ActiveTokenV2, 503},
139 {true, true, arvadostest.AdminToken, 0},
140 {true, false, arvadostest.AdminToken, 0},
141 {false, true, arvadostest.AdminToken, 403},
142 {false, false, arvadostest.AdminToken, 503},
144 c.Logf("trial %#v", trial)
145 s.cluster.Containers.ShellAccess.Admin = trial.configAdmin
146 s.cluster.Containers.ShellAccess.User = trial.configUser
147 ctx := ctrlctx.NewWithToken(s.ctx, s.cluster, trial.sendToken)
148 sshconn, err := s.localdb.ContainerSSH(ctx, arvados.ContainerSSHOptions{UUID: s.ctrUUID})
149 if trial.errorCode == 0 {
150 if !c.Check(err, check.IsNil) {
153 if !c.Check(sshconn.Conn, check.NotNil) {
158 c.Check(err, check.NotNil)
159 err, ok := err.(interface{ HTTPStatus() int })
160 if c.Check(ok, check.Equals, true) {
161 c.Check(err.HTTPStatus(), check.Equals, trial.errorCode)
167 func (s *ContainerGatewaySuite) TestDirectTCP(c *check.C) {
168 // Set up servers on a few TCP ports
170 for i := 0; i < 3; i++ {
171 ln, err := net.Listen("tcp", ":0")
172 c.Assert(err, check.IsNil)
174 addrs = append(addrs, ln.Addr().String())
177 conn, err := ln.Accept()
182 fmt.Fscanf(conn, "%s\n", &gotAddr)
183 c.Logf("stub server listening at %s received string %q from remote %s", ln.Addr().String(), gotAddr, conn.RemoteAddr())
184 if gotAddr == ln.Addr().String() {
185 fmt.Fprintf(conn, "%s\n", ln.Addr().String())
192 c.Logf("connecting to %s", s.gw.Address)
193 sshconn, err := s.localdb.ContainerSSH(s.userctx, arvados.ContainerSSHOptions{UUID: s.ctrUUID})
194 c.Assert(err, check.IsNil)
195 c.Assert(sshconn.Conn, check.NotNil)
196 defer sshconn.Conn.Close()
197 conn, chans, reqs, err := ssh.NewClientConn(sshconn.Conn, "zzzz-dz642-abcdeabcdeabcde", &ssh.ClientConfig{
198 HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error { return nil },
200 c.Assert(err, check.IsNil)
201 client := ssh.NewClient(conn, chans, reqs)
202 for _, expectAddr := range addrs {
203 _, port, err := net.SplitHostPort(expectAddr)
204 c.Assert(err, check.IsNil)
206 c.Logf("trying foo:%s", port)
208 conn, err := client.Dial("tcp", "foo:"+port)
209 c.Assert(err, check.IsNil)
210 conn.SetDeadline(time.Now().Add(time.Second))
211 buf, err := ioutil.ReadAll(conn)
212 c.Check(err, check.IsNil)
213 c.Check(string(buf), check.Equals, "")
216 c.Logf("trying localhost:%s", port)
218 conn, err := client.Dial("tcp", "localhost:"+port)
219 c.Assert(err, check.IsNil)
220 conn.SetDeadline(time.Now().Add(time.Second))
221 conn.Write([]byte(expectAddr + "\n"))
223 fmt.Fscanf(conn, "%s\n", &gotAddr)
224 c.Check(gotAddr, check.Equals, expectAddr)
229 func (s *ContainerGatewaySuite) setupLogCollection(c *check.C) {
230 files := map[string]string{
231 "stderr.txt": "hello world\n",
232 "a/b/c/d.html": "<html></html>\n",
234 client := arvados.NewClientFromEnv()
235 ac, err := arvadosclient.New(client)
236 c.Assert(err, check.IsNil)
237 kc, err := keepclient.MakeKeepClient(ac)
238 c.Assert(err, check.IsNil)
239 cfs, err := (&arvados.Collection{}).FileSystem(client, kc)
240 c.Assert(err, check.IsNil)
241 for name, content := range files {
242 for i, ch := range name {
244 err := cfs.Mkdir("/"+name[:i], 0777)
245 c.Assert(err, check.IsNil)
248 f, err := cfs.OpenFile("/"+name, os.O_CREATE|os.O_WRONLY, 0777)
249 c.Assert(err, check.IsNil)
250 f.Write([]byte(content))
252 c.Assert(err, check.IsNil)
255 s.gw.LogCollection = cfs
258 func (s *ContainerGatewaySuite) saveLogAndCloseGateway(c *check.C) {
259 rootctx := ctrlctx.NewWithToken(s.ctx, s.cluster, s.cluster.SystemRootToken)
260 txt, err := s.gw.LogCollection.MarshalManifest(".")
261 c.Assert(err, check.IsNil)
262 coll, err := s.localdb.CollectionCreate(rootctx, arvados.CreateOptions{
263 Attrs: map[string]interface{}{
264 "manifest_text": txt,
266 c.Assert(err, check.IsNil)
267 _, err = s.localdb.ContainerUpdate(rootctx, arvados.UpdateOptions{
269 Attrs: map[string]interface{}{
270 "state": arvados.ContainerStateComplete,
272 "log": coll.PortableDataHash,
274 c.Assert(err, check.IsNil)
275 updatedReq, err := s.localdb.ContainerRequestGet(rootctx, arvados.GetOptions{UUID: s.reqUUID})
276 c.Assert(err, check.IsNil)
277 c.Logf("container request log UUID is %s", updatedReq.LogUUID)
278 crLog, err := s.localdb.CollectionGet(rootctx, arvados.GetOptions{UUID: updatedReq.LogUUID, Select: []string{"manifest_text"}})
279 c.Assert(err, check.IsNil)
280 c.Logf("collection log manifest:\n%s", crLog.ManifestText)
281 // Ensure localdb can't circumvent the keep-web proxy test by
282 // getting content from the container gateway.
283 s.gw.LogCollection = nil
286 func (s *ContainerGatewaySuite) TestContainerRequestLogViaTunnel(c *check.C) {
287 forceProxyForTest = true
288 defer func() { forceProxyForTest = false }()
290 s.gw = s.setupGatewayWithTunnel(c)
291 s.setupLogCollection(c)
293 for _, broken := range []bool{false, true} {
294 c.Logf("broken=%v", broken)
297 delete(s.cluster.Services.Controller.InternalURLs, *forceInternalURLForTest)
299 s.cluster.Services.Controller.InternalURLs[*forceInternalURLForTest] = arvados.ServiceInstance{}
300 defer delete(s.cluster.Services.Controller.InternalURLs, *forceInternalURLForTest)
303 r, err := http.NewRequestWithContext(s.userctx, "GET", "https://controller.example/arvados/v1/container_requests/"+s.reqUUID+"/log/"+s.ctrUUID+"/stderr.txt", nil)
304 c.Assert(err, check.IsNil)
305 r.Header.Set("Authorization", "Bearer "+arvadostest.ActiveTokenV2)
306 handler, err := s.localdb.ContainerRequestLog(s.userctx, arvados.ContainerLogOptions{
308 WebDAVOptions: arvados.WebDAVOptions{
311 Path: "/" + s.ctrUUID + "/stderr.txt",
315 c.Check(err, check.ErrorMatches, `.*tunnel endpoint is invalid.*`)
318 c.Check(err, check.IsNil)
319 c.Assert(handler, check.NotNil)
320 rec := httptest.NewRecorder()
321 handler.ServeHTTP(rec, r)
323 c.Check(resp.StatusCode, check.Equals, http.StatusOK)
324 buf, err := ioutil.ReadAll(resp.Body)
325 c.Check(err, check.IsNil)
326 c.Check(string(buf), check.Equals, "hello world\n")
330 func (s *ContainerGatewaySuite) TestContainerRequestLogViaGateway(c *check.C) {
331 s.setupLogCollection(c)
332 s.testContainerRequestLog(c)
335 func (s *ContainerGatewaySuite) TestContainerRequestLogViaKeepWeb(c *check.C) {
336 s.setupLogCollection(c)
337 s.saveLogAndCloseGateway(c)
338 s.testContainerRequestLog(c)
341 func (s *ContainerGatewaySuite) testContainerRequestLog(c *check.C) {
342 for _, trial := range []struct {
349 expectHeader http.Header
353 path: s.ctrUUID + "/stderr.txt",
354 expectStatus: http.StatusOK,
355 expectBodyRe: "hello world\n",
356 expectHeader: http.Header{
357 "Content-Type": {"text/plain; charset=utf-8"},
362 path: s.ctrUUID + "/stderr.txt",
364 "Range": {"bytes=-6"},
366 expectStatus: http.StatusPartialContent,
367 expectBodyRe: "world\n",
368 expectHeader: http.Header{
369 "Content-Type": {"text/plain; charset=utf-8"},
370 "Content-Range": {"bytes 6-11/12"},
375 path: s.ctrUUID + "/stderr.txt",
376 expectStatus: http.StatusOK,
378 expectHeader: http.Header{
380 "Allow": {"OPTIONS, LOCK, GET, HEAD, POST, DELETE, PROPPATCH, COPY, MOVE, UNLOCK, PROPFIND, PUT"},
385 path: s.ctrUUID + "/stderr.txt",
386 unauthenticated: true,
388 "Access-Control-Request-Method": {"POST"},
390 expectStatus: http.StatusOK,
392 expectHeader: http.Header{
393 "Access-Control-Allow-Headers": {"Authorization, Content-Type, Range, Depth, Destination, If, Lock-Token, Overwrite, Timeout, Cache-Control"},
394 "Access-Control-Allow-Methods": {"COPY, DELETE, GET, LOCK, MKCOL, MOVE, OPTIONS, POST, PROPFIND, PROPPATCH, PUT, RMCOL, UNLOCK"},
395 "Access-Control-Allow-Origin": {"*"},
396 "Access-Control-Max-Age": {"86400"},
401 path: s.ctrUUID + "/",
402 expectStatus: http.StatusMultiStatus,
403 expectBodyRe: `.*\Q<D:displayname>stderr.txt</D:displayname>\E.*>\n?`,
404 expectHeader: http.Header{
405 "Content-Type": {"text/xml; charset=utf-8"},
411 expectStatus: http.StatusMultiStatus,
412 expectBodyRe: `.*\Q<D:displayname>stderr.txt</D:displayname>\E.*>\n?`,
413 expectHeader: http.Header{
414 "Content-Type": {"text/xml; charset=utf-8"},
419 path: s.ctrUUID + "/a/b/c/",
420 expectStatus: http.StatusMultiStatus,
421 expectBodyRe: `.*\Q<D:displayname>d.html</D:displayname>\E.*>\n?`,
422 expectHeader: http.Header{
423 "Content-Type": {"text/xml; charset=utf-8"},
428 path: s.ctrUUID + "/a/b/c/d.html",
429 expectStatus: http.StatusOK,
430 expectBodyRe: "<html></html>\n",
431 expectHeader: http.Header{
432 "Content-Type": {"text/html; charset=utf-8"},
436 c.Logf("trial %#v", trial)
438 if trial.unauthenticated {
439 ctx = auth.NewContext(context.Background(), auth.CredentialsFromRequest(&http.Request{URL: &url.URL{}, Header: http.Header{}}))
441 r, err := http.NewRequestWithContext(ctx, trial.method, "https://controller.example/arvados/v1/container_requests/"+s.reqUUID+"/log/"+trial.path, nil)
442 c.Assert(err, check.IsNil)
443 for k := range trial.header {
444 r.Header.Set(k, trial.header.Get(k))
446 handler, err := s.localdb.ContainerRequestLog(ctx, arvados.ContainerLogOptions{
448 WebDAVOptions: arvados.WebDAVOptions{
449 Method: trial.method,
451 Path: "/" + trial.path,
454 c.Assert(err, check.IsNil)
455 c.Assert(handler, check.NotNil)
456 rec := httptest.NewRecorder()
457 handler.ServeHTTP(rec, r)
459 c.Check(resp.StatusCode, check.Equals, trial.expectStatus)
460 for k := range trial.expectHeader {
461 c.Check(resp.Header[k], check.DeepEquals, trial.expectHeader[k])
463 buf, err := ioutil.ReadAll(resp.Body)
464 c.Check(err, check.IsNil)
465 c.Check(string(buf), check.Matches, trial.expectBodyRe)
469 func (s *ContainerGatewaySuite) TestContainerRequestLogViaCadaver(c *check.C) {
470 s.setupLogCollection(c)
472 out := s.runCadaver(c, arvadostest.ActiveToken, "/arvados/v1/container_requests/"+s.reqUUID+"/log/"+s.ctrUUID, "ls")
473 c.Check(out, check.Matches, `(?ms).*stderr\.txt\s+12\s.*`)
474 c.Check(out, check.Matches, `(?ms).*a\s+0\s.*`)
476 out = s.runCadaver(c, arvadostest.ActiveTokenV2, "/arvados/v1/container_requests/"+s.reqUUID+"/log/"+s.ctrUUID, "get stderr.txt")
477 c.Check(out, check.Matches, `(?ms).*Downloading .* to stderr\.txt: .* succeeded\..*`)
479 s.saveLogAndCloseGateway(c)
481 out = s.runCadaver(c, arvadostest.ActiveTokenV2, "/arvados/v1/container_requests/"+s.reqUUID+"/log/"+s.ctrUUID, "get stderr.txt")
482 c.Check(out, check.Matches, `(?ms).*Downloading .* to stderr\.txt: .* succeeded\..*`)
485 func (s *ContainerGatewaySuite) runCadaver(c *check.C, password, path, stdin string) string {
486 // Replace s.srv with an HTTP server, otherwise cadaver will
487 // just fail on TLS cert verification.
489 rtr := router.New(s.localdb, router.Config{})
490 s.srv = httptest.NewUnstartedServer(httpserver.AddRequestIDs(httpserver.LogRequests(rtr)))
493 tempdir, err := ioutil.TempDir("", "localdb-test-")
494 c.Assert(err, check.IsNil)
495 defer os.RemoveAll(tempdir)
497 cmd := exec.Command("cadaver", s.srv.URL+path)
499 cmd.Env = append(os.Environ(), "HOME="+tempdir)
500 f, err := os.OpenFile(filepath.Join(tempdir, ".netrc"), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600)
501 c.Assert(err, check.IsNil)
502 _, err = fmt.Fprintf(f, "default login none password %s\n", password)
503 c.Assert(err, check.IsNil)
504 c.Assert(f.Close(), check.IsNil)
506 cmd.Stdin = bytes.NewBufferString(stdin)
508 stdout, err := cmd.StdoutPipe()
509 c.Assert(err, check.Equals, nil)
510 cmd.Stderr = cmd.Stdout
511 c.Logf("cmd: %v", cmd.Args)
515 _, err = io.Copy(&buf, stdout)
516 c.Check(err, check.Equals, nil)
518 c.Check(err, check.Equals, nil)
522 func (s *ContainerGatewaySuite) TestConnect(c *check.C) {
523 c.Logf("connecting to %s", s.gw.Address)
524 sshconn, err := s.localdb.ContainerSSH(s.userctx, arvados.ContainerSSHOptions{UUID: s.ctrUUID})
525 c.Assert(err, check.IsNil)
526 c.Assert(sshconn.Conn, check.NotNil)
527 defer sshconn.Conn.Close()
529 done := make(chan struct{})
533 // Receive text banner
534 buf := make([]byte, 12)
535 _, err := io.ReadFull(sshconn.Conn, buf)
536 c.Check(err, check.IsNil)
537 c.Check(string(buf), check.Equals, "SSH-2.0-Go\r\n")
540 _, err = sshconn.Conn.Write([]byte("SSH-2.0-Fake\r\n"))
541 c.Check(err, check.IsNil)
544 _, err = io.ReadFull(sshconn.Conn, buf[:4])
545 c.Check(err, check.IsNil)
547 // If we can get this far into an SSH handshake...
548 c.Logf("was able to read %x -- success, tunnel is working", buf[:4])
552 case <-time.After(time.Second):
555 ctr, err := s.localdb.ContainerGet(s.userctx, arvados.GetOptions{UUID: s.ctrUUID})
556 c.Check(err, check.IsNil)
557 c.Check(ctr.InteractiveSessionStarted, check.Equals, true)
560 func (s *ContainerGatewaySuite) TestConnectFail(c *check.C) {
561 c.Log("trying with no token")
562 ctx := ctrlctx.NewWithToken(s.ctx, s.cluster, "")
563 _, err := s.localdb.ContainerSSH(ctx, arvados.ContainerSSHOptions{UUID: s.ctrUUID})
564 c.Check(err, check.ErrorMatches, `.* 401 .*`)
566 c.Log("trying with anonymous token")
567 ctx = ctrlctx.NewWithToken(s.ctx, s.cluster, arvadostest.AnonymousToken)
568 _, err = s.localdb.ContainerSSH(ctx, arvados.ContainerSSHOptions{UUID: s.ctrUUID})
569 c.Check(err, check.ErrorMatches, `.* 404 .*`)
572 func (s *ContainerGatewaySuite) TestCreateTunnel(c *check.C) {
574 conn, err := s.localdb.ContainerGatewayTunnel(s.userctx, arvados.ContainerGatewayTunnelOptions{
577 c.Check(err, check.ErrorMatches, `authentication error`)
578 c.Check(conn.Conn, check.IsNil)
581 conn, err = s.localdb.ContainerGatewayTunnel(s.userctx, arvados.ContainerGatewayTunnelOptions{
583 AuthSecret: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
585 c.Check(err, check.ErrorMatches, `authentication error`)
586 c.Check(conn.Conn, check.IsNil)
589 conn, err = s.localdb.ContainerGatewayTunnel(s.userctx, arvados.ContainerGatewayTunnelOptions{
591 AuthSecret: s.gw.AuthSecret,
593 c.Check(err, check.IsNil)
594 c.Check(conn.Conn, check.NotNil)
597 func (s *ContainerGatewaySuite) TestConnectThroughTunnelWithProxyOK(c *check.C) {
598 forceProxyForTest = true
599 defer func() { forceProxyForTest = false }()
600 s.cluster.Services.Controller.InternalURLs[*forceInternalURLForTest] = arvados.ServiceInstance{}
601 defer delete(s.cluster.Services.Controller.InternalURLs, *forceInternalURLForTest)
602 s.testConnectThroughTunnel(c, "")
605 func (s *ContainerGatewaySuite) TestConnectThroughTunnelWithProxyError(c *check.C) {
606 forceProxyForTest = true
607 defer func() { forceProxyForTest = false }()
608 // forceInternalURLForTest will not be usable because it isn't
609 // listed in s.cluster.Services.Controller.InternalURLs
610 s.testConnectThroughTunnel(c, `.*tunnel endpoint is invalid.*`)
613 func (s *ContainerGatewaySuite) TestConnectThroughTunnelNoProxyOK(c *check.C) {
614 s.testConnectThroughTunnel(c, "")
617 func (s *ContainerGatewaySuite) setupGatewayWithTunnel(c *check.C) *crunchrun.Gateway {
618 rootctx := ctrlctx.NewWithToken(s.ctx, s.cluster, s.cluster.SystemRootToken)
619 // Until the tunnel starts up, set gateway_address to a value
620 // that can't work. We want to ensure the only way we can
621 // reach the gateway is through the tunnel.
622 tungw := &crunchrun.Gateway{
623 ContainerUUID: s.ctrUUID,
624 AuthSecret: s.gw.AuthSecret,
625 Log: ctxlog.TestLogger(c),
626 Target: crunchrun.GatewayTargetStub{},
627 ArvadosClient: s.gw.ArvadosClient,
628 UpdateTunnelURL: func(url string) {
629 c.Logf("UpdateTunnelURL(%q)", url)
630 gwaddr := "tunnel " + url
631 s.localdb.ContainerUpdate(rootctx, arvados.UpdateOptions{
633 Attrs: map[string]interface{}{
634 "gateway_address": gwaddr}})
637 c.Assert(tungw.Start(), check.IsNil)
639 // We didn't supply an external hostname in the Address field,
640 // so Start() should assign a local address.
641 host, _, err := net.SplitHostPort(tungw.Address)
642 c.Assert(err, check.IsNil)
643 c.Check(host, check.Equals, "127.0.0.1")
645 _, err = s.localdb.ContainerUpdate(rootctx, arvados.UpdateOptions{
647 Attrs: map[string]interface{}{
648 "state": arvados.ContainerStateRunning,
650 c.Assert(err, check.IsNil)
652 for deadline := time.Now().Add(5 * time.Second); time.Now().Before(deadline); time.Sleep(time.Second / 2) {
653 ctr, err := s.localdb.ContainerGet(s.userctx, arvados.GetOptions{UUID: s.ctrUUID})
654 c.Assert(err, check.IsNil)
655 c.Check(ctr.InteractiveSessionStarted, check.Equals, false)
656 c.Logf("ctr.GatewayAddress == %s", ctr.GatewayAddress)
657 if strings.HasPrefix(ctr.GatewayAddress, "tunnel ") {
664 func (s *ContainerGatewaySuite) testConnectThroughTunnel(c *check.C, expectErrorMatch string) {
665 s.setupGatewayWithTunnel(c)
666 c.Log("connecting to gateway through tunnel")
667 arpc := rpc.NewConn("", &url.URL{Scheme: "https", Host: s.gw.ArvadosClient.APIHost}, true, rpc.PassthroughTokenProvider)
668 sshconn, err := arpc.ContainerSSH(s.userctx, arvados.ContainerSSHOptions{UUID: s.ctrUUID})
669 if expectErrorMatch != "" {
670 c.Check(err, check.ErrorMatches, expectErrorMatch)
673 c.Assert(err, check.IsNil)
674 c.Assert(sshconn.Conn, check.NotNil)
675 defer sshconn.Conn.Close()
677 done := make(chan struct{})
681 // Receive text banner
682 buf := make([]byte, 12)
683 _, err := io.ReadFull(sshconn.Conn, buf)
684 c.Check(err, check.IsNil)
685 c.Check(string(buf), check.Equals, "SSH-2.0-Go\r\n")
688 _, err = sshconn.Conn.Write([]byte("SSH-2.0-Fake\r\n"))
689 c.Check(err, check.IsNil)
692 _, err = io.ReadFull(sshconn.Conn, buf[:4])
693 c.Check(err, check.IsNil)
695 // If we can get this far into an SSH handshake...
696 c.Logf("was able to read %x -- success, tunnel is working", buf[:4])
700 case <-time.After(time.Second):
703 ctr, err := s.localdb.ContainerGet(s.userctx, arvados.GetOptions{UUID: s.ctrUUID})
704 c.Check(err, check.IsNil)
705 c.Check(ctr.InteractiveSessionStarted, check.Equals, true)