// Command pulse-agent collects read-only host telemetry and publishes it as bounded // snapshots for pulse-api to read. // // It exposes no network port, holds no Docker socket, and performs no mutation: it // reads procfs/sysfs and writes one row per capability through agentstore.Writer. See // docs/architecture/SYSTEM_ARCHITECTURE.md ("pulse-agent"), ADR-0005 and // docs/operations/WORKER_AGENT_HEALTHCHECK_CONTRACT.md. package main import ( "context" "errors" "log/slog" "os" "os/signal" "syscall" "github.com/itworx/pulse/internal/array" "github.com/itworx/pulse/internal/container" "github.com/itworx/pulse/internal/disk" "github.com/itworx/pulse/internal/host" "github.com/itworx/pulse/internal/hostcollect" "github.com/itworx/pulse/internal/pool" "github.com/itworx/pulse/internal/process" "github.com/itworx/pulse/internal/runtimeconfig" "github.com/itworx/pulse/internal/share" "github.com/itworx/pulse/internal/unraid" ) type configuredSource struct { host *hostcollect.Collector } func (s configuredSource) Host(ctx context.Context) (host.RawSnapshot, error) { return s.host.Host(ctx) } func (s configuredSource) Processes(ctx context.Context) (process.RawSnapshot, error) { return s.host.Processes(ctx) } type configuredUnraidSource struct { configuredSource containers interface { Snapshot(context.Context) (container.RawSnapshot, error) } array interface { Snapshot(context.Context) (array.RawSnapshot, error) } disks interface { Snapshot(context.Context) (disk.RawSnapshot, error) } pools interface { Snapshot(context.Context) (pool.RawSnapshot, error) } shares interface { Snapshot(context.Context) (share.RawSnapshot, error) } } func (s configuredUnraidSource) Containers(ctx context.Context) (container.RawSnapshot, error) { if s.containers == nil { return container.RawSnapshot{}, errUnraidSourceNotConfigured } return s.containers.Snapshot(ctx) } func (s configuredUnraidSource) Array(ctx context.Context) (array.RawSnapshot, error) { if s.array == nil { return array.RawSnapshot{}, errUnraidSourceNotConfigured } return s.array.Snapshot(ctx) } func (s configuredUnraidSource) Disks(ctx context.Context) (disk.RawSnapshot, error) { if s.disks == nil { return disk.RawSnapshot{}, errUnraidSourceNotConfigured } return s.disks.Snapshot(ctx) } func (s configuredUnraidSource) Pools(ctx context.Context) (pool.RawSnapshot, error) { if s.pools == nil { return pool.RawSnapshot{}, errUnraidSourceNotConfigured } return s.pools.Snapshot(ctx) } func (s configuredUnraidSource) Shares(ctx context.Context) (share.RawSnapshot, error) { if s.shares == nil { return share.RawSnapshot{}, errUnraidSourceNotConfigured } return s.shares.Snapshot(ctx) } var errUnraidSourceNotConfigured = errors.New("Unraid source is not configured") func main() { logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) if err := run(logger); err != nil { logger.Error("pulse agent failed", "error", err.Error()) os.Exit(1) } } func run(logger *slog.Logger) error { config, err := runtimeconfig.LoadAgent() if err != nil { return err } collector, err := hostcollect.New(hostcollect.Options{ ProcRoot: config.ProcRoot, SysRoot: config.SysRoot, FilesystemRoot: config.FilesystemRoot, HostName: config.HostName, SourceID: "host", ProcessLimits: process.Limits{MaxRows: config.MaxProcesses}, }) if err != nil { return err } ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop() writer, closeStore, err := openStore(ctx, config) if err != nil { return err } defer closeStore() logger.Info("pulse agent configuration loaded", "agent_id", config.AgentID, "proc_root", config.ProcRoot, "sys_root", config.SysRoot, "filesystem_root", config.FilesystemRoot, "collect_interval", config.CollectInterval.String(), "shutdown_timeout", config.Service.ShutdownAfter.String(), ) var source snapshotSource = configuredSource{host: collector} if config.UnraidURL != "" { var client *unraid.Client if config.UnraidCAFile == "" { client, err = unraid.New(config.UnraidURL, config.UnraidAPIToken, nil) } else { info, statErr := os.Stat(config.UnraidCAFile) if statErr != nil || info.Size() <= 0 || info.Size() > 1<<20 { return errors.New("PULSE_UNRAID_CA_FILE must be a readable certificate no larger than 1 MiB") } caPEM, readErr := os.ReadFile(config.UnraidCAFile) if readErr != nil { return errors.New("read PULSE_UNRAID_CA_FILE") } client, err = unraid.NewWithCAPEM(config.UnraidURL, config.UnraidAPIToken, caPEM) } if err != nil { return err } source = configuredUnraidSource{configuredSource: configuredSource{host: collector}, containers: unraid.ContainerSource{Client: client}, array: unraid.ArraySource{Client: client}, disks: unraid.DiskSource{Client: client}, pools: unraid.PoolSource{Client: client}, shares: unraid.ShareSource{Client: client}} } return newAgent(config, source, writer, logger).run(ctx) }