Fix legacy agent update recovery

Refs #1515
This commit is contained in:
rcourtman 2026-07-06 14:01:26 +01:00
parent e7334e66e4
commit 926c6bd433
4 changed files with 52 additions and 30 deletions

View file

@ -269,9 +269,12 @@ 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. 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. 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,
@ -1354,7 +1357,7 @@ surface and no new `internal/api/` lifecycle handler.
1. Update this contract when agent lifecycle ownership changes. Routes added under the shared `internal/api/` extension point that are clearly outside lifecycle ownership (for example `POST /api/ai/patrol/preflight`, the `patrol_preflight` snapshot field added to `/api/settings/ai`, the auto-trigger preflight dispatch on settings save, the startup-seed dispatch in `NewAISettingsHandler`, and the cached-preflight integration into the Patrol `tools` readiness check — all owned by ai-runtime) do not extend this subsystem's contract; they live in their owning subsystem.
2. Keep shared API proof routing aligned whenever install, register, or profile payloads change.
3. Update runtime and settings tests in the same slice when lifecycle behavior changes. Shell installer lifecycle changes must keep `scripts/installtests/install_sh_test.go` covering explicit flags, persisted connection state, legacy running-process/service recovery, and secure token-file service argument rendering for update re-entry.
3. Update runtime and settings tests in the same slice when lifecycle behavior changes. Shell installer lifecycle changes must keep `scripts/installtests/install_sh_test.go` covering explicit flags, persisted connection state, legacy running-process/service recovery, legacy single-dash v5 agent flag recovery, and secure token-file service argument rendering for update re-entry.
4. Keep host-agent test hooks, command-client factories, and timing overrides
instance-scoped under `internal/hostagent/agent.go`; lifecycle-owned
registration and update paths must not depend on package-global mutable test

View file

@ -279,7 +279,10 @@ 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.
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.
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.
@ -732,7 +735,7 @@ TLS floor in the dynamic config.
1. Update this contract when canonical deployment or installer entry points move
2. Keep deployment runtime and shared API proof routing aligned in `registry.json`
3. Preserve explicit coverage for installer parity, update planning, and deployment bootstrap behavior when these surfaces change. Shell installer update recovery changes must keep `scripts/installtests/install_sh_test.go` covering both persisted `connection.env` recovery and legacy running-process/service recovery, including the rule that upgraded service args use `--token-file` instead of raw `--token`.
3. Preserve explicit coverage for installer parity, update planning, and deployment bootstrap behavior when these surfaces change. Shell installer update recovery changes must keep `scripts/installtests/install_sh_test.go` covering both persisted `connection.env` recovery and legacy running-process/service recovery, including single-dash v5 agent flags and the rule that upgraded service args use `--token-file` instead of raw `--token`.
4. Keep stable and prerelease packet lineage explicit when `docs/releases/` or
`VERSION` changes: preserve already-shipped RC packets under dedicated
historical filenames before reusing canonical stable names, keep

View file

