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:
parent
efad01b717
commit
07d8381858
7 changed files with 71 additions and 20 deletions
|
|
@ -455,11 +455,24 @@ if (-not $NoService) {
|
||||||
if ($status -eq 'Running') {
|
if ($status -eq 'Running') {
|
||||||
PulseSuccess "Service started successfully!"
|
PulseSuccess "Service started successfully!"
|
||||||
|
|
||||||
PulseInfo "Waiting 10 seconds to validate agent reporting..."
|
PulseInfo "Waiting for agent to register with Pulse (up to 30s)..."
|
||||||
Start-Sleep -Seconds 10
|
|
||||||
|
$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) {
|
if ($lookupHost) {
|
||||||
PulseSuccess "Agent successfully registered with Pulse (host '$hostname')."
|
PulseSuccess "Agent successfully registered with Pulse (host '$hostname')."
|
||||||
if ($lookupHost.status) {
|
if ($lookupHost.status) {
|
||||||
|
|
|
||||||
|
|
@ -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}`,
|
w.Write([]byte(fmt.Sprintf(`{"error":"Invalid credentials","attempts":%d,"remaining":%d,"maxAttempts":%d}`,
|
||||||
attempts, remaining, maxFailedAttempts)))
|
attempts, remaining, maxFailedAttempts)))
|
||||||
} else {
|
} 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
|
return false
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ func (h *DockerAgentHandlers) HandleReport(w http.ResponseWriter, r *http.Reques
|
||||||
"id": cmd.ID,
|
"id": cmd.ID,
|
||||||
"type": cmd.Type,
|
"type": cmd.Type,
|
||||||
}
|
}
|
||||||
if payload != nil && len(payload) > 0 {
|
if len(payload) > 0 {
|
||||||
commandResponse["payload"] = payload
|
commandResponse["payload"] = payload
|
||||||
}
|
}
|
||||||
response["commands"] = []map[string]any{commandResponse}
|
response["commands"] = []map[string]any{commandResponse}
|
||||||
|
|
|
||||||
|
|
@ -112,6 +112,7 @@ func (h *HostAgentHandlers) HandleLookup(w http.ResponseWriter, r *http.Request)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !found && hostname != "" {
|
if !found && hostname != "" {
|
||||||
|
// First pass: exact match (case-insensitive)
|
||||||
for _, candidate := range state.Hosts {
|
for _, candidate := range state.Hosts {
|
||||||
if strings.EqualFold(candidate.Hostname, hostname) || strings.EqualFold(candidate.DisplayName, hostname) {
|
if strings.EqualFold(candidate.Hostname, hostname) || strings.EqualFold(candidate.DisplayName, hostname) {
|
||||||
host = candidate
|
host = candidate
|
||||||
|
|
@ -119,6 +120,26 @@ func (h *HostAgentHandlers) HandleLookup(w http.ResponseWriter, r *http.Request)
|
||||||
break
|
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 {
|
if !found {
|
||||||
|
|
|
||||||
|
|
@ -156,7 +156,7 @@ func (h *NotificationHandlers) GetWebhooks(w http.ResponseWriter, r *http.Reques
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mask headers - only show keys, not values
|
// 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)
|
maskedHeaders := make(map[string]string)
|
||||||
for key := range webhook.Headers {
|
for key := range webhook.Headers {
|
||||||
maskedHeaders[key] = "***REDACTED***"
|
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
|
// 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)
|
maskedFields := make(map[string]string)
|
||||||
for key := range webhook.CustomFields {
|
for key := range webhook.CustomFields {
|
||||||
maskedFields[key] = "***REDACTED***"
|
maskedFields[key] = "***REDACTED***"
|
||||||
|
|
|
||||||
|
|
@ -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
|
// NOTE: The envconfig tags are legacy and not used - configuration is loaded from encrypted JSON files
|
||||||
type Config struct {
|
type Config struct {
|
||||||
// Server settings
|
// 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"`
|
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"`
|
FrontendPort int `envconfig:"FRONTEND_PORT" default:"7655"`
|
||||||
ConfigPath string `envconfig:"CONFIG_PATH" default:"/etc/pulse"`
|
ConfigPath string `envconfig:"CONFIG_PATH" default:"/etc/pulse"`
|
||||||
DataPath string `envconfig:"DATA_PATH" default:"/var/lib/pulse"`
|
DataPath string `envconfig:"DATA_PATH" default:"/var/lib/pulse"`
|
||||||
|
|
@ -523,9 +523,9 @@ func Load() (*Config, error) {
|
||||||
|
|
||||||
// Initialize config with defaults
|
// Initialize config with defaults
|
||||||
cfg := &Config{
|
cfg := &Config{
|
||||||
BackendHost: "0.0.0.0",
|
BackendHost: "",
|
||||||
BackendPort: 3000,
|
BackendPort: 3000,
|
||||||
FrontendHost: "0.0.0.0",
|
FrontendHost: "",
|
||||||
FrontendPort: 7655,
|
FrontendPort: 7655,
|
||||||
ConfigPath: dataDir,
|
ConfigPath: dataDir,
|
||||||
DataPath: dataDir,
|
DataPath: dataDir,
|
||||||
|
|
|
||||||
|
|
@ -1087,14 +1087,31 @@ if [[ "$SERVICE_MODE" != "manual" && "$SERVICE_RUNNING" == true ]]; then
|
||||||
if [[ -z "$PULSE_TOKEN" ]]; then
|
if [[ -z "$PULSE_TOKEN" ]]; then
|
||||||
log_info "Registration check skipped (no API token available for lookup)."
|
log_info "Registration check skipped (no API token available for lookup)."
|
||||||
elif command -v curl &> /dev/null; then
|
elif command -v curl &> /dev/null; then
|
||||||
log_info "Verifying agent registration with Pulse server..."
|
log_info "Verifying agent registration with Pulse server (up to 30s)..."
|
||||||
LOOKUP_RESPONSE=$(curl -fsSL \
|
|
||||||
-H "Authorization: Bearer $PULSE_TOKEN" \
|
MAX_RETRIES=15
|
||||||
--get \
|
RETRY_DELAY=2
|
||||||
--data-urlencode "hostname=$HOSTNAME" \
|
LOOKUP_SUCCESS=false
|
||||||
"$PULSE_URL/api/agents/host/lookup" 2>/dev/null || true)
|
|
||||||
|
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')
|
host_status=$(printf '%s' "$LOOKUP_RESPONSE" | sed -n 's/.*"status":"\([^"]*\)".*/\1/p')
|
||||||
last_seen=$(printf '%s' "$LOOKUP_RESPONSE" | sed -n 's/.*"lastSeen":"\([^"]*\)".*/\1/p')
|
last_seen=$(printf '%s' "$LOOKUP_RESPONSE" | sed -n 's/.*"lastSeen":"\([^"]*\)".*/\1/p')
|
||||||
log_success "Agent successfully registered with Pulse server!"
|
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)"
|
log_info "Pulse reports status: $host_status (last seen $last_seen)"
|
||||||
fi
|
fi
|
||||||
else
|
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."
|
log_info "Service is running; metrics should appear shortly."
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue