mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
Fix Proxmox registration for host-token installs
The Settings > Infrastructure installer mints generic host install tokens, but install.sh auto-detects Proxmox and the agent presents type pve/pbs at /api/auto-register. The bootstrap grant required an exact install_type match, so every generic install on a Proxmox node was denied source creation and the denial was a single buried journal warn. Four-part fix (#1644): - server: extend the one-shot bootstrap grant to host-issued install tokens presenting a canonical Proxmox type. Typed tokens stay pinned, the grant keeps its settings-write mint requirement, first-hostname binding, serialized completion, and single consumption across types. - agent: a canRegister=false denial now logs at error level, returns a setup error, and records the operator-facing reason in a proxmox-<type>-registration-blocked state marker. - installer: report the Proxmox registration outcome in install output by reading the registered/blocked markers, and poll the server lookup for a bounded retry window before warning that registration was not confirmed (readyz flips before the first report cycle). - setup script: the auto-register transport now captures the HTTP status alongside the body (no -f), making the invalid-setup-token branch reachable via 401/403 instead of a dead server-string grep, and operator guidance names Settings -> Infrastructure instead of the retired Nodes page (also updated in docs/PBS.md and the pinned assertions in contract, setup-script, and repoctl docs tests). Regression proof: internal/api/issue1644_host_install_token_proxmox_test.go plus new install.sh proofs for the retry window and blocked-marker surfacing, and the updated hostagent blocked-registration test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -68,7 +68,7 @@ Example (what the UI generates):
|
||||
curl -fsSL "http://<pulse-ip>:7655/api/setup-script?type=pbs&host=https://<pbs-ip>:8007&pulse_url=http://<pulse-ip>:7655" | { if [ "$(id -u)" -eq 0 ]; then PULSE_SETUP_TOKEN="<setup-token>" bash; elif command -v sudo >/dev/null 2>&1; then sudo env PULSE_SETUP_TOKEN="<setup-token>" bash; else echo "Root privileges required. Run as root (su -) and retry." >&2; exit 1; fi; }
|
||||
```
|
||||
|
||||
Pulse generates that full command for you from **Settings → Nodes**, including
|
||||
Pulse generates that full command for you from **Settings → Infrastructure**, including
|
||||
the one-time setup token. The script creates a `pulse-monitor@pbs` user,
|
||||
generates a scoped API token, and registers the server with Pulse.
|
||||
|
||||
|
||||
@@ -2158,9 +2158,13 @@ Agent` secondary handoff against the live setup wizard instead of relying
|
||||
21. Keep Proxmox runtime health and bootstrap authority separate. Agent-token
|
||||
startup and periodic registration checks must call `/api/auto-register`
|
||||
directly; `/api/setup-script-url` remains `settings:write`. Ordinary agent
|
||||
tokens stay update-only, while a server-minted PVE/PBS install token may
|
||||
consume one declared-type initial registration after durable first-host
|
||||
binding and serialized completion.
|
||||
tokens stay update-only, while a server-minted install token may consume
|
||||
one initial registration after durable first-host binding and serialized
|
||||
completion: a PVE/PBS-typed token for its declared type, and a generic
|
||||
`host` install token for one detected canonical Proxmox type (#1644). A
|
||||
denied registration grant must fail loudly on the agent side through an
|
||||
error-level log, a returned setup error, and an installer-readable
|
||||
`proxmox-<type>-registration-blocked` state marker.
|
||||
|
||||
22. A rejected legacy RBAC import must not destroy the store. The import is
|
||||
transactional and leaves the legacy files in place, so a failure leaves the
|
||||
@@ -5317,11 +5321,33 @@ A positive registration preflight for an already-existing source consumes the
|
||||
fresh-install grant without rotating local credentials; a negative preflight
|
||||
keeps it available for the immediately following initial completion.
|
||||
|
||||
The bootstrap exception covers both install-token shapes the settings-write
|
||||
control plane actually mints. A PVE/PBS-typed agent-install token stays pinned
|
||||
to its declared type. A generic `host` install token minted by the Settings →
|
||||
Infrastructure installer additionally holds the same bounded grant for one
|
||||
canonical Proxmox type, because the unified installer auto-detects PVE/PBS on
|
||||
the target machine and the agent presents the detected type at registration
|
||||
(#1644). The host-token grant keeps every existing bound: settings-authorized
|
||||
mint via `issued_via`, first-presenting-hostname binding before local Proxmox
|
||||
credentials are mutated, serialized completion, and one-shot consumption — a
|
||||
host token that completes a PVE bootstrap cannot later bootstrap PBS, and
|
||||
non-Proxmox request types never hold a grant.
|
||||
|
||||
A blocked registration is not a silent skip. When the pre-registration check
|
||||
reports `canRegister=false`, the agent must log at error level, return a setup
|
||||
error to its caller, and record the operator-facing reason in a
|
||||
`proxmox-<type>-registration-blocked` marker in its state directory; the
|
||||
marker is cleared once a later run finds the source registered or proceeds to
|
||||
register. The shell installer reads that marker after agent health
|
||||
verification and surfaces the denial with remediation in its own output, so
|
||||
the failure is visible at install time instead of buried in the agent journal.
|
||||
|
||||
Token-optional installations retain the setup-token bootstrap path. Ordinary
|
||||
hosts never enter this Proxmox registration loop, while PVE, PBS, mixed-product
|
||||
hosts, restarts, and hosted-tenant install tokens retain the same authority
|
||||
split. `internal/hostagent/proxmox_setup_test.go`,
|
||||
`internal/api/config_handlers_auto_register_test.go`, and
|
||||
`internal/api/proxmox_install_registration_test.go` prove the recurring health,
|
||||
first-install, type/host binding, one-time consumption, rejection, and
|
||||
concurrent-completion contracts.
|
||||
`internal/api/config_handlers_auto_register_test.go`,
|
||||
`internal/api/proxmox_install_registration_test.go`, and
|
||||
`internal/api/issue1644_host_install_token_proxmox_test.go` prove the recurring
|
||||
health, first-install, type/host binding, one-time consumption, rejection,
|
||||
host-token bootstrap, and concurrent-completion contracts.
|
||||
|
||||
@@ -1619,6 +1619,15 @@ payload shape change when the portal presents compact client rows.
|
||||
match the primary configured endpoint may still drive disconnected-source
|
||||
repair.
|
||||
That same contract now owns stale-marker verification as well: setup-token-authenticated `checkRegistration` requests may omit token completion fields and must answer `{registered:boolean}` from canonical candidate-host matching so runtime repair can distinguish real registrations from stale local marker files without rotating tokens first.
|
||||
That same shared boundary owns the install-token bootstrap type rule: the
|
||||
one-shot Proxmox source-creation grant carried by agent-install tokens
|
||||
accepts either a matching declared `install_type` of `pve`/`pbs` or a
|
||||
generic `host` install token presenting one canonical Proxmox type
|
||||
(#1644), because the Settings → Infrastructure installer mints `host`
|
||||
tokens while the unified installer auto-detects PVE/PBS on the target.
|
||||
Non-canonical request types never hold the grant, and the grant keeps its
|
||||
settings-authorized `issued_via` requirement, first-hostname binding,
|
||||
serialized completion, and single consumption across types.
|
||||
That same contract owns auto-register WebSocket event intent: a successful
|
||||
completion that creates a new PVE/PBS node may broadcast
|
||||
`node_auto_registered`, but successful idempotent matches or credential
|
||||
@@ -1727,6 +1736,13 @@ payload shape change when the portal presents compact client rows.
|
||||
`${HOST_URL%/}/api2/json/nodes` with `PVEAPIToken` authentication before
|
||||
it posts the canonical `/api/auto-register` payload. Smoke-check failure is
|
||||
a manual-completion state, not a successful auto-registration response.
|
||||
Rendered auto-register transport must capture the response body and HTTP
|
||||
status together (no `-f` body suppression), and the invalid-setup-token
|
||||
branch must key off a 401/403 status instead of grepping for server error
|
||||
strings the backend does not emit, so the auth-failure guidance is actually
|
||||
reachable (#1644). Operator-facing manual-completion and rerun guidance in
|
||||
the rendered scripts must name the live `Settings → Infrastructure` page,
|
||||
not the retired Nodes page.
|
||||
82. `internal/api/slo.go` shared with `performance-and-scalability`: the SLO endpoint is both an API contract surface and a protected performance hot-path boundary.
|
||||
83. `internal/api/system_settings.go` shared with `security-privacy`: the system settings telemetry and auth controls are both a security/privacy control surface and a canonical API payload contract boundary.
|
||||
84. `internal/api/unified_agent.go` shared with `agent-lifecycle`: unified agent download and installer handlers are both an agent lifecycle control surface and a canonical API payload contract boundary.
|
||||
|
||||
@@ -303,6 +303,18 @@ TLS floor in the dynamic config.
|
||||
and token-file paths without storing the token value, update rewrites the
|
||||
same secure service shape, and uninstall removes the discovered canonical
|
||||
directory rather than only `/var/lib/pulse-agent`.
|
||||
Post-install verification must not declare server registration
|
||||
unconfirmed from a single lookup: the local `/readyz` gate flips before the
|
||||
agent's first report cycle completes, so the installer polls the server
|
||||
lookup for a bounded retry window before warning, while a definitive
|
||||
401/403 token rejection still short-circuits immediately (#1644).
|
||||
When Proxmox integration is enabled, the installer must also report the
|
||||
agent's Proxmox registration outcome in its own output: it waits a bounded
|
||||
window on the agent state directory, surfaces the reason recorded in a
|
||||
`proxmox-<type>-registration-blocked` marker as an installer error with
|
||||
remediation, reports success on a `proxmox-<type>-registered` marker, and
|
||||
clears both marker families when resetting Proxmox state for a fresh
|
||||
registration.
|
||||
Existing-agent update commands copied from the settings UI must use the
|
||||
installer-owned `--update` mode rather than serializing a fresh enrollment
|
||||
token into platform notice links. In `--update` mode, `scripts/install.sh`
|
||||
|
||||
@@ -4115,6 +4115,12 @@ That same dependency also assumes those generated setup scripts fail closed on
|
||||
auto-register HTTP and transport failures, so adjacent setup flows do not
|
||||
reinterpret shared backend stderr or HTTP-failure output as a successful
|
||||
registration payload.
|
||||
That same dependency also assumes those generated setup scripts capture the
|
||||
auto-register HTTP status alongside the response body and key the
|
||||
invalid-setup-token branch off a 401/403 status, and that their manual
|
||||
completion and rerun guidance names the live Settings → Infrastructure page,
|
||||
so adjacent setup flows surface reachable auth-failure guidance instead of a
|
||||
dead server-string grep and a retired page name (#1644).
|
||||
That same shared dependency also assumes generated setup scripts preserve
|
||||
setup-token auth guidance, so adjacent setup flows do not regress back to
|
||||
stale API-token instructions after the backend has already standardized on the
|
||||
|
||||
@@ -456,7 +456,7 @@ func TestPVESetupScript_UsesFailFastRetryGuidance(t *testing.T) {
|
||||
if containsString(script, `echo " curl -sSL \"$SETUP_SCRIPT_URL\" | bash"`) || containsString(script, `echo " curl -sSL \"$PULSE_URL/api/setup-script?type=pve&host=YOUR_PVE_URL&pulse_url=$PULSE_URL\" | bash"`) {
|
||||
t.Fatalf("expected stale non-fail-fast guidance to be removed, got: %s", truncate(script, 900))
|
||||
}
|
||||
if containsString(script, `echo "Manual setup steps:"`) || containsString(script, `echo " 2. In Pulse: Settings → Nodes → Add Node (enter token from above)"`) {
|
||||
if containsString(script, `echo "Manual setup steps:"`) || containsString(script, `echo " 2. In Pulse: Settings → Infrastructure → Add Node (enter token from above)"`) {
|
||||
t.Fatalf("expected stale off-host manual token flow to be removed from setup script, got: %s", truncate(script, 900))
|
||||
}
|
||||
}
|
||||
@@ -780,8 +780,20 @@ func TestPVESetupScript_FailsClosedOnAutoRegisterSuccessDetection(t *testing.T)
|
||||
}
|
||||
|
||||
script := rr.Body.String()
|
||||
if !containsString(script, `curl -fsS -X POST "$PULSE_URL/api/auto-register"`) {
|
||||
t.Fatalf("expected fail-fast auto-register transport in setup script, got: %s", truncate(script, 900))
|
||||
if !containsString(script, `curl -sS -w $'\n%{http_code}' -X POST "$PULSE_URL/api/auto-register"`) {
|
||||
t.Fatalf("expected status-capturing auto-register transport in setup script, got: %s", truncate(script, 900))
|
||||
}
|
||||
if containsString(script, `curl -fsS -X POST "$PULSE_URL/api/auto-register"`) {
|
||||
t.Fatalf("expected body-suppressing fail-fast auto-register transport to be removed, got: %s", truncate(script, 900))
|
||||
}
|
||||
if !containsString(script, `REGISTER_STATUS="${REGISTER_OUTPUT##*$'\n'}"`) {
|
||||
t.Fatalf("expected auto-register HTTP status extraction in setup script, got: %s", truncate(script, 900))
|
||||
}
|
||||
if !containsString(script, `elif [ "$REGISTER_STATUS" = "401" ] || [ "$REGISTER_STATUS" = "403" ]; then`) {
|
||||
t.Fatalf("expected reachable auth-failure status branch in setup script, got: %s", truncate(script, 900))
|
||||
}
|
||||
if containsString(script, `grep -q "Authentication required"`) {
|
||||
t.Fatalf("expected unreachable Authentication required grep branch to be removed, got: %s", truncate(script, 900))
|
||||
}
|
||||
if !containsString(script, `"source":"script"`) {
|
||||
t.Fatalf("expected PVE setup script to use canonical /api/auto-register source marker, got: %s", truncate(script, 900))
|
||||
@@ -801,7 +813,7 @@ func TestPVESetupScript_FailsClosedOnAutoRegisterSuccessDetection(t *testing.T)
|
||||
if !containsString(script, `echo "The provided Pulse setup token was invalid or expired"`) {
|
||||
t.Fatalf("expected invalid setup-token guidance in setup script, got: %s", truncate(script, 900))
|
||||
}
|
||||
if !containsString(script, `echo "Get a fresh setup token from Pulse Settings → Nodes and rerun this script."`) {
|
||||
if !containsString(script, `echo "Get a fresh setup token from Pulse Settings → Infrastructure and rerun this script."`) {
|
||||
t.Fatalf("expected fresh setup-token rerun guidance in setup script, got: %s", truncate(script, 900))
|
||||
}
|
||||
if !containsString(script, `SETUP_TOKEN_INVALID=true`) {
|
||||
@@ -813,13 +825,13 @@ func TestPVESetupScript_FailsClosedOnAutoRegisterSuccessDetection(t *testing.T)
|
||||
if !containsString(script, `echo "Pulse setup token authentication failed."`) {
|
||||
t.Fatalf("expected PVE auth-failure completion guidance in setup script, got: %s", truncate(script, 900))
|
||||
}
|
||||
if !containsString(script, `echo "📝 Use the token details below in Pulse Settings → Nodes to finish registration."`) {
|
||||
if !containsString(script, `echo "📝 Use the token details below in Pulse Settings → Infrastructure to finish registration."`) {
|
||||
t.Fatalf("expected canonical PVE auto-register failure continuation guidance in setup script, got: %s", truncate(script, 900))
|
||||
}
|
||||
if !containsString(script, `if [ "$AUTO_REG_SUCCESS" != true ] && [ "$SETUP_TOKEN_INVALID" != true ]; then`) {
|
||||
t.Fatalf("expected PVE manual footer to stay disabled on auth failure, got: %s", truncate(script, 900))
|
||||
}
|
||||
if !containsString(script, `echo "⚠️ Auto-registration failed. Finish registration manually in Pulse Settings → Nodes."`) {
|
||||
if !containsString(script, `echo "⚠️ Auto-registration failed. Finish registration manually in Pulse Settings → Infrastructure."`) {
|
||||
t.Fatalf("expected canonical PVE auto-register failure summary guidance in setup script, got: %s", truncate(script, 900))
|
||||
}
|
||||
if containsString(script, `echo "To enable auto-registration, add your API token to the setup URL"`) {
|
||||
@@ -876,7 +888,7 @@ func TestPVESetupScript_FailsClosedOnAutoRegisterSuccessDetection(t *testing.T)
|
||||
if !containsString(script, `echo "Add this server to Pulse with:"`) {
|
||||
t.Fatalf("expected canonical PVE manual-add heading in setup script, got: %s", truncate(script, 900))
|
||||
}
|
||||
if !containsString(script, `echo "Use these details in Pulse Settings → Nodes to finish registration."`) {
|
||||
if !containsString(script, `echo "Use these details in Pulse Settings → Infrastructure to finish registration."`) {
|
||||
t.Fatalf("expected canonical PVE manual registration continuation guidance in setup script, got: %s", truncate(script, 900))
|
||||
}
|
||||
if !containsString(script, `echo " Host URL: $SERVER_HOST"`) {
|
||||
@@ -1295,8 +1307,20 @@ func TestPBSSetupScript_FailsClosedOnAutoRegisterSuccessDetection(t *testing.T)
|
||||
}
|
||||
|
||||
script := rr.Body.String()
|
||||
if !containsString(script, `curl -fsS -X POST "$PULSE_URL/api/auto-register"`) {
|
||||
t.Fatalf("expected fail-fast PBS auto-register transport in setup script, got: %s", truncate(script, 900))
|
||||
if !containsString(script, `curl -sS -w $'\n%{http_code}' -X POST "$PULSE_URL/api/auto-register"`) {
|
||||
t.Fatalf("expected status-capturing PBS auto-register transport in setup script, got: %s", truncate(script, 900))
|
||||
}
|
||||
if containsString(script, `curl -fsS -X POST "$PULSE_URL/api/auto-register"`) {
|
||||
t.Fatalf("expected body-suppressing fail-fast PBS auto-register transport to be removed, got: %s", truncate(script, 900))
|
||||
}
|
||||
if !containsString(script, `REGISTER_STATUS="${REGISTER_OUTPUT##*$'\n'}"`) {
|
||||
t.Fatalf("expected PBS auto-register HTTP status extraction in setup script, got: %s", truncate(script, 900))
|
||||
}
|
||||
if !containsString(script, `elif [ "$REGISTER_STATUS" = "401" ] || [ "$REGISTER_STATUS" = "403" ]; then`) {
|
||||
t.Fatalf("expected reachable PBS auth-failure status branch in setup script, got: %s", truncate(script, 900))
|
||||
}
|
||||
if containsString(script, `grep -q "Authentication required"`) {
|
||||
t.Fatalf("expected unreachable PBS Authentication required grep branch to be removed, got: %s", truncate(script, 900))
|
||||
}
|
||||
if !containsString(script, `"source":"script"`) {
|
||||
t.Fatalf("expected PBS setup script to use canonical /api/auto-register source marker, got: %s", truncate(script, 900))
|
||||
@@ -1349,7 +1373,7 @@ func TestPBSSetupScript_FailsClosedOnAutoRegisterSuccessDetection(t *testing.T)
|
||||
if !containsString(script, `echo "The provided Pulse setup token was invalid or expired"`) {
|
||||
t.Fatalf("expected invalid PBS setup-token guidance in setup script, got: %s", truncate(script, 900))
|
||||
}
|
||||
if !containsString(script, `echo "Get a fresh setup token from Pulse Settings → Nodes and rerun this script."`) {
|
||||
if !containsString(script, `echo "Get a fresh setup token from Pulse Settings → Infrastructure and rerun this script."`) {
|
||||
t.Fatalf("expected fresh PBS setup-token rerun guidance in setup script, got: %s", truncate(script, 900))
|
||||
}
|
||||
if !containsString(script, `SETUP_TOKEN_INVALID=true`) {
|
||||
@@ -1361,16 +1385,16 @@ func TestPBSSetupScript_FailsClosedOnAutoRegisterSuccessDetection(t *testing.T)
|
||||
if !containsString(script, `echo "Pulse setup token authentication failed."`) {
|
||||
t.Fatalf("expected PBS auth-failure completion guidance in setup script, got: %s", truncate(script, 900))
|
||||
}
|
||||
if !containsString(script, `echo "📝 Use the token details below in Pulse Settings → Nodes to finish registration."`) {
|
||||
if !containsString(script, `echo "📝 Use the token details below in Pulse Settings → Infrastructure to finish registration."`) {
|
||||
t.Fatalf("expected canonical PBS auto-register failure continuation guidance in setup script, got: %s", truncate(script, 900))
|
||||
}
|
||||
if !containsString(script, `if [ "$AUTO_REG_SUCCESS" != true ] && [ "$SETUP_TOKEN_INVALID" != true ]; then`) {
|
||||
t.Fatalf("expected PBS manual footer to stay disabled on auth failure, got: %s", truncate(script, 900))
|
||||
}
|
||||
if strings.Count(script, `echo "📝 Use the token details below in Pulse Settings → Nodes to finish registration."`) < 2 {
|
||||
if strings.Count(script, `echo "📝 Use the token details below in Pulse Settings → Infrastructure to finish registration."`) < 2 {
|
||||
t.Fatalf("expected PBS request-failure and response-failure branches to share canonical manual continuation guidance, got: %s", truncate(script, 900))
|
||||
}
|
||||
if !containsString(script, `echo "⚠️ Auto-registration failed. Finish registration manually in Pulse Settings → Nodes."`) {
|
||||
if !containsString(script, `echo "⚠️ Auto-registration failed. Finish registration manually in Pulse Settings → Infrastructure."`) {
|
||||
t.Fatalf("expected canonical PBS auto-register failure summary guidance in setup script, got: %s", truncate(script, 900))
|
||||
}
|
||||
if containsString(script, `echo "To enable auto-registration, add your API token to the setup URL"`) {
|
||||
@@ -1421,7 +1445,7 @@ func TestPBSSetupScript_FailsClosedOnAutoRegisterSuccessDetection(t *testing.T)
|
||||
if !containsString(script, `echo "Finish registration in Pulse using the manual setup details below."`) {
|
||||
t.Fatalf("expected truthful PBS manual registration guidance in setup script, got: %s", truncate(script, 900))
|
||||
}
|
||||
if !containsString(script, `echo "Use these details in Pulse Settings → Nodes to finish registration."`) {
|
||||
if !containsString(script, `echo "Use these details in Pulse Settings → Infrastructure to finish registration."`) {
|
||||
t.Fatalf("expected canonical PBS manual registration continuation guidance in setup script, got: %s", truncate(script, 900))
|
||||
}
|
||||
if !containsString(script, `echo " Host URL: $HOST_URL"`) {
|
||||
|
||||
@@ -58,6 +58,20 @@ const (
|
||||
proxmoxInstallRegistrationAtKey = "proxmox_registration_at"
|
||||
)
|
||||
|
||||
// installTokenTypeGrantsBootstrap decides whether an install token minted for
|
||||
// installType may bootstrap a source of requestedType. Proxmox-typed tokens
|
||||
// stay pinned to their declared type. A generic host-agent install token may
|
||||
// bootstrap either canonical Proxmox type: the unified installer auto-detects
|
||||
// PVE/PBS on the target machine (#1644), and the token is still bounded by the
|
||||
// same settings:write mint requirement, one-shot completion marker, and
|
||||
// first-use hostname binding as a typed token.
|
||||
func installTokenTypeGrantsBootstrap(installType, requestedType string) bool {
|
||||
if installType == requestedType && isCanonicalAutoRegisterType(requestedType) {
|
||||
return true
|
||||
}
|
||||
return installType == agentInstallTypeHost && isCanonicalAutoRegisterType(requestedType)
|
||||
}
|
||||
|
||||
func canBootstrapProxmoxInstallRegistration(record *config.APITokenRecord, req *AutoRegisterRequest) bool {
|
||||
if record == nil || req == nil || strings.TrimSpace(req.Source) != "agent" {
|
||||
return false
|
||||
@@ -65,7 +79,7 @@ func canBootstrapProxmoxInstallRegistration(record *config.APITokenRecord, req *
|
||||
if strings.EqualFold(strings.TrimSpace(record.Metadata[proxmoxInstallRegistrationCompletedKey]), "true") {
|
||||
return false
|
||||
}
|
||||
if strings.TrimSpace(record.Metadata["install_type"]) != strings.TrimSpace(req.Type) {
|
||||
if !installTokenTypeGrantsBootstrap(strings.TrimSpace(record.Metadata["install_type"]), strings.TrimSpace(req.Type)) {
|
||||
return false
|
||||
}
|
||||
switch strings.TrimSpace(record.Metadata["issued_via"]) {
|
||||
|
||||
@@ -6673,7 +6673,7 @@ func TestContract_SetupScriptEmbedsFailFastGuidance(t *testing.T) {
|
||||
if strings.Contains(script, `echo " curl -sSL \"$SETUP_SCRIPT_URL\" | bash"`) || strings.Contains(script, `echo " curl -sSL \"$PULSE_URL/api/setup-script?type=pve&host=YOUR_PVE_URL&pulse_url=$PULSE_URL\" | bash"`) {
|
||||
t.Fatalf("setup script preserved stale non-fail-fast guidance: %s", script)
|
||||
}
|
||||
if strings.Contains(script, `echo "Manual setup steps:"`) || strings.Contains(script, `echo " 2. In Pulse: Settings → Nodes → Add Node (enter token from above)"`) {
|
||||
if strings.Contains(script, `echo "Manual setup steps:"`) || strings.Contains(script, `echo " 2. In Pulse: Settings → Infrastructure → Add Node (enter token from above)"`) {
|
||||
t.Fatalf("setup script preserved stale off-host manual token flow: %s", script)
|
||||
}
|
||||
if !strings.Contains(script, `done <<< "$OLD_TOKENS_PVE"`) {
|
||||
@@ -6762,8 +6762,17 @@ func TestContract_SetupScriptEmbedsFailFastGuidance(t *testing.T) {
|
||||
if strings.Contains(script, `grep -q "success"`) {
|
||||
t.Fatalf("setup script preserved broad success substring detection: %s", script)
|
||||
}
|
||||
if !strings.Contains(script, `curl -fsS -X POST "$PULSE_URL/api/auto-register"`) {
|
||||
t.Fatalf("setup script missing fail-fast auto-register transport: %s", script)
|
||||
if !strings.Contains(script, `curl -sS -w $'\n%{http_code}' -X POST "$PULSE_URL/api/auto-register"`) {
|
||||
t.Fatalf("setup script missing status-capturing auto-register transport: %s", script)
|
||||
}
|
||||
if strings.Contains(script, `curl -fsS -X POST "$PULSE_URL/api/auto-register"`) {
|
||||
t.Fatalf("setup script preserved body-suppressing fail-fast auto-register transport: %s", script)
|
||||
}
|
||||
if !strings.Contains(script, `elif [ "$REGISTER_STATUS" = "401" ] || [ "$REGISTER_STATUS" = "403" ]; then`) {
|
||||
t.Fatalf("setup script missing reachable auth-failure status branch: %s", script)
|
||||
}
|
||||
if strings.Contains(script, `grep -q "Authentication required"`) {
|
||||
t.Fatalf("setup script preserved unreachable Authentication required grep branch: %s", script)
|
||||
}
|
||||
if !strings.Contains(script, `curl -fsS -X POST "$PULSE_URL/api/auto-unregister"`) {
|
||||
t.Fatalf("setup script missing fail-fast auto-unregister transport: %s", script)
|
||||
@@ -6786,7 +6795,7 @@ func TestContract_SetupScriptEmbedsFailFastGuidance(t *testing.T) {
|
||||
if !strings.Contains(script, `echo "The provided Pulse setup token was invalid or expired"`) {
|
||||
t.Fatalf("setup script missing invalid setup-token guidance: %s", script)
|
||||
}
|
||||
if !strings.Contains(script, `echo "Get a fresh setup token from Pulse Settings → Nodes and rerun this script."`) {
|
||||
if !strings.Contains(script, `echo "Get a fresh setup token from Pulse Settings → Infrastructure and rerun this script."`) {
|
||||
t.Fatalf("setup script missing fresh setup-token rerun guidance: %s", script)
|
||||
}
|
||||
if !strings.Contains(script, `SETUP_TOKEN_INVALID=true`) {
|
||||
@@ -6798,7 +6807,7 @@ func TestContract_SetupScriptEmbedsFailFastGuidance(t *testing.T) {
|
||||
if !strings.Contains(script, `if [ "$AUTO_REG_SUCCESS" != true ] && [ "$SETUP_TOKEN_INVALID" != true ]; then`) {
|
||||
t.Fatalf("setup script missing PVE auth-failure footer guard: %s", script)
|
||||
}
|
||||
if !strings.Contains(script, `echo "📝 Use the token details below in Pulse Settings → Nodes to finish registration."`) {
|
||||
if !strings.Contains(script, `echo "📝 Use the token details below in Pulse Settings → Infrastructure to finish registration."`) {
|
||||
t.Fatalf("setup script missing canonical auto-register failure continuation guidance: %s", script)
|
||||
}
|
||||
if strings.Contains(script, `echo "To enable auto-registration, add your API token to the setup URL"`) {
|
||||
@@ -6843,10 +6852,10 @@ func TestContract_SetupScriptEmbedsFailFastGuidance(t *testing.T) {
|
||||
if !strings.Contains(script, `echo "Add this server to Pulse with:"`) {
|
||||
t.Fatalf("setup script missing canonical manual-add heading: %s", script)
|
||||
}
|
||||
if !strings.Contains(script, `echo "Use these details in Pulse Settings → Nodes to finish registration."`) {
|
||||
if !strings.Contains(script, `echo "Use these details in Pulse Settings → Infrastructure to finish registration."`) {
|
||||
t.Fatalf("setup script missing canonical manual-add continuation guidance: %s", script)
|
||||
}
|
||||
if !strings.Contains(script, `echo "⚠️ Auto-registration failed. Finish registration manually in Pulse Settings → Nodes."`) {
|
||||
if !strings.Contains(script, `echo "⚠️ Auto-registration failed. Finish registration manually in Pulse Settings → Infrastructure."`) {
|
||||
t.Fatalf("setup script missing canonical auto-register failure summary: %s", script)
|
||||
}
|
||||
if !strings.Contains(script, `echo " Host URL: $SERVER_HOST"`) {
|
||||
@@ -6943,7 +6952,7 @@ fi`) {
|
||||
if !strings.Contains(pbsScript, `echo "The provided Pulse setup token was invalid or expired"`) {
|
||||
t.Fatalf("setup script missing invalid PBS setup-token guidance: %s", pbsScript)
|
||||
}
|
||||
if !strings.Contains(pbsScript, `echo "Get a fresh setup token from Pulse Settings → Nodes and rerun this script."`) {
|
||||
if !strings.Contains(pbsScript, `echo "Get a fresh setup token from Pulse Settings → Infrastructure and rerun this script."`) {
|
||||
t.Fatalf("setup script missing fresh PBS setup-token rerun guidance: %s", pbsScript)
|
||||
}
|
||||
if !strings.Contains(pbsScript, `SETUP_TOKEN_INVALID=true`) {
|
||||
@@ -7032,10 +7041,10 @@ fi`) {
|
||||
if strings.Contains(pbsScript, `echo "To enable auto-registration, rerun with a valid Pulse setup token"`) {
|
||||
t.Fatalf("setup script preserved stale split PBS setup-token auth guidance: %s", pbsScript)
|
||||
}
|
||||
if !strings.Contains(pbsScript, `echo "⚠️ Auto-registration failed. Finish registration manually in Pulse Settings → Nodes."`) {
|
||||
if !strings.Contains(pbsScript, `echo "⚠️ Auto-registration failed. Finish registration manually in Pulse Settings → Infrastructure."`) {
|
||||
t.Fatalf("setup script missing canonical PBS auto-register failure summary: %s", pbsScript)
|
||||
}
|
||||
if strings.Count(pbsScript, `echo "📝 Use the token details below in Pulse Settings → Nodes to finish registration."`) < 2 {
|
||||
if strings.Count(pbsScript, `echo "📝 Use the token details below in Pulse Settings → Infrastructure to finish registration."`) < 2 {
|
||||
t.Fatalf("setup script missing canonical PBS request-failure/manual-response continuity: %s", pbsScript)
|
||||
}
|
||||
if strings.Contains(pbsScript, `echo "⚠️ Auto-registration failed. Manual configuration may be needed."`) {
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
||||
)
|
||||
|
||||
// Issue #1644: the Settings > Infrastructure installer mints generic host
|
||||
// install tokens, while install.sh auto-detects Proxmox on the target machine
|
||||
// and presents type "pve"/"pbs" to /api/auto-register. The bootstrap grant
|
||||
// must accept a host-issued install token for a canonical Proxmox type, while
|
||||
// staying one-shot and hostname-bound.
|
||||
|
||||
func mintHostInstallToken(t *testing.T, handler *ConfigHandlers) string {
|
||||
t.Helper()
|
||||
installReq := httptest.NewRequest(
|
||||
http.MethodPost,
|
||||
"/api/agent-install-command",
|
||||
strings.NewReader(`{"type":"host","name":"issue-1644-host"}`),
|
||||
)
|
||||
installReq.Host = "pulse.example:7655"
|
||||
installRec := httptest.NewRecorder()
|
||||
handler.HandleAgentInstallCommand(installRec, installReq)
|
||||
if installRec.Code != http.StatusOK {
|
||||
t.Fatalf("host install token mint status = %d, body=%s", installRec.Code, installRec.Body.String())
|
||||
}
|
||||
var install AgentInstallCommandResponse
|
||||
if err := json.Unmarshal(installRec.Body.Bytes(), &install); err != nil {
|
||||
t.Fatalf("decode host install token response: %v", err)
|
||||
}
|
||||
if strings.TrimSpace(install.Token) == "" {
|
||||
t.Fatal("host install token mint omitted runtime token")
|
||||
}
|
||||
return install.Token
|
||||
}
|
||||
|
||||
func TestIssue1644HostInstallTokenBootstrapsDetectedProxmoxSource(t *testing.T) {
|
||||
stubAutoRegisterNetworkDeps(t)
|
||||
|
||||
cfg := &config.Config{
|
||||
DataPath: t.TempDir(),
|
||||
AuthUser: "admin",
|
||||
AuthPass: "hashed-password",
|
||||
}
|
||||
handler := newTestConfigHandlers(t, cfg)
|
||||
rawToken := mintHostInstallToken(t, handler)
|
||||
if len(cfg.APITokens) != 1 {
|
||||
t.Fatalf("API tokens = %d, want 1", len(cfg.APITokens))
|
||||
}
|
||||
if got := cfg.APITokens[0].Metadata["install_type"]; got != agentInstallTypeHost {
|
||||
t.Fatalf("minted install_type = %q, want %q", got, agentInstallTypeHost)
|
||||
}
|
||||
|
||||
// The agent's pre-registration check must report canRegister=true so
|
||||
// runForType proceeds instead of aborting with a blocked registration.
|
||||
checkRec := runAgentAutoRegister(t, handler, rawToken, AutoRegisterRequest{
|
||||
Type: "pve",
|
||||
Host: "https://pve-host.local:8006",
|
||||
ServerName: "pve-host",
|
||||
Source: "agent",
|
||||
CheckRegistration: true,
|
||||
})
|
||||
if checkRec.Code != http.StatusOK {
|
||||
t.Fatalf("registration check status = %d, body=%s", checkRec.Code, checkRec.Body.String())
|
||||
}
|
||||
var check autoRegisterCheckResponse
|
||||
if err := json.Unmarshal(checkRec.Body.Bytes(), &check); err != nil {
|
||||
t.Fatalf("decode registration check: %v", err)
|
||||
}
|
||||
if !check.CanRegister {
|
||||
t.Fatalf("host install token presenting pve reported canRegister=%v, want true", check.CanRegister)
|
||||
}
|
||||
|
||||
registerRec := runAgentAutoRegister(t, handler, rawToken, AutoRegisterRequest{
|
||||
Type: "pve",
|
||||
Host: "https://pve-host.local:8006",
|
||||
TokenID: "pulse-monitor@pve!pulse-pve-host",
|
||||
TokenValue: "proxmox-secret",
|
||||
ServerName: "pve-host",
|
||||
Source: "agent",
|
||||
})
|
||||
if registerRec.Code != http.StatusOK {
|
||||
t.Fatalf("host-token pve registration status = %d, body=%s", registerRec.Code, registerRec.Body.String())
|
||||
}
|
||||
if len(cfg.PVEInstances) != 1 {
|
||||
t.Fatalf("PVE instances = %d, want 1", len(cfg.PVEInstances))
|
||||
}
|
||||
if !strings.EqualFold(cfg.APITokens[0].Metadata[proxmoxInstallRegistrationCompletedKey], "true") {
|
||||
t.Fatal("host-token pve registration did not consume the install grant")
|
||||
}
|
||||
|
||||
// The grant stays one-shot: after the pve bootstrap completes, the same
|
||||
// token cannot bootstrap another source of any type.
|
||||
reuseRec := runAgentAutoRegister(t, handler, rawToken, AutoRegisterRequest{
|
||||
Type: "pbs",
|
||||
Host: "https://pbs-host.local:8007",
|
||||
TokenID: "pulse-monitor@pbs!pulse-pbs-host",
|
||||
TokenValue: "other-proxmox-secret",
|
||||
ServerName: "pve-host",
|
||||
Source: "agent",
|
||||
})
|
||||
if reuseRec.Code != http.StatusForbidden {
|
||||
t.Fatalf("consumed host-token grant reuse status = %d, want 403; body=%s", reuseRec.Code, reuseRec.Body.String())
|
||||
}
|
||||
if len(cfg.PBSInstances) != 0 {
|
||||
t.Fatalf("PBS instances = %d, want 0 after consumed grant", len(cfg.PBSInstances))
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssue1644HostInstallTokenGrantStaysHostnameBound(t *testing.T) {
|
||||
stubAutoRegisterNetworkDeps(t)
|
||||
|
||||
rawToken := "issue-1644-host-bound.12345678"
|
||||
record := newTokenRecord(t, rawToken, []string{config.ScopeAgentReport}, map[string]string{
|
||||
"install_type": agentInstallTypeHost,
|
||||
"issued_via": agentInstallIssuedViaConfig,
|
||||
})
|
||||
cfg := &config.Config{
|
||||
DataPath: t.TempDir(),
|
||||
APITokens: []config.APITokenRecord{record},
|
||||
}
|
||||
handler := newTestConfigHandlers(t, cfg)
|
||||
|
||||
firstRec := runAgentAutoRegister(t, handler, rawToken, AutoRegisterRequest{
|
||||
Type: "pve",
|
||||
Host: "https://pve-bound.local:8006",
|
||||
TokenID: "pulse-monitor@pve!pulse-pve-bound",
|
||||
TokenValue: "proxmox-secret",
|
||||
ServerName: "pve-bound",
|
||||
Source: "agent",
|
||||
})
|
||||
if firstRec.Code != http.StatusOK {
|
||||
t.Fatalf("bound host-token registration status = %d, body=%s", firstRec.Code, firstRec.Body.String())
|
||||
}
|
||||
if got := cfg.APITokens[0].Metadata["bound_hostname"]; got != "pve-bound" {
|
||||
t.Fatalf("bound hostname = %q, want %q", got, "pve-bound")
|
||||
}
|
||||
|
||||
otherHostRec := runAgentAutoRegister(t, handler, rawToken, AutoRegisterRequest{
|
||||
Type: "pve",
|
||||
Host: "https://pve-other.local:8006",
|
||||
TokenID: "pulse-monitor@pve!pulse-pve-other",
|
||||
TokenValue: "proxmox-secret",
|
||||
ServerName: "pve-other",
|
||||
Source: "agent",
|
||||
})
|
||||
if otherHostRec.Code != http.StatusForbidden {
|
||||
t.Fatalf("cross-host host-token registration status = %d, want 403; body=%s", otherHostRec.Code, otherHostRec.Body.String())
|
||||
}
|
||||
if len(cfg.PVEInstances) != 1 {
|
||||
t.Fatalf("PVE instances = %d, want 1 after cross-host rejection", len(cfg.PVEInstances))
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssue1644HostInstallTokenRejectsNonCanonicalType(t *testing.T) {
|
||||
rawToken := "issue-1644-host-bad-type.12345678"
|
||||
record := newTokenRecord(t, rawToken, []string{config.ScopeAgentReport}, map[string]string{
|
||||
"install_type": agentInstallTypeHost,
|
||||
"issued_via": agentInstallIssuedViaConfig,
|
||||
})
|
||||
req := &AutoRegisterRequest{
|
||||
Type: "host",
|
||||
ServerName: "some-host",
|
||||
Source: "agent",
|
||||
}
|
||||
if canBootstrapProxmoxInstallRegistration(&record, req) {
|
||||
t.Fatal("host install token must not hold a bootstrap grant for non-Proxmox types")
|
||||
}
|
||||
}
|
||||
|
||||
func runIssue1644AutoRegisterRaw(t *testing.T, handler *ConfigHandlers, rawToken string, body []byte) *httptest.ResponseRecorder {
|
||||
t.Helper()
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/auto-register", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-API-Token", rawToken)
|
||||
rec := httptest.NewRecorder()
|
||||
handler.HandleAutoRegister(rec, req)
|
||||
return rec
|
||||
}
|
||||
|
||||
func TestIssue1644TypedInstallTokenStaysPinnedToItsType(t *testing.T) {
|
||||
rawToken := "issue-1644-typed-pinned.12345678"
|
||||
record := newTokenRecord(t, rawToken, []string{config.ScopeAgentReport}, map[string]string{
|
||||
"install_type": "pve",
|
||||
"issued_via": agentInstallIssuedViaConfig,
|
||||
})
|
||||
cfg := &config.Config{
|
||||
DataPath: t.TempDir(),
|
||||
APITokens: []config.APITokenRecord{record},
|
||||
}
|
||||
handler := newTestConfigHandlers(t, cfg)
|
||||
|
||||
payload, err := json.Marshal(AutoRegisterRequest{
|
||||
Type: "pbs",
|
||||
Host: "https://pbs-pinned.local:8007",
|
||||
TokenID: "pulse-monitor@pbs!pulse-pbs-pinned",
|
||||
TokenValue: "proxmox-secret",
|
||||
ServerName: "pbs-pinned",
|
||||
Source: "agent",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("marshal pinned-type payload: %v", err)
|
||||
}
|
||||
rec := runIssue1644AutoRegisterRaw(t, handler, rawToken, payload)
|
||||
if rec.Code != http.StatusForbidden {
|
||||
t.Fatalf("pve-typed token presenting pbs status = %d, want 403; body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
}
|
||||
@@ -696,7 +696,7 @@ smoke_test_pve_token() {
|
||||
echo "⚠️ Created API token, but the local Proxmox API smoke check failed."
|
||||
echo " Response: $SMOKE_OUTPUT"
|
||||
echo ""
|
||||
echo "📝 Use the token details below in Pulse Settings → Nodes after resolving the API connectivity issue."
|
||||
echo "📝 Use the token details below in Pulse Settings → Infrastructure after resolving the API connectivity issue."
|
||||
TOKEN_READY=false
|
||||
AUTO_REG_SUCCESS=false
|
||||
return 1
|
||||
@@ -741,34 +741,37 @@ attempt_auto_registration() {
|
||||
|
||||
REGISTER_JSON='{"type":"pve","host":"'"$HOST_URL"'","serverName":"'"$SERVER_HOSTNAME"'","tokenId":"'"$PULSE_TOKEN_ID"'","tokenValue":"'"$TOKEN_VALUE"'","authToken":"'"$PULSE_SETUP_TOKEN"'","source":"script"}'
|
||||
|
||||
REGISTER_RESPONSE=$(echo "$REGISTER_JSON" | curl -fsS -X POST "$PULSE_URL/api/auto-register" \
|
||||
# No -f: the response body and HTTP status are both needed so a rejected
|
||||
# setup token (401/403) can be told apart from other failures.
|
||||
REGISTER_OUTPUT=$(echo "$REGISTER_JSON" | curl -sS -w $'\n%%{http_code}' -X POST "$PULSE_URL/api/auto-register" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d @- 2>&1)
|
||||
REGISTER_RC=$?
|
||||
REGISTER_STATUS="${REGISTER_OUTPUT##*$'\n'}"
|
||||
REGISTER_RESPONSE="${REGISTER_OUTPUT%%$'\n'*}"
|
||||
|
||||
AUTO_REG_SUCCESS=false
|
||||
if [ "$REGISTER_RC" -ne 0 ]; then
|
||||
echo "⚠️ Auto-registration request failed before success confirmation."
|
||||
echo " Response: $REGISTER_RESPONSE"
|
||||
echo ""
|
||||
echo "📝 Use the token details below in Pulse Settings → Nodes to finish registration."
|
||||
echo "📝 Use the token details below in Pulse Settings → Infrastructure to finish registration."
|
||||
elif [ "$REGISTER_STATUS" = "401" ] || [ "$REGISTER_STATUS" = "403" ]; then
|
||||
SETUP_TOKEN_INVALID=true
|
||||
echo "Error: Auto-registration failed - authentication required"
|
||||
echo " Response: $REGISTER_RESPONSE"
|
||||
echo ""
|
||||
echo "The provided Pulse setup token was invalid or expired"
|
||||
echo "Get a fresh setup token from Pulse Settings → Infrastructure and rerun this script."
|
||||
elif echo "$REGISTER_RESPONSE" | grep -Eq '"status"[[:space:]]*:[[:space:]]*"success"'; then
|
||||
AUTO_REG_SUCCESS=true
|
||||
echo "Successfully registered with Pulse monitoring."
|
||||
echo ""
|
||||
else
|
||||
if echo "$REGISTER_RESPONSE" | grep -q "Authentication required"; then
|
||||
SETUP_TOKEN_INVALID=true
|
||||
echo "Error: Auto-registration failed - authentication required"
|
||||
echo ""
|
||||
echo "The provided Pulse setup token was invalid or expired"
|
||||
echo "Get a fresh setup token from Pulse Settings → Nodes and rerun this script."
|
||||
else
|
||||
echo "⚠️ Auto-registration failed. Finish registration manually in Pulse Settings → Nodes."
|
||||
echo " Response: $REGISTER_RESPONSE"
|
||||
echo ""
|
||||
echo "📝 Use the token details below in Pulse Settings → Nodes to finish registration."
|
||||
fi
|
||||
echo "⚠️ Auto-registration failed. Finish registration manually in Pulse Settings → Infrastructure."
|
||||
echo " Response: $REGISTER_RESPONSE"
|
||||
echo ""
|
||||
echo "📝 Use the token details below in Pulse Settings → Infrastructure to finish registration."
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -1488,7 +1491,7 @@ elif [ "$TOKEN_CREATED" != true ]; then
|
||||
echo ""
|
||||
elif [ "$SETUP_TOKEN_INVALID" = true ]; then
|
||||
echo "Pulse setup token authentication failed."
|
||||
echo "Get a fresh setup token from Pulse Settings → Nodes and rerun this script."
|
||||
echo "Get a fresh setup token from Pulse Settings → Infrastructure and rerun this script."
|
||||
echo ""
|
||||
elif [ "$TOKEN_READY" != true ]; then
|
||||
echo "Pulse monitoring token setup could not be completed."
|
||||
@@ -1512,7 +1515,7 @@ if [ "$AUTO_REG_SUCCESS" != true ] && [ "$SETUP_TOKEN_INVALID" != true ]; then
|
||||
fi
|
||||
echo " Host URL: $SERVER_HOST"
|
||||
echo ""
|
||||
echo "Use these details in Pulse Settings → Nodes to finish registration."
|
||||
echo "Use these details in Pulse Settings → Infrastructure to finish registration."
|
||||
echo ""
|
||||
fi
|
||||
fi
|
||||
@@ -1706,12 +1709,16 @@ else
|
||||
# Construct registration request with setup token
|
||||
REGISTER_JSON='{"type":"pbs","host":"'"$HOST_URL"'","serverName":"'"$SERVER_HOSTNAME"'","tokenId":"'"$PULSE_TOKEN_ID"'","tokenValue":"'"$TOKEN_VALUE"'","authToken":"'"$PULSE_SETUP_TOKEN"'","source":"script"}'
|
||||
|
||||
# Send registration with setup token
|
||||
# Send registration with setup token. No -f: the response body and
|
||||
# HTTP status are both needed so a rejected setup token (401/403)
|
||||
# can be told apart from other failures.
|
||||
REGISTER_ATTEMPTED=true
|
||||
REGISTER_RESPONSE=$(echo "$REGISTER_JSON" | curl -fsS -X POST "$PULSE_URL/api/auto-register" \
|
||||
REGISTER_OUTPUT=$(echo "$REGISTER_JSON" | curl -sS -w $'\n%%{http_code}' -X POST "$PULSE_URL/api/auto-register" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d @- 2>&1)
|
||||
REGISTER_RC=$?
|
||||
REGISTER_STATUS="${REGISTER_OUTPUT##*$'\n'}"
|
||||
REGISTER_RESPONSE="${REGISTER_OUTPUT%%$'\n'*}"
|
||||
else
|
||||
echo "⚠️ Auto-registration skipped: no setup token provided"
|
||||
AUTO_REG_SUCCESS=false
|
||||
@@ -1726,23 +1733,22 @@ else
|
||||
echo "⚠️ Auto-registration request failed before success confirmation."
|
||||
echo " Response: $REGISTER_RESPONSE"
|
||||
echo ""
|
||||
echo "📝 Use the token details below in Pulse Settings → Nodes to finish registration."
|
||||
echo "📝 Use the token details below in Pulse Settings → Infrastructure to finish registration."
|
||||
elif [ "$REGISTER_STATUS" = "401" ] || [ "$REGISTER_STATUS" = "403" ]; then
|
||||
SETUP_TOKEN_INVALID=true
|
||||
echo "Error: Auto-registration failed - authentication required"
|
||||
echo " Response: $REGISTER_RESPONSE"
|
||||
echo ""
|
||||
echo "The provided Pulse setup token was invalid or expired"
|
||||
echo "Get a fresh setup token from Pulse Settings → Infrastructure and rerun this script."
|
||||
elif echo "$REGISTER_RESPONSE" | grep -Eq '"status"[[:space:]]*:[[:space:]]*"success"'; then
|
||||
AUTO_REG_SUCCESS=true
|
||||
echo "Successfully registered with Pulse monitoring."
|
||||
else
|
||||
if echo "$REGISTER_RESPONSE" | grep -q "Authentication required"; then
|
||||
SETUP_TOKEN_INVALID=true
|
||||
echo "Error: Auto-registration failed - authentication required"
|
||||
echo ""
|
||||
echo "The provided Pulse setup token was invalid or expired"
|
||||
echo "Get a fresh setup token from Pulse Settings → Nodes and rerun this script."
|
||||
else
|
||||
echo "⚠️ Auto-registration failed. Finish registration manually in Pulse Settings → Nodes."
|
||||
echo " Response: $REGISTER_RESPONSE"
|
||||
echo ""
|
||||
echo "📝 Use the token details below in Pulse Settings → Nodes to finish registration."
|
||||
fi
|
||||
echo "⚠️ Auto-registration failed. Finish registration manually in Pulse Settings → Infrastructure."
|
||||
echo " Response: $REGISTER_RESPONSE"
|
||||
echo ""
|
||||
echo "📝 Use the token details below in Pulse Settings → Infrastructure to finish registration."
|
||||
fi
|
||||
echo ""
|
||||
fi
|
||||
@@ -1766,7 +1772,7 @@ elif [ "$TOKEN_CREATED" != true ]; then
|
||||
echo ""
|
||||
elif [ "$SETUP_TOKEN_INVALID" = true ]; then
|
||||
echo "Pulse setup token authentication failed."
|
||||
echo "Get a fresh setup token from Pulse Settings → Nodes and rerun this script."
|
||||
echo "Get a fresh setup token from Pulse Settings → Infrastructure and rerun this script."
|
||||
echo ""
|
||||
elif [ "$TOKEN_READY" != true ]; then
|
||||
echo "Pulse monitoring token setup could not be completed."
|
||||
@@ -1790,7 +1796,7 @@ if [ "$AUTO_REG_SUCCESS" != true ] && [ "$SETUP_TOKEN_INVALID" != true ]; then
|
||||
fi
|
||||
echo " Host URL: $HOST_URL"
|
||||
echo ""
|
||||
echo "Use these details in Pulse Settings → Nodes to finish registration."
|
||||
echo "Use these details in Pulse Settings → Infrastructure to finish registration."
|
||||
echo ""
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -322,6 +322,29 @@ func (p *ProxmoxSetup) pbsStateFilePath() string {
|
||||
return filepath.Join(p.stateDir, "proxmox-pbs-registered")
|
||||
}
|
||||
|
||||
// registrationBlockedFilePath is the marker the installer reads to surface a
|
||||
// denied registration grant in its own output instead of leaving the failure
|
||||
// buried in the agent journal (#1644).
|
||||
func (p *ProxmoxSetup) registrationBlockedFilePath(ptype proxmoxProductType) string {
|
||||
return filepath.Join(p.stateDir, fmt.Sprintf("proxmox-%s-registration-blocked", ptype))
|
||||
}
|
||||
|
||||
func (p *ProxmoxSetup) writeRegistrationBlockedMarker(ptype proxmoxProductType, reason string) {
|
||||
if err := os.MkdirAll(p.stateDir, proxmoxStateDirPerm); err != nil {
|
||||
p.logger.Debug().Err(err).Str("dir", p.stateDir).Msg("Failed to create state dir for registration-blocked marker")
|
||||
return
|
||||
}
|
||||
if err := os.WriteFile(p.registrationBlockedFilePath(ptype), []byte(reason+"\n"), proxmoxStateFilePerm); err != nil {
|
||||
p.logger.Debug().Err(err).Str("type", string(ptype)).Msg("Failed to write registration-blocked marker")
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ProxmoxSetup) clearRegistrationBlockedMarker(ptype proxmoxProductType) {
|
||||
if err := os.Remove(p.registrationBlockedFilePath(ptype)); err != nil && !os.IsNotExist(err) {
|
||||
p.logger.Debug().Err(err).Str("type", string(ptype)).Msg("Failed to remove registration-blocked marker")
|
||||
}
|
||||
}
|
||||
|
||||
func parseProxmoxProductType(rawType string) proxmoxProductType {
|
||||
switch strings.ToLower(strings.TrimSpace(rawType)) {
|
||||
case string(proxmoxProductPVE):
|
||||
@@ -567,17 +590,24 @@ func (p *ProxmoxSetup) runForType(ctx context.Context, ptype proxmoxProductType)
|
||||
if !p.isTypeRegistered(ptype) {
|
||||
p.markTypeAsRegistered(ptype)
|
||||
}
|
||||
p.clearRegistrationBlockedMarker(ptype)
|
||||
p.logger.Info().Str("type", string(ptype)).Msg("Proxmox type already registered, skipping")
|
||||
return nil, nil
|
||||
}
|
||||
if !registration.canRegister {
|
||||
p.logger.Warn().
|
||||
reason := fmt.Sprintf(
|
||||
"Pulse at %s has no %s source for %s and this agent's token cannot create one. Local Proxmox credentials were left unchanged. Generate a fresh agent install command in Pulse Settings -> Infrastructure and re-run it on this host.",
|
||||
p.pulseURL, strings.ToUpper(string(ptype)), hostURL,
|
||||
)
|
||||
p.writeRegistrationBlockedMarker(ptype, reason)
|
||||
p.logger.Error().
|
||||
Str("type", string(ptype)).
|
||||
Str("host", hostURL).
|
||||
Bool("source_exists", registration.sourceExists).
|
||||
Msg("Pulse has no matching Proxmox source and this agent token cannot create one; leaving local credentials unchanged and requiring a fresh install command")
|
||||
return nil, nil
|
||||
Msg("Proxmox registration blocked: Pulse has no matching source and this agent token cannot create one")
|
||||
return nil, fmt.Errorf("proxmox %s registration blocked: %s", ptype, reason)
|
||||
}
|
||||
p.clearRegistrationBlockedMarker(ptype)
|
||||
if p.isTypeRegistered(ptype) {
|
||||
p.logger.Info().
|
||||
Str("type", string(ptype)).
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -386,6 +387,7 @@ func TestRunAllLeavesCredentialsUnchangedWhenAgentCannotCreateMissingSource(t *t
|
||||
return "", nil
|
||||
},
|
||||
}
|
||||
stateDir := t.TempDir()
|
||||
setup := NewProxmoxSetup(
|
||||
logger,
|
||||
server.Client(),
|
||||
@@ -395,12 +397,15 @@ func TestRunAllLeavesCredentialsUnchangedWhenAgentCannotCreateMissingSource(t *t
|
||||
"pve",
|
||||
"node-1",
|
||||
"10.0.0.9",
|
||||
t.TempDir(),
|
||||
stateDir,
|
||||
false,
|
||||
)
|
||||
results, err := setup.RunAll(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("RunAll(): %v", err)
|
||||
if err == nil {
|
||||
t.Fatal("RunAll() returned nil error for a blocked registration; the denial must be loud (#1644)")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "registration blocked") {
|
||||
t.Fatalf("RunAll() error = %v, want blocked-registration diagnostic", err)
|
||||
}
|
||||
if len(results) != 0 {
|
||||
t.Fatalf("missing source returned setup results %#v", results)
|
||||
@@ -411,9 +416,16 @@ func TestRunAllLeavesCredentialsUnchangedWhenAgentCannotCreateMissingSource(t *t
|
||||
if setupArtifactRequests != 0 {
|
||||
t.Fatalf("setup-script-url requests = %d, want 0", setupArtifactRequests)
|
||||
}
|
||||
if got := logs.String(); !strings.Contains(got, "leaving local credentials unchanged") {
|
||||
if got := logs.String(); !strings.Contains(got, "Proxmox registration blocked") {
|
||||
t.Fatalf("missing-source diagnostic absent from logs: %s", got)
|
||||
}
|
||||
marker, readErr := os.ReadFile(filepath.Join(stateDir, "proxmox-pve-registration-blocked"))
|
||||
if readErr != nil {
|
||||
t.Fatalf("read registration-blocked marker: %v", readErr)
|
||||
}
|
||||
if !strings.Contains(string(marker), "Settings -> Infrastructure") {
|
||||
t.Fatalf("registration-blocked marker lacks operator remediation: %s", marker)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisterWithPulse_AcceptsServerSelectedFallbackHost(t *testing.T) {
|
||||
|
||||
@@ -18,7 +18,7 @@ func TestSetupBootstrapDocsStayOnCanonicalArtifactContract(t *testing.T) {
|
||||
pbsDoc := readRepoFile(t, pbsRel)
|
||||
assertContainsAll(t, pbsRel, pbsDoc, []string{
|
||||
`curl -fsSL "http://<pulse-ip>:7655/api/setup-script?type=pbs&host=https://<pbs-ip>:8007&pulse_url=http://<pulse-ip>:7655" | { if [ "$(id -u)" -eq 0 ]; then PULSE_SETUP_TOKEN="<setup-token>" bash; elif command -v sudo >/dev/null 2>&1; then sudo env PULSE_SETUP_TOKEN="<setup-token>" bash; else echo "Root privileges required. Run as root (su -) and retry." >&2; exit 1; fi; }`,
|
||||
"Pulse generates that full command for you from **Settings → Nodes**",
|
||||
"Pulse generates that full command for you from **Settings → Infrastructure**",
|
||||
})
|
||||
assertContainsNone(t, pbsRel, pbsDoc, []string{
|
||||
`curl -sSL "http://<pulse-ip>:7655/api/setup-script?type=pbs&host=https://<pbs-ip>:8007&pulse_url=http://<pulse-ip>:7655" | bash`,
|
||||
|
||||
+75
-2
@@ -548,6 +548,37 @@ verify_agent_server_registration() {
|
||||
return 1
|
||||
}
|
||||
|
||||
# verify_agent_server_registration_with_retry polls the server-side lookup for
|
||||
# a short window before declaring registration unconfirmed. The local /readyz
|
||||
# endpoint flips before the agent's first report cycle completes, so a single
|
||||
# immediate lookup routinely misses a perfectly healthy registration (#1644).
|
||||
# Return codes mirror verify_agent_server_registration.
|
||||
verify_agent_server_registration_with_retry() {
|
||||
local max_attempts=10
|
||||
local interval=3
|
||||
local attempt=0
|
||||
local reg_rc=1
|
||||
|
||||
if [[ -z "$PULSE_URL" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
while [ $attempt -lt $max_attempts ]; do
|
||||
verify_agent_server_registration
|
||||
reg_rc=$?
|
||||
# 0 = confirmed; 2 = token rejected, which is definitive and will not
|
||||
# change with more polling.
|
||||
if [[ $reg_rc -eq 0 || $reg_rc -eq 2 ]]; then
|
||||
return $reg_rc
|
||||
fi
|
||||
attempt=$((attempt + 1))
|
||||
if [ $attempt -lt $max_attempts ]; then
|
||||
sleep $interval
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
resolve_agent_health_url() {
|
||||
if [[ "$HEALTH_ADDR_SET" == "true" && -z "$HEALTH_ADDR" ]]; then
|
||||
return 1
|
||||
@@ -604,7 +635,7 @@ verify_agent_started() {
|
||||
if [[ -z "$health_url" ]]; then
|
||||
while [ $iteration -lt $max_iterations ]; do
|
||||
if agent_process_running; then
|
||||
verify_agent_server_registration
|
||||
verify_agent_server_registration_with_retry
|
||||
local reg_rc=$?
|
||||
if [[ $reg_rc -eq 0 ]]; then
|
||||
log_info "Agent process is running and registered with Pulse."
|
||||
@@ -630,7 +661,7 @@ verify_agent_started() {
|
||||
while [ $iteration -lt $max_iterations ]; do
|
||||
# Check the readiness endpoint first — this is the definitive signal
|
||||
if curl -sf --max-time 2 "$health_url" >/dev/null 2>&1; then
|
||||
verify_agent_server_registration
|
||||
verify_agent_server_registration_with_retry
|
||||
local reg_rc=$?
|
||||
if [[ $reg_rc -eq 0 ]]; then
|
||||
log_info "Agent is running, healthy, and registered with Pulse."
|
||||
@@ -1157,6 +1188,45 @@ EOF
|
||||
chmod +x "$script_path"
|
||||
}
|
||||
|
||||
# report_proxmox_registration_outcome surfaces the agent's Proxmox
|
||||
# registration result in installer output. The agent records a denied
|
||||
# registration grant in a proxmox-<type>-registration-blocked marker file so
|
||||
# the failure is not buried in its journal (#1644).
|
||||
report_proxmox_registration_outcome() {
|
||||
local state_dir="$1"
|
||||
local max_iterations=15
|
||||
local interval=2
|
||||
local iteration=0
|
||||
local blocked_file=""
|
||||
local ptype=""
|
||||
|
||||
if [[ "$ENABLE_PROXMOX" != "true" || -z "$state_dir" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_info "Waiting for Proxmox registration result..."
|
||||
while [ $iteration -lt $max_iterations ]; do
|
||||
for ptype in pve pbs; do
|
||||
blocked_file="${state_dir}/proxmox-${ptype}-registration-blocked"
|
||||
if [ -f "$blocked_file" ]; then
|
||||
log_error "Proxmox ${ptype} registration failed:"
|
||||
while IFS= read -r line; do log_error " $line"; done < "$blocked_file"
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
if [ -f "${state_dir}/proxmox-pve-registered" ] || [ -f "${state_dir}/proxmox-pbs-registered" ]; then
|
||||
log_info "Proxmox node registered with Pulse."
|
||||
return 0
|
||||
fi
|
||||
sleep $interval
|
||||
iteration=$((iteration + 1))
|
||||
done
|
||||
|
||||
log_warn "Proxmox registration was not confirmed within ~$((max_iterations * interval))s. The agent keeps retrying in the background."
|
||||
log_warn "Check the agent logs for Proxmox registration status if the node does not appear in Pulse."
|
||||
return 0
|
||||
}
|
||||
|
||||
complete_installation_flow() {
|
||||
local state_dir="$1"
|
||||
local install_success_message="$2"
|
||||
@@ -1165,6 +1235,7 @@ complete_installation_flow() {
|
||||
|
||||
save_connection_info "$state_dir"
|
||||
if verify_agent_started; then
|
||||
report_proxmox_registration_outcome "$state_dir" || true
|
||||
if [[ "$UPGRADE_MODE" == "true" ]]; then
|
||||
log_info "$upgrade_success_message"
|
||||
json_event "complete" "updated" "Installation updated"
|
||||
@@ -1789,6 +1860,8 @@ clear_proxmox_state_if_needed() {
|
||||
rm -f "${STATE_DIR}/proxmox-registered" 2>/dev/null || true
|
||||
rm -f "${STATE_DIR}/proxmox-pve-registered" 2>/dev/null || true
|
||||
rm -f "${STATE_DIR}/proxmox-pbs-registered" 2>/dev/null || true
|
||||
rm -f "${STATE_DIR}/proxmox-pve-registration-blocked" 2>/dev/null || true
|
||||
rm -f "${STATE_DIR}/proxmox-pbs-registration-blocked" 2>/dev/null || true
|
||||
}
|
||||
|
||||
write_connection_state_value() {
|
||||
|
||||
@@ -2,6 +2,7 @@ package installtests
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -4532,6 +4533,170 @@ func TestInstallSHVerifyAgentServerRegistrationDetectsRejectedToken(t *testing.T
|
||||
}
|
||||
}
|
||||
|
||||
// TestInstallSHRegistrationRetryWindowOutlastsFirstReportCycle verifies that
|
||||
// the post-install registration check polls for a window instead of warning
|
||||
// after a single lookup: /readyz flips before the agent's first report cycle
|
||||
// completes, so an immediate one-shot lookup routinely misses a healthy
|
||||
// registration (issue #1644). A rejected token still short-circuits because
|
||||
// more polling cannot change a definitive 401/403.
|
||||
func TestInstallSHRegistrationRetryWindowOutlastsFirstReportCycle(t *testing.T) {
|
||||
urlEncode := extractInstallShellFunction(t, "url_encode")
|
||||
curlWithPulseToken := extractInstallShellFunction(t, "curl_with_pulse_token")
|
||||
verifyFn := extractInstallShellFunction(t, "verify_agent_server_registration")
|
||||
retryFn := extractInstallShellFunction(t, "verify_agent_server_registration_with_retry")
|
||||
|
||||
verifyStarted := extractInstallShellFunction(t, "verify_agent_started")
|
||||
if !strings.Contains(verifyStarted, "verify_agent_server_registration_with_retry") {
|
||||
t.Fatal("verify_agent_started does not use the registration retry window")
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
// statuses is consumed one per lookup; the last value repeats.
|
||||
statuses []int
|
||||
wantRC string
|
||||
}{
|
||||
{"registered after report cycle", []int{http.StatusNotFound, http.StatusNotFound, http.StatusOK}, "rc=0"},
|
||||
{"rejected token short-circuits", []int{http.StatusUnauthorized}, "rc=2"},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var mu sync.Mutex
|
||||
lookups := 0
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if !strings.HasPrefix(r.URL.Path, "/api/agents/agent/lookup") {
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
mu.Lock()
|
||||
idx := lookups
|
||||
lookups++
|
||||
mu.Unlock()
|
||||
if idx >= len(tc.statuses) {
|
||||
idx = len(tc.statuses) - 1
|
||||
}
|
||||
w.WriteHeader(tc.statuses[idx])
|
||||
if tc.statuses[idx] == http.StatusOK {
|
||||
_, _ = w.Write([]byte(`{"success":true,"agent":{"id":"agent-1644"}}`))
|
||||
} else {
|
||||
_, _ = w.Write([]byte(`{"error":"agent_not_found"}`))
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
script := `
|
||||
PULSE_URL="` + server.URL + `"
|
||||
PULSE_TOKEN="install-token"
|
||||
HOSTNAME_OVERRIDE="pve-1644"
|
||||
INSECURE="false"
|
||||
CURL_CA_BUNDLE=""
|
||||
sleep() { :; }
|
||||
` + curlWithPulseToken + `
|
||||
` + urlEncode + `
|
||||
` + verifyFn + `
|
||||
` + retryFn + `
|
||||
verify_agent_server_registration_with_retry
|
||||
echo "rc=$?"
|
||||
`
|
||||
out, err := exec.Command("bash", "-c", script).CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("bash: %v\n%s", err, out)
|
||||
}
|
||||
if !strings.Contains(string(out), tc.wantRC) {
|
||||
t.Fatalf("case %q: want %s, got:\n%s", tc.name, tc.wantRC, out)
|
||||
}
|
||||
if tc.wantRC == "rc=2" {
|
||||
mu.Lock()
|
||||
got := lookups
|
||||
mu.Unlock()
|
||||
if got != 1 {
|
||||
t.Fatalf("rejected token was retried %d times, want 1 lookup", got)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestInstallSHSurfacesBlockedProxmoxRegistration verifies the installer reads
|
||||
// the agent's proxmox-<type>-registration-blocked marker and prints the denial
|
||||
// in its own output instead of leaving it buried in the agent journal (#1644).
|
||||
func TestInstallSHSurfacesBlockedProxmoxRegistration(t *testing.T) {
|
||||
logInfo := extractInstallShellFunction(t, "log_info")
|
||||
logWarn := extractInstallShellFunction(t, "log_warn")
|
||||
logError := extractInstallShellFunction(t, "log_error")
|
||||
reportFn := extractInstallShellFunction(t, "report_proxmox_registration_outcome")
|
||||
|
||||
completeFlow := extractInstallShellFunction(t, "complete_installation_flow")
|
||||
if !strings.Contains(completeFlow, `report_proxmox_registration_outcome "$state_dir"`) {
|
||||
t.Fatal("complete_installation_flow does not report the Proxmox registration outcome")
|
||||
}
|
||||
clearFn := extractInstallShellFunction(t, "clear_proxmox_state_if_needed")
|
||||
for _, marker := range []string{"proxmox-pve-registration-blocked", "proxmox-pbs-registration-blocked"} {
|
||||
if !strings.Contains(clearFn, marker) {
|
||||
t.Fatalf("clear_proxmox_state_if_needed does not clear %s", marker)
|
||||
}
|
||||
}
|
||||
|
||||
runReport := func(t *testing.T, stateDir string) (string, int) {
|
||||
t.Helper()
|
||||
script := `
|
||||
NON_INTERACTIVE="false"
|
||||
ENABLE_PROXMOX="true"
|
||||
redact_token() { printf '%s' "$1"; }
|
||||
sleep() { :; }
|
||||
` + logInfo + `
|
||||
` + logWarn + `
|
||||
` + logError + `
|
||||
` + reportFn + `
|
||||
report_proxmox_registration_outcome "` + stateDir + `"
|
||||
`
|
||||
out, err := exec.Command("bash", "-c", script).CombinedOutput()
|
||||
rc := 0
|
||||
if err != nil {
|
||||
var exitErr *exec.ExitError
|
||||
if !errors.As(err, &exitErr) {
|
||||
t.Fatalf("bash: %v\n%s", err, out)
|
||||
}
|
||||
rc = exitErr.ExitCode()
|
||||
}
|
||||
return string(out), rc
|
||||
}
|
||||
|
||||
t.Run("blocked marker surfaces reason", func(t *testing.T) {
|
||||
stateDir := t.TempDir()
|
||||
reason := "Pulse at https://pulse.local has no PVE source for https://pve.local:8006 and this agent's token cannot create one."
|
||||
if err := os.WriteFile(filepath.Join(stateDir, "proxmox-pve-registration-blocked"), []byte(reason+"\n"), 0o600); err != nil {
|
||||
t.Fatalf("write blocked marker: %v", err)
|
||||
}
|
||||
out, rc := runReport(t, stateDir)
|
||||
if rc == 0 {
|
||||
t.Fatalf("blocked registration reported success:\n%s", out)
|
||||
}
|
||||
if !strings.Contains(out, "[ERROR]") || !strings.Contains(out, "registration failed") {
|
||||
t.Fatalf("blocked registration not surfaced as an error:\n%s", out)
|
||||
}
|
||||
if !strings.Contains(out, reason) {
|
||||
t.Fatalf("blocked registration output missing agent-recorded reason:\n%s", out)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("registered marker reports success", func(t *testing.T) {
|
||||
stateDir := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(stateDir, "proxmox-pve-registered"), []byte("ok\n"), 0o600); err != nil {
|
||||
t.Fatalf("write registered marker: %v", err)
|
||||
}
|
||||
out, rc := runReport(t, stateDir)
|
||||
if rc != 0 {
|
||||
t.Fatalf("registered marker returned rc=%d:\n%s", rc, out)
|
||||
}
|
||||
if !strings.Contains(out, "Proxmox node registered with Pulse.") {
|
||||
t.Fatalf("registered marker did not report success:\n%s", out)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestInstallSHWarnAgentTokenRejectedIsActionable pins the actionable recovery
|
||||
// copy the installer prints when the server rejects the agent's token.
|
||||
func TestInstallSHWarnAgentTokenRejectedIsActionable(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user