Все проверки выполнены успешно
CI / test (push) Successful in 3m13s
Docker / Build and publish worker image (push) Successful in 10m35s
396 строки
14 KiB
Go
396 строки
14 KiB
Go
package installer
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"rocketgit.ru/rsmon/worker/internal/sshinstall"
|
|
)
|
|
|
|
// cannedSHA is a fixed 64-hex value used to exercise script rendering.
|
|
const cannedSHA = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
|
|
|
func TestPackageScript(t *testing.T) {
|
|
pkgs := []string{"git", "ca-certificates", "curl", "tar", "gzip"}
|
|
|
|
apk := packageScript(sshinstall.PkgApk, pkgs)
|
|
if !strings.HasPrefix(apk, "apk add --no-cache ") {
|
|
t.Fatalf("apk script = %q", apk)
|
|
}
|
|
for _, p := range pkgs {
|
|
if !strings.Contains(apk, shellQuote(p)) {
|
|
t.Fatalf("apk script missing quoted package %q: %q", p, apk)
|
|
}
|
|
}
|
|
|
|
apt := packageScript(sshinstall.PkgApt, pkgs)
|
|
for _, want := range []string{"export DEBIAN_FRONTEND=noninteractive", "apt-get update", "apt-get install -y --no-install-recommends"} {
|
|
if !strings.Contains(apt, want) {
|
|
t.Fatalf("apt script missing %q: %q", want, apt)
|
|
}
|
|
}
|
|
|
|
pacman := packageScript(sshinstall.PkgPacman, pkgs)
|
|
if !strings.Contains(pacman, "pacman -Sy --noconfirm --needed") {
|
|
t.Fatalf("pacman script = %q", pacman)
|
|
}
|
|
|
|
dnf := packageScript(sshinstall.PkgDnf, pkgs)
|
|
if !strings.Contains(dnf, "dnf install -y") {
|
|
t.Fatalf("dnf script = %q", dnf)
|
|
}
|
|
|
|
if got := packageScript(sshinstall.PkgUnknown, pkgs); got != "" {
|
|
t.Fatalf("unknown pkg script = %q, want empty", got)
|
|
}
|
|
}
|
|
|
|
func TestPackageScriptNeverIncludesCompiler(t *testing.T) {
|
|
pkgs := []string{"git", "ca-certificates", "curl", "tar", "gzip"}
|
|
for _, pkg := range []sshinstall.PackageManager{sshinstall.PkgApk, sshinstall.PkgApt, sshinstall.PkgPacman, sshinstall.PkgDnf} {
|
|
script := packageScript(pkg, pkgs)
|
|
for _, bad := range []string{"build-essential", "gcc", "g++", "base-devel", "make", "gcc-c++"} {
|
|
if strings.Contains(script, bad) {
|
|
t.Fatalf("script for %s includes compiler hint %q: %q", pkg, bad, script)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestToolchainScript(t *testing.T) {
|
|
tc := sshinstall.Toolchain{
|
|
Version: "1.26.0",
|
|
Arch: "linux-amd64",
|
|
URL: "https://go.dev/dl/go1.26.0.linux-amd64.tar.gz",
|
|
SHA256: cannedSHA,
|
|
}
|
|
script := toolchainScript(tc, "/usr/local/go")
|
|
for _, want := range []string{
|
|
"set -eu",
|
|
"mktemp -d /tmp/rsmon-toolchain-XXXXXX",
|
|
"mktemp -d \"$parent/.go-staging-XXXXXX\"",
|
|
"trap 'rm -rf \"$work\" \"$staging\"' EXIT HUP INT TERM",
|
|
"curl -fsSL --retry 3 --retry-delay 2 -o \"$archive\" 'https://go.dev/dl/go1.26.0.linux-amd64.tar.gz'",
|
|
"sha256sum -c -",
|
|
cannedSHA,
|
|
"tar -C \"$staging\" -xzf \"$archive\"",
|
|
"staged=\"$($staging/go/bin/go version | awk '{print $3}')\"",
|
|
"backup=\"$parent/.go-backup\"",
|
|
"mv \"$toolchain\" \"$backup\"",
|
|
"mv \"$staging/go\" \"$toolchain\"",
|
|
"\"$toolchain/bin/go\" version",
|
|
"go toolchain already present",
|
|
} {
|
|
if !strings.Contains(script, want) {
|
|
t.Fatalf("toolchain script missing %q:\n%s", want, script)
|
|
}
|
|
}
|
|
if !strings.Contains(script, shellQuote("go1.26.0")) {
|
|
t.Fatalf("toolchain script missing version guard:\n%s", script)
|
|
}
|
|
}
|
|
|
|
func TestToolchainScriptIdempotentSkipOnlyForMatchingVersion(t *testing.T) {
|
|
script := toolchainScript(sshinstall.Toolchain{Version: "1.26.0", Arch: "linux-amd64", URL: "u", SHA256: cannedSHA}, "/usr/local/go")
|
|
if strings.Count(script, "exit 0") != 1 {
|
|
t.Fatalf("toolchain script should skip only once:\n%s", script)
|
|
}
|
|
// Replacement must be atomic: the prior toolchain is moved aside and
|
|
// restored when the swap fails, never removed before staging is ready.
|
|
for _, want := range []string{
|
|
"if [ -e \"$toolchain\" ]; then",
|
|
"mv \"$toolchain\" \"$backup\"",
|
|
"if ! mv \"$staging/go\" \"$toolchain\"; then",
|
|
"mv \"$backup\" \"$toolchain\"",
|
|
} {
|
|
if !strings.Contains(script, want) {
|
|
t.Fatalf("toolchain script missing %q:\n%s", want, script)
|
|
}
|
|
}
|
|
if strings.Contains(script, "rm -rf \"$toolchain\"") {
|
|
t.Fatalf("toolchain script must not delete the live toolchain directly:\n%s", script)
|
|
}
|
|
}
|
|
|
|
func TestCloneUpdateScript(t *testing.T) {
|
|
script := cloneUpdateScript("https://example.test/worker.git", "/opt/rsmon-worker-src")
|
|
for _, want := range []string{
|
|
"set -u",
|
|
"[ ! -d \"$dir/.git\" ]",
|
|
"git clone \"$repo\" \"$dir\"",
|
|
"while [ \"$attempt\" -lt 3 ]",
|
|
"sleep 2",
|
|
"git -C \"$dir\" config --get remote.origin.url",
|
|
"existing checkout origin does not match configured repository",
|
|
"git -C \"$dir\" fetch --prune origin",
|
|
"exit 1",
|
|
} {
|
|
if !strings.Contains(script, want) {
|
|
t.Fatalf("clone/update script missing %q:\n%s", want, script)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCheckoutScriptFailsClosed(t *testing.T) {
|
|
script := checkoutScript("/opt/rsmon-worker-src", "master")
|
|
if !strings.HasPrefix(script, "set -eu\n") {
|
|
t.Fatalf("checkout script must fail closed with set -eu:\n%s", script)
|
|
}
|
|
for _, want := range []string{
|
|
"git -C '/opt/rsmon-worker-src' diff --quiet ||",
|
|
"git -C '/opt/rsmon-worker-src' diff --cached --quiet ||",
|
|
"refusing to overwrite",
|
|
"checkout -q -B 'master' 'origin/master'",
|
|
"rev-parse HEAD",
|
|
} {
|
|
if !strings.Contains(script, want) {
|
|
t.Fatalf("checkout script missing %q:\n%s", want, script)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestResolveBranchScriptFailsClosed(t *testing.T) {
|
|
if !strings.HasPrefix(resolveBranchScript("/opt/rsmon-worker-src"), "set -eu\n") {
|
|
t.Fatalf("resolve-branch script must fail closed:\n%s", resolveBranchScript("/opt/rsmon-worker-src"))
|
|
}
|
|
}
|
|
|
|
func TestResolveBranchScript(t *testing.T) {
|
|
script := resolveBranchScript("/opt/rsmon-worker-src")
|
|
for _, want := range []string{
|
|
"remote set-head origin --auto",
|
|
"symbolic-ref --short refs/remotes/origin/HEAD",
|
|
} {
|
|
if !strings.Contains(script, want) {
|
|
t.Fatalf("resolve-branch script missing %q:\n%s", want, script)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCheckoutScript(t *testing.T) {
|
|
script := checkoutScript("/opt/rsmon-worker-src", "master")
|
|
for _, want := range []string{
|
|
"checkout -q -B 'master' 'origin/master'",
|
|
"rev-parse HEAD",
|
|
} {
|
|
if !strings.Contains(script, want) {
|
|
t.Fatalf("checkout script missing %q:\n%s", want, script)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRefExistsScript(t *testing.T) {
|
|
script := refExistsScript("/opt/rsmon-worker-src", "refs/remotes/origin/main")
|
|
if !strings.Contains(script, "show-ref --verify --quiet 'refs/remotes/origin/main'") {
|
|
t.Fatalf("ref-exists script = %q", script)
|
|
}
|
|
}
|
|
|
|
func TestCommitRecordScript(t *testing.T) {
|
|
script := commitRecordScript("/opt/rsmon-worker-src", "master", strings.Repeat("a", 40))
|
|
for _, want := range []string{
|
|
"branch=%s\\ncommit=%s\\n",
|
|
"'master'",
|
|
strings.Repeat("a", 40),
|
|
"'/opt/rsmon-worker-src/rsmon-worker.commit'",
|
|
"chmod 0644",
|
|
} {
|
|
if !strings.Contains(script, want) {
|
|
t.Fatalf("record script missing %q:\n%s", want, script)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBuildScript(t *testing.T) {
|
|
script := buildScript("/usr/local/go", "/opt/rsmon-worker-src", "", "/opt/rsmon-worker-src/rsmon-worker",
|
|
`-s -w -X main.version=dev -X main.commit=abcdef012345 -X main.buildDate=2026-08-12T00:00:00Z`)
|
|
for _, want := range []string{
|
|
"set -eu",
|
|
"cd '/opt/rsmon-worker-src'",
|
|
"export PATH='/usr/local/go/bin':$PATH",
|
|
"export GOCACHE='/opt/rsmon-worker-src/.gocache'",
|
|
"export GOMODCACHE='/opt/rsmon-worker-src/.gomodcache'",
|
|
"CGO_ENABLED=0 '/usr/local/go/bin/go' build -trimpath",
|
|
"-X main.commit=abcdef012345",
|
|
"-o \"$tmp\" ./cmd/rsmon-worker",
|
|
"tmp=\"$stage.new\"",
|
|
"\"$tmp\" --version",
|
|
"mv -f \"$tmp\" \"$stage\"",
|
|
"trap 'rm -f \"$tmp\"' EXIT HUP INT TERM",
|
|
} {
|
|
if !strings.Contains(script, want) {
|
|
t.Fatalf("build script missing %q:\n%s", want, script)
|
|
}
|
|
}
|
|
if strings.Contains(script, "GOPROXY") {
|
|
t.Fatalf("empty GOPROXY must not be exported:\n%s", script)
|
|
}
|
|
|
|
withProxy := buildScript("/usr/local/go", "/opt/rsmon-worker-src", "https://proxy.golang.org,direct", "/opt/rsmon-worker-src/rsmon-worker", "-s -w")
|
|
if !strings.Contains(withProxy, "export GOPROXY='https://proxy.golang.org,direct'") {
|
|
t.Fatalf("GOPROXY override not rendered:\n%s", withProxy)
|
|
}
|
|
}
|
|
|
|
func TestBuildScriptVerifiesBeforeSwap(t *testing.T) {
|
|
script := buildScript("/usr/local/go", "/opt/rsmon-worker-src", "", "/opt/rsmon-worker-src/rsmon-worker", "-s -w")
|
|
verify := strings.Index(script, "\"$tmp\" --version")
|
|
swap := strings.Index(script, "mv -f \"$tmp\" \"$stage\"")
|
|
if verify < 0 || swap < 0 || verify > swap {
|
|
t.Fatalf("build script must verify the temp binary before swapping it in:\n%s", script)
|
|
}
|
|
}
|
|
|
|
func TestParseResolvedCommit(t *testing.T) {
|
|
commit := strings.Repeat("abcdef", 6) + "abcd" // 40 hex
|
|
if got, err := parseResolvedCommit(commit + "\n"); err != nil || got != commit {
|
|
t.Fatalf("parseResolvedCommit() = %q, %v", got, err)
|
|
}
|
|
if got, err := parseResolvedCommit("ignored\n" + commit + "\n"); err != nil || got != commit {
|
|
t.Fatalf("parseResolvedCommit() multi-line = %q, %v", got, err)
|
|
}
|
|
for _, bad := range []string{"", "abc", strings.Repeat("A", 40), strings.Repeat("a", 39), "x" + strings.Repeat("a", 39)} {
|
|
if _, err := parseResolvedCommit(bad); err == nil {
|
|
t.Fatalf("parseResolvedCommit(%q) succeeded", bad)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParseResolvedBranch(t *testing.T) {
|
|
if got, err := parseResolvedBranch("origin/master\n"); err != nil || got != "master" {
|
|
t.Fatalf("parseResolvedBranch(origin/master) = %q, %v", got, err)
|
|
}
|
|
if got, err := parseResolvedBranch("master\n"); err != nil || got != "master" {
|
|
t.Fatalf("parseResolvedBranch(master) = %q, %v", got, err)
|
|
}
|
|
// A multi-component short name is legal git and stays safe because
|
|
// the value is validated and single-quoted everywhere it is used.
|
|
if got, err := parseResolvedBranch("origin/release/1.0\n"); err != nil || got != "release/1.0" {
|
|
t.Fatalf("parseResolvedBranch(nested) = %q, %v", got, err)
|
|
}
|
|
for _, bad := range []string{"origin/../evil\n", "origin/x y\n", "origin/x..y\n", "origin/x@{y\n", "origin/-x\n"} {
|
|
if _, err := parseResolvedBranch(bad); err == nil {
|
|
t.Fatalf("parseResolvedBranch(%q) succeeded", bad)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestValidBranchName(t *testing.T) {
|
|
for _, ok := range []string{"main", "master", "release-1.0", "feature/x", "a", "v1.2.3", "a_b", "release/1.0"} {
|
|
if !validBranchName(ok) {
|
|
t.Fatalf("validBranchName(%q) rejected", ok)
|
|
}
|
|
}
|
|
for _, bad := range []string{"", "-bad", "x..y", "x@{y", "x y", "/x", "x.", "x/", "x//y", "x\\y", "x;y", "$x", "`x`"} {
|
|
if validBranchName(bad) {
|
|
t.Fatalf("validBranchName(%q) accepted", bad)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestQuoteList(t *testing.T) {
|
|
if got, want := quoteList([]string{"git", "ca-certificates"}), "'git' 'ca-certificates'"; got != want {
|
|
t.Fatalf("quoteList() = %q, want %q", got, want)
|
|
}
|
|
if got := quoteList(nil); got != "" {
|
|
t.Fatalf("quoteList(nil) = %q, want empty", got)
|
|
}
|
|
}
|
|
|
|
func TestNormalizeSourceOptions(t *testing.T) {
|
|
o, err := normalizeSourceOptions(SourceInstallOptions{SSHOptions: SSHOptions{Host: "h", User: "u"}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if o.Port != 22 || o.ToolchainDir != "/usr/local/go" || o.SessionTimeout != defaultSessionTimeout {
|
|
t.Fatalf("defaults not applied: %+v", o)
|
|
}
|
|
if o, err := normalizeSourceOptions(SourceInstallOptions{SSHOptions: SSHOptions{Host: "h", User: "u"}, SessionTimeout: 7 * time.Minute}); err != nil || o.SessionTimeout != 7*time.Minute {
|
|
t.Fatalf("explicit session timeout not honored: %+v, %v", o, err)
|
|
}
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
opts SourceInstallOptions
|
|
}{
|
|
{name: "missing host", opts: SourceInstallOptions{SSHOptions: SSHOptions{User: "u"}}},
|
|
{name: "missing user", opts: SourceInstallOptions{SSHOptions: SSHOptions{Host: "h"}}},
|
|
{name: "bad port", opts: SourceInstallOptions{SSHOptions: SSHOptions{Host: "h", User: "u", Port: 70000}}},
|
|
{name: "bad branch", opts: SourceInstallOptions{SSHOptions: SSHOptions{Host: "h", User: "u"}, Branch: "x y"}},
|
|
{name: "bad go version", opts: SourceInstallOptions{SSHOptions: SSHOptions{Host: "h", User: "u"}, GoVersion: "1.26;rm"}},
|
|
{name: "bad go arch", opts: SourceInstallOptions{SSHOptions: SSHOptions{Host: "h", User: "u"}, GoArch: "amd64;rm"}},
|
|
{name: "bad repo scheme", opts: SourceInstallOptions{SSHOptions: SSHOptions{Host: "h", User: "u"}, Repo: "http://x/y"}},
|
|
{name: "repo userinfo", opts: SourceInstallOptions{SSHOptions: SSHOptions{Host: "h", User: "u"}, Repo: "https://user:pass@x/y"}},
|
|
{name: "relative toolchain", opts: SourceInstallOptions{SSHOptions: SSHOptions{Host: "h", User: "u"}, ToolchainDir: "usr/local/go"}},
|
|
{name: "non-go toolchain basename", opts: SourceInstallOptions{SSHOptions: SSHOptions{Host: "h", User: "u"}, ToolchainDir: "/opt/golang"}},
|
|
{name: "relative stage", opts: SourceInstallOptions{SSHOptions: SSHOptions{Host: "h", User: "u"}, StageBinary: "bin/worker"}},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if _, err := normalizeSourceOptions(tc.opts); err == nil {
|
|
t.Fatalf("normalizeSourceOptions(%s) succeeded", tc.name)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSudoWrap(t *testing.T) {
|
|
cmd, stdin := sudoWrap("root", "", "echo hi")
|
|
if cmd != "echo hi" || stdin != nil {
|
|
t.Fatalf("root wrap = %q, %q", cmd, stdin)
|
|
}
|
|
|
|
cmd, stdin = sudoWrap("deploy", "", "echo hi")
|
|
if cmd != "sudo -n -- echo hi" || stdin != nil {
|
|
t.Fatalf("passwordless sudo wrap = %q, %q", cmd, stdin)
|
|
}
|
|
|
|
cmd, stdin = sudoWrap("deploy", "supersecret", "echo hi")
|
|
if cmd != "sudo -S -p '' -- echo hi" || string(stdin) != "supersecret\n" {
|
|
t.Fatalf("sudo -S wrap = %q, %q", cmd, stdin)
|
|
}
|
|
if strings.Contains(cmd, "supersecret") {
|
|
t.Fatal("sudo password leaked into the command string")
|
|
}
|
|
}
|
|
|
|
// TestSourceScriptsNeverContainSecrets documents the "secrets absent"
|
|
// contract: none of the rendered remote scripts carry a credential.
|
|
func TestBoundedBuffer(t *testing.T) {
|
|
var b boundedBuffer
|
|
b.max = 8
|
|
if _, err := b.Write([]byte("12345")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if b.truncated {
|
|
t.Fatal("truncated before exceeding max")
|
|
}
|
|
if _, err := b.Write([]byte("6789abcdef")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !b.truncated {
|
|
t.Fatal("overflow not flagged")
|
|
}
|
|
if got, want := b.String(), "12345678"; got != want {
|
|
t.Fatalf("boundedBuffer = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestSourceScriptsNeverContainSecrets(t *testing.T) {
|
|
const secret = "super-secret-token-value"
|
|
scripts := []string{
|
|
packageScript(sshinstall.PkgApk, []string{"git", "ca-certificates", "curl", "tar", "gzip"}),
|
|
toolchainScript(sshinstall.Toolchain{Version: "1.26.0", Arch: "linux-amd64", URL: "https://go.dev/dl/go1.26.0.linux-amd64.tar.gz", SHA256: cannedSHA}, "/usr/local/go"),
|
|
cloneUpdateScript("https://example.test/worker.git", "/opt/rsmon-worker-src"),
|
|
resolveBranchScript("/opt/rsmon-worker-src"),
|
|
checkoutScript("/opt/rsmon-worker-src", "master"),
|
|
commitRecordScript("/opt/rsmon-worker-src", "master", strings.Repeat("a", 40)),
|
|
buildScript("/usr/local/go", "/opt/rsmon-worker-src", "", "/opt/rsmon-worker-src/rsmon-worker", "-s -w"),
|
|
}
|
|
for i, script := range scripts {
|
|
if strings.Contains(script, secret) {
|
|
t.Fatalf("script %d contains a secret", i)
|
|
}
|
|
}
|
|
}
|