Skip to content

Commit 9434f00

Browse files
sarg3ntclaude
andcommitted
feat(agent): probe phase + startup capability table
Adds a lightweight version of the ProbeableGear mechanism from #60 §1. Each gear self-reports whether its prerequisites are present; gears that probe non-Available are skipped entirely (no Initialize, no Start, no routes, no collectors/streamers). After the probe phase the manager writes a summary table to stderr — readable in the systemd journal — so operators can see at startup which gears are running on this host and which aren't: Gear probe summary: GEAR STATUS REASON certificates enabled haproxy enabled logs enabled metrics enabled security disabled neither fail2ban-client nor nft found on PATH traffic enabled updates enabled Framework changes (internal/framework/gear/): - Add ProbeStatus enum: available, not_installed, inaccessible, disabled - Add ProbeResult struct + helper constructors (ProbeAvailable, etc.) - Add ProbeableGear sub-interface; gears that don't implement it default to Available, so the migration is incremental - Add Manager.ProbeAll, ProbeResults, isLoaded; Initialize/Start/ RegisterRoutes/startCollectors/startStreamers/setupEventHandlers all skip non-Available gears silently (the table already explained why) - Inject tableWriter so tests can capture the rendered output Per-gear probes: - certificates: certbot or acme.sh on PATH/common paths (side-effect-free variant of the existing detectCertbot) - haproxy: stats URL, stats socket existence, or haproxy binary present (with distinct inaccessible reason when the socket path is set but missing — operator's fix differs from "install HAProxy") - logs: journalctl or tail on PATH - metrics: /proc/stat readable (containerized agents without /proc bind- mounted land here as inaccessible) - security: fail2ban-client or nft on PATH - traffic: same surface as haproxy (stick tables go over the same socket) - updates: any of apt-get/apt/dnf/yum/zypper/apk on PATH Tests cover lifecycle skip behavior, default-Available for non-probeable gears, the snapshot copy semantics of ProbeResults, and table formatting (headers, alignment, reasons surfaced only for disabled rows). 9 tests pass under -race. Refs #60 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 667268d commit 9434f00

12 files changed

Lines changed: 739 additions & 12 deletions

File tree

gearbox-agent/cmd/gearbox-agent/main.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -388,8 +388,15 @@ func main() {
388388
// Create plugin manager
389389
gearManager := gear.NewManager(gearDeps, logger)
390390

391-
// Initialize all plugins
391+
// Probe the host: each gear self-reports whether its prerequisites are
392+
// present. Gears that probe negative are skipped for the rest of the
393+
// lifecycle (no Initialize, no Start, no routes). A summary table is
394+
// written to stderr (→ systemd journal) so operators can see at a
395+
// glance which gears are running on this box and why others aren't.
392396
ctx := context.Background()
397+
gearManager.ProbeAll(ctx)
398+
399+
// Initialize the gears that probed Available.
393400
if err := gearManager.InitializeAll(ctx); err != nil {
394401
logger.Error("Failed to initialize plugins", "error", err)
395402
os.Exit(1)

gearbox-agent/internal/framework/gear/interface.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,3 +204,94 @@ func NewUnhealthyStatus(message string) HealthStatus {
204204
LastCheck: time.Now(),
205205
}
206206
}
207+
208+
// ProbeStatus is the agent's verdict on whether a gear can run on this host.
209+
// Operators reading the startup table and the upcoming
210+
// /api/v1/system/capabilities endpoint distinguish the four states because
211+
// the appropriate fix differs — "install the thing", "fix the access", or
212+
// "change the config" — and conflating them costs debugging time.
213+
type ProbeStatus string
214+
215+
const (
216+
// ProbeStatusAvailable means the gear's prerequisites are present and
217+
// reachable. The gear will go through Initialize/Start normally.
218+
ProbeStatusAvailable ProbeStatus = "available"
219+
220+
// ProbeStatusNotInstalled means the thing the gear manages isn't on
221+
// this host at all. Expected on hosts that don't run this software.
222+
ProbeStatusNotInstalled ProbeStatus = "not_installed"
223+
224+
// ProbeStatusInaccessible means the prerequisites exist but the agent
225+
// can't reach them — bind mount missing, permission denied, socket
226+
// unreadable. The fix is access, not installation.
227+
ProbeStatusInaccessible ProbeStatus = "inaccessible"
228+
229+
// ProbeStatusDisabled means the gear was turned off by configuration.
230+
// Reserved for a future GEARBOX_AGENT_DISABLE_GEARS-style mechanism;
231+
// no gear returns this yet.
232+
ProbeStatusDisabled ProbeStatus = "disabled"
233+
)
234+
235+
// ProbeResult is what a ProbeableGear returns from Probe(). Status drives
236+
// the manager's load decision; Reason is a human-readable sentence shown
237+
// in the startup table and the capabilities API.
238+
type ProbeResult struct {
239+
// Status is the verdict. Only Available causes the gear to load.
240+
Status ProbeStatus
241+
242+
// Reason is a free-text sentence written for an operator: name the
243+
// surface that was probed and what was wrong. Mandatory unless the
244+
// status is Available (in which case it may describe what was found).
245+
Reason string
246+
247+
// Capabilities is optional detected facts the gear wants to surface
248+
// (e.g. "haproxy_version": "2.8.5", "stats_socket": "/run/haproxy/admin.sock").
249+
// Populated mainly when Status == Available.
250+
Capabilities map[string]string
251+
}
252+
253+
// IsAvailable reports whether the gear should be loaded.
254+
func (r ProbeResult) IsAvailable() bool {
255+
return r.Status == ProbeStatusAvailable
256+
}
257+
258+
// ProbeAvailable returns an available ProbeResult with an optional reason
259+
// (typically the detected version or path) and capabilities map.
260+
func ProbeAvailable(reason string, capabilities map[string]string) ProbeResult {
261+
return ProbeResult{Status: ProbeStatusAvailable, Reason: reason, Capabilities: capabilities}
262+
}
263+
264+
// ProbeNotInstalled returns a result for the case where the software the
265+
// gear manages isn't on this host.
266+
func ProbeNotInstalled(reason string) ProbeResult {
267+
return ProbeResult{Status: ProbeStatusNotInstalled, Reason: reason}
268+
}
269+
270+
// ProbeInaccessible returns a result for the case where prereqs exist but
271+
// the agent can't reach them. Use when the fix is access, not installation.
272+
func ProbeInaccessible(reason string) ProbeResult {
273+
return ProbeResult{Status: ProbeStatusInaccessible, Reason: reason}
274+
}
275+
276+
// ProbeDisabled returns a result for gears that have been turned off by
277+
// configuration.
278+
func ProbeDisabled(reason string) ProbeResult {
279+
return ProbeResult{Status: ProbeStatusDisabled, Reason: reason}
280+
}
281+
282+
// ProbeableGear is implemented by gears that can self-report whether they
283+
// have what they need to run on the current host. Gears that do not
284+
// implement this interface are treated as always-available.
285+
//
286+
// Probe runs before Initialize. A non-Available result causes the manager
287+
// to skip Initialize, Start, and route registration for that gear — it
288+
// does not exist for this run.
289+
type ProbeableGear interface {
290+
Gear
291+
292+
// Probe inspects the host for the gear's prerequisites and returns the
293+
// verdict. It must be side-effect-free: do not connect to anything,
294+
// do not mutate state, do not log loudly. The manager logs a single
295+
// summary line per probe.
296+
Probe(ctx context.Context, deps Dependencies) ProbeResult
297+
}

0 commit comments

Comments
 (0)