Public source validation / validate (push) Failing after 3m8s
149 lines
5.5 KiB
Go
149 lines
5.5 KiB
Go
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
|
|
}
|