19129: add missing exit_code from select
[arvados.git] / lib / controller / localdb / container_gateway_test.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 package localdb
6
7 import (
8         "context"
9         "crypto/hmac"
10         "crypto/sha256"
11         "fmt"
12         "io"
13         "io/ioutil"
14         "net"
15         "time"
16
17         "git.arvados.org/arvados.git/lib/config"
18         "git.arvados.org/arvados.git/lib/crunchrun"
19         "git.arvados.org/arvados.git/sdk/go/arvados"
20         "git.arvados.org/arvados.git/sdk/go/arvadostest"
21         "git.arvados.org/arvados.git/sdk/go/auth"
22         "git.arvados.org/arvados.git/sdk/go/ctxlog"
23         "golang.org/x/crypto/ssh"
24         check "gopkg.in/check.v1"
25 )
26
27 var _ = check.Suite(&ContainerGatewaySuite{})
28
29 type ContainerGatewaySuite struct {
30         cluster *arvados.Cluster
31         localdb *Conn
32         ctx     context.Context
33         ctrUUID string
34         gw      *crunchrun.Gateway
35 }
36
37 func (s *ContainerGatewaySuite) TearDownSuite(c *check.C) {
38         // Undo any changes/additions to the user database so they
39         // don't affect subsequent tests.
40         arvadostest.ResetEnv()
41         c.Check(arvados.NewClientFromEnv().RequestAndDecode(nil, "POST", "database/reset", nil, nil), check.IsNil)
42 }
43
44 func (s *ContainerGatewaySuite) SetUpSuite(c *check.C) {
45         cfg, err := config.NewLoader(nil, ctxlog.TestLogger(c)).Load()
46         c.Assert(err, check.IsNil)
47         s.cluster, err = cfg.GetCluster("")
48         c.Assert(err, check.IsNil)
49         s.localdb = NewConn(s.cluster)
50         s.ctx = auth.NewContext(context.Background(), &auth.Credentials{Tokens: []string{arvadostest.ActiveTokenV2}})
51
52         s.ctrUUID = arvadostest.QueuedContainerUUID
53
54         h := hmac.New(sha256.New, []byte(s.cluster.SystemRootToken))
55         fmt.Fprint(h, s.ctrUUID)
56         authKey := fmt.Sprintf("%x", h.Sum(nil))
57
58         s.gw = &crunchrun.Gateway{
59                 DockerContainerID:  new(string),
60                 ContainerUUID:      s.ctrUUID,
61                 AuthSecret:         authKey,
62                 Address:            "localhost:0",
63                 Log:                ctxlog.TestLogger(c),
64                 ContainerIPAddress: func() (string, error) { return "localhost", nil },
65         }
66         c.Assert(s.gw.Start(), check.IsNil)
67         rootctx := auth.NewContext(context.Background(), &auth.Credentials{Tokens: []string{s.cluster.SystemRootToken}})
68         _, err = s.localdb.ContainerUpdate(rootctx, arvados.UpdateOptions{
69                 UUID: s.ctrUUID,
70                 Attrs: map[string]interface{}{
71                         "state": arvados.ContainerStateLocked}})
72         c.Assert(err, check.IsNil)
73         _, err = s.localdb.ContainerUpdate(rootctx, arvados.UpdateOptions{
74                 UUID: s.ctrUUID,
75                 Attrs: map[string]interface{}{
76                         "state":           arvados.ContainerStateRunning,
77                         "gateway_address": s.gw.Address}})
78         c.Assert(err, check.IsNil)
79 }
80
81 func (s *ContainerGatewaySuite) SetUpTest(c *check.C) {
82         s.cluster.Containers.ShellAccess.Admin = true
83         s.cluster.Containers.ShellAccess.User = true
84         _, err := arvadostest.DB(c, s.cluster).Exec(`update containers set interactive_session_started=$1 where uuid=$2`, false, s.ctrUUID)
85         c.Check(err, check.IsNil)
86 }
87
88 func (s *ContainerGatewaySuite) TestConfig(c *check.C) {
89         for _, trial := range []struct {
90                 configAdmin bool
91                 configUser  bool
92                 sendToken   string
93                 errorCode   int
94         }{
95                 {true, true, arvadostest.ActiveTokenV2, 0},
96                 {true, false, arvadostest.ActiveTokenV2, 503},
97                 {false, true, arvadostest.ActiveTokenV2, 0},
98                 {false, false, arvadostest.ActiveTokenV2, 503},
99                 {true, true, arvadostest.AdminToken, 0},
100                 {true, false, arvadostest.AdminToken, 0},
101                 {false, true, arvadostest.AdminToken, 403},
102                 {false, false, arvadostest.AdminToken, 503},
103         } {
104                 c.Logf("trial %#v", trial)
105                 s.cluster.Containers.ShellAccess.Admin = trial.configAdmin
106                 s.cluster.Containers.ShellAccess.User = trial.configUser
107                 ctx := auth.NewContext(s.ctx, &auth.Credentials{Tokens: []string{trial.sendToken}})
108                 sshconn, err := s.localdb.ContainerSSH(ctx, arvados.ContainerSSHOptions{UUID: s.ctrUUID})
109                 if trial.errorCode == 0 {
110                         if !c.Check(err, check.IsNil) {
111                                 continue
112                         }
113                         if !c.Check(sshconn.Conn, check.NotNil) {
114                                 continue
115                         }
116                         sshconn.Conn.Close()
117                 } else {
118                         c.Check(err, check.NotNil)
119                         err, ok := err.(interface{ HTTPStatus() int })
120                         if c.Check(ok, check.Equals, true) {
121                                 c.Check(err.HTTPStatus(), check.Equals, trial.errorCode)
122                         }
123                 }
124         }
125 }
126
127 func (s *ContainerGatewaySuite) TestDirectTCP(c *check.C) {
128         // Set up servers on a few TCP ports
129         var addrs []string
130         for i := 0; i < 3; i++ {
131                 ln, err := net.Listen("tcp", ":0")
132                 c.Assert(err, check.IsNil)
133                 defer ln.Close()
134                 addrs = append(addrs, ln.Addr().String())
135                 go func() {
136                         for {
137                                 conn, err := ln.Accept()
138                                 if err != nil {
139                                         return
140                                 }
141                                 var gotAddr string
142                                 fmt.Fscanf(conn, "%s\n", &gotAddr)
143                                 c.Logf("stub server listening at %s received string %q from remote %s", ln.Addr().String(), gotAddr, conn.RemoteAddr())
144                                 if gotAddr == ln.Addr().String() {
145                                         fmt.Fprintf(conn, "%s\n", ln.Addr().String())
146                                 }
147                                 conn.Close()
148                         }
149                 }()
150         }
151
152         c.Logf("connecting to %s", s.gw.Address)
153         sshconn, err := s.localdb.ContainerSSH(s.ctx, arvados.ContainerSSHOptions{UUID: s.ctrUUID})
154         c.Assert(err, check.IsNil)
155         c.Assert(sshconn.Conn, check.NotNil)
156         defer sshconn.Conn.Close()
157         conn, chans, reqs, err := ssh.NewClientConn(sshconn.Conn, "zzzz-dz642-abcdeabcdeabcde", &ssh.ClientConfig{
158                 HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error { return nil },
159         })
160         c.Assert(err, check.IsNil)
161         client := ssh.NewClient(conn, chans, reqs)
162         for _, expectAddr := range addrs {
163                 _, port, err := net.SplitHostPort(expectAddr)
164                 c.Assert(err, check.IsNil)
165
166                 c.Logf("trying foo:%s", port)
167                 {
168                         conn, err := client.Dial("tcp", "foo:"+port)
169                         c.Assert(err, check.IsNil)
170                         conn.SetDeadline(time.Now().Add(time.Second))
171                         buf, err := ioutil.ReadAll(conn)
172                         c.Check(err, check.IsNil)
173                         c.Check(string(buf), check.Equals, "")
174                 }
175
176                 c.Logf("trying localhost:%s", port)
177                 {
178                         conn, err := client.Dial("tcp", "localhost:"+port)
179                         c.Assert(err, check.IsNil)
180                         conn.SetDeadline(time.Now().Add(time.Second))
181                         conn.Write([]byte(expectAddr + "\n"))
182                         var gotAddr string
183                         fmt.Fscanf(conn, "%s\n", &gotAddr)
184                         c.Check(gotAddr, check.Equals, expectAddr)
185                 }
186         }
187 }
188
189 func (s *ContainerGatewaySuite) TestConnect(c *check.C) {
190         c.Logf("connecting to %s", s.gw.Address)
191         sshconn, err := s.localdb.ContainerSSH(s.ctx, arvados.ContainerSSHOptions{UUID: s.ctrUUID})
192         c.Assert(err, check.IsNil)
193         c.Assert(sshconn.Conn, check.NotNil)
194         defer sshconn.Conn.Close()
195
196         done := make(chan struct{})
197         go func() {
198                 defer close(done)
199
200                 // Receive text banner
201                 buf := make([]byte, 12)
202                 _, err := io.ReadFull(sshconn.Conn, buf)
203                 c.Check(err, check.IsNil)
204                 c.Check(string(buf), check.Equals, "SSH-2.0-Go\r\n")
205
206                 // Send text banner
207                 _, err = sshconn.Conn.Write([]byte("SSH-2.0-Fake\r\n"))
208                 c.Check(err, check.IsNil)
209
210                 // Receive binary
211                 _, err = io.ReadFull(sshconn.Conn, buf[:4])
212                 c.Check(err, check.IsNil)
213
214                 // If we can get this far into an SSH handshake...
215                 c.Logf("was able to read %x -- success, tunnel is working", buf[:4])
216         }()
217         select {
218         case <-done:
219         case <-time.After(time.Second):
220                 c.Fail()
221         }
222         ctr, err := s.localdb.ContainerGet(s.ctx, arvados.GetOptions{UUID: s.ctrUUID})
223         c.Check(err, check.IsNil)
224         c.Check(ctr.InteractiveSessionStarted, check.Equals, true)
225 }
226
227 func (s *ContainerGatewaySuite) TestConnectFail(c *check.C) {
228         c.Log("trying with no token")
229         ctx := auth.NewContext(context.Background(), &auth.Credentials{})
230         _, err := s.localdb.ContainerSSH(ctx, arvados.ContainerSSHOptions{UUID: s.ctrUUID})
231         c.Check(err, check.ErrorMatches, `.* 401 .*`)
232
233         c.Log("trying with anonymous token")
234         ctx = auth.NewContext(context.Background(), &auth.Credentials{Tokens: []string{arvadostest.AnonymousToken}})
235         _, err = s.localdb.ContainerSSH(ctx, arvados.ContainerSSHOptions{UUID: s.ctrUUID})
236         c.Check(err, check.ErrorMatches, `.* 404 .*`)
237 }