1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
21 "git.arvados.org/arvados.git/lib/controller/router"
22 "git.arvados.org/arvados.git/lib/controller/rpc"
23 "git.arvados.org/arvados.git/lib/crunchrun"
24 "git.arvados.org/arvados.git/lib/ctrlctx"
25 "git.arvados.org/arvados.git/sdk/go/arvados"
26 "git.arvados.org/arvados.git/sdk/go/arvadosclient"
27 "git.arvados.org/arvados.git/sdk/go/arvadostest"
28 "git.arvados.org/arvados.git/sdk/go/ctxlog"
29 "git.arvados.org/arvados.git/sdk/go/keepclient"
30 "golang.org/x/crypto/ssh"
31 check "gopkg.in/check.v1"
34 var _ = check.Suite(&ContainerGatewaySuite{})
36 type ContainerGatewaySuite struct {
42 func (s *ContainerGatewaySuite) SetUpTest(c *check.C) {
43 s.localdbSuite.SetUpTest(c)
45 s.ctrUUID = arvadostest.QueuedContainerUUID
47 h := hmac.New(sha256.New, []byte(s.cluster.SystemRootToken))
48 fmt.Fprint(h, s.ctrUUID)
49 authKey := fmt.Sprintf("%x", h.Sum(nil))
51 rtr := router.New(s.localdb, router.Config{})
52 srv := httptest.NewUnstartedServer(rtr)
54 // the test setup doesn't use lib/service so
55 // service.URLFromContext() returns nothing -- instead, this
56 // is how we advertise our internal URL and enable
57 // proxy-to-other-controller mode,
58 forceInternalURLForTest = &arvados.URL{Scheme: "https", Host: srv.Listener.Addr().String()}
59 ac := &arvados.Client{
60 APIHost: srv.Listener.Addr().String(),
61 AuthToken: arvadostest.Dispatch1Token,
64 s.gw = &crunchrun.Gateway{
65 ContainerUUID: s.ctrUUID,
67 Address: "localhost:0",
68 Log: ctxlog.TestLogger(c),
69 Target: crunchrun.GatewayTargetStub{},
72 c.Assert(s.gw.Start(), check.IsNil)
73 rootctx := ctrlctx.NewWithToken(s.ctx, s.cluster, s.cluster.SystemRootToken)
74 // OK if this line fails (because state is already Running
75 // from a previous test case) as long as the following line
77 s.localdb.ContainerUpdate(rootctx, arvados.UpdateOptions{
79 Attrs: map[string]interface{}{
80 "state": arvados.ContainerStateLocked}})
81 _, err := s.localdb.ContainerUpdate(rootctx, arvados.UpdateOptions{
83 Attrs: map[string]interface{}{
84 "state": arvados.ContainerStateRunning,
85 "gateway_address": s.gw.Address}})
86 c.Assert(err, check.IsNil)
88 s.cluster.Containers.ShellAccess.Admin = true
89 s.cluster.Containers.ShellAccess.User = true
90 _, err = s.db.Exec(`update containers set interactive_session_started=$1 where uuid=$2`, false, s.ctrUUID)
91 c.Check(err, check.IsNil)
94 func (s *ContainerGatewaySuite) TestConfig(c *check.C) {
95 for _, trial := range []struct {
101 {true, true, arvadostest.ActiveTokenV2, 0},
102 {true, false, arvadostest.ActiveTokenV2, 503},
103 {false, true, arvadostest.ActiveTokenV2, 0},
104 {false, false, arvadostest.ActiveTokenV2, 503},
105 {true, true, arvadostest.AdminToken, 0},
106 {true, false, arvadostest.AdminToken, 0},
107 {false, true, arvadostest.AdminToken, 403},
108 {false, false, arvadostest.AdminToken, 503},
110 c.Logf("trial %#v", trial)
111 s.cluster.Containers.ShellAccess.Admin = trial.configAdmin
112 s.cluster.Containers.ShellAccess.User = trial.configUser
113 ctx := ctrlctx.NewWithToken(s.ctx, s.cluster, trial.sendToken)
114 sshconn, err := s.localdb.ContainerSSH(ctx, arvados.ContainerSSHOptions{UUID: s.ctrUUID})
115 if trial.errorCode == 0 {
116 if !c.Check(err, check.IsNil) {
119 if !c.Check(sshconn.Conn, check.NotNil) {
124 c.Check(err, check.NotNil)
125 err, ok := err.(interface{ HTTPStatus() int })
126 if c.Check(ok, check.Equals, true) {
127 c.Check(err.HTTPStatus(), check.Equals, trial.errorCode)
133 func (s *ContainerGatewaySuite) TestDirectTCP(c *check.C) {
134 // Set up servers on a few TCP ports
136 for i := 0; i < 3; i++ {
137 ln, err := net.Listen("tcp", ":0")
138 c.Assert(err, check.IsNil)
140 addrs = append(addrs, ln.Addr().String())
143 conn, err := ln.Accept()
148 fmt.Fscanf(conn, "%s\n", &gotAddr)
149 c.Logf("stub server listening at %s received string %q from remote %s", ln.Addr().String(), gotAddr, conn.RemoteAddr())
150 if gotAddr == ln.Addr().String() {
151 fmt.Fprintf(conn, "%s\n", ln.Addr().String())
158 c.Logf("connecting to %s", s.gw.Address)
159 sshconn, err := s.localdb.ContainerSSH(s.userctx, arvados.ContainerSSHOptions{UUID: s.ctrUUID})
160 c.Assert(err, check.IsNil)
161 c.Assert(sshconn.Conn, check.NotNil)
162 defer sshconn.Conn.Close()
163 conn, chans, reqs, err := ssh.NewClientConn(sshconn.Conn, "zzzz-dz642-abcdeabcdeabcde", &ssh.ClientConfig{
164 HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error { return nil },
166 c.Assert(err, check.IsNil)
167 client := ssh.NewClient(conn, chans, reqs)
168 for _, expectAddr := range addrs {
169 _, port, err := net.SplitHostPort(expectAddr)
170 c.Assert(err, check.IsNil)
172 c.Logf("trying foo:%s", port)
174 conn, err := client.Dial("tcp", "foo:"+port)
175 c.Assert(err, check.IsNil)
176 conn.SetDeadline(time.Now().Add(time.Second))
177 buf, err := ioutil.ReadAll(conn)
178 c.Check(err, check.IsNil)
179 c.Check(string(buf), check.Equals, "")
182 c.Logf("trying localhost:%s", port)
184 conn, err := client.Dial("tcp", "localhost:"+port)
185 c.Assert(err, check.IsNil)
186 conn.SetDeadline(time.Now().Add(time.Second))
187 conn.Write([]byte(expectAddr + "\n"))
189 fmt.Fscanf(conn, "%s\n", &gotAddr)
190 c.Check(gotAddr, check.Equals, expectAddr)
195 func (s *ContainerGatewaySuite) setupLogCollection(c *check.C, files map[string]string) {
196 client := arvados.NewClientFromEnv()
197 ac, err := arvadosclient.New(client)
198 c.Assert(err, check.IsNil)
199 kc, err := keepclient.MakeKeepClient(ac)
200 c.Assert(err, check.IsNil)
201 cfs, err := (&arvados.Collection{}).FileSystem(client, kc)
202 c.Assert(err, check.IsNil)
203 for name, content := range files {
204 for i, ch := range name {
206 err := cfs.Mkdir("/"+name[:i], 0777)
207 c.Assert(err, check.IsNil)
210 f, err := cfs.OpenFile("/"+name, os.O_CREATE|os.O_WRONLY, 0777)
211 c.Assert(err, check.IsNil)
212 f.Write([]byte(content))
214 c.Assert(err, check.IsNil)
217 s.gw.LogCollection = cfs
220 func (s *ContainerGatewaySuite) TestContainerLogViaTunnel(c *check.C) {
221 forceProxyForTest = true
222 defer func() { forceProxyForTest = false }()
224 s.gw = s.setupGatewayWithTunnel(c)
225 s.setupLogCollection(c, map[string]string{
226 "stderr.txt": "hello world\n",
229 for _, broken := range []bool{false, true} {
230 c.Logf("broken=%v", broken)
233 delete(s.cluster.Services.Controller.InternalURLs, *forceInternalURLForTest)
235 s.cluster.Services.Controller.InternalURLs[*forceInternalURLForTest] = arvados.ServiceInstance{}
236 defer delete(s.cluster.Services.Controller.InternalURLs, *forceInternalURLForTest)
239 handler, err := s.localdb.ContainerLog(s.userctx, arvados.ContainerLogOptions{
241 WebDAVOptions: arvados.WebDAVOptions{Path: "/stderr.txt"},
244 c.Check(err, check.ErrorMatches, `.*tunnel endpoint is invalid.*`)
247 c.Check(err, check.IsNil)
248 c.Assert(handler, check.NotNil)
249 r, err := http.NewRequestWithContext(s.userctx, "GET", "https://controller.example/arvados/v1/containers/"+s.ctrUUID+"/log/stderr.txt", nil)
250 c.Assert(err, check.IsNil)
251 r.Header.Set("Authorization", "Bearer "+arvadostest.ActiveTokenV2)
252 rec := httptest.NewRecorder()
253 handler.ServeHTTP(rec, r)
255 c.Check(resp.StatusCode, check.Equals, http.StatusOK)
256 buf, err := ioutil.ReadAll(resp.Body)
257 c.Check(err, check.IsNil)
258 c.Check(string(buf), check.Equals, "hello world\n")
262 func (s *ContainerGatewaySuite) TestContainerLogViaGateway(c *check.C) {
263 s.testContainerLog(c, true)
266 func (s *ContainerGatewaySuite) TestContainerLogViaKeepWeb(c *check.C) {
267 s.testContainerLog(c, false)
270 func (s *ContainerGatewaySuite) testContainerLog(c *check.C, viaGateway bool) {
271 s.setupLogCollection(c, map[string]string{
272 "stderr.txt": "hello world\n",
273 "a/b/c/d.html": "<html></html>\n",
276 rootctx := ctrlctx.NewWithToken(s.ctx, s.cluster, s.cluster.SystemRootToken)
277 txt, err := s.gw.LogCollection.MarshalManifest(".")
278 c.Assert(err, check.IsNil)
279 coll, err := s.localdb.CollectionCreate(rootctx, arvados.CreateOptions{
280 Attrs: map[string]interface{}{
281 "manifest_text": txt,
283 c.Assert(err, check.IsNil)
284 _, err = s.localdb.ContainerUpdate(rootctx, arvados.UpdateOptions{
286 Attrs: map[string]interface{}{
287 "log": coll.PortableDataHash,
288 "gateway_address": "",
290 c.Assert(err, check.IsNil)
291 // _, err = s.localdb.ContainerUpdate(rootctx, arvados.UpdateOptions{
293 // Attrs: map[string]interface{}{
294 // "state": "Cancelled",
296 // c.Assert(err, check.IsNil)
297 // gateway_address="" above already ensures localdb
298 // can't circumvent the keep-web proxy test by getting
299 // content from the container gateway; this is just
301 s.gw.LogCollection = nil
303 for _, trial := range []struct {
309 expectHeader http.Header
314 expectStatus: http.StatusOK,
315 expectBodyRe: "hello world\n",
316 expectHeader: http.Header{
317 "Content-Type": {"text/plain; charset=utf-8"},
324 "Range": {"bytes=-6"},
326 expectStatus: http.StatusPartialContent,
327 expectBodyRe: "world\n",
328 expectHeader: http.Header{
329 "Content-Type": {"text/plain; charset=utf-8"},
330 "Content-Range": {"bytes 6-11/12"},
336 expectStatus: http.StatusOK,
338 expectHeader: http.Header{
340 "Allow": {"OPTIONS, LOCK, GET, HEAD, POST, DELETE, PROPPATCH, COPY, MOVE, UNLOCK, PROPFIND, PUT"},
346 expectStatus: http.StatusMultiStatus,
347 expectBodyRe: `.*\Q<D:displayname>stderr.txt</D:displayname>\E.*`,
348 expectHeader: http.Header{
349 "Content-Type": {"text/xml; charset=utf-8"},
355 expectStatus: http.StatusMultiStatus,
356 expectBodyRe: `.*\Q<D:displayname>d.html</D:displayname>\E.*`,
357 expectHeader: http.Header{
358 "Content-Type": {"text/xml; charset=utf-8"},
363 path: "/a/b/c/d.html",
364 expectStatus: http.StatusOK,
365 expectBodyRe: "<html></html>\n",
366 expectHeader: http.Header{
367 "Content-Type": {"text/html; charset=utf-8"},
371 c.Logf("trial %#v", trial)
372 handler, err := s.localdb.ContainerLog(s.userctx, arvados.ContainerLogOptions{
374 WebDAVOptions: arvados.WebDAVOptions{Path: trial.path},
376 c.Assert(err, check.IsNil)
377 c.Assert(handler, check.NotNil)
378 r, err := http.NewRequestWithContext(s.userctx, trial.method, "https://controller.example/arvados/v1/containers/"+s.ctrUUID+"/log"+trial.path, nil)
379 c.Assert(err, check.IsNil)
380 for k := range trial.header {
381 r.Header.Set(k, trial.header.Get(k))
383 rec := httptest.NewRecorder()
384 handler.ServeHTTP(rec, r)
386 c.Check(resp.StatusCode, check.Equals, trial.expectStatus)
387 for k := range trial.expectHeader {
388 c.Check(resp.Header.Get(k), check.Equals, trial.expectHeader.Get(k))
390 buf, err := ioutil.ReadAll(resp.Body)
391 c.Check(err, check.IsNil)
392 c.Check(string(buf), check.Matches, trial.expectBodyRe)
396 func (s *ContainerGatewaySuite) TestConnect(c *check.C) {
397 c.Logf("connecting to %s", s.gw.Address)
398 sshconn, err := s.localdb.ContainerSSH(s.userctx, arvados.ContainerSSHOptions{UUID: s.ctrUUID})
399 c.Assert(err, check.IsNil)
400 c.Assert(sshconn.Conn, check.NotNil)
401 defer sshconn.Conn.Close()
403 done := make(chan struct{})
407 // Receive text banner
408 buf := make([]byte, 12)
409 _, err := io.ReadFull(sshconn.Conn, buf)
410 c.Check(err, check.IsNil)
411 c.Check(string(buf), check.Equals, "SSH-2.0-Go\r\n")
414 _, err = sshconn.Conn.Write([]byte("SSH-2.0-Fake\r\n"))
415 c.Check(err, check.IsNil)
418 _, err = io.ReadFull(sshconn.Conn, buf[:4])
419 c.Check(err, check.IsNil)
421 // If we can get this far into an SSH handshake...
422 c.Logf("was able to read %x -- success, tunnel is working", buf[:4])
426 case <-time.After(time.Second):
429 ctr, err := s.localdb.ContainerGet(s.userctx, arvados.GetOptions{UUID: s.ctrUUID})
430 c.Check(err, check.IsNil)
431 c.Check(ctr.InteractiveSessionStarted, check.Equals, true)
434 func (s *ContainerGatewaySuite) TestConnectFail(c *check.C) {
435 c.Log("trying with no token")
436 ctx := ctrlctx.NewWithToken(s.ctx, s.cluster, "")
437 _, err := s.localdb.ContainerSSH(ctx, arvados.ContainerSSHOptions{UUID: s.ctrUUID})
438 c.Check(err, check.ErrorMatches, `.* 401 .*`)
440 c.Log("trying with anonymous token")
441 ctx = ctrlctx.NewWithToken(s.ctx, s.cluster, arvadostest.AnonymousToken)
442 _, err = s.localdb.ContainerSSH(ctx, arvados.ContainerSSHOptions{UUID: s.ctrUUID})
443 c.Check(err, check.ErrorMatches, `.* 404 .*`)
446 func (s *ContainerGatewaySuite) TestCreateTunnel(c *check.C) {
448 conn, err := s.localdb.ContainerGatewayTunnel(s.userctx, arvados.ContainerGatewayTunnelOptions{
451 c.Check(err, check.ErrorMatches, `authentication error`)
452 c.Check(conn.Conn, check.IsNil)
455 conn, err = s.localdb.ContainerGatewayTunnel(s.userctx, arvados.ContainerGatewayTunnelOptions{
457 AuthSecret: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
459 c.Check(err, check.ErrorMatches, `authentication error`)
460 c.Check(conn.Conn, check.IsNil)
463 conn, err = s.localdb.ContainerGatewayTunnel(s.userctx, arvados.ContainerGatewayTunnelOptions{
465 AuthSecret: s.gw.AuthSecret,
467 c.Check(err, check.IsNil)
468 c.Check(conn.Conn, check.NotNil)
471 func (s *ContainerGatewaySuite) TestConnectThroughTunnelWithProxyOK(c *check.C) {
472 forceProxyForTest = true
473 defer func() { forceProxyForTest = false }()
474 s.cluster.Services.Controller.InternalURLs[*forceInternalURLForTest] = arvados.ServiceInstance{}
475 defer delete(s.cluster.Services.Controller.InternalURLs, *forceInternalURLForTest)
476 s.testConnectThroughTunnel(c, "")
479 func (s *ContainerGatewaySuite) TestConnectThroughTunnelWithProxyError(c *check.C) {
480 forceProxyForTest = true
481 defer func() { forceProxyForTest = false }()
482 // forceInternalURLForTest will not be usable because it isn't
483 // listed in s.cluster.Services.Controller.InternalURLs
484 s.testConnectThroughTunnel(c, `.*tunnel endpoint is invalid.*`)
487 func (s *ContainerGatewaySuite) TestConnectThroughTunnelNoProxyOK(c *check.C) {
488 s.testConnectThroughTunnel(c, "")
491 func (s *ContainerGatewaySuite) setupGatewayWithTunnel(c *check.C) *crunchrun.Gateway {
492 rootctx := ctrlctx.NewWithToken(s.ctx, s.cluster, s.cluster.SystemRootToken)
493 // Until the tunnel starts up, set gateway_address to a value
494 // that can't work. We want to ensure the only way we can
495 // reach the gateway is through the tunnel.
496 tungw := &crunchrun.Gateway{
497 ContainerUUID: s.ctrUUID,
498 AuthSecret: s.gw.AuthSecret,
499 Log: ctxlog.TestLogger(c),
500 Target: crunchrun.GatewayTargetStub{},
501 ArvadosClient: s.gw.ArvadosClient,
502 UpdateTunnelURL: func(url string) {
503 c.Logf("UpdateTunnelURL(%q)", url)
504 gwaddr := "tunnel " + url
505 s.localdb.ContainerUpdate(rootctx, arvados.UpdateOptions{
507 Attrs: map[string]interface{}{
508 "gateway_address": gwaddr}})
511 c.Assert(tungw.Start(), check.IsNil)
513 // We didn't supply an external hostname in the Address field,
514 // so Start() should assign a local address.
515 host, _, err := net.SplitHostPort(tungw.Address)
516 c.Assert(err, check.IsNil)
517 c.Check(host, check.Equals, "127.0.0.1")
519 _, err = s.localdb.ContainerUpdate(rootctx, arvados.UpdateOptions{
521 Attrs: map[string]interface{}{
522 "state": arvados.ContainerStateRunning,
524 c.Assert(err, check.IsNil)
526 for deadline := time.Now().Add(5 * time.Second); time.Now().Before(deadline); time.Sleep(time.Second / 2) {
527 ctr, err := s.localdb.ContainerGet(s.userctx, arvados.GetOptions{UUID: s.ctrUUID})
528 c.Assert(err, check.IsNil)
529 c.Check(ctr.InteractiveSessionStarted, check.Equals, false)
530 c.Logf("ctr.GatewayAddress == %s", ctr.GatewayAddress)
531 if strings.HasPrefix(ctr.GatewayAddress, "tunnel ") {
538 func (s *ContainerGatewaySuite) testConnectThroughTunnel(c *check.C, expectErrorMatch string) {
539 s.setupGatewayWithTunnel(c)
540 c.Log("connecting to gateway through tunnel")
541 arpc := rpc.NewConn("", &url.URL{Scheme: "https", Host: s.gw.ArvadosClient.APIHost}, true, rpc.PassthroughTokenProvider)
542 sshconn, err := arpc.ContainerSSH(s.userctx, arvados.ContainerSSHOptions{UUID: s.ctrUUID})
543 if expectErrorMatch != "" {
544 c.Check(err, check.ErrorMatches, expectErrorMatch)
547 c.Assert(err, check.IsNil)
548 c.Assert(sshconn.Conn, check.NotNil)
549 defer sshconn.Conn.Close()
551 done := make(chan struct{})
555 // Receive text banner
556 buf := make([]byte, 12)
557 _, err := io.ReadFull(sshconn.Conn, buf)
558 c.Check(err, check.IsNil)
559 c.Check(string(buf), check.Equals, "SSH-2.0-Go\r\n")
562 _, err = sshconn.Conn.Write([]byte("SSH-2.0-Fake\r\n"))
563 c.Check(err, check.IsNil)
566 _, err = io.ReadFull(sshconn.Conn, buf[:4])
567 c.Check(err, check.IsNil)
569 // If we can get this far into an SSH handshake...
570 c.Logf("was able to read %x -- success, tunnel is working", buf[:4])
574 case <-time.After(time.Second):
577 ctr, err := s.localdb.ContainerGet(s.userctx, arvados.GetOptions{UUID: s.ctrUUID})
578 c.Check(err, check.IsNil)
579 c.Check(ctr.InteractiveSessionStarted, check.Equals, true)