Publish ITWorx Pulse source
Public source validation / validate (push) Failing after 3m8s

This commit is contained in:
ITWorx Pulse release export
2026-09-03 02:09:19 +02:00
commit bd774932d5
614 changed files with 77116 additions and 0 deletions
+148
View File
@@ -0,0 +1,148 @@
package runtimeconfig
import (
"fmt"
"os"
"path"
"time"
"github.com/itworx/pulse/internal/config"
)
const (
defaultAgentID = "pulse-agent"
defaultCollectInterval = 10 * time.Second
minimumCollectInterval = time.Second
maximumCollectInterval = 5 * time.Minute
defaultProcRoot = "/proc"
defaultSysRoot = "/sys"
maximumAgentIDLength = 120
maximumHostNameLength = 255
maximumConfigurableRowCount = 5000
)
// AgentConfig is everything pulse-agent needs to run. It is deliberately separate from
// internal/config: the agent container receives only PULSE_DATABASE_URL and its own
// settings, and must not require the public URL, OIDC or Prometheus configuration the
// API validates.
type AgentConfig struct {
Service ServiceConfig
// AgentID identifies this agent in every snapshot and in the protocol hello.
AgentID string
// DatabaseURL is the only credential the agent holds; it is never logged.
DatabaseURL string
// CollectInterval is how often a full collection pass runs. The scheduling loop
// itself ticks faster when this is large, so the heartbeat stays inside the
// 10 second window the healthcheck contract requires.
CollectInterval time.Duration
// ProcRoot and SysRoot are the read-only mounts the collector reads.
ProcRoot string
SysRoot string
// FilesystemRoot prefixes mount points before statfs. Empty disables filesystem
// capacity collection, which is the safe default in a container where the host's
// mount points do not resolve.
FilesystemRoot string
// HostName overrides the name read from the reader's UTS namespace, which in a
// container is the container's name rather than the host's.
HostName string
// MaxProcesses bounds the process inventory; zero uses the domain default.
MaxProcesses int
// UnraidURL and UnraidAPIToken are an optional, read-only source for bounded
// container snapshots. They must be configured together; the token is never logged.
UnraidURL string
UnraidAPIToken string
// UnraidCAFile optionally adds one private CA/leaf certificate while retaining
// normal TLS hostname and chain verification.
UnraidCAFile string
}
// LoadAgent reads and validates the agent's environment.
func LoadAgent() (AgentConfig, error) {
service, err := Load("agent")
if err != nil {
return AgentConfig{}, err
}
agent := AgentConfig{
Service: service,
AgentID: valueOrDefault("PULSE_AGENT_ID", defaultAgentID),
DatabaseURL: os.Getenv("PULSE_DATABASE_URL"),
ProcRoot: valueOrDefault("PULSE_AGENT_PROC_ROOT", defaultProcRoot),
SysRoot: valueOrDefault("PULSE_AGENT_SYS_ROOT", defaultSysRoot),
FilesystemRoot: os.Getenv("PULSE_AGENT_FS_ROOT"),
HostName: os.Getenv("PULSE_AGENT_HOST_NAME"),
UnraidURL: os.Getenv("PULSE_UNRAID_URL"),
UnraidAPIToken: os.Getenv("PULSE_UNRAID_API_TOKEN"),
UnraidCAFile: os.Getenv("PULSE_UNRAID_CA_FILE"),
}
if len(agent.AgentID) > maximumAgentIDLength {
return AgentConfig{}, fmt.Errorf("PULSE_AGENT_ID must be at most %d characters", maximumAgentIDLength)
}
if agent.DatabaseURL == "" {
return AgentConfig{}, fmt.Errorf("PULSE_DATABASE_URL is required: the agent publishes snapshots through the database")
}
if err := config.ValidateDatabaseURL(agent.DatabaseURL); err != nil {
return AgentConfig{}, err
}
interval, err := durationOrDefault("PULSE_AGENT_COLLECT_INTERVAL", defaultCollectInterval, minimumCollectInterval, maximumCollectInterval)
if err != nil {
return AgentConfig{}, err
}
agent.CollectInterval = interval
for name, value := range map[string]string{
"PULSE_AGENT_PROC_ROOT": agent.ProcRoot,
"PULSE_AGENT_SYS_ROOT": agent.SysRoot,
} {
if !path.IsAbs(value) {
return AgentConfig{}, fmt.Errorf("%s must be an absolute path", name)
}
}
if agent.FilesystemRoot != "" && !path.IsAbs(agent.FilesystemRoot) {
return AgentConfig{}, fmt.Errorf("PULSE_AGENT_FS_ROOT must be an absolute path when set")
}
if len(agent.HostName) > maximumHostNameLength {
return AgentConfig{}, fmt.Errorf("PULSE_AGENT_HOST_NAME must be at most %d characters", maximumHostNameLength)
}
if (agent.UnraidURL == "") != (agent.UnraidAPIToken == "") {
return AgentConfig{}, fmt.Errorf("PULSE_UNRAID_URL and PULSE_UNRAID_API_TOKEN must be configured together")
}
if agent.UnraidURL != "" {
if err := config.ValidateURL(agent.UnraidURL, true); err != nil {
return AgentConfig{}, fmt.Errorf("PULSE_UNRAID_URL: %w", err)
}
}
if agent.UnraidCAFile != "" {
if agent.UnraidURL == "" || !path.IsAbs(agent.UnraidCAFile) {
return AgentConfig{}, fmt.Errorf("PULSE_UNRAID_CA_FILE requires a configured Unraid source and an absolute path")
}
}
rows, err := intOrDefault("PULSE_AGENT_MAX_PROCESSES", 0, 1, maximumConfigurableRowCount)
if err != nil {
return AgentConfig{}, err
}
agent.MaxProcesses = rows
return agent, nil
}
func durationOrDefault(key string, fallback, minimum, maximum time.Duration) (time.Duration, error) {
raw := os.Getenv(key)
if raw == "" {
return fallback, nil
}
value, err := time.ParseDuration(raw)
if err != nil || value < minimum || value > maximum {
return 0, fmt.Errorf("%s must be a duration between %s and %s", key, minimum, maximum)
}
return value, nil
}
func intOrDefault(key string, fallback, minimum, maximum int) (int, error) {
raw := os.Getenv(key)
if raw == "" {
return fallback, nil
}
value, err := parseInt(raw)
if err != nil || value < minimum || value > maximum {
return 0, fmt.Errorf("%s must be an integer between %d and %d", key, minimum, maximum)
}
return value, nil
}