From 1dfb3cc28c696102e5deb74c1262574eebf6ac3b Mon Sep 17 00:00:00 2001 From: mrhid6 Date: Thu, 13 Aug 2026 10:49:24 +0000 Subject: [PATCH] refactor: Split the agent workloads package by build tag --- agent/internal/sync/workloads.go | 9 --- agent/internal/workloads/control.go | 49 ++-------------- agent/internal/workloads/control_linux.go | 56 +++++++++++++++++++ agent/internal/workloads/logs.go | 25 ++------- agent/internal/workloads/logs_linux.go | 30 ++++++++++ .../{systemd.go => systemd_linux.go} | 4 +- agent/internal/workloads/units_other.go | 22 ++++++++ agent/internal/workloads/workloads.go | 10 +--- 8 files changed, 121 insertions(+), 84 deletions(-) create mode 100644 agent/internal/workloads/control_linux.go create mode 100644 agent/internal/workloads/logs_linux.go rename agent/internal/workloads/{systemd.go => systemd_linux.go} (94%) create mode 100644 agent/internal/workloads/units_other.go diff --git a/agent/internal/sync/workloads.go b/agent/internal/sync/workloads.go index e37e580..8def86f 100644 --- a/agent/internal/sync/workloads.go +++ b/agent/internal/sync/workloads.go @@ -3,7 +3,6 @@ package agentsync import ( "context" "log" - "runtime" "time" "gitea.hostxtra.co.uk/mrhid6/vantage/agent/internal/config" @@ -18,10 +17,6 @@ const workloadInterval = 60 * time.Second // runWorkloads reports what this host runs, on its own ticker. func runWorkloads(ctx context.Context, cfg *config.Config) { - if runtime.GOOS != "linux" { - return - } - reportWorkloads(cfg) ticker := time.NewTicker(workloadInterval) @@ -42,10 +37,6 @@ func runWorkloads(ctx context.Context, cfg *config.Config) { // This is the ONLY writer of the server_workloads collection. RefreshWorkloadsCmd // calls straight into here rather than answering with data of its own. func reportWorkloads(cfg *config.Config) { - if runtime.GOOS != "linux" { - return - } - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) defer cancel() diff --git a/agent/internal/workloads/control.go b/agent/internal/workloads/control.go index f4b38d1..9f838e0 100644 --- a/agent/internal/workloads/control.go +++ b/agent/internal/workloads/control.go @@ -4,9 +4,6 @@ import ( "context" "errors" "fmt" - "os" - "os/exec" - "regexp" "strings" "time" ) @@ -14,34 +11,12 @@ import ( // ErrProtected is returned for a workload the agent will not act on. var ErrProtected = errors.New("workload is protected") -// AgentUnit is the systemd unit this agent runs as. -const AgentUnit = "vantage-agent.service" - // controlTimeout bounds a stop that may never finish on its own. `docker stop` -// waits on a container that may ignore SIGTERM, and `systemctl stop` on a unit -// with a long TimeoutStopSec blocks for exactly as long as that says. A +// waits on a container that may ignore SIGTERM, and both systemctl and +// Stop-Service block for as long as the unit's own stop timeout says. A // timeout must return a real error rather than an ack implying success. const controlTimeout = 90 * time.Second -// ownContainerID is read once: the container this agent runs in, if any. -var ownContainerID = detectOwnContainer() - -var cgroupContainerRe = regexp.MustCompile(`[0-9a-f]{64}`) - -// detectOwnContainer returns this process's container ID, or "" on a host -// install. The agent is normally a systemd service, so "" is the common case; -// this exists so containerising it later cannot silently remove the guard. -func detectOwnContainer() string { - b, err := os.ReadFile("/proc/self/cgroup") - if err != nil { - return "" - } - if m := cgroupContainerRe.FindString(string(b)); m != "" { - return m - } - return "" -} - // isProtected reports whether the agent refuses to act on this workload. // // The refusal lives here, in the agent, and not in the control plane. As with @@ -50,7 +25,7 @@ func detectOwnContainer() string { // denylist alone would be bypassed by the next dispatch path someone adds. func isProtected(kind, id, name string) bool { if kind == "unit" { - return id == AgentUnit || name == strings.TrimSuffix(AgentUnit, ".service") + return isProtectedUnit(id, name) } if ownContainerID == "" { return false @@ -85,21 +60,5 @@ func Control(ctx context.Context, kind, id, action string) error { ctx, cancel := context.WithTimeout(ctx, controlTimeout) defer cancel() - var cmd *exec.Cmd - switch kind { - case "container": - cmd = exec.CommandContext(ctx, "docker", action, id) - case "unit": - cmd = exec.CommandContext(ctx, "systemctl", action, id) - default: - return fmt.Errorf("unknown workload kind %q", kind) - } - - if out, err := cmd.CombinedOutput(); err != nil { - if ctx.Err() == context.DeadlineExceeded { - return fmt.Errorf("%s %s timed out after %s", action, id, controlTimeout) - } - return fmt.Errorf("%s %s: %s", action, id, strings.TrimSpace(string(out))) - } - return nil + return controlPlatform(ctx, kind, id, action) } diff --git a/agent/internal/workloads/control_linux.go b/agent/internal/workloads/control_linux.go new file mode 100644 index 0000000..9c1977d --- /dev/null +++ b/agent/internal/workloads/control_linux.go @@ -0,0 +1,56 @@ +package workloads + +import ( + "context" + "fmt" + "os" + "os/exec" + "regexp" + "strings" +) + +// AgentUnit is the systemd unit this agent runs as. +const AgentUnit = "vantage-agent.service" + +// ownContainerID is read once: the container this agent runs in, if any. +var ownContainerID = detectOwnContainer() + +var cgroupContainerRe = regexp.MustCompile(`[0-9a-f]{64}`) + +// detectOwnContainer returns this process's container ID, or "" on a host +// install. The agent is normally a systemd service, so "" is the common case; +// this exists so containerising it later cannot silently remove the guard. +func detectOwnContainer() string { + b, err := os.ReadFile("/proc/self/cgroup") + if err != nil { + return "" + } + if m := cgroupContainerRe.FindString(string(b)); m != "" { + return m + } + return "" +} + +func isProtectedUnit(id, name string) bool { + return id == AgentUnit || name == strings.TrimSuffix(AgentUnit, ".service") +} + +func controlPlatform(ctx context.Context, kind, id, action string) error { + var cmd *exec.Cmd + switch kind { + case "container": + cmd = exec.CommandContext(ctx, "docker", action, id) + case "unit": + cmd = exec.CommandContext(ctx, "systemctl", action, id) + default: + return fmt.Errorf("unknown workload kind %q", kind) + } + + if out, err := cmd.CombinedOutput(); err != nil { + if ctx.Err() == context.DeadlineExceeded { + return fmt.Errorf("%s %s timed out after %s", action, id, controlTimeout) + } + return fmt.Errorf("%s %s: %s", action, id, strings.TrimSpace(string(out))) + } + return nil +} diff --git a/agent/internal/workloads/logs.go b/agent/internal/workloads/logs.go index 955ab59..45c5ed5 100644 --- a/agent/internal/workloads/logs.go +++ b/agent/internal/workloads/logs.go @@ -2,9 +2,6 @@ package workloads import ( "context" - "fmt" - "os/exec" - "strconv" "strings" "time" ) @@ -35,26 +32,12 @@ func Logs(ctx context.Context, kind, id string, tail int) (string, bool, error) ctx, cancel := context.WithTimeout(ctx, logTimeout) defer cancel() - var cmd *exec.Cmd - switch kind { - case "container": - cmd = exec.CommandContext(ctx, "docker", "logs", - "--tail", strconv.Itoa(tail), "--timestamps", id) - case "unit": - cmd = exec.CommandContext(ctx, "journalctl", "-u", id, - "-n", strconv.Itoa(tail), "--no-pager", "--output=short-iso") - default: - return "", false, fmt.Errorf("unknown workload kind %q", kind) + out, err := logsPlatform(ctx, kind, id, tail) + if err != nil { + return "", false, err } - // docker logs writes container stderr to our stderr, so both streams must - // be captured or half the output silently disappears. - out, err := cmd.CombinedOutput() - if err != nil && len(out) == 0 { - return "", false, fmt.Errorf("read logs for %s: %s", id, errText(err)) - } - - text, truncated := capLog(string(out)) + text, truncated := capLog(out) return text, truncated, nil } diff --git a/agent/internal/workloads/logs_linux.go b/agent/internal/workloads/logs_linux.go new file mode 100644 index 0000000..efc47c9 --- /dev/null +++ b/agent/internal/workloads/logs_linux.go @@ -0,0 +1,30 @@ +package workloads + +import ( + "context" + "fmt" + "os/exec" + "strconv" +) + +func logsPlatform(ctx context.Context, kind, id string, tail int) (string, error) { + var cmd *exec.Cmd + switch kind { + case "container": + cmd = exec.CommandContext(ctx, "docker", "logs", + "--tail", strconv.Itoa(tail), "--timestamps", id) + case "unit": + cmd = exec.CommandContext(ctx, "journalctl", "-u", id, + "-n", strconv.Itoa(tail), "--no-pager", "--output=short-iso") + default: + return "", fmt.Errorf("unknown workload kind %q", kind) + } + + // docker logs writes container stderr to our stderr, so both streams must + // be captured or half the output silently disappears. + out, err := cmd.CombinedOutput() + if err != nil && len(out) == 0 { + return "", fmt.Errorf("read logs for %s: %s", id, errText(err)) + } + return string(out), nil +} diff --git a/agent/internal/workloads/systemd.go b/agent/internal/workloads/systemd_linux.go similarity index 94% rename from agent/internal/workloads/systemd.go rename to agent/internal/workloads/systemd_linux.go index aecd7b0..49a4c58 100644 --- a/agent/internal/workloads/systemd.go +++ b/agent/internal/workloads/systemd_linux.go @@ -14,10 +14,10 @@ const systemdTimeout = 30 * time.Second // ten anyone cares about. var excludedPrefixes = []string{"systemd-", "user@", "user-", "session-", "init.scope"} -// collectSystemd enumerates services in two passes, because "running or +// collectUnits enumerates services in two passes, because "running or // failed" and "enabled but stopped" are different questions — and an enabled // unit that is not running is exactly the one worth seeing. -func collectSystemd(ctx context.Context) ([]Workload, bool, string) { +func collectUnits(ctx context.Context) ([]Workload, bool, string) { if _, err := exec.LookPath("systemctl"); err != nil { return nil, false, "" } diff --git a/agent/internal/workloads/units_other.go b/agent/internal/workloads/units_other.go new file mode 100644 index 0000000..3d15b08 --- /dev/null +++ b/agent/internal/workloads/units_other.go @@ -0,0 +1,22 @@ +//go:build !linux && !windows + +// The build constraint is load-bearing — see updates_other.go. +package workloads + +import ( + "context" + "fmt" +) + +var ownContainerID = "" + +func collectUnits(context.Context) ([]Workload, bool, string) { return nil, false, "" } +func isProtectedUnit(string, string) bool { return false } + +func controlPlatform(context.Context, string, string, string) error { + return fmt.Errorf("workload control is not supported on this platform") +} + +func logsPlatform(context.Context, string, string, int) (string, error) { + return "", fmt.Errorf("workload logs are not supported on this platform") +} diff --git a/agent/internal/workloads/workloads.go b/agent/internal/workloads/workloads.go index 51975a7..c516d3f 100644 --- a/agent/internal/workloads/workloads.go +++ b/agent/internal/workloads/workloads.go @@ -4,7 +4,6 @@ import ( "context" "crypto/sha256" "encoding/hex" - "runtime" "sort" "strconv" "strings" @@ -19,15 +18,12 @@ type Result struct { SystemdError string } -// Collect enumerates every workload on this host. Linux only. +// Collect enumerates every workload on this host: containers from Docker, and +// units from systemd on Linux or the service control manager on Windows. func Collect(ctx context.Context) Result { - if runtime.GOOS != "linux" { - return Result{} - } - var r Result containers, dockerOK, dockerErr := collectDocker(ctx) - units, systemdOK, systemdErr := collectSystemd(ctx) + units, systemdOK, systemdErr := collectUnits(ctx) r.DockerOK, r.DockerError = dockerOK, dockerErr r.SystemdOK, r.SystemdError = systemdOK, systemdErr