fix: return error if agent init script fails to download valid binary (#13280)

This commit is contained in:
Danny Kopping
2024-05-30 13:33:00 +02:00
committed by GitHub
parent e176867d77
commit 59ab5053b1
6 changed files with 154 additions and 30 deletions
+3 -3
View File
@@ -39,9 +39,9 @@ var (
}
)
// AgentScriptEnv returns a key-pair of scripts that are consumed
// by the Coder Terraform Provider. See:
// https://github.com/coder/terraform-provider-coder/blob/main/internal/provider/provider.go#L97
// AgentScriptEnv returns a key-pair of scripts that are consumed by the Coder Terraform Provider.
// https://github.com/coder/terraform-provider-coder/blob/main/provider/agent.go (updateInitScript)
// performs additional string substitutions.
func AgentScriptEnv() map[string]string {
env := map[string]string{}
for operatingSystem, scripts := range agentScripts {
+117 -22
View File
@@ -7,6 +7,9 @@
package provisionersdk_test
import (
"bytes"
"context"
"errors"
"fmt"
"net/http"
"net/http/httptest"
@@ -14,50 +17,142 @@ import (
"os/exec"
"runtime"
"strings"
"sync"
"testing"
"time"
"github.com/go-chi/render"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/testutil"
"github.com/coder/coder/v2/provisionersdk"
)
// 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 $@`
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("Run", func(t *testing.T) {
t.Parallel()
srv := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
render.Status(r, http.StatusOK)
render.Data(rw, r, []byte(bashEcho))
}))
defer 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")
t.Run("Valid", func(t *testing.T) {
t.Parallel()
script := serveScript(t, bashEcho)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
t.Cleanup(cancel)
var output bytes.Buffer
// 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
output, err := exec.Command("sh", "-c", "sh -c '"+script+"'").CombinedOutput()
t.Log(string(output))
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(string(output)), "\n")
lines := strings.Split(strings.TrimSpace(output.String()), "\n")
lastLine := lines[len(lines)-1]
// Because we use the "echo" binary, we should expect the arguments provided
// When we use the "bashEcho" binary, we should expect the arguments provided
// as the response to executing our script.
require.Equal(t, "agent", lastLine)
require.Equal(t, versionOutput, lastLine)
})
t.Run("Invalid", func(t *testing.T) {
t.Parallel()
script := serveScript(t, unexpectedEcho)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
t.Cleanup(cancel)
var output bytes.Buffer
// 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.Add(1)
go func() {
defer wg.Done()
// 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.
require.NoError(t, cmd.Cancel())
wg.Wait()
t.Log(output.String())
require.Eventually(t, func() bool {
return bytes.Contains(output.Bytes(), []byte("ERROR: Downloaded agent binary returned unexpected version output"))
}, testutil.WaitShort, testutil.IntervalSlow)
})
}
// 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) {
render.Status(r, http.StatusOK)
render.Data(rw, r, []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
}
+10 -2
View File
@@ -4,7 +4,7 @@ set -eux
# This is to allow folks to exec into a failed workspace and poke around to
# troubleshoot.
waitonexit() {
echo "=== Agent script exited with non-zero code. Sleeping 24h to preserve logs..."
echo "=== Agent script exited with non-zero code ($?). Sleeping 24h to preserve logs..."
sleep 86400
}
trap waitonexit EXIT
@@ -31,4 +31,12 @@ fi
export CODER_AGENT_AUTH="${AUTH_TYPE}"
export CODER_AGENT_URL="${ACCESS_URL}"
exec ./$BINARY_NAME agent
output=$(./${BINARY_NAME} --version | head -n1)
if ! echo "${output}" | grep -q Coder; then
echo >&2 "ERROR: Downloaded agent binary returned unexpected version output"
echo >&2 "${BINARY_NAME} --version output: \"${output}\""
exit 2
fi
exec ./${BINARY_NAME} agent
+10 -2
View File
@@ -4,7 +4,7 @@ set -eux
# This is to allow folks to exec into a failed workspace and poke around to
# troubleshoot.
waitonexit() {
echo "=== Agent script exited with non-zero code. Sleeping 24h to preserve logs..."
echo "=== Agent script exited with non-zero code ($?). Sleeping 24h to preserve logs..."
sleep 86400
}
trap waitonexit EXIT
@@ -86,4 +86,12 @@ fi
export CODER_AGENT_AUTH="${AUTH_TYPE}"
export CODER_AGENT_URL="${ACCESS_URL}"
exec ./$BINARY_NAME agent
output=$(./${BINARY_NAME} --version | head -n1)
if ! echo "${output}" | grep -q Coder; then
echo >&2 "ERROR: Downloaded agent binary returned unexpected version output"
echo >&2 "${BINARY_NAME} --version output: \"${output}\""
exit 2
fi
exec ./${BINARY_NAME} agent
@@ -35,6 +35,19 @@ if (-not (Get-Command 'Set-MpPreference' -ErrorAction SilentlyContinue)) {
$env:CODER_AGENT_AUTH = "${AUTH_TYPE}"
$env:CODER_AGENT_URL = "${ACCESS_URL}"
$psi = [System.Diagnostics.ProcessStartInfo]::new("$env:TEMP\sshd.exe", '--version')
$psi.UseShellExecute = $false
$psi.RedirectStandardOutput = $true
$p = [System.Diagnostics.Process]::Start($psi)
$output = $p.StandardOutput.ReadToEnd()
$p.WaitForExit()
if ($output -notlike "*Coder*") {
Write-Output "$env:TEMP\sshd.exe --version output: `"$output"`"
Write-Error "ERROR: Downloaded agent binary returned unexpected version output"
Throw "unexpected binary"
}
# Check if we're running inside a Windows container!
$inContainer = $false
if ((Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control' -Name 'ContainerType' -ErrorAction SilentlyContinue) -ne $null) {
+1 -1
View File
@@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
set -euo pipefail
# shellcheck source=scripts/lib.sh