Skip to content

Commit 4f35fc3

Browse files
committed
refactor: use last_pull/updated_at for bouncer status instead of revoked field
Rework based on CrowdSec team feedback (Issue #47, @LaurenceJJones): - "revoked" is always false and will never be used by CrowdSec - CrowdSec deletes bouncers rather than revoking them - A bouncer in the list is valid by definition Changes: - Remove Revoked field from Bouncer struct (unused by CrowdSec) - Add CreatedAt and UpdatedAt fields for activity detection - Set Valid = true for all listed bouncers (existence = valid) - Determine status from last_pull with updated_at as fallback when last_pull is null (updated_at - created_at > 5s = has pulled) - New "registered" status for bouncers that enrolled but never pulled - Update BouncerStatusMonitor to show status instead of valid/invalid - Add "registered" badge to Bouncers page Status matrix: | last_pull | Status | |--------------------|-------------| | < 5 min ago | connected | | 5-60 min ago | connected | | > 60 min ago | stale | | null + updated_at | connected | | null + just created| registered | Fixes #47
1 parent df0039c commit 4f35fc3

4 files changed

Lines changed: 49 additions & 26 deletions

File tree

internal/api/handlers/health_diagnostics.go

Lines changed: 37 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -31,40 +31,49 @@ func parseBouncersJSON(bouncerOutput string, computeStatus bool) ([]models.Bounc
3131
if ipAddr, err := jsonparser.GetString(value, "ip_address"); err == nil {
3232
bouncer.IPAddress = ipAddr
3333
}
34-
if lastPull, err := jsonparser.GetString(value, "last_pull"); err == nil {
35-
for _, layout := range []string{time.RFC3339Nano, time.RFC3339} {
36-
if t, err := time.Parse(layout, lastPull); err == nil {
37-
bouncer.LastPull = t
38-
break
34+
// Parse timestamps with multiple format support
35+
parseTime := func(key string) time.Time {
36+
if s, err := jsonparser.GetString(value, key); err == nil {
37+
for _, layout := range []string{time.RFC3339Nano, time.RFC3339} {
38+
if t, err := time.Parse(layout, s); err == nil {
39+
return t
40+
}
3941
}
4042
}
43+
return time.Time{}
4144
}
45+
46+
bouncer.LastPull = parseTime("last_pull")
47+
bouncer.CreatedAt = parseTime("created_at")
48+
bouncer.UpdatedAt = parseTime("updated_at")
49+
4250
if bouncerType, err := jsonparser.GetString(value, "type"); err == nil {
4351
bouncer.Type = bouncerType
4452
}
4553
if version, err := jsonparser.GetString(value, "version"); err == nil {
4654
bouncer.Version = version
4755
}
4856

49-
// Determine bouncer validity in a single block for clarity and efficiency.
50-
// CrowdSec >= v1.7.x uses "revoked", older versions use "valid".
51-
if revoked, err := jsonparser.GetBoolean(value, "revoked"); err == nil {
52-
bouncer.Revoked = revoked
53-
bouncer.Valid = !revoked
54-
} else if valid, err := jsonparser.GetBoolean(value, "valid"); err == nil {
55-
bouncer.Valid = valid
56-
} else {
57-
// Neither field present — the bouncer exists in the list,
58-
// so it was not deleted. Treat as valid.
59-
bouncer.Valid = true
60-
}
57+
// A bouncer that exists in the list is valid by definition.
58+
// CrowdSec deletes revoked bouncers rather than marking them,
59+
// so the "revoked" and legacy "valid" fields are not reliable
60+
// indicators of actual connectivity (confirmed by CrowdSec team, see #47).
61+
bouncer.Valid = true
6162

6263
if computeStatus {
63-
if !bouncer.Valid {
64-
bouncer.Status = "disconnected"
65-
} else if bouncer.LastPull.IsZero() {
66-
bouncer.Status = "pending" // valid key, never pulled yet
67-
} else if time.Since(bouncer.LastPull) <= 60*time.Minute {
64+
// Determine last activity: prefer last_pull, fall back to updated_at
65+
// if the bouncer has been active since registration (updated_at > created_at + 5s).
66+
lastActivity := bouncer.LastPull
67+
if lastActivity.IsZero() &&
68+
bouncer.UpdatedAt.After(bouncer.CreatedAt.Add(5*time.Second)) {
69+
lastActivity = bouncer.UpdatedAt
70+
}
71+
72+
if !lastActivity.IsZero() && time.Since(lastActivity) <= 5*time.Minute {
73+
bouncer.Status = "connected"
74+
} else if lastActivity.IsZero() {
75+
bouncer.Status = "registered" // enrolled but never pulled
76+
} else if time.Since(lastActivity) <= 60*time.Minute {
6877
bouncer.Status = "connected"
6978
} else {
7079
bouncer.Status = "stale"
@@ -109,7 +118,12 @@ func checkBouncersHealth(dockerClient *docker.Client, containerName string) mode
109118

110119
activeBouncers := 0
111120
for _, b := range bouncers {
112-
if b.Valid && time.Since(b.LastPull) <= 60*time.Minute {
121+
lastActivity := b.LastPull
122+
if lastActivity.IsZero() &&
123+
b.UpdatedAt.After(b.CreatedAt.Add(5*time.Second)) {
124+
lastActivity = b.UpdatedAt
125+
}
126+
if !lastActivity.IsZero() && time.Since(lastActivity) <= 60*time.Minute {
113127
activeBouncers++
114128
}
115129
}

internal/models/models.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,9 +149,10 @@ func (d *DecisionRaw) Normalize() Decision {
149149
type Bouncer struct {
150150
Name string `json:"name"`
151151
IPAddress string `json:"ip_address"`
152-
Revoked bool `json:"revoked"`
153152
Valid bool `json:"valid"`
154153
LastPull time.Time `json:"last_pull"`
154+
CreatedAt time.Time `json:"created_at"`
155+
UpdatedAt time.Time `json:"updated_at"`
155156
Type string `json:"type"`
156157
Version string `json:"version"`
157158
Status string `json:"status"`

web/src/components/health/BouncerStatusMonitor.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,14 @@ function BouncerStatusMonitor({ bouncers, className }: BouncerStatusMonitorProps
5151
<TableCell>{bouncer.type || '-'}</TableCell>
5252
<TableCell>{bouncer.version || '-'}</TableCell>
5353
<TableCell>
54-
<Badge variant={bouncer.valid ? 'success' : 'destructive'}>
55-
{bouncer.valid ? 'Valid' : 'Invalid'}
54+
<Badge variant={
55+
bouncer.status === 'connected' ? 'success' :
56+
bouncer.status === 'stale' ? 'warning' :
57+
bouncer.status === 'registered' ? 'outline' :
58+
bouncer.status === 'disconnected' ? 'destructive' :
59+
bouncer.valid ? 'success' : 'destructive'
60+
}>
61+
{bouncer.status || (bouncer.valid ? 'Valid' : 'Invalid')}
5662
</Badge>
5763
</TableCell>
5864
<TableCell className="text-sm text-muted-foreground">

web/src/pages/Bouncers.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,8 @@ export default function Bouncers() {
124124
return <Badge variant="warning">Stale</Badge>
125125
case 'pending':
126126
return <Badge variant="warning">Pending</Badge>
127+
case 'registered':
128+
return <Badge variant="outline">Registered</Badge>
127129
default:
128130
return <Badge variant="secondary">{status || 'Unknown'}</Badge>
129131
}

0 commit comments

Comments
 (0)