Все проверки выполнены успешно
CI / test (push) Successful in 4m30s
Docker / Build and publish worker image (push) Successful in 17m26s
410 строки
15 KiB
Go
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")
|
|
}
|
|
}
|