Skip to content

Commit 59ab505

Browse files
authored
fix: return error if agent init script fails to download valid binary (#13280)
1 parent e176867 commit 59ab505

File tree

6 files changed

+153
-29
lines changed

6 files changed

+153
-29
lines changed

provisionersdk/agent.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,9 @@ var (
3939
}
4040
)
4141

42-
// AgentScriptEnv returns a key-pair of scripts that are consumed
43-
// by the Coder Terraform Provider. See:
44-
// https://github.com/coder/terraform-provider-coder/blob/main/internal/provider/provider.go#L97
42+
// AgentScriptEnv returns a key-pair of scripts that are consumed by the Coder Terraform Provider.
43+
// https://github.com/coder/terraform-provider-coder/blob/main/provider/agent.go (updateInitScript)
44+
// performs additional string substitutions.
4545
func AgentScriptEnv() map[string]string {
4646
env := map[string]string{}
4747
for operatingSystem, scripts := range agentScripts {

provisionersdk/agent_test.go

Lines changed: 116 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,57 +7,152 @@
77
package provisionersdk_test
88

99
import (
10+
"bytes"
11+
"context"
12+
"errors"
1013
"fmt"
1114
"net/http"
1215
"net/http/httptest"
1316
"net/url"
1417
"os/exec"
1518
"runtime"
1619
"strings"
20+
"sync"
1721
"testing"
22+
"time"
1823

1924
"github.com/go-chi/render"
2025
"github.com/stretchr/testify/require"
2126

27+
"github.com/coder/coder/v2/testutil"
28+
2229
"github.com/coder/coder/v2/provisionersdk"
2330
)
2431

32+
// mimicking the --version output which we use to test the binary (see provisionersdk/scripts/bootstrap_*).
33+
const versionOutput = `Coder v2.11.0+8979bfe Tue May 7 17:30:19 UTC 2024`
34+
2535
// bashEcho is a script that calls the local `echo` with the arguments. This is preferable to
2636
// sending the real `echo` binary since macOS 14.4+ immediately sigkills `echo` if it is copied to
2737
// another directory and run locally.
2838
const bashEcho = `#!/usr/bin/env bash
29-
echo $@`
39+
echo "` + versionOutput + `"`
40+
41+
const unexpectedEcho = `#!/usr/bin/env bash
42+
echo "this is not the agent you are looking for"`
3043

3144
func TestAgentScript(t *testing.T) {
3245
t.Parallel()
33-
t.Run("Run", func(t *testing.T) {
46+
47+
t.Run("Valid", func(t *testing.T) {
3448
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-
defer srv.Close()
40-
srvURL, err := url.Parse(srv.URL)
41-
require.NoError(t, err)
4249

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+
var output bytes.Buffer
5056
// This is intentionally ran in single quotes to mimic how a customer may
5157
// embed our script. Our scripts should not include any single quotes.
5258
// 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+
if err != nil {
66+
var exitErr *exec.ExitError
67+
if errors.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())
5575
require.NoError(t, err)
5676
// 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")
5878
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
6080
// as the response to executing our script.
61-
require.Equal(t, "agent", lastLine)
81+
require.Equal(t, versionOutput, lastLine)
6282
})
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+
var output 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(chan error, 1)
103+
var wg sync.WaitGroup
104+
wg.Add(1)
105+
go func() {
106+
defer wg.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+
case err := <-done:
119+
// If done signals before context times out, script behaved in an unexpected way.
120+
if err != 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+
return bytes.Contains(output.Bytes(), []byte("ERROR: Downloaded agent binary returned unexpected version output"))
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+
func serveScript(t *testing.T, in string) 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+
return script
63158
}

provisionersdk/scripts/bootstrap_darwin.sh

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ set -eux
44
# This is to allow folks to exec into a failed workspace and poke around to
55
# troubleshoot.
66
waitonexit() {
7-
echo "=== Agent script exited with non-zero code. Sleeping 24h to preserve logs..."
7+
echo "=== Agent script exited with non-zero code ($?). Sleeping 24h to preserve logs..."
88
sleep 86400
99
}
1010
trap waitonexit EXIT
@@ -31,4 +31,12 @@ fi
3131

3232
export CODER_AGENT_AUTH="${AUTH_TYPE}"
3333
export CODER_AGENT_URL="${ACCESS_URL}"
34-
exec ./$BINARY_NAME agent
34+
35+
output=$(./${BINARY_NAME} --version | head -n1)
36+
if ! echo "${output}" | grep -q Coder; then
37+
echo >&2 "ERROR: Downloaded agent binary returned unexpected version output"
38+
echo >&2 "${BINARY_NAME} --version output: \"${output}\""
39+
exit 2
40+
fi
41+
42+
exec ./${BINARY_NAME} agent

provisionersdk/scripts/bootstrap_linux.sh

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ set -eux
44
# This is to allow folks to exec into a failed workspace and poke around to
55
# troubleshoot.
66
waitonexit() {
7-
echo "=== Agent script exited with non-zero code. Sleeping 24h to preserve logs..."
7+
echo "=== Agent script exited with non-zero code ($?). Sleeping 24h to preserve logs..."
88
sleep 86400
99
}
1010
trap waitonexit EXIT
@@ -86,4 +86,12 @@ fi
8686

8787
export CODER_AGENT_AUTH="${AUTH_TYPE}"
8888
export CODER_AGENT_URL="${ACCESS_URL}"
89-
exec ./$BINARY_NAME agent
89+
90+
output=$(./${BINARY_NAME} --version | head -n1)
91+
if ! echo "${output}" | grep -q Coder; then
92+
echo >&2 "ERROR: Downloaded agent binary returned unexpected version output"
93+
echo >&2 "${BINARY_NAME} --version output: \"${output}\""
94+
exit 2
95+
fi
96+
97+
exec ./${BINARY_NAME} agent

provisionersdk/scripts/bootstrap_windows.ps1

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,19 @@ if (-not (Get-Command 'Set-MpPreference' -ErrorAction SilentlyContinue)) {
3535
$env:CODER_AGENT_AUTH = "${AUTH_TYPE}"
3636
$env:CODER_AGENT_URL = "${ACCESS_URL}"
3737

38+
$psi = [System.Diagnostics.ProcessStartInfo]::new("$env:TEMP\sshd.exe", '--version')
39+
$psi.UseShellExecute = $false
40+
$psi.RedirectStandardOutput = $true
41+
$p = [System.Diagnostics.Process]::Start($psi)
42+
$output = $p.StandardOutput.ReadToEnd()
43+
$p.WaitForExit()
44+
45+
if ($output -notlike "*Coder*") {
46+
Write-Output "$env:TEMP\sshd.exe --version output: `"$output"`"
47+
Write-Error "ERROR: Downloaded agent binary returned unexpected version output"
48+
Throw "unexpected binary"
49+
}
50+
3851
# Check if we're running inside a Windows container!
3952
$inContainer = $false
4053
if ((Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control' -Name 'ContainerType' -ErrorAction SilentlyContinue) -ne $null) {

scripts/check_site_icons.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#!/bin/bash
1+
#!/usr/bin/env bash
22

33
set -euo pipefail
44
# shellcheck source=scripts/lib.sh

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy