Fix host agent registration verification issues (#746)

- Change default server listen addresses to empty string (listen on all interfaces including IPv6)
- Add short hostname matching fallback in host lookup API to handle FQDN vs short name mismatches
- Implement retry loop (30s) in both Windows and Linux/macOS installers for registration verification
- Fix lint errors: remove unnecessary fmt.Sprintf and nil checks before len()

This resolves the 'Installer could not yet confirm host registration with Pulse' warning
by addressing timing issues, hostname matching, and network connectivity.
This commit is contained in:
courtmanr@gmail.com 2025-11-24 14:28:09 +00:00
parent efad01b717
commit 07d8381858
7 changed files with 71 additions and 20 deletions

View file

@ -455,11 +455,24 @@ if (-not $NoService) {
if ($status -eq 'Running') {
PulseSuccess "Service started successfully!"
PulseInfo "Waiting 10 seconds to validate agent reporting..."
Start-Sleep -Seconds 10
PulseInfo "Waiting for agent to register with Pulse (up to 30s)..."
$maxRetries = 15
$retryDelay = 2
$lookupHost = $null
for ($i = 1; $i -le $maxRetries; $i++) {
Start-Sleep -Seconds $retryDelay
$hostname = $env:COMPUTERNAME
$lookupHost = Test-AgentRegistration -PulseUrl $PulseUrl -Hostname $hostname -Token $Token
if ($lookupHost) {
break
}
Write-Host "." -NoNewline
}
Write-Host "" # Newline after dots
$hostname = $env:COMPUTERNAME
$lookupHost = Test-AgentRegistration -PulseUrl $PulseUrl -Hostname $hostname -Token $Token
if ($lookupHost) {
PulseSuccess "Agent successfully registered with Pulse (host '$hostname')."
if ($lookupHost.status) {

View file

@ -482,7 +482,7 @@ func CheckAuth(cfg *config.Config, w http.ResponseWriter, r *http.Request) bool
w.Write([]byte(fmt.Sprintf(`{"error":"Invalid credentials","attempts":%d,"remaining":%d,"maxAttempts":%d}`,
attempts, remaining, maxFailedAttempts)))
} else {
w.Write([]byte(fmt.Sprintf(`{"error":"Invalid credentials","locked":true,"message":"Account locked for 15 minutes"}`)))
w.Write([]byte(`{"error":"Invalid credentials","locked":true,"message":"Account locked for 15 minutes"}`))
}
return false
}

View file

@ -82,7 +82,7 @@ func (h *DockerAgentHandlers) HandleReport(w http.ResponseWriter, r *http.Reques
"id": cmd.ID,
"type": cmd.Type,
}
if payload != nil && len(payload) > 0 {
if len(payload) > 0 {
commandResponse["payload"] = payload
}
response["commands"] = []map[string]any{commandResponse}

View file

@ -112,6 +112,7 @@ func (h *HostAgentHandlers) HandleLookup(w http.ResponseWriter, r *http.Request)
}
if !found && hostname != "" {
// First pass: exact match (case-insensitive)
for _, candidate := range state.Hosts {
if strings.EqualFold(candidate.Hostname, hostname) || strings.EqualFold(candidate.DisplayName, hostname) {
host = candidate
@ -119,6 +120,26 @@ func (h *HostAgentHandlers) HandleLookup(w http.ResponseWriter, r *http.Request)
break
}
}
// Second pass: short hostname match (if exact match failed)
if !found {
// Helper to get short hostname (before first dot)
getShortName := func(h string) string {
if idx := strings.Index(h, "."); idx != -1 {
return h[:idx]
}
return h
}
shortLookup := getShortName(hostname)
for _, candidate := range state.Hosts {
if strings.EqualFold(getShortName(candidate.Hostname), shortLookup) {
host = candidate
found = true
break
}
}
}
}
if !found {

View file

@ -156,7 +156,7 @@ func (h *NotificationHandlers) GetWebhooks(w http.ResponseWriter, r *http.Reques
}
// Mask headers - only show keys, not values
if webhook.Headers != nil && len(webhook.Headers) > 0 {
if len(webhook.Headers) > 0 {
maskedHeaders := make(map[string]string)
for key := range webhook.Headers {
maskedHeaders[key] = "***REDACTED***"
@ -165,7 +165,7 @@ func (h *NotificationHandlers) GetWebhooks(w http.ResponseWriter, r *http.Reques
}
// Mask custom fields - only show keys, not values
if webhook.CustomFields != nil && len(webhook.CustomFields) > 0 {
if len(webhook.CustomFields) > 0 {
maskedFields := make(map[string]string)
for key := range webhook.CustomFields {
maskedFields[key] = "***REDACTED***"

View file

@ -71,9 +71,9 @@ func IsPasswordHashed(password string) bool {
// NOTE: The envconfig tags are legacy and not used - configuration is loaded from encrypted JSON files
type Config struct {
// Server settings
BackendHost string `envconfig:"BACKEND_HOST" default:"0.0.0.0"`
BackendHost string `envconfig:"BACKEND_HOST" default:""`
BackendPort int `envconfig:"BACKEND_PORT" default:"3000"`
FrontendHost string `envconfig:"FRONTEND_HOST" default:"0.0.0.0"`
FrontendHost string `envconfig:"FRONTEND_HOST" default:""`
FrontendPort int `envconfig:"FRONTEND_PORT" default:"7655"`
ConfigPath string `envconfig:"CONFIG_PATH" default:"/etc/pulse"`
DataPath string `envconfig:"DATA_PATH" default:"/var/lib/pulse"`
@ -523,9 +523,9 @@ func Load() (*Config, error) {
// Initialize config with defaults
cfg := &Config{
BackendHost: "0.0.0.0",
BackendHost: "",
BackendPort: 3000,
FrontendHost: "0.0.0.0",
FrontendHost: "",
FrontendPort: 7655,
ConfigPath: dataDir,
DataPath: dataDir,

View file

@ -1087,14 +1087,31 @@ if [[ "$SERVICE_MODE" != "manual" && "$SERVICE_RUNNING" == true ]]; then
if [[ -z "$PULSE_TOKEN" ]]; then
log_info "Registration check skipped (no API token available for lookup)."
elif command -v curl &> /dev/null; then
log_info "Verifying agent registration with Pulse server..."
LOOKUP_RESPONSE=$(curl -fsSL \
-H "Authorization: Bearer $PULSE_TOKEN" \
--get \
--data-urlencode "hostname=$HOSTNAME" \
"$PULSE_URL/api/agents/host/lookup" 2>/dev/null || true)
log_info "Verifying agent registration with Pulse server (up to 30s)..."
MAX_RETRIES=15
RETRY_DELAY=2
LOOKUP_SUCCESS=false
for ((i=1; i<=MAX_RETRIES; i++)); do
LOOKUP_RESPONSE=$(curl -fsSL \
-H "Authorization: Bearer $PULSE_TOKEN" \
--get \
--data-urlencode "hostname=$HOSTNAME" \
"$PULSE_URL/api/agents/host/lookup" 2>/dev/null || true)
if [[ "$LOOKUP_RESPONSE" == *'"success":true'* ]]; then
if [[ "$LOOKUP_RESPONSE" == *'"success":true'* ]]; then
LOOKUP_SUCCESS=true
break
fi
# Print a dot without newline to show progress
printf "."
sleep $RETRY_DELAY
done
echo "" # Newline after dots
if [[ "$LOOKUP_SUCCESS" == true ]]; then
host_status=$(printf '%s' "$LOOKUP_RESPONSE" | sed -n 's/.*"status":"\([^"]*\)".*/\1/p')
last_seen=$(printf '%s' "$LOOKUP_RESPONSE" | sed -n 's/.*"lastSeen":"\([^"]*\)".*/\1/p')
log_success "Agent successfully registered with Pulse server!"
@ -1102,7 +1119,7 @@ if [[ "$SERVICE_MODE" != "manual" && "$SERVICE_RUNNING" == true ]]; then
log_info "Pulse reports status: $host_status (last seen $last_seen)"
fi
else
log_warn "Agent lookup did not confirm registration yet (response: ${LOOKUP_RESPONSE:-no data})."
log_warn "Agent lookup did not confirm registration after 30s (response: ${LOOKUP_RESPONSE:-no data})."
log_info "Service is running; metrics should appear shortly."
fi
else