Files
ITWorx-Pulse-Public/cmd/agent/main.go
T
ITWorx Pulse release export bd774932d5
Public source validation / validate (push) Failing after 3m8s
Publish ITWorx Pulse source
2026-09-03 02:09:19 +02:00

158 lines
4.9 KiB
Go

// 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)
}