[Agent] Replace dockerapi container with Redis-based control bus

This commit is contained in:
FreddleSpl0it
2026-05-20 20:54:51 +02:00
parent 4ddcee28e4
commit 689d753264
75 changed files with 3740 additions and 2462 deletions
@@ -0,0 +1,83 @@
// Package commands defines the per-service handler table. The bus dispatcher
// looks up handlers by name and wraps the result in an envelope.Response.
package commands
import (
"context"
"errors"
)
// ErrNotFound signals that the target (queue id, mailbox, …) doesn't live on
// this node. For broadcast operations the aggregator still counts success if
// any other node returns ok.
var ErrNotFound = errors.New("not_found")
// ErrValidation indicates a missing or malformed argument.
var ErrValidation = errors.New("validation")
// Handler executes a single command for a service.
type Handler func(ctx context.Context, args map[string]any) (any, error)
// HealthProbe returns nil if the supervised service is healthy, error otherwise.
// Shared between the `healthcheck` sub-command and the agent's heartbeat loop.
type HealthProbe func(ctx context.Context) error
// Table is the per-service command registry built once at startup.
type Table struct {
Service string
Handlers map[string]Handler
HealthProbe HealthProbe
}
// New constructs an empty table for a service.
func New(service string) *Table {
return &Table{Service: service, Handlers: make(map[string]Handler)}
}
// Register adds a handler. Duplicate registration panics — wiring bugs should
// be loud.
func (t *Table) Register(cmd string, h Handler) {
if _, dup := t.Handlers[cmd]; dup {
panic("commands: duplicate handler " + t.Service + "/" + cmd)
}
t.Handlers[cmd] = h
}
// Lookup returns the handler for cmd or nil.
func (t *Table) Lookup(cmd string) Handler {
return t.Handlers[cmd]
}
// ArgString extracts a required string argument.
func ArgString(args map[string]any, key string) (string, error) {
v, ok := args[key]
if !ok {
return "", errArg(key, "missing")
}
s, ok := v.(string)
if !ok || s == "" {
return "", errArg(key, "must be non-empty string")
}
return s, nil
}
// ArgStringOpt returns an optional string argument with a default.
func ArgStringOpt(args map[string]any, key, def string) string {
if v, ok := args[key]; ok {
if s, ok := v.(string); ok && s != "" {
return s
}
}
return def
}
func errArg(key, reason string) error {
return &validationError{key: key, reason: reason}
}
type validationError struct{ key, reason string }
func (e *validationError) Error() string { return "arg " + e.key + ": " + e.reason }
func (e *validationError) Is(target error) bool {
return target == ErrValidation
}
@@ -0,0 +1,60 @@
package commands
import (
"bytes"
"context"
"fmt"
"os/exec"
)
// RunOptions configures a single Run invocation.
type RunOptions struct {
// Stdin, if non-nil, is written to the process stdin.
Stdin []byte
// CombinedOutputCap limits the captured output (truncated at the end).
// 0 means unlimited. The agent uses ~1 MiB for cat-queue, smaller for
// status-style commands.
OutputCap int
}
// RunResult is what every shell-style command returns.
type RunResult struct {
Stdout string `json:"stdout,omitempty"`
Stderr string `json:"stderr,omitempty"`
ExitCode int `json:"exit_code"`
}
// Run executes argv[0] argv[1:] under ctx (the bus deadline). It does not
// translate exit codes to errors — callers inspect r.ExitCode themselves so
// they can map e.g. "queue id not found" exit codes to ErrNotFound.
func Run(ctx context.Context, opts RunOptions, argv ...string) (*RunResult, error) {
if len(argv) == 0 {
return nil, fmt.Errorf("commands.Run: empty argv")
}
cmd := exec.CommandContext(ctx, argv[0], argv[1:]...)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if opts.Stdin != nil {
cmd.Stdin = bytes.NewReader(opts.Stdin)
}
err := cmd.Run()
out := stdout.String()
errOut := stderr.String()
if opts.OutputCap > 0 {
if len(out) > opts.OutputCap {
out = out[:opts.OutputCap] + "\n…(truncated)"
}
if len(errOut) > opts.OutputCap {
errOut = errOut[:opts.OutputCap] + "\n…(truncated)"
}
}
exit := 0
if exitErr, ok := err.(*exec.ExitError); ok {
exit = exitErr.ExitCode()
err = nil
}
return &RunResult{Stdout: out, Stderr: errOut, ExitCode: exit}, err
}