@ -1524,10 +1524,19 @@ strip_recovered_arg_quotes() {
printf '%s\n' "$value"
}
normalize_recovered_agent_arg_key() {
local key="$1"
key="${key#--}"
key="${key#-}"
printf '%s\n' "$key"
}
apply_recovered_agent_arg_value() {
local key="$1"
local value="$2"
key=$(normalize_recovered_agent_arg_key "$key")
value=$(strip_recovered_arg_quotes "$value")
case "$key" in
@ -1615,81 +1624,81 @@ recover_connection_state_from_arg_stream() {
fi
case "$arg" in
--url|--pulse-url|--token|--token-file|--interval|--agent-id|--hostname|--cacert|--health-addr|--state-dir|--kubeconfig|--proxmox-type|--disk-exclude)
pending_key="${arg#--}"
--url|--pulse-url|--token|--token-file|--interval|--agent-id|--hostname|--cacert|--health-addr|--state-dir|--kubeconfig|--proxmox-type|--disk-exclude|-url|-pulse-url|-token|-token-file|-interval|-agent-id|-hostname|-cacert|-health-addr|-state-dir|-kubeconfig|-proxmox-type|-disk-exclude)
pending_key=$(normalize_recovered_agent_arg_key "$arg")
;;
--url=*|--pulse-url=*|--token=*|--token-file=*|--interval=*|--agent-id=*|--hostname=*|--cacert=*|--health-addr=*|--state-dir=*|--kubeconfig=*|--proxmox-type=*|--disk-exclude=*)
--url=*|--pulse-url=*|--token=*|--token-file=*|--interval=*|--agent-id=*|--hostname=*|--cacert=*|--health-addr=*|--state-dir=*|--kubeconfig=*|--proxmox-type=*|--disk-exclude=*|-url=*|-pulse-url=*|-token=*|-token-file=*|-interval=*|-agent-id=*|-hostname=*|-cacert=*|-health-addr=*|-state-dir=*|-kubeconfig=*|-proxmox-type=*|-disk-exclude=*)
key="${arg%%=*}"
value="${arg#*=}"
apply_recovered_agent_arg_value "${key#--}" "$value"
apply_recovered_agent_arg_value "$key" "$value"
;;
--enable-host|--enable-host=true)
--enable-host|-enable-host|--enable-host=true|-enable-host=true)
if [[ "$HOST_EXPLICIT" != "true" ]]; then ENABLE_HOST="true"; fi
RECOVERED_AGENT_ARG_STATE="true"
;;
--enable-host=false|--disable-host)
--enable-host=false|-enable-host=false|--disable-host|-disable-host)
if [[ "$HOST_EXPLICIT" != "true" ]]; then ENABLE_HOST="false"; fi
RECOVERED_AGENT_ARG_STATE="true"
;;
--enable-docker|--enable-docker=true)
--enable-docker|-enable-docker|--enable-docker=true|-enable-docker=true)
if [[ "$DOCKER_EXPLICIT" != "true" ]]; then
ENABLE_DOCKER="true"
DOCKER_EXPLICIT="true"
fi
RECOVERED_AGENT_ARG_STATE="true"
;;
--enable-docker=false|--disable-docker)
--enable-docker=false|-enable-docker=false|--disable-docker|-disable-docker)
if [[ "$DOCKER_EXPLICIT" != "true" ]]; then
ENABLE_DOCKER="false"
DOCKER_EXPLICIT="true"
fi
RECOVERED_AGENT_ARG_STATE="true"
;;
--enable-kubernetes|--enable-kubernetes=true)
--enable-kubernetes|-enable-kubernetes|--enable-kubernetes=true|-enable-kubernetes=true)
if [[ "$KUBERNETES_EXPLICIT" != "true" ]]; then
ENABLE_KUBERNETES="true"
KUBERNETES_EXPLICIT="true"
fi
RECOVERED_AGENT_ARG_STATE="true"
;;
--enable-kubernetes=false|--disable-kubernetes)
--enable-kubernetes=false|-enable-kubernetes=false|--disable-kubernetes|-disable-kubernetes)
if [[ "$KUBERNETES_EXPLICIT" != "true" ]]; then
ENABLE_KUBERNETES="false"
KUBERNETES_EXPLICIT="true"
fi
RECOVERED_AGENT_ARG_STATE="true"
;;
--enable-proxmox|--enable-proxmox=true)
--enable-proxmox|-enable-proxmox|--enable-proxmox=true|-enable-proxmox=true)
if [[ "$PROXMOX_EXPLICIT" != "true" ]]; then
ENABLE_PROXMOX="true"
PROXMOX_EXPLICIT="true"
fi
RECOVERED_AGENT_ARG_STATE="true"
;;
--enable-proxmox=false|--disable-proxmox)
--enable-proxmox=false|-enable-proxmox=false|--disable-proxmox|-disable-proxmox)
if [[ "$PROXMOX_EXPLICIT" != "true" ]]; then
ENABLE_PROXMOX="false"
PROXMOX_EXPLICIT="true"
fi
RECOVERED_AGENT_ARG_STATE="true"
;;
--insecure)
--insecure|-insecure)
INSECURE="true"
RECOVERED_AGENT_ARG_STATE="true"
;;
--enable-commands)
--enable-commands|-enable-commands)
ENABLE_COMMANDS="true"
RECOVERED_AGENT_ARG_STATE="true"
;;
--enroll)
--enroll|-enroll)
ENROLL="true"
RECOVERED_AGENT_ARG_STATE="true"
;;
--kube-include-all-pods)
--kube-include-all-pods|-kube-include-all-pods)
KUBE_INCLUDE_ALL_PODS="true"
RECOVERED_AGENT_ARG_STATE="true"
;;
--kube-include-all-deployments)
--kube-include-all-deployments|-kube-include-all-deployments)
KUBE_INCLUDE_ALL_DEPLOYMENTS="true"
RECOVERED_AGENT_ARG_STATE="true"
;;

View file

@ -644,6 +644,9 @@ func TestInstallSHSupportsSavedStateUpdateMode(t *testing.T) {
`recover_connection_state_from_running_agent`,
`recover_connection_state_from_systemd_unit`,
`recover_connection_state_from_arg_stream`,
`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)`,
`recover_connection_state_from_env_stream`,
`recovered_connection_state_ready() {`,
`update_connection_state_incomplete() {`,
@ -696,6 +699,7 @@ func TestInstallSHRecoversV5ProcessArgsForSavedStateUpdate(t *testing.T) {
DISK_EXCLUDES=()
RUNTIME_TOKEN_FILE="/var/lib/pulse-agent/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_connection_state_from_arg_stream") + `
@ -778,6 +782,7 @@ func TestInstallSHRejectsPartialRecoveredProcessConnectionState(t *testing.T) {
KUBE_INCLUDE_ALL_DEPLOYMENTS="false"
DISK_EXCLUDES=()
` + 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_connection_state_from_arg_stream") + `
@ -845,6 +850,7 @@ func TestInstallSHCombinesRecoveredProcessArgsAndEnvConnectionState(t *testing.T
KUBE_INCLUDE_ALL_DEPLOYMENTS="false"
DISK_EXCLUDES=()
` + 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_connection_state_from_arg_stream") + `
@ -920,6 +926,7 @@ func TestInstallSHUpdateModeMergesExplicitURLWithRunningV5ProcessState(t *testin
DISK_EXCLUDES=()
RUNTIME_TOKEN_FILE="/var/lib/pulse-agent/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, "update_connection_state_incomplete") + `
@ -932,17 +939,17 @@ func TestInstallSHUpdateModeMergesExplicitURLWithRunningV5ProcessState(t *testin
recover_connection_state_from_existing_agent() {
recover_connection_state_from_arg_stream <<'ARGS'
/usr/local/bin/pulse-agent
--url
-url
http://192.168.2.96:7655
--token
-token
deadbeef
--interval
-interval
30s
--enable-host
--enable-docker
--agent-id
-enable-host
-enable-docker
-agent-id
machine-1
--hostname
-hostname
docker1
ARGS
}