mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
348 lines
12 KiB
Go
348 lines
12 KiB
Go
package installtests
|
|
|
|
import (
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestUninstallSensorProxyScriptContract(t *testing.T) {
|
|
scriptPath := repoFile("scripts", "uninstall-sensor-proxy.sh")
|
|
content, err := os.ReadFile(scriptPath)
|
|
if err != nil {
|
|
t.Fatalf("read uninstall-sensor-proxy.sh: %v", err)
|
|
}
|
|
|
|
script := string(content)
|
|
required := []string{
|
|
`--remove-proxmox-access`,
|
|
`--local-only`,
|
|
`--ssh-known-hosts`,
|
|
`StrictHostKeyChecking=yes`,
|
|
`UpdateHostKeys=no`,
|
|
`GlobalKnownHostsFile=none`,
|
|
`pulse-sensor-proxy-selfheal.timer`,
|
|
`pulse-sensor-cleanup.path`,
|
|
`remove_managed_keys_from_authorized_keys_file()`,
|
|
`cleanup_stale_sensor_proxy_mounts()`,
|
|
`pulse-monitor@pam`,
|
|
`# pulse-(managed|proxy)-key$`,
|
|
}
|
|
for _, needle := range required {
|
|
if !strings.Contains(script, needle) {
|
|
t.Fatalf("uninstall-sensor-proxy.sh missing cleanup contract: %s", needle)
|
|
}
|
|
}
|
|
|
|
if out, err := exec.Command("bash", "-n", scriptPath).CombinedOutput(); err != nil {
|
|
t.Fatalf("bash -n uninstall-sensor-proxy.sh: %v\n%s", err, out)
|
|
}
|
|
}
|
|
|
|
func TestUninstallSensorProxyRemoteCleanupUsesProvisionedHostKeys(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
binDir := filepath.Join(tmpDir, "bin")
|
|
argsLog := filepath.Join(tmpDir, "ssh-args")
|
|
knownHosts := filepath.Join(tmpDir, "known_hosts")
|
|
if err := os.MkdirAll(binDir, 0o755); err != nil {
|
|
t.Fatalf("mkdir bin dir: %v", err)
|
|
}
|
|
sshStub := `#!/bin/sh
|
|
printf '%s\n' "$@" >"$SSH_ARGS_LOG"
|
|
exit "${SSH_EXIT_STATUS:-0}"
|
|
`
|
|
if err := os.WriteFile(filepath.Join(binDir, "ssh"), []byte(sshStub), 0o755); err != nil {
|
|
t.Fatalf("write ssh stub: %v", err)
|
|
}
|
|
if err := os.WriteFile(knownHosts, []byte("node ssh-ed25519 AAAAtest\n"), 0o600); err != nil {
|
|
t.Fatalf("write known_hosts: %v", err)
|
|
}
|
|
resolvedKnownHosts, err := filepath.EvalSymlinks(knownHosts)
|
|
if err != nil {
|
|
t.Fatalf("resolve known_hosts: %v", err)
|
|
}
|
|
|
|
command := `source "$1"
|
|
SSH_KNOWN_HOSTS_PATH="$2"
|
|
cleanup_remote_authorized_keys 192.0.2.10
|
|
`
|
|
cmd := exec.Command("bash", "-c", command, "bash", repoFile("scripts", "uninstall-sensor-proxy.sh"), knownHosts)
|
|
cmd.Env = append(os.Environ(),
|
|
"PATH="+binDir+string(os.PathListSeparator)+os.Getenv("PATH"),
|
|
"SSH_ARGS_LOG="+argsLog,
|
|
)
|
|
if out, err := cmd.CombinedOutput(); err != nil {
|
|
t.Fatalf("remote cleanup with provisioned trust failed: %v\n%s", err, out)
|
|
}
|
|
|
|
args, err := os.ReadFile(argsLog)
|
|
if err != nil {
|
|
t.Fatalf("read ssh args: %v", err)
|
|
}
|
|
got := string(args)
|
|
for _, option := range []string{
|
|
"StrictHostKeyChecking=yes",
|
|
"UpdateHostKeys=no",
|
|
"BatchMode=yes",
|
|
"ConnectTimeout=5",
|
|
"UserKnownHostsFile=" + resolvedKnownHosts,
|
|
"GlobalKnownHostsFile=none",
|
|
"root@192.0.2.10",
|
|
} {
|
|
if !strings.Contains(got, option) {
|
|
t.Fatalf("ssh invocation missing %q:\n%s", option, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUninstallSensorProxyRemoteCleanupUsesStrictConfiguredTrustByDefault(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
binDir := filepath.Join(tmpDir, "bin")
|
|
argsLog := filepath.Join(tmpDir, "ssh-args")
|
|
if err := os.MkdirAll(binDir, 0o755); err != nil {
|
|
t.Fatalf("mkdir bin dir: %v", err)
|
|
}
|
|
sshStub := `#!/bin/sh
|
|
printf '%s\n' "$@" >"$SSH_ARGS_LOG"
|
|
exit 0
|
|
`
|
|
if err := os.WriteFile(filepath.Join(binDir, "ssh"), []byte(sshStub), 0o755); err != nil {
|
|
t.Fatalf("write ssh stub: %v", err)
|
|
}
|
|
|
|
command := `source "$1"
|
|
SSH_KNOWN_HOSTS_PATH=""
|
|
cleanup_remote_authorized_keys 192.0.2.20
|
|
`
|
|
cmd := exec.Command("bash", "-c", command, "bash", repoFile("scripts", "uninstall-sensor-proxy.sh"))
|
|
cmd.Env = append(os.Environ(),
|
|
"PATH="+binDir+string(os.PathListSeparator)+os.Getenv("PATH"),
|
|
"SSH_ARGS_LOG="+argsLog,
|
|
)
|
|
if out, err := cmd.CombinedOutput(); err != nil {
|
|
t.Fatalf("remote cleanup with configured OpenSSH trust failed: %v\n%s", err, out)
|
|
}
|
|
|
|
args, err := os.ReadFile(argsLog)
|
|
if err != nil {
|
|
t.Fatalf("read ssh args: %v", err)
|
|
}
|
|
got := string(args)
|
|
if !strings.Contains(got, "StrictHostKeyChecking=yes") {
|
|
t.Fatalf("default ssh invocation did not require configured trust:\n%s", got)
|
|
}
|
|
if !strings.Contains(got, "UpdateHostKeys=no") {
|
|
t.Fatalf("default ssh invocation could mutate host trust:\n%s", got)
|
|
}
|
|
for _, isolatedOption := range []string{"UserKnownHostsFile=", "GlobalKnownHostsFile="} {
|
|
if strings.Contains(got, isolatedOption) {
|
|
t.Fatalf("default ssh invocation unexpectedly replaced configured OpenSSH trust with %q:\n%s", isolatedOption, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUninstallSensorProxyRemoteCleanupFailsWithoutProvisionedTrust(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
binDir := filepath.Join(tmpDir, "bin")
|
|
argsLog := filepath.Join(tmpDir, "ssh-args")
|
|
if err := os.MkdirAll(binDir, 0o755); err != nil {
|
|
t.Fatalf("mkdir bin dir: %v", err)
|
|
}
|
|
sshStub := `#!/bin/sh
|
|
printf '%s\n' "$@" >"$SSH_ARGS_LOG"
|
|
exit 0
|
|
`
|
|
if err := os.WriteFile(filepath.Join(binDir, "ssh"), []byte(sshStub), 0o755); err != nil {
|
|
t.Fatalf("write ssh stub: %v", err)
|
|
}
|
|
|
|
missingKnownHosts := filepath.Join(tmpDir, "missing-known-hosts")
|
|
command := `source "$1"
|
|
SSH_KNOWN_HOSTS_PATH="$2"
|
|
cleanup_remote_authorized_keys 192.0.2.11
|
|
`
|
|
cmd := exec.Command("bash", "-c", command, "bash", repoFile("scripts", "uninstall-sensor-proxy.sh"), missingKnownHosts)
|
|
cmd.Env = append(os.Environ(),
|
|
"PATH="+binDir+string(os.PathListSeparator)+os.Getenv("PATH"),
|
|
"SSH_ARGS_LOG="+argsLog,
|
|
)
|
|
out, err := cmd.CombinedOutput()
|
|
if err == nil {
|
|
t.Fatalf("remote cleanup unexpectedly accepted missing trust material:\n%s", out)
|
|
}
|
|
if !strings.Contains(string(out), "known_hosts file is missing, unreadable, or empty") {
|
|
t.Fatalf("missing trust failure was not actionable:\n%s", out)
|
|
}
|
|
if _, statErr := os.Stat(argsLog); !os.IsNotExist(statErr) {
|
|
t.Fatalf("ssh ran before provisioned trust was validated, stat err=%v", statErr)
|
|
}
|
|
}
|
|
|
|
func TestUninstallSensorProxyRemoteCleanupRejectsHostKeyMismatch(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
binDir := filepath.Join(tmpDir, "bin")
|
|
knownHosts := filepath.Join(tmpDir, "known_hosts")
|
|
if err := os.MkdirAll(binDir, 0o755); err != nil {
|
|
t.Fatalf("mkdir bin dir: %v", err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(binDir, "ssh"), []byte("#!/bin/sh\nexit 255\n"), 0o755); err != nil {
|
|
t.Fatalf("write ssh stub: %v", err)
|
|
}
|
|
if err := os.WriteFile(knownHosts, []byte("node ssh-ed25519 AAAAold\n"), 0o600); err != nil {
|
|
t.Fatalf("write known_hosts: %v", err)
|
|
}
|
|
|
|
command := `source "$1"
|
|
SSH_KNOWN_HOSTS_PATH="$2"
|
|
cleanup_remote_authorized_keys 192.0.2.12
|
|
`
|
|
cmd := exec.Command("bash", "-c", command, "bash", repoFile("scripts", "uninstall-sensor-proxy.sh"), knownHosts)
|
|
cmd.Env = append(os.Environ(), "PATH="+binDir+string(os.PathListSeparator)+os.Getenv("PATH"))
|
|
out, err := cmd.CombinedOutput()
|
|
if err == nil {
|
|
t.Fatalf("remote cleanup unexpectedly ignored SSH host-key rejection:\n%s", out)
|
|
}
|
|
if !strings.Contains(string(out), "Unable to verify 192.0.2.12's SSH host key") {
|
|
t.Fatalf("host-key rejection was not actionable:\n%s", out)
|
|
}
|
|
}
|
|
|
|
func TestUninstallSensorProxyLocalOnlyDoesNotUseSSH(t *testing.T) {
|
|
marker := filepath.Join(t.TempDir(), "cleanup-marker")
|
|
command := `source "$1"
|
|
LOCAL_ONLY=true
|
|
cleanup_local_authorized_keys() { printf 'local\n' >"$LOCAL_CLEANUP_MARKER"; }
|
|
cleanup_remote_authorized_keys() { return 99; }
|
|
cleanup_cluster_authorized_keys
|
|
`
|
|
cmd := exec.Command("bash", "-c", command, "bash", repoFile("scripts", "uninstall-sensor-proxy.sh"))
|
|
cmd.Env = append(os.Environ(), "LOCAL_CLEANUP_MARKER="+marker)
|
|
if out, err := cmd.CombinedOutput(); err != nil {
|
|
t.Fatalf("local-only cleanup failed: %v\n%s", err, out)
|
|
}
|
|
content, err := os.ReadFile(marker)
|
|
if err != nil {
|
|
t.Fatalf("local-only cleanup did not run local key removal: %v", err)
|
|
}
|
|
if string(content) != "local\n" {
|
|
t.Fatalf("unexpected local-only marker: %q", content)
|
|
}
|
|
}
|
|
|
|
func TestUninstallSensorProxyReportsRemoteTrustFailureAfterLocalCleanup(t *testing.T) {
|
|
marker := filepath.Join(t.TempDir(), "cleanup-marker")
|
|
command := `source "$1"
|
|
disable_legacy_units() { :; }
|
|
cleanup_cluster_authorized_keys() { return 23; }
|
|
cleanup_stale_sensor_proxy_mounts() { printf 'mounts\n' >>"$LOCAL_CLEANUP_MARKER"; }
|
|
remove_legacy_files() { printf 'files\n' >>"$LOCAL_CLEANUP_MARKER"; }
|
|
remove_proxmox_access() { printf 'access\n' >>"$LOCAL_CLEANUP_MARKER"; }
|
|
systemctl_if_available() { :; }
|
|
main --quiet
|
|
`
|
|
cmd := exec.Command("bash", "-c", command, "bash", repoFile("scripts", "uninstall-sensor-proxy.sh"))
|
|
cmd.Env = append(os.Environ(), "LOCAL_CLEANUP_MARKER="+marker)
|
|
out, err := cmd.CombinedOutput()
|
|
if err == nil {
|
|
t.Fatalf("main unexpectedly hid remote trust failure:\n%s", out)
|
|
}
|
|
exitErr, ok := err.(*exec.ExitError)
|
|
if !ok || exitErr.ExitCode() != 23 {
|
|
t.Fatalf("main returned %v, want remote cleanup status 23:\n%s", err, out)
|
|
}
|
|
if !strings.Contains(string(out), "Local cleanup completed") {
|
|
t.Fatalf("remote trust failure did not explain local cleanup state:\n%s", out)
|
|
}
|
|
content, readErr := os.ReadFile(marker)
|
|
if readErr != nil {
|
|
t.Fatalf("read local cleanup marker: %v", readErr)
|
|
}
|
|
if string(content) != "mounts\nfiles\naccess\n" {
|
|
t.Fatalf("local cleanup did not complete before remote failure was returned: %q", content)
|
|
}
|
|
}
|
|
|
|
func TestUninstallSensorProxyScriptRemovesTempFootprintAndManagedKeys(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
binDir := filepath.Join(tmpDir, "bin")
|
|
if err := os.MkdirAll(binDir, 0755); err != nil {
|
|
t.Fatalf("mkdir bin dir: %v", err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(binDir, "systemctl"), []byte("#!/bin/sh\nexit 0\n"), 0755); err != nil {
|
|
t.Fatalf("write systemctl stub: %v", err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(binDir, "pct"), []byte("#!/bin/sh\nexit 0\n"), 0755); err != nil {
|
|
t.Fatalf("write pct stub: %v", err)
|
|
}
|
|
|
|
binaryPath := filepath.Join(tmpDir, "pulse-sensor-proxy")
|
|
installRoot := filepath.Join(tmpDir, "install-root")
|
|
servicePath := filepath.Join(tmpDir, "pulse-sensor-proxy.service")
|
|
runtimeDir := filepath.Join(tmpDir, "run")
|
|
socketPath := filepath.Join(runtimeDir, "pulse-sensor-proxy.sock")
|
|
workDir := filepath.Join(tmpDir, "work")
|
|
configDir := filepath.Join(tmpDir, "config")
|
|
logDir := filepath.Join(tmpDir, "logs")
|
|
authKeys := filepath.Join(tmpDir, "authorized_keys")
|
|
|
|
for _, dir := range []string{installRoot, runtimeDir, workDir, configDir, logDir} {
|
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
|
t.Fatalf("mkdir %s: %v", dir, err)
|
|
}
|
|
}
|
|
for _, path := range []string{binaryPath, servicePath, socketPath, filepath.Join(installRoot, "bin", "pulse-sensor-proxy")} {
|
|
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
|
t.Fatalf("mkdir parent for %s: %v", path, err)
|
|
}
|
|
if err := os.WriteFile(path, []byte("legacy"), 0644); err != nil {
|
|
t.Fatalf("write %s: %v", path, err)
|
|
}
|
|
}
|
|
if err := os.WriteFile(authKeys, []byte(strings.Join([]string{
|
|
"ssh-ed25519 AAAAlegacy1 pulse # pulse-managed-key",
|
|
"ssh-ed25519 AAAAkeep user-key",
|
|
"ssh-ed25519 AAAAlegacy2 pulse # pulse-proxy-key",
|
|
"",
|
|
}, "\n")), 0600); err != nil {
|
|
t.Fatalf("write authorized_keys: %v", err)
|
|
}
|
|
|
|
cmd := exec.Command("bash", repoFile("scripts", "uninstall-sensor-proxy.sh"), "--uninstall", "--purge", "--quiet")
|
|
cmd.Env = append(os.Environ(),
|
|
"PATH="+binDir+string(os.PathListSeparator)+os.Getenv("PATH"),
|
|
"PULSE_SENSOR_PROXY_BINARY_PATH="+binaryPath,
|
|
"PULSE_SENSOR_PROXY_INSTALL_ROOT="+installRoot,
|
|
"PULSE_SENSOR_PROXY_SERVICE_PATH="+servicePath,
|
|
"PULSE_SENSOR_PROXY_RUNTIME_DIR="+runtimeDir,
|
|
"PULSE_SENSOR_PROXY_SOCKET_PATH="+socketPath,
|
|
"PULSE_SENSOR_PROXY_WORK_DIR="+workDir,
|
|
"PULSE_SENSOR_PROXY_CONFIG_DIR="+configDir,
|
|
"PULSE_SENSOR_PROXY_LOG_DIR="+logDir,
|
|
"PULSE_SENSOR_PROXY_SERVICE_USER=pulse-sensor-proxy-test-user",
|
|
"PULSE_SENSOR_PROXY_AUTHORIZED_KEYS_PATH="+authKeys,
|
|
)
|
|
if out, err := cmd.CombinedOutput(); err != nil {
|
|
t.Fatalf("uninstall-sensor-proxy.sh failed: %v\n%s", err, out)
|
|
}
|
|
|
|
for _, path := range []string{binaryPath, installRoot, servicePath, runtimeDir, workDir, configDir, logDir} {
|
|
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
|
t.Fatalf("expected %s to be removed, stat err=%v", path, err)
|
|
}
|
|
}
|
|
|
|
content, err := os.ReadFile(authKeys)
|
|
if err != nil {
|
|
t.Fatalf("read authorized_keys: %v", err)
|
|
}
|
|
got := string(content)
|
|
if strings.Contains(got, "pulse-managed-key") || strings.Contains(got, "pulse-proxy-key") {
|
|
t.Fatalf("managed Pulse SSH keys were not removed:\n%s", got)
|
|
}
|
|
if !strings.Contains(got, "AAAAkeep") {
|
|
t.Fatalf("non-Pulse SSH key was not preserved:\n%s", got)
|
|
}
|
|
}
|