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") } }