Fix legacy agent update token recovery

Refs #1515
This commit is contained in:
rcourtman
2026-07-07 09:31:21 +01:00
parent dded079297
commit b503ee6fd3
4 changed files with 151 additions and 10 deletions
@@ -269,12 +269,17 @@ process or unit may seed later recovery attempts, but it must not be logged or
treated as recovered update state. That fallback must still run when an
operator supplies the update URL explicitly but token, identity, feature-flag,
or trust continuity remains recoverable only from a legacy v5 process or
service. Because v5.1.x agents were launched by Go flag parsing, that legacy
fallback must treat single-dash and double-dash agent flag spellings as the
same recovered state while preserving the same fail-closed URL-plus-token
threshold. Windows stale-agent update commands remain on the existing
token-gated install transport until the Windows installer owns an equivalent
saved-state update mode.
service. Legacy v5.1.x Linux services that omitted `--token` and
`--token-file` because the Go agent read `/var/lib/pulse-agent/token`
implicitly may complete recovery from that installer state-dir/default token
file only after a running process, service unit, or saved state supplies local
agent connection context; the token file alone must not hide a missing
control-plane URL. Because v5.1.x agents were launched by Go flag parsing,
that legacy fallback must treat single-dash and double-dash agent flag
spellings as the same recovered state while preserving the same fail-closed
URL-plus-token threshold. Windows stale-agent update commands remain on the
existing token-gated install transport until the Windows installer owns an
equivalent saved-state update mode.
Agent Fleet Doctor diagnostics extend that same read-only lifecycle triage
surface: `GET /api/agents/diagnostics` may explain stale versions, missing
reports, profile deployment drift, expected Docker/Kubernetes telemetry gaps,
@@ -279,10 +279,14 @@ TLS floor in the dynamic config.
raw token in process arguments. That fallback remains required when the
operator supplies `--url` on the update command but token, identity,
feature-flag, or trust continuity still exists only in legacy process or
service state. Because v5.1.x agents used Go's single-dash flag spelling,
the installer-owned recovery path must accept both single-dash and
double-dash forms for recovered agent args without weakening the existing
missing-state failure behavior.
service state. Legacy v5.1.x Linux services that relied on the Go agent's
implicit `/var/lib/pulse-agent/token` fallback may recover that default
token file only after local process, service, or saved-state context has
supplied the agent connection shape; the token file alone is not enough to
convert a missing-state update into a new install. Because v5.1.x agents
used Go's single-dash flag spelling, the installer-owned recovery path must
accept both single-dash and double-dash forms for recovered agent args
without weakening the existing missing-state failure behavior.
The shell installer must disclose `--enable-commands` as Pulse command
execution, disabled by default, and must name both Patrol actions and
Proxmox LXC Docker inventory as the operator-visible reasons to enable it.
+33
View File
@@ -1479,6 +1479,28 @@ read_connection_state_value() {
' "$file" 2>/dev/null || true
}
recover_token_from_default_agent_token_file() {
local token_path=""
local recovered_token=""
if [[ -n "$PULSE_TOKEN" ]]; then
return 0
fi
# v5.1.x Linux services could omit --token and --token-file because the
# Go agent read this default file itself.
for token_path in "${STATE_DIR%/}/token" "/var/lib/pulse-agent/token" "$TRUENAS_STATE_DIR/token"; do
[[ -n "$token_path" && -f "$token_path" ]] || continue
recovered_token=$(cat "$token_path" 2>/dev/null || true)
if [[ -n "$recovered_token" ]]; then
PULSE_TOKEN="$recovered_token"
return 0
fi
done
return 1
}
recover_connection_state() {
local file="$1"
@@ -1495,6 +1517,9 @@ recover_connection_state() {
PULSE_TOKEN=$(cat "$saved_token_file")
fi
fi
if [[ -z "$PULSE_TOKEN" && -n "$PULSE_URL" ]]; then
recover_token_from_default_agent_token_file || true
fi
if [[ -z "$AGENT_ID" ]]; then
AGENT_ID=$(read_connection_state_value "$file" "PULSE_AGENT_ID")
fi
@@ -1705,6 +1730,10 @@ recover_connection_state_from_arg_stream() {
esac
done
if [[ "$RECOVERED_AGENT_ARG_STATE" == "true" && -z "$PULSE_TOKEN" && -n "$PULSE_URL" ]]; then
recover_token_from_default_agent_token_file || true
fi
[[ "$RECOVERED_AGENT_ARG_STATE" == "true" ]] && recovered_connection_state_ready
}
@@ -1755,6 +1784,10 @@ recover_connection_state_from_env_stream() {
esac
done
if [[ "$RECOVERED_AGENT_ENV_STATE" == "true" && -z "$PULSE_TOKEN" && -n "$PULSE_URL" ]]; then
recover_token_from_default_agent_token_file || true
fi
[[ "$RECOVERED_AGENT_ENV_STATE" == "true" ]] && recovered_connection_state_ready
}
+99
View File
@@ -644,6 +644,7 @@ func TestInstallSHSupportsSavedStateUpdateMode(t *testing.T) {
`recover_connection_state_from_running_agent`,
`recover_connection_state_from_systemd_unit`,
`recover_connection_state_from_arg_stream`,
`recover_token_from_default_agent_token_file() {`,
`normalize_recovered_agent_arg_key() {`,
`-url|-pulse-url|-token|-token-file|-interval|-agent-id|-hostname|-cacert|-health-addr|-state-dir|-kubeconfig|-proxmox-type|-disk-exclude)`,
`--enable-host|-enable-host|--enable-host=true|-enable-host=true)`,
@@ -702,6 +703,7 @@ func TestInstallSHRecoversV5ProcessArgsForSavedStateUpdate(t *testing.T) {
` + extractInstallShellFunction(t, "normalize_recovered_agent_arg_key") + `
` + extractInstallShellFunction(t, "apply_recovered_agent_arg_value") + `
` + extractInstallShellFunction(t, "recovered_connection_state_ready") + `
` + extractInstallShellFunction(t, "recover_token_from_default_agent_token_file") + `
` + extractInstallShellFunction(t, "recover_connection_state_from_arg_stream") + `
` + extractInstallShellFunction(t, "build_exec_arg_items") + `
` + extractInstallShellFunction(t, "join_exec_arg_items") + `
@@ -785,6 +787,7 @@ func TestInstallSHRejectsPartialRecoveredProcessConnectionState(t *testing.T) {
` + extractInstallShellFunction(t, "normalize_recovered_agent_arg_key") + `
` + extractInstallShellFunction(t, "apply_recovered_agent_arg_value") + `
` + extractInstallShellFunction(t, "recovered_connection_state_ready") + `
` + extractInstallShellFunction(t, "recover_token_from_default_agent_token_file") + `
` + extractInstallShellFunction(t, "recover_connection_state_from_arg_stream") + `
if recover_connection_state_from_arg_stream <<'ARGS'
/usr/local/bin/pulse-agent
@@ -821,6 +824,100 @@ ARGS
}
}
func TestInstallSHRecoversLegacyDefaultTokenFileForSavedStateUpdate(t *testing.T) {
stateDir := t.TempDir()
if err := os.WriteFile(filepath.Join(stateDir, "token"), []byte("deadbeef\n"), 0600); err != nil {
t.Fatalf("write legacy token file: %v", err)
}
script := `
fail() { echo "FAIL:$1"; exit 99; }
PULSE_URL="http://192.168.2.96:7655"
PULSE_TOKEN=""
INTERVAL="30s"
INTERVAL_EXPLICIT="false"
ENABLE_HOST="true"
HOST_EXPLICIT="false"
ENABLE_DOCKER=""
DOCKER_EXPLICIT="false"
ENABLE_KUBERNETES=""
KUBERNETES_EXPLICIT="false"
KUBECONFIG_PATH=""
ENABLE_PROXMOX=""
PROXMOX_EXPLICIT="false"
PROXMOX_TYPE=""
INSECURE="true"
ENABLE_COMMANDS="false"
ENROLL="false"
HEALTH_ADDR=""
HEALTH_ADDR_SET="false"
AGENT_ID=""
HOSTNAME_OVERRIDE=""
STATE_DIR="${PULSE_TEST_STATE_DIR:?}"
TRUENAS_STATE_DIR="${PULSE_TEST_STATE_DIR:?}/truenas"
CURL_CA_BUNDLE=""
KUBE_INCLUDE_ALL_PODS="false"
KUBE_INCLUDE_ALL_DEPLOYMENTS="false"
DISK_EXCLUDES=()
RUNTIME_TOKEN_FILE="${STATE_DIR}/token"
` + extractInstallShellFunction(t, "strip_recovered_arg_quotes") + `
` + extractInstallShellFunction(t, "normalize_recovered_agent_arg_key") + `
` + extractInstallShellFunction(t, "apply_recovered_agent_arg_value") + `
` + extractInstallShellFunction(t, "recovered_connection_state_ready") + `
` + extractInstallShellFunction(t, "recover_token_from_default_agent_token_file") + `
` + extractInstallShellFunction(t, "recover_connection_state_from_arg_stream") + `
` + extractInstallShellFunction(t, "build_exec_arg_items") + `
` + extractInstallShellFunction(t, "join_exec_arg_items") + `
` + extractInstallShellFunction(t, "build_exec_args") + `
if recover_connection_state_from_arg_stream <<'ARGS'
/usr/local/bin/pulse-agent
-url
http://192.168.2.96:7655
-interval
30s
-enable-host
-enable-docker
-agent-id
machine-1
-hostname
docker1
ARGS
then
echo "READY"
else
echo "NOT_READY"
fi
build_exec_args
printf 'URL=%s\nTOKEN=%s\nDOCKER=%s\nAGENT_ID=%s\nHOSTNAME=%s\nEXEC_ARGS=%s\n' \
"$PULSE_URL" "$PULSE_TOKEN" "$ENABLE_DOCKER" "$AGENT_ID" "$HOSTNAME_OVERRIDE" "$EXEC_ARGS"
`
cmd := exec.Command("bash", "-c", script)
cmd.Env = append(os.Environ(), "PULSE_TEST_STATE_DIR="+stateDir)
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
got := string(out)
for _, needle := range []string{
"READY",
"URL=http://192.168.2.96:7655",
"TOKEN=deadbeef",
"DOCKER=true",
"AGENT_ID=machine-1",
"HOSTNAME=docker1",
"--token-file",
stateDir + "/token",
} {
if !strings.Contains(got, needle) {
t.Fatalf("legacy default token recovery missing %q:\n%s", needle, got)
}
}
if strings.Contains(got, "--token deadbeef") {
t.Fatalf("legacy default token recovery leaked raw token into service args:\n%s", got)
}
}
func TestInstallSHCombinesRecoveredProcessArgsAndEnvConnectionState(t *testing.T) {
script := `
PULSE_URL=""
@@ -853,6 +950,7 @@ func TestInstallSHCombinesRecoveredProcessArgsAndEnvConnectionState(t *testing.T
` + extractInstallShellFunction(t, "normalize_recovered_agent_arg_key") + `
` + extractInstallShellFunction(t, "apply_recovered_agent_arg_value") + `
` + extractInstallShellFunction(t, "recovered_connection_state_ready") + `
` + extractInstallShellFunction(t, "recover_token_from_default_agent_token_file") + `
` + extractInstallShellFunction(t, "recover_connection_state_from_arg_stream") + `
` + extractInstallShellFunction(t, "recover_connection_state_from_env_stream") + `
if recover_connection_state_from_arg_stream <<'ARGS'
@@ -930,6 +1028,7 @@ func TestInstallSHUpdateModeMergesExplicitURLWithRunningV5ProcessState(t *testin
` + extractInstallShellFunction(t, "apply_recovered_agent_arg_value") + `
` + extractInstallShellFunction(t, "recovered_connection_state_ready") + `
` + extractInstallShellFunction(t, "update_connection_state_incomplete") + `
` + extractInstallShellFunction(t, "recover_token_from_default_agent_token_file") + `
` + extractInstallShellFunction(t, "recover_connection_state_from_arg_stream") + `
` + extractInstallShellFunction(t, "build_exec_arg_items") + `
` + extractInstallShellFunction(t, "join_exec_arg_items") + `