This commit is contained in:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user