|
7 | 7 | package provisionersdk_test
|
8 | 8 |
|
9 | 9 | import (
|
| 10 | +"bytes" |
| 11 | +"context" |
| 12 | +"errors" |
10 | 13 | "fmt"
|
11 | 14 | "net/http"
|
12 | 15 | "net/http/httptest"
|
13 | 16 | "net/url"
|
14 | 17 | "os/exec"
|
15 | 18 | "runtime"
|
16 | 19 | "strings"
|
| 20 | +"sync" |
17 | 21 | "testing"
|
| 22 | +"time" |
18 | 23 |
|
19 | 24 | "github.com/go-chi/render"
|
20 | 25 | "github.com/stretchr/testify/require"
|
21 | 26 |
|
| 27 | +"github.com/coder/coder/v2/testutil" |
| 28 | + |
22 | 29 | "github.com/coder/coder/v2/provisionersdk"
|
23 | 30 | )
|
24 | 31 |
|
| 32 | +// mimicking the --version output which we use to test the binary (see provisionersdk/scripts/bootstrap_*). |
| 33 | +constversionOutput=`Coder v2.11.0+8979bfe Tue May 7 17:30:19 UTC 2024` |
| 34 | + |
25 | 35 | // bashEcho is a script that calls the local `echo` with the arguments. This is preferable to
|
26 | 36 | // sending the real `echo` binary since macOS 14.4+ immediately sigkills `echo` if it is copied to
|
27 | 37 | // another directory and run locally.
|
28 | 38 | constbashEcho=`#!/usr/bin/env bash
|
29 |
| -echo $@` |
| 39 | +echo "`+versionOutput+`"` |
| 40 | + |
| 41 | +constunexpectedEcho=`#!/usr/bin/env bash |
| 42 | +echo "this is not the agent you are looking for"` |
30 | 43 |
|
31 | 44 | funcTestAgentScript(t*testing.T) {
|
32 | 45 | t.Parallel()
|
33 |
| -t.Run("Run",func(t*testing.T) { |
| 46 | + |
| 47 | +t.Run("Valid",func(t*testing.T) { |
34 | 48 | t.Parallel()
|
35 |
| -srv:=httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter,r*http.Request) { |
36 |
| -render.Status(r,http.StatusOK) |
37 |
| -render.Data(rw,r, []byte(bashEcho)) |
38 |
| -})) |
39 |
| -defersrv.Close() |
40 |
| -srvURL,err:=url.Parse(srv.URL) |
41 |
| -require.NoError(t,err) |
42 | 49 |
|
43 |
| -script,exists:=provisionersdk.AgentScriptEnv()[fmt.Sprintf("CODER_AGENT_SCRIPT_%s_%s",runtime.GOOS,runtime.GOARCH)] |
44 |
| -if!exists { |
45 |
| -t.Skip("Agent not supported...") |
46 |
| -return |
47 |
| -} |
48 |
| -script=strings.ReplaceAll(script,"${ACCESS_URL}",srvURL.String()+"/") |
49 |
| -script=strings.ReplaceAll(script,"${AUTH_TYPE}","token") |
| 50 | +script:=serveScript(t,bashEcho) |
| 51 | + |
| 52 | +ctx,cancel:=context.WithTimeout(context.Background(),testutil.WaitShort) |
| 53 | +t.Cleanup(cancel) |
| 54 | + |
| 55 | +varoutput bytes.Buffer |
50 | 56 | // This is intentionally ran in single quotes to mimic how a customer may
|
51 | 57 | // embed our script. Our scripts should not include any single quotes.
|
52 | 58 | // nolint:gosec
|
53 |
| -output,err:=exec.Command("sh","-c","sh -c '"+script+"'").CombinedOutput() |
54 |
| -t.Log(string(output)) |
| 59 | +cmd:=exec.CommandContext(ctx,"sh","-c","sh -c '"+script+"'") |
| 60 | +cmd.Stdout=&output |
| 61 | +cmd.Stderr=&output |
| 62 | +require.NoError(t,cmd.Start()) |
| 63 | + |
| 64 | +err:=cmd.Wait() |
| 65 | +iferr!=nil { |
| 66 | +varexitErr*exec.ExitError |
| 67 | +iferrors.As(err,&exitErr) { |
| 68 | +require.Equal(t,0,exitErr.ExitCode()) |
| 69 | +}else { |
| 70 | +t.Fatalf("unexpected err: %s",err) |
| 71 | +} |
| 72 | +} |
| 73 | + |
| 74 | +t.Log(output.String()) |
55 | 75 | require.NoError(t,err)
|
56 | 76 | // Ignore debug output from `set -x`, we're only interested in the last line.
|
57 |
| -lines:=strings.Split(strings.TrimSpace(string(output)),"\n") |
| 77 | +lines:=strings.Split(strings.TrimSpace(output.String()),"\n") |
58 | 78 | lastLine:=lines[len(lines)-1]
|
59 |
| -//Because we use the "echo" binary, we should expect the arguments provided |
| 79 | +//When we use the "bashEcho" binary, we should expect the arguments provided |
60 | 80 | // as the response to executing our script.
|
61 |
| -require.Equal(t,"agent",lastLine) |
| 81 | +require.Equal(t,versionOutput,lastLine) |
62 | 82 | })
|
| 83 | + |
| 84 | +t.Run("Invalid",func(t*testing.T) { |
| 85 | +t.Parallel() |
| 86 | + |
| 87 | +script:=serveScript(t,unexpectedEcho) |
| 88 | + |
| 89 | +ctx,cancel:=context.WithTimeout(context.Background(),testutil.WaitShort) |
| 90 | +t.Cleanup(cancel) |
| 91 | + |
| 92 | +varoutput bytes.Buffer |
| 93 | +// This is intentionally ran in single quotes to mimic how a customer may |
| 94 | +// embed our script. Our scripts should not include any single quotes. |
| 95 | +// nolint:gosec |
| 96 | +cmd:=exec.CommandContext(ctx,"sh","-c","sh -c '"+script+"'") |
| 97 | +cmd.WaitDelay=time.Second |
| 98 | +cmd.Stdout=&output |
| 99 | +cmd.Stderr=&output |
| 100 | +require.NoError(t,cmd.Start()) |
| 101 | + |
| 102 | +done:=make(chanerror,1) |
| 103 | +varwg sync.WaitGroup |
| 104 | +wg.Add(1) |
| 105 | +gofunc() { |
| 106 | +deferwg.Done() |
| 107 | + |
| 108 | +// The bootstrap scripts trap exit codes to allow operators to view the script logs and debug the process |
| 109 | +// while it is still running. We do not expect Wait() to complete. |
| 110 | +err:=cmd.Wait() |
| 111 | +done<-err |
| 112 | +}() |
| 113 | + |
| 114 | +select { |
| 115 | +case<-ctx.Done(): |
| 116 | +// Timeout. |
| 117 | +break |
| 118 | +caseerr:=<-done: |
| 119 | +// If done signals before context times out, script behaved in an unexpected way. |
| 120 | +iferr!=nil { |
| 121 | +t.Fatalf("unexpected err: %s",err) |
| 122 | +} |
| 123 | +} |
| 124 | + |
| 125 | +// Kill the command, wait for the command to yield. |
| 126 | +require.NoError(t,cmd.Cancel()) |
| 127 | +wg.Wait() |
| 128 | + |
| 129 | +t.Log(output.String()) |
| 130 | + |
| 131 | +require.Eventually(t,func()bool { |
| 132 | +returnbytes.Contains(output.Bytes(), []byte("ERROR: Downloaded agent binary is invalid")) |
| 133 | +},testutil.WaitShort,testutil.IntervalSlow) |
| 134 | +}) |
| 135 | +} |
| 136 | + |
| 137 | +// serveScript creates a fake HTTP server which serves a requested "agent binary" (which is actually the given input string) |
| 138 | +// which will be attempted to run to verify that it is correct. |
| 139 | +funcserveScript(t*testing.T,instring)string { |
| 140 | +t.Helper() |
| 141 | + |
| 142 | +srv:=httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter,r*http.Request) { |
| 143 | +render.Status(r,http.StatusOK) |
| 144 | +render.Data(rw,r, []byte(in)) |
| 145 | +})) |
| 146 | +t.Cleanup(srv.Close) |
| 147 | +srvURL,err:=url.Parse(srv.URL) |
| 148 | +require.NoError(t,err) |
| 149 | + |
| 150 | +script,exists:=provisionersdk.AgentScriptEnv()[fmt.Sprintf("CODER_AGENT_SCRIPT_%s_%s",runtime.GOOS,runtime.GOARCH)] |
| 151 | +if!exists { |
| 152 | +t.Skip("Agent not supported...") |
| 153 | +return"" |
| 154 | +} |
| 155 | +script=strings.ReplaceAll(script,"${ACCESS_URL}",srvURL.String()+"/") |
| 156 | +script=strings.ReplaceAll(script,"${AUTH_TYPE}","token") |
| 157 | +returnscript |
63 | 158 | }
|