-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Expand file tree
/
Copy pathagent_test.go
More file actions
151 lines (126 loc) · 4.39 KB
/
Copy pathagent_test.go
File metadata and controls
151 lines (126 loc) · 4.39 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
//go:build !windows
// +build !windows
// There isn't a portable Windows binary equivalent to "echo".
// This can be tested, but it's a bit harder.
package provisionersdk_test
import (
"errors"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"os"
"os/exec"
"runtime"
"strings"
"sync"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/provisionersdk"
"github.com/coder/coder/v2/testutil"
)
// mimicking the --version output which we use to test the binary (see provisionersdk/scripts/bootstrap_*).
const versionOutput = `Coder v2.11.0+8979bfe Tue May 7 17:30:19 UTC 2024`
// bashEcho is a script that calls the local `echo` with the arguments. This is preferable to
// sending the real `echo` binary since macOS 14.4+ immediately sigkills `echo` if it is copied to
// another directory and run locally.
const bashEcho = `#!/usr/bin/env bash
echo "` + versionOutput + `"`
const unexpectedEcho = `#!/usr/bin/env bash
echo "this is not the agent you are looking for"`
func TestAgentScript(t *testing.T) {
t.Parallel()
t.Run("Valid", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)
script := serveScript(t, bashEcho)
output := testutil.NewWaitBuffer()
// This is intentionally ran in single quotes to mimic how a customer may
// embed our script. Our scripts should not include any single quotes.
// nolint:gosec
cmd := exec.CommandContext(ctx, "sh", "-c", "sh -c '"+script+"'")
cmd.Stdout = output
cmd.Stderr = output
require.NoError(t, cmd.Start())
err := cmd.Wait()
if err != nil {
var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
require.Equal(t, 0, exitErr.ExitCode())
} else {
t.Fatalf("unexpected err: %s", err)
}
}
t.Log(output.String())
require.NoError(t, err)
// Ignore debug output from `set -x`, we're only interested in the last line.
lines := strings.Split(strings.TrimSpace(output.String()), "\n")
lastLine := lines[len(lines)-1]
// When we use the "bashEcho" binary, we should expect the arguments provided
// as the response to executing our script.
require.Equal(t, versionOutput, lastLine)
})
t.Run("Invalid", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)
script := serveScript(t, unexpectedEcho)
output := testutil.NewWaitBuffer()
// This is intentionally ran in single quotes to mimic how a customer may
// embed our script. Our scripts should not include any single quotes.
// nolint:gosec
cmd := exec.CommandContext(ctx, "sh", "-c", "sh -c '"+script+"'")
cmd.WaitDelay = time.Second
cmd.Stdout = output
cmd.Stderr = output
require.NoError(t, cmd.Start())
done := make(chan error, 1)
var wg sync.WaitGroup
wg.Go(func() {
// The bootstrap scripts trap exit codes to allow operators to view the script logs and debug the process
// while it is still running. We do not expect Wait() to complete.
err := cmd.Wait()
done <- err
})
select {
case <-ctx.Done():
// Timeout.
break
case err := <-done:
// If done signals before context times out, script behaved in an unexpected way.
if err != nil {
t.Fatalf("unexpected err: %s", err)
}
}
// Kill the command, wait for the command to yield.
err := cmd.Cancel()
if errors.Is(err, os.ErrProcessDone) {
t.Log("script has already finished execution")
} else if err != nil {
t.Fatalf("unable to cancel the command: %v, see logs:\n%s", err, output.String())
}
wg.Wait()
t.Log(output.String())
output.RequireWaitFor(ctx, t, "ERROR: Downloaded agent binary returned unexpected version output")
})
}
// serveScript creates a fake HTTP server which serves a requested "agent binary" (which is actually the given input string)
// which will be attempted to run to verify that it is correct.
func serveScript(t *testing.T, in string) string {
t.Helper()
srv := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
rw.WriteHeader(http.StatusOK)
_, _ = rw.Write([]byte(in))
}))
t.Cleanup(srv.Close)
srvURL, err := url.Parse(srv.URL)
require.NoError(t, err)
script, exists := provisionersdk.AgentScriptEnv()[fmt.Sprintf("CODER_AGENT_SCRIPT_%s_%s", runtime.GOOS, runtime.GOARCH)]
if !exists {
t.Skip("Agent not supported...")
return ""
}
script = strings.ReplaceAll(script, "${ACCESS_URL}", srvURL.String()+"/")
script = strings.ReplaceAll(script, "${AUTH_TYPE}", "token")
return script
}