1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: Apache-2.0
25 "git.arvados.org/arvados.git/lib/controller/rpc"
26 "git.arvados.org/arvados.git/lib/crunchrun"
27 "git.arvados.org/arvados.git/sdk/go/arvados"
28 "git.arvados.org/arvados.git/sdk/go/arvadosclient"
29 "git.arvados.org/arvados.git/sdk/go/arvadostest"
30 "git.arvados.org/arvados.git/sdk/go/ctxlog"
31 "git.arvados.org/arvados.git/sdk/go/httpserver"
32 "git.arvados.org/arvados.git/sdk/go/keepclient"
33 check "gopkg.in/check.v1"
36 var _ = check.Suite(&shellSuite{})
38 type shellSuite struct {
44 func (s *shellSuite) SetUpSuite(c *check.C) {
46 s.gobindir = tmpdir + "/bin"
47 c.Check(os.Mkdir(s.gobindir, 0777), check.IsNil)
48 s.homedir = tmpdir + "/home"
49 c.Check(os.Mkdir(s.homedir, 0777), check.IsNil)
51 // We explicitly build a client binary in our tempdir here,
52 // instead of using "go run .", because (a) we're going to
53 // invoke the same binary several times, and (b) we're going
54 // to change $HOME to a temp dir in some of the tests, which
55 // would force "go run ." to recompile the world instead of
56 // using the cached object files in the real $HOME.
57 c.Logf("building arvados-client binary in %s", s.gobindir)
58 cmd := exec.Command("go", "install", ".")
59 cmd.Env = append(os.Environ(), "GOBIN="+s.gobindir)
60 cmd.Stdout = os.Stdout
61 cmd.Stderr = os.Stderr
62 c.Assert(cmd.Run(), check.IsNil)
64 s.runningUUID = arvadostest.RunningContainerUUID
65 h := hmac.New(sha256.New, []byte(arvadostest.SystemRootToken))
66 fmt.Fprint(h, s.runningUUID)
67 authSecret := fmt.Sprintf("%x", h.Sum(nil))
68 gw := crunchrun.Gateway{
69 ContainerUUID: s.runningUUID,
71 AuthSecret: authSecret,
72 Log: ctxlog.TestLogger(c),
73 // Just forward connections to localhost instead of a
74 // container, so we can test without running a
76 Target: crunchrun.GatewayTargetStub{},
79 c.Assert(err, check.IsNil)
81 rpcconn := rpc.NewConn("",
84 Host: os.Getenv("ARVADOS_API_HOST"),
87 func(context.Context) ([]string, error) {
88 return []string{arvadostest.SystemRootToken}, nil
90 _, err = rpcconn.ContainerUpdate(context.TODO(), arvados.UpdateOptions{UUID: s.runningUUID, Attrs: map[string]interface{}{
91 "gateway_address": gw.Address,
93 c.Assert(err, check.IsNil)
96 func (s *shellSuite) TearDownSuite(c *check.C) {
97 c.Check(arvados.NewClientFromEnv().RequestAndDecode(nil, "POST", "database/reset", nil, nil), check.IsNil)
100 func (s *shellSuite) TestShellGatewayNotAvailable(c *check.C) {
101 var stdout, stderr bytes.Buffer
102 cmd := exec.Command(s.gobindir+"/arvados-client", "shell", arvadostest.QueuedContainerUUID, "-o", "controlpath=none", "echo", "ok")
103 cmd.Env = append(cmd.Env, os.Environ()...)
104 cmd.Env = append(cmd.Env, "ARVADOS_API_TOKEN="+arvadostest.ActiveTokenV2)
107 c.Check(cmd.Run(), check.NotNil)
108 c.Log(stderr.String())
109 c.Check(stderr.String(), check.Matches, `(?ms).*container is not running yet \(state is "Queued"\).*`)
112 func (s *shellSuite) TestShellGatewayUsingEnvVars(c *check.C) {
113 s.testShellGateway(c, false)
115 func (s *shellSuite) TestShellGatewayUsingSettingsConf(c *check.C) {
116 s.testShellGateway(c, true)
118 func (s *shellSuite) testShellGateway(c *check.C, useSettingsConf bool) {
119 var stdout, stderr bytes.Buffer
121 s.gobindir+"/arvados-client", "shell", s.runningUUID,
122 "-o", "controlpath=none",
123 "-o", "userknownhostsfile="+s.homedir+"/known_hosts",
126 settings := "ARVADOS_API_HOST=" + os.Getenv("ARVADOS_API_HOST") + "\nARVADOS_API_TOKEN=" + arvadostest.ActiveTokenV2 + "\nARVADOS_API_HOST_INSECURE=true\n"
127 err := os.MkdirAll(s.homedir+"/.config/arvados", 0777)
128 c.Assert(err, check.IsNil)
129 err = os.WriteFile(s.homedir+"/.config/arvados/settings.conf", []byte(settings), 0777)
130 c.Assert(err, check.IsNil)
131 for _, kv := range os.Environ() {
132 if !strings.HasPrefix(kv, "ARVADOS_") && !strings.HasPrefix(kv, "HOME=") {
133 cmd.Env = append(cmd.Env, kv)
136 cmd.Env = append(cmd.Env, "HOME="+s.homedir)
138 err := os.Remove(s.homedir + "/.config/arvados/settings.conf")
139 if !os.IsNotExist(err) {
140 c.Assert(err, check.IsNil)
142 cmd.Env = append(cmd.Env, os.Environ()...)
143 cmd.Env = append(cmd.Env, "ARVADOS_API_TOKEN="+arvadostest.ActiveTokenV2)
147 stdin, err := cmd.StdinPipe()
148 c.Assert(err, check.IsNil)
149 go fmt.Fprintln(stdin, "data appears on stdin, but stdin does not close; cmd should exit anyway, not hang")
150 timeout := time.AfterFunc(5*time.Second, func() {
151 c.Errorf("timed out -- remote end is probably hung waiting for us to close stdin")
154 c.Logf("cmd.Args: %s", cmd.Args)
155 c.Check(cmd.Run(), check.IsNil)
157 c.Check(stdout.String(), check.Equals, "ok\n")
160 func (s *shellSuite) TestShellGatewayPortForwarding(c *check.C) {
161 c.Log("setting up an http server")
162 // Set up an http server, and try using "arvados-client shell"
163 // to forward traffic to it.
164 httpTarget := &httpserver.Server{}
165 httpTarget.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
166 c.Logf("httpTarget.Handler: incoming request: %s %s", r.Method, r.URL)
167 if r.URL.Path == "/foo" {
168 fmt.Fprintln(w, "bar baz")
170 w.WriteHeader(http.StatusNotFound)
173 err := httpTarget.Start()
174 c.Assert(err, check.IsNil)
176 ln, err := net.Listen("tcp", ":0")
177 c.Assert(err, check.IsNil)
178 _, forwardedPort, _ := net.SplitHostPort(ln.Addr().String())
182 var stdout, stderr bytes.Buffer
183 ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(10*time.Second))
185 cmd := exec.CommandContext(ctx,
186 s.gobindir+"/arvados-client", "shell", s.runningUUID,
187 "-L", forwardedPort+":"+httpTarget.Addr,
188 "-o", "controlpath=none",
189 "-o", "userknownhostsfile="+s.homedir+"/known_hosts",
192 cmd.Env = append(cmd.Env, os.Environ()...)
193 cmd.Env = append(cmd.Env, "ARVADOS_API_TOKEN="+arvadostest.ActiveTokenV2)
196 c.Logf("cmd.Args: %s", cmd.Args)
199 forwardedURL := fmt.Sprintf("http://localhost:%s/foo", forwardedPort)
201 for range time.NewTicker(time.Second / 20).C {
202 resp, err := http.Get(forwardedURL)
204 if !strings.Contains(err.Error(), "connect") {
206 } else if ctx.Err() != nil {
207 if cmd.Process.Signal(syscall.Signal(0)) != nil {
208 c.Error("OpenSSH exited")
210 c.Errorf("timed out trying to connect: %s", err)
212 c.Logf("OpenSSH stdout:\n%s", stdout.String())
213 c.Logf("OpenSSH stderr:\n%s", stderr.String())
216 // Retry until OpenSSH starts listening
219 c.Check(resp.StatusCode, check.Equals, http.StatusOK)
220 body, err := ioutil.ReadAll(resp.Body)
221 c.Check(err, check.IsNil)
222 c.Check(string(body), check.Equals, "bar baz\n")
226 var wg sync.WaitGroup
227 for i := 0; i < 10; i++ {
231 resp, err := http.Get(forwardedURL)
232 if !c.Check(err, check.IsNil) {
235 body, err := ioutil.ReadAll(resp.Body)
236 c.Check(err, check.IsNil)
237 c.Check(string(body), check.Equals, "bar baz\n")
243 var _ = check.Suite(&logsSuite{})
245 type logsSuite struct{}
247 func (s *logsSuite) TestContainerRequestLog(c *check.C) {
248 arvadostest.StartKeep(2, true)
249 ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(30*time.Second))
252 rpcconn := rpc.NewConn("",
255 Host: os.Getenv("ARVADOS_API_HOST"),
258 func(context.Context) ([]string, error) {
259 return []string{arvadostest.SystemRootToken}, nil
261 imageColl, err := rpcconn.CollectionCreate(ctx, arvados.CreateOptions{Attrs: map[string]interface{}{
262 "manifest_text": ". d41d8cd98f00b204e9800998ecf8427e+0 0:0:sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.tar\n",
264 c.Assert(err, check.IsNil)
265 c.Logf("imageColl %+v", imageColl)
266 cr, err := rpcconn.ContainerRequestCreate(ctx, arvados.CreateOptions{Attrs: map[string]interface{}{
267 "state": "Committed",
268 "command": []string{"echo", fmt.Sprintf("%d", time.Now().Unix())},
269 "container_image": imageColl.PortableDataHash,
273 "runtime_constraints": arvados.RuntimeConstraints{
277 "container_count_max": 1,
279 c.Assert(err, check.IsNil)
280 h := hmac.New(sha256.New, []byte(arvadostest.SystemRootToken))
281 fmt.Fprint(h, cr.ContainerUUID)
282 authSecret := fmt.Sprintf("%x", h.Sum(nil))
284 coll := arvados.Collection{}
285 client := arvados.NewClientFromEnv()
286 ac, err := arvadosclient.New(client)
287 c.Assert(err, check.IsNil)
288 kc, err := keepclient.MakeKeepClient(ac)
289 c.Assert(err, check.IsNil)
290 cfs, err := coll.FileSystem(client, kc)
291 c.Assert(err, check.IsNil)
293 c.Log("running logs command on queued container")
294 var stdout, stderr bytes.Buffer
295 cmd := exec.CommandContext(ctx, "go", "run", ".", "logs", "-f", "-poll=250ms", cr.UUID)
296 cmd.Env = append(cmd.Env, os.Environ()...)
297 cmd.Env = append(cmd.Env, "ARVADOS_API_TOKEN="+arvadostest.SystemRootToken)
298 cmd.Stdout = io.MultiWriter(&stdout, os.Stderr)
299 cmd.Stderr = io.MultiWriter(&stderr, os.Stderr)
301 c.Assert(err, check.Equals, nil)
303 c.Log("changing container state to Locked")
304 _, err = rpcconn.ContainerUpdate(ctx, arvados.UpdateOptions{UUID: cr.ContainerUUID, Attrs: map[string]interface{}{
305 "state": arvados.ContainerStateLocked,
307 c.Assert(err, check.IsNil)
308 c.Log("starting gateway")
309 gw := crunchrun.Gateway{
310 ContainerUUID: cr.ContainerUUID,
311 Address: "0.0.0.0:0",
312 AuthSecret: authSecret,
313 Log: ctxlog.TestLogger(c),
314 Target: crunchrun.GatewayTargetStub{},
318 c.Assert(err, check.IsNil)
319 c.Log("updating container gateway address")
320 _, err = rpcconn.ContainerUpdate(ctx, arvados.UpdateOptions{UUID: cr.ContainerUUID, Attrs: map[string]interface{}{
321 "gateway_address": gw.Address,
322 "state": arvados.ContainerStateRunning,
324 c.Assert(err, check.IsNil)
326 const rfc3339NanoFixed = "2006-01-02T15:04:05.000000000Z07:00"
327 fCrunchrun, err := cfs.OpenFile("crunch-run.txt", os.O_CREATE|os.O_WRONLY, 0777)
328 c.Assert(err, check.IsNil)
329 _, err = fmt.Fprintf(fCrunchrun, "%s line 1 of crunch-run.txt\n", time.Now().UTC().Format(rfc3339NanoFixed))
330 c.Assert(err, check.IsNil)
331 fStderr, err := cfs.OpenFile("stderr.txt", os.O_CREATE|os.O_WRONLY, 0777)
332 c.Assert(err, check.IsNil)
333 _, err = fmt.Fprintf(fStderr, "%s line 1 of stderr\n", time.Now().UTC().Format(rfc3339NanoFixed))
334 c.Assert(err, check.IsNil)
337 // Without "-f", just show the existing logs and
338 // exit. Timeout needs to be long enough for "go run".
339 ctxNoFollow, cancel := context.WithDeadline(ctx, time.Now().Add(time.Second*5))
341 cmdNoFollow := exec.CommandContext(ctxNoFollow, "go", "run", ".", "logs", "-poll=250ms", cr.UUID)
342 buf, err := cmdNoFollow.CombinedOutput()
343 c.Check(err, check.IsNil)
344 c.Check(string(buf), check.Matches, `(?ms).*line 1 of stderr\n`)
347 time.Sleep(time.Second * 2)
348 _, err = fmt.Fprintf(fCrunchrun, "%s line 2 of crunch-run.txt", time.Now().UTC().Format(rfc3339NanoFixed))
349 c.Assert(err, check.IsNil)
350 _, err = fmt.Fprintf(fStderr, "%s --end--", time.Now().UTC().Format(rfc3339NanoFixed))
351 c.Assert(err, check.IsNil)
353 for deadline := time.Now().Add(20 * time.Second); time.Now().Before(deadline) && !strings.Contains(stdout.String(), "--end--"); time.Sleep(time.Second / 10) {
355 c.Check(stdout.String(), check.Matches, `(?ms).*stderr\.txt +20\S+Z --end--\n.*`)
357 mtxt, err := cfs.MarshalManifest(".")
358 c.Assert(err, check.IsNil)
359 savedLog, err := rpcconn.CollectionCreate(ctx, arvados.CreateOptions{Attrs: map[string]interface{}{
360 "manifest_text": mtxt,
362 c.Assert(err, check.IsNil)
363 _, err = rpcconn.ContainerUpdate(ctx, arvados.UpdateOptions{UUID: cr.ContainerUUID, Attrs: map[string]interface{}{
364 "state": arvados.ContainerStateComplete,
365 "log": savedLog.PortableDataHash,
366 "output": "d41d8cd98f00b204e9800998ecf8427e+0",
369 c.Assert(err, check.IsNil)
372 c.Check(err, check.IsNil)
373 // Ensure controller doesn't cheat by fetching data from the
374 // gateway after the container is complete.
375 gw.LogCollection = nil
377 c.Logf("re-running logs command on completed container")
379 ctx, cancel := context.WithDeadline(ctx, time.Now().Add(time.Second*5))
381 cmd := exec.CommandContext(ctx, "go", "run", ".", "logs", "-f", cr.UUID)
382 cmd.Env = append(cmd.Env, os.Environ()...)
383 cmd.Env = append(cmd.Env, "ARVADOS_API_TOKEN="+arvadostest.SystemRootToken)
384 buf, err := cmd.CombinedOutput()
385 c.Check(err, check.Equals, nil)
386 c.Check(string(buf), check.Matches, `(?ms).*--end--\n`)