1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
24 "git.arvados.org/arvados.git/lib/controller/router"
25 "git.arvados.org/arvados.git/lib/controller/rpc"
26 "git.arvados.org/arvados.git/lib/crunchrun"
27 "git.arvados.org/arvados.git/lib/ctrlctx"
28 "git.arvados.org/arvados.git/sdk/go/arvados"
29 "git.arvados.org/arvados.git/sdk/go/arvadosclient"
30 "git.arvados.org/arvados.git/sdk/go/arvadostest"
31 "git.arvados.org/arvados.git/sdk/go/ctxlog"
32 "git.arvados.org/arvados.git/sdk/go/httpserver"
33 "git.arvados.org/arvados.git/sdk/go/keepclient"
34 "golang.org/x/crypto/ssh"
35 check "gopkg.in/check.v1"
38 var _ = check.Suite(&ContainerGatewaySuite{})
40 type ContainerGatewaySuite struct {
47 func (s *ContainerGatewaySuite) SetUpTest(c *check.C) {
48 s.localdbSuite.SetUpTest(c)
50 s.ctrUUID = arvadostest.QueuedContainerUUID
52 h := hmac.New(sha256.New, []byte(s.cluster.SystemRootToken))
53 fmt.Fprint(h, s.ctrUUID)
54 authKey := fmt.Sprintf("%x", h.Sum(nil))
56 rtr := router.New(s.localdb, router.Config{})
57 s.srv = httptest.NewUnstartedServer(httpserver.AddRequestIDs(httpserver.LogRequests(rtr)))
59 // the test setup doesn't use lib/service so
60 // service.URLFromContext() returns nothing -- instead, this
61 // is how we advertise our internal URL and enable
62 // proxy-to-other-controller mode,
63 forceInternalURLForTest = &arvados.URL{Scheme: "https", Host: s.srv.Listener.Addr().String()}
64 ac := &arvados.Client{
65 APIHost: s.srv.Listener.Addr().String(),
66 AuthToken: arvadostest.Dispatch1Token,
69 s.gw = &crunchrun.Gateway{
70 ContainerUUID: s.ctrUUID,
72 Address: "localhost:0",
73 Log: ctxlog.TestLogger(c),
74 Target: crunchrun.GatewayTargetStub{},
77 c.Assert(s.gw.Start(), check.IsNil)
78 rootctx := ctrlctx.NewWithToken(s.ctx, s.cluster, s.cluster.SystemRootToken)
79 // OK if this line fails (because state is already Running
80 // from a previous test case) as long as the following line
82 s.localdb.ContainerUpdate(rootctx, arvados.UpdateOptions{
84 Attrs: map[string]interface{}{
85 "state": arvados.ContainerStateLocked}})
86 _, err := s.localdb.ContainerUpdate(rootctx, arvados.UpdateOptions{
88 Attrs: map[string]interface{}{
89 "state": arvados.ContainerStateRunning,
90 "gateway_address": s.gw.Address}})
91 c.Assert(err, check.IsNil)
93 s.cluster.Containers.ShellAccess.Admin = true
94 s.cluster.Containers.ShellAccess.User = true
95 _, err = s.db.Exec(`update containers set interactive_session_started=$1 where uuid=$2`, false, s.ctrUUID)
96 c.Check(err, check.IsNil)
99 func (s *ContainerGatewaySuite) TearDownTest(c *check.C) {
101 s.localdbSuite.TearDownTest(c)
104 func (s *ContainerGatewaySuite) TestConfig(c *check.C) {
105 for _, trial := range []struct {
111 {true, true, arvadostest.ActiveTokenV2, 0},
112 {true, false, arvadostest.ActiveTokenV2, 503},
113 {false, true, arvadostest.ActiveTokenV2, 0},
114 {false, false, arvadostest.ActiveTokenV2, 503},
115 {true, true, arvadostest.AdminToken, 0},
116 {true, false, arvadostest.AdminToken, 0},
117 {false, true, arvadostest.AdminToken, 403},
118 {false, false, arvadostest.AdminToken, 503},
120 c.Logf("trial %#v", trial)
121 s.cluster.Containers.ShellAccess.Admin = trial.configAdmin
122 s.cluster.Containers.ShellAccess.User = trial.configUser
123 ctx := ctrlctx.NewWithToken(s.ctx, s.cluster, trial.sendToken)
124 sshconn, err := s.localdb.ContainerSSH(ctx, arvados.ContainerSSHOptions{UUID: s.ctrUUID})
125 if trial.errorCode == 0 {
126 if !c.Check(err, check.IsNil) {
129 if !c.Check(sshconn.Conn, check.NotNil) {
134 c.Check(err, check.NotNil)
135 err, ok := err.(interface{ HTTPStatus() int })
136 if c.Check(ok, check.Equals, true) {
137 c.Check(err.HTTPStatus(), check.Equals, trial.errorCode)
143 func (s *ContainerGatewaySuite) TestDirectTCP(c *check.C) {
144 // Set up servers on a few TCP ports
146 for i := 0; i < 3; i++ {
147 ln, err := net.Listen("tcp", ":0")
148 c.Assert(err, check.IsNil)
150 addrs = append(addrs, ln.Addr().String())
153 conn, err := ln.Accept()
158 fmt.Fscanf(conn, "%s\n", &gotAddr)
159 c.Logf("stub server listening at %s received string %q from remote %s", ln.Addr().String(), gotAddr, conn.RemoteAddr())
160 if gotAddr == ln.Addr().String() {
161 fmt.Fprintf(conn, "%s\n", ln.Addr().String())
168 c.Logf("connecting to %s", s.gw.Address)
169 sshconn, err := s.localdb.ContainerSSH(s.userctx, arvados.ContainerSSHOptions{UUID: s.ctrUUID})
170 c.Assert(err, check.IsNil)
171 c.Assert(sshconn.Conn, check.NotNil)
172 defer sshconn.Conn.Close()
173 conn, chans, reqs, err := ssh.NewClientConn(sshconn.Conn, "zzzz-dz642-abcdeabcdeabcde", &ssh.ClientConfig{
174 HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error { return nil },
176 c.Assert(err, check.IsNil)
177 client := ssh.NewClient(conn, chans, reqs)
178 for _, expectAddr := range addrs {
179 _, port, err := net.SplitHostPort(expectAddr)
180 c.Assert(err, check.IsNil)
182 c.Logf("trying foo:%s", port)
184 conn, err := client.Dial("tcp", "foo:"+port)
185 c.Assert(err, check.IsNil)
186 conn.SetDeadline(time.Now().Add(time.Second))
187 buf, err := ioutil.ReadAll(conn)
188 c.Check(err, check.IsNil)
189 c.Check(string(buf), check.Equals, "")
192 c.Logf("trying localhost:%s", port)
194 conn, err := client.Dial("tcp", "localhost:"+port)
195 c.Assert(err, check.IsNil)
196 conn.SetDeadline(time.Now().Add(time.Second))
197 conn.Write([]byte(expectAddr + "\n"))
199 fmt.Fscanf(conn, "%s\n", &gotAddr)
200 c.Check(gotAddr, check.Equals, expectAddr)
205 func (s *ContainerGatewaySuite) setupLogCollection(c *check.C) {
206 files := map[string]string{
207 "stderr.txt": "hello world\n",
208 "a/b/c/d.html": "<html></html>\n",
210 client := arvados.NewClientFromEnv()
211 ac, err := arvadosclient.New(client)
212 c.Assert(err, check.IsNil)
213 kc, err := keepclient.MakeKeepClient(ac)
214 c.Assert(err, check.IsNil)
215 cfs, err := (&arvados.Collection{}).FileSystem(client, kc)
216 c.Assert(err, check.IsNil)
217 for name, content := range files {
218 for i, ch := range name {
220 err := cfs.Mkdir("/"+name[:i], 0777)
221 c.Assert(err, check.IsNil)
224 f, err := cfs.OpenFile("/"+name, os.O_CREATE|os.O_WRONLY, 0777)
225 c.Assert(err, check.IsNil)
226 f.Write([]byte(content))
228 c.Assert(err, check.IsNil)
231 s.gw.LogCollection = cfs
234 func (s *ContainerGatewaySuite) saveLogAndCloseGateway(c *check.C) {
235 rootctx := ctrlctx.NewWithToken(s.ctx, s.cluster, s.cluster.SystemRootToken)
236 txt, err := s.gw.LogCollection.MarshalManifest(".")
237 c.Assert(err, check.IsNil)
238 coll, err := s.localdb.CollectionCreate(rootctx, arvados.CreateOptions{
239 Attrs: map[string]interface{}{
240 "manifest_text": txt,
242 c.Assert(err, check.IsNil)
243 _, err = s.localdb.ContainerUpdate(rootctx, arvados.UpdateOptions{
245 Attrs: map[string]interface{}{
246 "log": coll.PortableDataHash,
247 "gateway_address": "",
249 c.Assert(err, check.IsNil)
250 // gateway_address="" above already ensures localdb
251 // can't circumvent the keep-web proxy test by getting
252 // content from the container gateway; this is just
254 s.gw.LogCollection = nil
257 func (s *ContainerGatewaySuite) TestContainerLogViaTunnel(c *check.C) {
258 forceProxyForTest = true
259 defer func() { forceProxyForTest = false }()
261 s.gw = s.setupGatewayWithTunnel(c)
262 s.setupLogCollection(c)
264 for _, broken := range []bool{false, true} {
265 c.Logf("broken=%v", broken)
268 delete(s.cluster.Services.Controller.InternalURLs, *forceInternalURLForTest)
270 s.cluster.Services.Controller.InternalURLs[*forceInternalURLForTest] = arvados.ServiceInstance{}
271 defer delete(s.cluster.Services.Controller.InternalURLs, *forceInternalURLForTest)
274 handler, err := s.localdb.ContainerLog(s.userctx, arvados.ContainerLogOptions{
276 WebDAVOptions: arvados.WebDAVOptions{Path: "/stderr.txt"},
279 c.Check(err, check.ErrorMatches, `.*tunnel endpoint is invalid.*`)
282 c.Check(err, check.IsNil)
283 c.Assert(handler, check.NotNil)
284 r, err := http.NewRequestWithContext(s.userctx, "GET", "https://controller.example/arvados/v1/containers/"+s.ctrUUID+"/log/stderr.txt", nil)
285 c.Assert(err, check.IsNil)
286 r.Header.Set("Authorization", "Bearer "+arvadostest.ActiveTokenV2)
287 rec := httptest.NewRecorder()
288 handler.ServeHTTP(rec, r)
290 c.Check(resp.StatusCode, check.Equals, http.StatusOK)
291 buf, err := ioutil.ReadAll(resp.Body)
292 c.Check(err, check.IsNil)
293 c.Check(string(buf), check.Equals, "hello world\n")
297 func (s *ContainerGatewaySuite) TestContainerLogViaGateway(c *check.C) {
298 s.setupLogCollection(c)
299 s.testContainerLog(c)
302 func (s *ContainerGatewaySuite) TestContainerLogViaKeepWeb(c *check.C) {
303 s.setupLogCollection(c)
304 s.saveLogAndCloseGateway(c)
305 s.testContainerLog(c)
308 func (s *ContainerGatewaySuite) testContainerLog(c *check.C) {
309 for _, trial := range []struct {
315 expectHeader http.Header
320 expectStatus: http.StatusOK,
321 expectBodyRe: "hello world\n",
322 expectHeader: http.Header{
323 "Content-Type": {"text/plain; charset=utf-8"},
330 "Range": {"bytes=-6"},
332 expectStatus: http.StatusPartialContent,
333 expectBodyRe: "world\n",
334 expectHeader: http.Header{
335 "Content-Type": {"text/plain; charset=utf-8"},
336 "Content-Range": {"bytes 6-11/12"},
342 expectStatus: http.StatusOK,
344 expectHeader: http.Header{
346 "Allow": {"OPTIONS, LOCK, GET, HEAD, POST, DELETE, PROPPATCH, COPY, MOVE, UNLOCK, PROPFIND, PUT"},
352 expectStatus: http.StatusMultiStatus,
353 expectBodyRe: `.*\Q<D:displayname>stderr.txt</D:displayname>\E.*`,
354 expectHeader: http.Header{
355 "Content-Type": {"text/xml; charset=utf-8"},
361 expectStatus: http.StatusMultiStatus,
362 expectBodyRe: `.*\Q<D:displayname>d.html</D:displayname>\E.*`,
363 expectHeader: http.Header{
364 "Content-Type": {"text/xml; charset=utf-8"},
369 path: "/a/b/c/d.html",
370 expectStatus: http.StatusOK,
371 expectBodyRe: "<html></html>\n",
372 expectHeader: http.Header{
373 "Content-Type": {"text/html; charset=utf-8"},
377 c.Logf("trial %#v", trial)
378 handler, err := s.localdb.ContainerLog(s.userctx, arvados.ContainerLogOptions{
380 WebDAVOptions: arvados.WebDAVOptions{Path: trial.path},
382 c.Assert(err, check.IsNil)
383 c.Assert(handler, check.NotNil)
384 r, err := http.NewRequestWithContext(s.userctx, trial.method, "https://controller.example/arvados/v1/containers/"+s.ctrUUID+"/log"+trial.path, nil)
385 c.Assert(err, check.IsNil)
386 for k := range trial.header {
387 r.Header.Set(k, trial.header.Get(k))
389 rec := httptest.NewRecorder()
390 handler.ServeHTTP(rec, r)
392 c.Check(resp.StatusCode, check.Equals, trial.expectStatus)
393 for k := range trial.expectHeader {
394 c.Check(resp.Header.Get(k), check.Equals, trial.expectHeader.Get(k))
396 buf, err := ioutil.ReadAll(resp.Body)
397 c.Check(err, check.IsNil)
398 c.Check(string(buf), check.Matches, trial.expectBodyRe)
402 func (s *ContainerGatewaySuite) TestContainerLogViaCadaver(c *check.C) {
403 s.setupLogCollection(c)
405 out := s.runCadaver(c, arvadostest.ActiveToken, "/arvados/v1/containers/"+s.ctrUUID+"/log", "ls")
406 c.Check(out, check.Matches, `(?ms).*stderr\.txt\s+12\s.*`)
407 c.Check(out, check.Matches, `(?ms).*a\s+0\s.*`)
409 out = s.runCadaver(c, arvadostest.ActiveTokenV2, "/arvados/v1/containers/"+s.ctrUUID+"/log", "get stderr.txt")
410 c.Check(out, check.Matches, `(?ms).*Downloading .* to stderr\.txt: .* succeeded\..*`)
412 s.saveLogAndCloseGateway(c)
414 out = s.runCadaver(c, arvadostest.ActiveTokenV2, "/arvados/v1/containers/"+s.ctrUUID+"/log", "get stderr.txt")
415 c.Check(out, check.Matches, `(?ms).*Downloading .* to stderr\.txt: .* succeeded\..*`)
418 func (s *ContainerGatewaySuite) runCadaver(c *check.C, password, path, stdin string) string {
419 // Replace s.srv with an HTTP server, otherwise cadaver will
420 // just fail on TLS cert verification.
422 rtr := router.New(s.localdb, router.Config{})
423 s.srv = httptest.NewUnstartedServer(httpserver.AddRequestIDs(httpserver.LogRequests(rtr)))
426 tempdir, err := ioutil.TempDir("", "localdb-test-")
427 c.Assert(err, check.IsNil)
428 defer os.RemoveAll(tempdir)
430 cmd := exec.Command("cadaver", s.srv.URL+path)
432 cmd.Env = append(os.Environ(), "HOME="+tempdir)
433 f, err := os.OpenFile(filepath.Join(tempdir, ".netrc"), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600)
434 c.Assert(err, check.IsNil)
435 _, err = fmt.Fprintf(f, "default login none password %s\n", password)
436 c.Assert(err, check.IsNil)
437 c.Assert(f.Close(), check.IsNil)
439 cmd.Stdin = bytes.NewBufferString(stdin)
441 stdout, err := cmd.StdoutPipe()
442 c.Assert(err, check.Equals, nil)
443 cmd.Stderr = cmd.Stdout
444 c.Logf("cmd: %v", cmd.Args)
448 _, err = io.Copy(&buf, stdout)
449 c.Check(err, check.Equals, nil)
451 c.Check(err, check.Equals, nil)
455 func (s *ContainerGatewaySuite) TestConnect(c *check.C) {
456 c.Logf("connecting to %s", s.gw.Address)
457 sshconn, err := s.localdb.ContainerSSH(s.userctx, arvados.ContainerSSHOptions{UUID: s.ctrUUID})
458 c.Assert(err, check.IsNil)
459 c.Assert(sshconn.Conn, check.NotNil)
460 defer sshconn.Conn.Close()
462 done := make(chan struct{})
466 // Receive text banner
467 buf := make([]byte, 12)
468 _, err := io.ReadFull(sshconn.Conn, buf)
469 c.Check(err, check.IsNil)
470 c.Check(string(buf), check.Equals, "SSH-2.0-Go\r\n")
473 _, err = sshconn.Conn.Write([]byte("SSH-2.0-Fake\r\n"))
474 c.Check(err, check.IsNil)
477 _, err = io.ReadFull(sshconn.Conn, buf[:4])
478 c.Check(err, check.IsNil)
480 // If we can get this far into an SSH handshake...
481 c.Logf("was able to read %x -- success, tunnel is working", buf[:4])
485 case <-time.After(time.Second):
488 ctr, err := s.localdb.ContainerGet(s.userctx, arvados.GetOptions{UUID: s.ctrUUID})
489 c.Check(err, check.IsNil)
490 c.Check(ctr.InteractiveSessionStarted, check.Equals, true)
493 func (s *ContainerGatewaySuite) TestConnectFail(c *check.C) {
494 c.Log("trying with no token")
495 ctx := ctrlctx.NewWithToken(s.ctx, s.cluster, "")
496 _, err := s.localdb.ContainerSSH(ctx, arvados.ContainerSSHOptions{UUID: s.ctrUUID})
497 c.Check(err, check.ErrorMatches, `.* 401 .*`)
499 c.Log("trying with anonymous token")
500 ctx = ctrlctx.NewWithToken(s.ctx, s.cluster, arvadostest.AnonymousToken)
501 _, err = s.localdb.ContainerSSH(ctx, arvados.ContainerSSHOptions{UUID: s.ctrUUID})
502 c.Check(err, check.ErrorMatches, `.* 404 .*`)
505 func (s *ContainerGatewaySuite) TestCreateTunnel(c *check.C) {
507 conn, err := s.localdb.ContainerGatewayTunnel(s.userctx, arvados.ContainerGatewayTunnelOptions{
510 c.Check(err, check.ErrorMatches, `authentication error`)
511 c.Check(conn.Conn, check.IsNil)
514 conn, err = s.localdb.ContainerGatewayTunnel(s.userctx, arvados.ContainerGatewayTunnelOptions{
516 AuthSecret: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
518 c.Check(err, check.ErrorMatches, `authentication error`)
519 c.Check(conn.Conn, check.IsNil)
522 conn, err = s.localdb.ContainerGatewayTunnel(s.userctx, arvados.ContainerGatewayTunnelOptions{
524 AuthSecret: s.gw.AuthSecret,
526 c.Check(err, check.IsNil)
527 c.Check(conn.Conn, check.NotNil)
530 func (s *ContainerGatewaySuite) TestConnectThroughTunnelWithProxyOK(c *check.C) {
531 forceProxyForTest = true
532 defer func() { forceProxyForTest = false }()
533 s.cluster.Services.Controller.InternalURLs[*forceInternalURLForTest] = arvados.ServiceInstance{}
534 defer delete(s.cluster.Services.Controller.InternalURLs, *forceInternalURLForTest)
535 s.testConnectThroughTunnel(c, "")
538 func (s *ContainerGatewaySuite) TestConnectThroughTunnelWithProxyError(c *check.C) {
539 forceProxyForTest = true
540 defer func() { forceProxyForTest = false }()
541 // forceInternalURLForTest will not be usable because it isn't
542 // listed in s.cluster.Services.Controller.InternalURLs
543 s.testConnectThroughTunnel(c, `.*tunnel endpoint is invalid.*`)
546 func (s *ContainerGatewaySuite) TestConnectThroughTunnelNoProxyOK(c *check.C) {
547 s.testConnectThroughTunnel(c, "")
550 func (s *ContainerGatewaySuite) setupGatewayWithTunnel(c *check.C) *crunchrun.Gateway {
551 rootctx := ctrlctx.NewWithToken(s.ctx, s.cluster, s.cluster.SystemRootToken)
552 // Until the tunnel starts up, set gateway_address to a value
553 // that can't work. We want to ensure the only way we can
554 // reach the gateway is through the tunnel.
555 tungw := &crunchrun.Gateway{
556 ContainerUUID: s.ctrUUID,
557 AuthSecret: s.gw.AuthSecret,
558 Log: ctxlog.TestLogger(c),
559 Target: crunchrun.GatewayTargetStub{},
560 ArvadosClient: s.gw.ArvadosClient,
561 UpdateTunnelURL: func(url string) {
562 c.Logf("UpdateTunnelURL(%q)", url)
563 gwaddr := "tunnel " + url
564 s.localdb.ContainerUpdate(rootctx, arvados.UpdateOptions{
566 Attrs: map[string]interface{}{
567 "gateway_address": gwaddr}})
570 c.Assert(tungw.Start(), check.IsNil)
572 // We didn't supply an external hostname in the Address field,
573 // so Start() should assign a local address.
574 host, _, err := net.SplitHostPort(tungw.Address)
575 c.Assert(err, check.IsNil)
576 c.Check(host, check.Equals, "127.0.0.1")
578 _, err = s.localdb.ContainerUpdate(rootctx, arvados.UpdateOptions{
580 Attrs: map[string]interface{}{
581 "state": arvados.ContainerStateRunning,
583 c.Assert(err, check.IsNil)
585 for deadline := time.Now().Add(5 * time.Second); time.Now().Before(deadline); time.Sleep(time.Second / 2) {
586 ctr, err := s.localdb.ContainerGet(s.userctx, arvados.GetOptions{UUID: s.ctrUUID})
587 c.Assert(err, check.IsNil)
588 c.Check(ctr.InteractiveSessionStarted, check.Equals, false)
589 c.Logf("ctr.GatewayAddress == %s", ctr.GatewayAddress)
590 if strings.HasPrefix(ctr.GatewayAddress, "tunnel ") {
597 func (s *ContainerGatewaySuite) testConnectThroughTunnel(c *check.C, expectErrorMatch string) {
598 s.setupGatewayWithTunnel(c)
599 c.Log("connecting to gateway through tunnel")
600 arpc := rpc.NewConn("", &url.URL{Scheme: "https", Host: s.gw.ArvadosClient.APIHost}, true, rpc.PassthroughTokenProvider)
601 sshconn, err := arpc.ContainerSSH(s.userctx, arvados.ContainerSSHOptions{UUID: s.ctrUUID})
602 if expectErrorMatch != "" {
603 c.Check(err, check.ErrorMatches, expectErrorMatch)
606 c.Assert(err, check.IsNil)
607 c.Assert(sshconn.Conn, check.NotNil)
608 defer sshconn.Conn.Close()
610 done := make(chan struct{})
614 // Receive text banner
615 buf := make([]byte, 12)
616 _, err := io.ReadFull(sshconn.Conn, buf)
617 c.Check(err, check.IsNil)
618 c.Check(string(buf), check.Equals, "SSH-2.0-Go\r\n")
621 _, err = sshconn.Conn.Write([]byte("SSH-2.0-Fake\r\n"))
622 c.Check(err, check.IsNil)
625 _, err = io.ReadFull(sshconn.Conn, buf[:4])
626 c.Check(err, check.IsNil)
628 // If we can get this far into an SSH handshake...
629 c.Logf("was able to read %x -- success, tunnel is working", buf[:4])
633 case <-time.After(time.Second):
636 ctr, err := s.localdb.ContainerGet(s.userctx, arvados.GetOptions{UUID: s.ctrUUID})
637 c.Check(err, check.IsNil)
638 c.Check(ctr.InteractiveSessionStarted, check.Equals, true)