Files
worker/internal/installer/sourceactivate_test.go
Gleb Tv 674a7d82bf
Все проверки выполнены успешно
CI / test (push) Successful in 4m30s
Docker / Build and publish worker image (push) Successful in 17m26s
feat(installer): activate source builds atomically
2026-08-13 02:26:37 +03:00

410 строки
15 KiB
Go

package installer
import (
"os"
"path/filepath"
"strings"
"testing"
"rocketgit.ru/rsmon/worker/internal/sshinstall"
)
func TestActivateScriptMarkers(t *testing.T) {
script := activateScript(activateParams{
Stage: "/opt/rsmon-worker-src/rsmon-worker",
Binary: "/usr/local/bin/rsmon-worker",
ConfigDir: "/etc/rsmon-worker",
EnvFile: "/etc/rsmon-worker/worker.env",
EnvTmp: "/tmp/rsmon-worker-abc.env",
DataDir: "/var/lib/rsmon-worker",
UnitTmp: "/tmp/rsmon-worker-abc.service",
UnitFile: "/etc/systemd/system/rsmon-worker.service",
UnitMode: "0644",
UnitName: "rsmon-worker.service",
RCName: "rsmon-worker",
Supervisor: "none",
NoStart: "0",
})
for _, want := range []string{
"set -eu",
// staged binary validated before any state is touched
"\"$stage\" --version >/dev/null",
// snapshot + rollback
"backup_dir=\"$data_dir/.rsmon-backup\"",
"rm -rf \"$backup_dir\"",
"cp -p \"$binary\" \"$backup_dir/binary\"",
"cp -p \"$env_file\" \"$backup_dir/worker.env\"",
"printf '%s\\n' \"$unit_enabled\" > \"$backup_dir/.marker\"",
"trap rollback EXIT HUP INT TERM",
"restoring the prior install",
// atomic installs with the right perms
"install -m 0755 \"$stage\" \"$binary.new\"",
"mv -f \"$binary.new\" \"$binary\"",
"install -m 0600 \"$env_tmp\" \"$env_file.new\"",
"mv -f \"$env_file.new\" \"$env_file\"",
"chmod 0750 \"$config_dir\"",
"mkdir -p \"$data_dir\" \"$data_dir/webapp\"",
"install -m \"$unit_mode\" \"$unit_tmp\" \"$unit_file.new\"",
"mv -f \"$unit_file.new\" \"$unit_file\"",
// supervisor + pid + health verification
"pid_file=\"$data_dir/worker.pid\"",
"env -i PATH=\"/usr/bin:/bin:/sbin:/usr/sbin\"",
"RSMON_WORKER_ENV_FILE=\"$env_file\"",
". \"$RSMON_WORKER_ENV_FILE\"",
"exec \"$@\"",
"nohup \"$1\" >>\"$2\" 2>&1 & echo $! > \"$3\"",
"run_env \"$binary\" liveness >/dev/null 2>&1",
"while [ \"$i\" -lt 30 ]; do",
"rsmon-worker activated:",
// activation lock + interrupted-run recovery
"lock_dir=\"$data_dir/.rsmon-activate.lock\"",
"acquire_lock",
"another rsmon-worker activation is in progress",
"recovering interrupted activation from $backup_dir",
".marker",
// pid belongs to the expected binary before kill
"readlink \"/proc/$1/exe\"",
"\"$binary (deleted)\"",
"process_is_worker \"$pid\"",
// rollback preserves metadata and enable state
"cp -p \"$backup_dir/binary\" \"$binary\"",
"cp -p \"$backup_dir/worker.env\" \"$env_file\"",
"set_unit_enabled \"$unit_enabled\"",
"systemctl is-enabled",
// temp/backup cleanup on success
"rm -rf \"$backup_dir\"",
"release_lock",
"trap - EXIT HUP INT TERM",
} {
if !strings.Contains(script, want) {
t.Fatalf("activate script missing %q:\n%s", want, script)
}
}
// The rollback must be armed only after the snapshot so a corrupt
// staging binary (validated first) never triggers a destructive
// rollback of the prior install.
if !strings.Contains(script, "\"$stage\" --version") || !strings.Contains(script, "trap rollback EXIT HUP INT TERM") {
t.Fatalf("activate script must validate the staged binary before arming rollback:\n%s", script)
}
stageCheck := strings.Index(script, "\"$stage\" --version")
trapIdx := strings.Index(script, "trap rollback EXIT HUP INT TERM")
if stageCheck < 0 || trapIdx < stageCheck {
t.Fatalf("staged-binary validation must precede the rollback trap:\n%s", script)
}
}
func TestActivateScriptSystemdSupervisor(t *testing.T) {
script := activateScript(activateParams{
Stage: "/opt/rsmon-worker-src/rsmon-worker",
Binary: "/usr/local/bin/rsmon-worker",
ConfigDir: "/etc/rsmon-worker",
EnvFile: "/etc/rsmon-worker/worker.env",
EnvTmp: "/tmp/e.env",
DataDir: "/var/lib/rsmon-worker",
UnitTmp: "/tmp/u.service",
UnitFile: "/etc/systemd/system/rsmon-worker.service",
UnitMode: "0644",
UnitName: "rsmon-worker.service",
RCName: "rsmon-worker",
Supervisor: "systemd",
NoStart: "0",
})
for _, want := range []string{
"supervisor=systemd",
"systemctl daemon-reload",
"systemctl restart \"$unit_name\"",
"systemctl is-active --quiet \"$unit_name\"",
"systemctl enable \"$unit_name\"",
"svc_active()",
"start failure: systemctl restart $unit_name failed",
"start failure: worker service is not active",
} {
if !strings.Contains(script, want) {
t.Fatalf("systemd supervisor missing %q:\n%s", want, script)
}
}
// The systemd health loop must use `systemctl is-active`, never the
// embedded supervisor's pid file.
if !strings.Contains(script, "systemd) systemctl is-active --quiet \"$unit_name\"") {
t.Fatalf("systemd health loop must use systemctl is-active:\n%s", script)
}
}
func TestActivateScriptOpenRCSupervisor(t *testing.T) {
script := activateScript(activateParams{
Stage: "/opt/rsmon-worker-src/rsmon-worker",
Binary: "/usr/local/bin/rsmon-worker",
ConfigDir: "/etc/rsmon-worker",
EnvFile: "/etc/rsmon-worker/worker.env",
EnvTmp: "/tmp/e.env",
DataDir: "/var/lib/rsmon-worker",
UnitTmp: "/tmp/u",
UnitFile: "/etc/init.d/rsmon-worker",
UnitMode: "0755",
UnitName: "rsmon-worker.service",
RCName: "rsmon-worker",
Supervisor: "openrc",
NoStart: "0",
})
for _, want := range []string{
"rc-service \"$rc_name\" restart",
"rc-service \"$rc_name\" status",
"rc-update add \"$rc_name\" default",
"start failure: rc-service restart $rc_name failed",
"rc-update del \"$rc_name\" default",
} {
if !strings.Contains(script, want) {
t.Fatalf("openrc supervisor missing %q:\n%s", want, script)
}
}
}
func TestActivateScriptNoStartSkipsRestart(t *testing.T) {
script := activateScript(activateParams{
Stage: "/opt/rsmon-worker-src/rsmon-worker", Binary: "/usr/local/bin/rsmon-worker",
ConfigDir: "/etc/rsmon-worker", EnvFile: "/etc/rsmon-worker/worker.env", EnvTmp: "/tmp/e.env",
DataDir: "/var/lib/rsmon-worker", UnitTmp: "", UnitFile: "", UnitMode: "",
UnitName: "rsmon-worker.service", RCName: "rsmon-worker", Supervisor: "none", NoStart: "1",
})
if !strings.Contains(script, "[ \"$no_start\" -ne 1 ]") {
t.Fatalf("no-start gate missing:\n%s", script)
}
if !strings.Contains(script, "started=no") {
t.Fatalf("no-start summary missing:\n%s", script)
}
}
func TestActivateScriptNeverContainsSecrets(t *testing.T) {
const secret = "super-secret-token-value"
params := activateParams{
Stage: "/opt/rsmon-worker-src/rsmon-worker", Binary: "/usr/local/bin/rsmon-worker",
ConfigDir: "/etc/rsmon-worker", EnvFile: "/etc/rsmon-worker/worker.env", EnvTmp: "/tmp/e.env",
DataDir: "/var/lib/rsmon-worker", UnitTmp: "/tmp/u", UnitFile: "/etc/systemd/system/rsmon-worker.service",
UnitMode: "0644", UnitName: "rsmon-worker.service", RCName: "rsmon-worker",
Supervisor: "systemd", NoStart: "0",
}
script := activateScript(params)
if strings.Contains(script, secret) {
t.Fatalf("activate script contains a secret:\n%s", script)
}
// The env file is referenced by path; its contents (the secrets) are
// only sourced at runtime and never echoed.
for _, want := range []string{"RSMON_TOKEN=", secret} {
if strings.Contains(script, want) {
t.Fatalf("activate script must not embed env contents (%q):\n%s", want, script)
}
}
}
func TestOpenRCInit(t *testing.T) {
unit := openrcInitFor(resolvePaths(""))
for _, want := range []string{
"#!/sbin/openrc-run",
"name=rsmon-worker",
"description=RSMon distributed monitoring worker",
"command=/usr/local/bin/rsmon-worker",
"command_background=true",
"pidfile=/var/lib/rsmon-worker/worker.pid",
"output_log=/var/lib/rsmon-worker/worker.log",
"need net",
". /etc/rsmon-worker/worker.env",
"set -a",
"set +a",
"RSMON_WEBAPP_DATA_DIR=/var/lib/rsmon-worker/webapp",
} {
if !strings.Contains(unit, want) {
t.Fatalf("openrc init missing %q:\n%s", want, unit)
}
}
named := openrcInitFor(resolvePaths("edge"))
for _, want := range []string{
"name=rsmon-worker-edge", "command=/usr/local/bin/rsmon-worker-edge",
"pidfile=/var/lib/rsmon-worker-edge/worker.pid", "/etc/rsmon-worker-edge/worker.env", "(edge)",
} {
if !strings.Contains(named, want) {
t.Fatalf("named openrc init missing %q:\n%s", want, named)
}
}
}
func TestRunningInitScript(t *testing.T) {
for _, want := range []string{"set -eu", "/run/systemd/system", "systemctl", "/run/openrc/softlevel", "rc-service", "echo none"} {
if !strings.Contains(runningInitScript, want) {
t.Fatalf("running-init script missing %q:\n%s", want, runningInitScript)
}
}
}
func TestNormalizeSupervisor(t *testing.T) {
for _, in := range []string{"systemd", "openrc", "systemd\n", " openrc "} {
want := strings.TrimSpace(in)
if got := normalizeSupervisor(in); got != want {
t.Fatalf("normalizeSupervisor(%q) = %q, want %q", in, got, want)
}
}
for _, in := range []string{"", "none", "sysvinit", " "} {
if got := normalizeSupervisor(in); got != "none" {
t.Fatalf("normalizeSupervisor(%q) = %q, want none", in, got)
}
}
}
func TestUnitContent(t *testing.T) {
p := resolvePaths("")
systemd, file, mode := unitContent(sshinstall.InitSystemd, p)
if systemd == "" || file != "/etc/systemd/system/rsmon-worker.service" || mode != "0644" {
t.Fatalf("systemd unit content = %q, %q, %q", systemd, file, mode)
}
if !strings.Contains(systemd, "ExecStart=/usr/local/bin/rsmon-worker\n") {
t.Fatalf("systemd unit not the classic hardened unit:\n%s", systemd)
}
openrc, file, mode := unitContent(sshinstall.InitOpenRC, p)
if openrc == "" || file != "/etc/init.d/rsmon-worker" || mode != "0755" {
t.Fatalf("openrc unit content = %q, %q, %q", openrc, file, mode)
}
if content, file, mode := unitContent(sshinstall.InitUnknown, p); content != "" || file != "" || mode != "" {
t.Fatalf("unknown-init unit content = %q, %q, %q, want empty (no-service gate)", content, file, mode)
}
}
func TestRenderEnvActivation(t *testing.T) {
t.Setenv("RSMON_URL", "")
t.Setenv("RSMON_TOKEN", "")
t.Setenv("WORKER_HOST", "")
t.Setenv("WORKER_PORT", "")
t.Setenv("WORKER_LOGIN", "")
t.Setenv("WORKER_PASSWORD", "")
data, err := (ActivationOptions{Activate: true, URL: "https://rsmon.ru", Token: "secret"}).renderEnv()
if err != nil {
t.Fatal(err)
}
for _, want := range []string{"RSMON_URL=https://rsmon.ru\n", "RSMON_TOKEN=secret\n", "WORKER_HOST=127.0.0.1\n", "WORKER_PORT=27401\n"} {
if !strings.Contains(string(data), want) {
t.Fatalf("rendered env missing %q:\n%s", want, data)
}
}
// PUBLIC_URL canonicalization is reused from the classic installer.
data, err = (ActivationOptions{
Activate: true, URL: "https://rsmon.ru", Token: "secret",
PublicURL: "https://worker.example.com",
}).renderEnv()
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(data), "PUBLIC_URL=https://worker.example.com\n") {
t.Fatalf("rendered env missing PUBLIC_URL:\n%s", data)
}
}
func TestRenderEnvActivationRejectsMixedBasicAuth(t *testing.T) {
t.Setenv("RSMON_TOKEN", "")
if _, err := (ActivationOptions{
Activate: true, URL: "https://rsmon.ru", Token: "secret",
Login: "admin",
}).renderEnv(); err == nil {
t.Fatal("login-only basic auth accepted")
}
}
func TestRenderEnvActivationRequiresToken(t *testing.T) {
if _, err := (ActivationOptions{Activate: true, URL: "https://rsmon.ru"}).renderEnv(); err == nil {
t.Fatal("activation without a token accepted")
}
if _, err := (ActivationOptions{Activate: true, Token: "x", URL: "not-a-url"}).renderEnv(); err == nil {
t.Fatal("activation with a malformed URL accepted")
}
if _, err := (ActivationOptions{Activate: true, Token: "x", URL: "https://rsmon.ru", Name: "Bad_Name"}).renderEnv(); err == nil {
t.Fatal("activation with an invalid instance name accepted")
}
if _, err := (ActivationOptions{Activate: true, Token: "x", URL: "https://rsmon.ru", Name: "edge"}).renderEnv(); err == nil {
t.Fatal("named activation without WORKER_PORT accepted")
}
}
// TestRenderEnvActivationReadsEnvFileOnce proves the env file is read a
// single time and validated from the in-memory bytes: swapping the file
// after the read cannot smuggle a different value into the render, and a
// malformed file fails on the read bytes.
func TestRenderEnvActivationReadsEnvFileOnce(t *testing.T) {
t.Setenv("RSMON_TOKEN", "")
path := filepath.Join(t.TempDir(), "worker.env")
if err := os.WriteFile(path, []byte("RSMON_URL=https://rsmon.ru\nRSMON_TOKEN=file-token\nWORKER_PORT=28888\n"), 0o600); err != nil {
t.Fatal(err)
}
data, err := (ActivationOptions{Activate: true, EnvFile: path}).renderEnv()
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(data), "RSMON_TOKEN=file-token\n") || !strings.Contains(string(data), "WORKER_PORT=28888\n") {
t.Fatalf("env file values not rendered:\n%s", data)
}
// A malformed file must be rejected from the same read.
if err := os.WriteFile(path, []byte("RSMON_URL=https://rsmon.ru\nRSMON_TOKEN=has space\n"), 0o600); err != nil {
t.Fatal(err)
}
if _, err := (ActivationOptions{Activate: true, EnvFile: path}).renderEnv(); err == nil {
t.Fatal("malformed env file accepted")
}
}
// TestNormalizeSourceOptionsActivationRendersOnce verifies the rendered
// env is computed during normalization and reused, so a later call cannot
// re-read a changed env file.
func TestNormalizeSourceOptionsActivationRendersOnce(t *testing.T) {
t.Setenv("RSMON_TOKEN", "")
path := filepath.Join(t.TempDir(), "worker.env")
if err := os.WriteFile(path, []byte("RSMON_URL=https://rsmon.ru\nRSMON_TOKEN=once-token\n"), 0o600); err != nil {
t.Fatal(err)
}
base := SourceInstallOptions{
SSHOptions: SSHOptions{Host: "h", User: "u"},
Activation: ActivationOptions{Activate: true, EnvFile: path},
}
norm, err := normalizeSourceOptions(base)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(norm.activationEnv), "RSMON_TOKEN=once-token\n") {
t.Fatalf("activation env not cached during normalization: %q", norm.activationEnv)
}
// Even after the file changes, the cached render is authoritative.
if err := os.WriteFile(path, []byte("RSMON_URL=https://evil.test\nRSMON_TOKEN=evil\n"), 0o600); err != nil {
t.Fatal(err)
}
if strings.Contains(string(norm.activationEnv), "evil") {
t.Fatalf("changed env file leaked into the cached render: %q", norm.activationEnv)
}
}
func TestNormalizeSourceOptionsActivation(t *testing.T) {
t.Setenv("RSMON_TOKEN", "")
base := SourceInstallOptions{SSHOptions: SSHOptions{Host: "h", User: "u"}}
if _, err := normalizeSourceOptions(base); err != nil {
t.Fatalf("staging-only options must stay valid: %v", err)
}
act := base
act.Activation = ActivationOptions{Activate: true, URL: "https://rsmon.ru"}
if _, err := normalizeSourceOptions(act); err == nil || !strings.Contains(err.Error(), "RSMON_TOKEN") {
t.Fatalf("activation without a token error = %v", err)
}
ok := base
ok.Activation = ActivationOptions{Activate: true, URL: "https://rsmon.ru", Token: "secret"}
if _, err := normalizeSourceOptions(ok); err != nil {
t.Fatalf("valid activation rejected: %v", err)
}
}
func TestValidateRenderedEnv(t *testing.T) {
if err := validateRenderedEnv([]byte("RSMON_URL=https://rsmon.ru\nRSMON_TOKEN=secret\n")); err != nil {
t.Fatal(err)
}
if err := validateRenderedEnv([]byte("RSMON_TOKEN=secret\n")); err == nil {
t.Fatal("env without RSMON_URL accepted")
}
if err := validateRenderedEnv([]byte("RSMON_URL=https://rsmon.ru\nRSMON_TOKEN=secret value\n")); err == nil {
t.Fatal("env with whitespace accepted")
}
}