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
}
+144
View File
@@ -0,0 +1,144 @@
package runtimeconfig
import (
"testing"
"time"
)
func setAgentEnvironment(t *testing.T) {
t.Helper()
t.Setenv("PULSE_DATABASE_URL", "postgres://pulse:secret@postgres:5432/pulse?sslmode=disable")
t.Setenv("PULSE_AGENT_ID", "")
t.Setenv("PULSE_AGENT_COLLECT_INTERVAL", "")
t.Setenv("PULSE_AGENT_PROC_ROOT", "")
t.Setenv("PULSE_AGENT_SYS_ROOT", "")
t.Setenv("PULSE_AGENT_FS_ROOT", "")
t.Setenv("PULSE_AGENT_HOST_NAME", "")
t.Setenv("PULSE_AGENT_MAX_PROCESSES", "")
t.Setenv("PULSE_UNRAID_URL", "")
t.Setenv("PULSE_UNRAID_API_TOKEN", "")
t.Setenv("PULSE_UNRAID_CA_FILE", "")
t.Setenv("PULSE_HEARTBEAT_FILE", "")
}
func TestAgentPrivateCARequiresConfiguredHTTPSUnraidSource(t *testing.T) {
setAgentEnvironment(t)
t.Setenv("PULSE_UNRAID_CA_FILE", "/run/pulse/unraid-ca.pem")
if _, err := LoadAgent(); err == nil {
t.Fatal("expected CA without Unraid source rejection")
}
t.Setenv("PULSE_UNRAID_URL", "https://unraid.example.test:5001/graphql")
t.Setenv("PULSE_UNRAID_API_TOKEN", "viewer-key")
config, err := LoadAgent()
if err != nil {
t.Fatal(err)
}
if config.UnraidCAFile != "/run/pulse/unraid-ca.pem" {
t.Fatalf("CA file = %q", config.UnraidCAFile)
}
}
func TestLoadAgentRequiresCompleteOptionalUnraidSource(t *testing.T) {
setAgentEnvironment(t)
t.Setenv("PULSE_UNRAID_URL", "https://tower.example.invalid/graphql")
if _, err := LoadAgent(); err == nil {
t.Fatal("expected incomplete Unraid configuration to fail")
}
t.Setenv("PULSE_UNRAID_API_TOKEN", "test-key")
if _, err := LoadAgent(); err != nil {
t.Fatalf("complete HTTPS Unraid configuration failed: %v", err)
}
t.Setenv("PULSE_UNRAID_URL", "http://tower.example.invalid/graphql")
if _, err := LoadAgent(); err == nil {
t.Fatal("expected non-HTTPS Unraid URL to fail")
}
}
func TestLoadAgentUsesSafeDefaults(t *testing.T) {
setAgentEnvironment(t)
config, err := LoadAgent()
if err != nil {
t.Fatalf("LoadAgent returned error: %v", err)
}
if config.AgentID != defaultAgentID {
t.Fatalf("agent id = %q", config.AgentID)
}
if config.CollectInterval != defaultCollectInterval {
t.Fatalf("interval = %s", config.CollectInterval)
}
if config.ProcRoot != "/proc" || config.SysRoot != "/sys" {
t.Fatalf("unexpected roots: %+v", config)
}
if config.FilesystemRoot != "" {
t.Fatal("filesystem collection must stay opt-in")
}
if config.Service.HeartbeatFile != defaultHeartbeatFile {
t.Fatalf("heartbeat file = %q", config.Service.HeartbeatFile)
}
}
func TestLoadAgentRequiresADatabaseURL(t *testing.T) {
setAgentEnvironment(t)
t.Setenv("PULSE_DATABASE_URL", "")
if _, err := LoadAgent(); err == nil {
t.Fatal("expected a missing database URL to fail")
}
t.Setenv("PULSE_DATABASE_URL", "mysql://nope")
if _, err := LoadAgent(); err == nil {
t.Fatal("expected a non-PostgreSQL URL to fail")
}
}
func TestLoadAgentRejectsOutOfRangeValues(t *testing.T) {
cases := map[string]map[string]string{
"interval too small": {"PULSE_AGENT_COLLECT_INTERVAL": "10ms"},
"interval too large": {"PULSE_AGENT_COLLECT_INTERVAL": "10m"},
"interval malformed": {"PULSE_AGENT_COLLECT_INTERVAL": "soon"},
"relative proc root": {"PULSE_AGENT_PROC_ROOT": "proc"},
"relative sys root": {"PULSE_AGENT_SYS_ROOT": "sys"},
"relative fs root": {"PULSE_AGENT_FS_ROOT": "host"},
"row count too large": {"PULSE_AGENT_MAX_PROCESSES": "99999"},
"row count malformed": {"PULSE_AGENT_MAX_PROCESSES": "many"},
"relative heartbeat": {"PULSE_HEARTBEAT_FILE": "healthy"},
}
for name, environment := range cases {
t.Run(name, func(t *testing.T) {
setAgentEnvironment(t)
for key, value := range environment {
t.Setenv(key, value)
}
if _, err := LoadAgent(); err == nil {
t.Fatal("expected a validation error")
}
})
}
}
func TestLoadAgentAcceptsExplicitSettings(t *testing.T) {
setAgentEnvironment(t)
t.Setenv("PULSE_AGENT_ID", "tower-agent")
t.Setenv("PULSE_AGENT_COLLECT_INTERVAL", "30s")
t.Setenv("PULSE_AGENT_PROC_ROOT", "/host/proc")
t.Setenv("PULSE_AGENT_SYS_ROOT", "/host/sys")
t.Setenv("PULSE_AGENT_FS_ROOT", "/host/root")
t.Setenv("PULSE_AGENT_HOST_NAME", "tower")
t.Setenv("PULSE_AGENT_MAX_PROCESSES", "250")
t.Setenv("PULSE_HEARTBEAT_FILE", "/tmp/agent-healthy")
config, err := LoadAgent()
if err != nil {
t.Fatalf("LoadAgent returned error: %v", err)
}
if config.AgentID != "tower-agent" || config.CollectInterval != 30*time.Second {
t.Fatalf("unexpected config: %+v", config)
}
if config.ProcRoot != "/host/proc" || config.SysRoot != "/host/sys" || config.FilesystemRoot != "/host/root" {
t.Fatalf("unexpected roots: %+v", config)
}
if config.HostName != "tower" || config.MaxProcesses != 250 {
t.Fatalf("unexpected identity or limits: %+v", config)
}
if config.Service.HeartbeatFile != "/tmp/agent-healthy" {
t.Fatalf("heartbeat file = %q", config.Service.HeartbeatFile)
}
}
+91
View File
@@ -0,0 +1,91 @@
package runtimeconfig
import (
"fmt"
"net"
"os"
"strconv"
"strings"
"time"
)
const (
defaultAPIAddress = "127.0.0.1:8081"
defaultShutdown = 10 * time.Second
// defaultHeartbeatFile matches deploy/healthcheck-heartbeat.sh. See
// docs/operations/WORKER_AGENT_HEALTHCHECK_CONTRACT.md.
defaultHeartbeatFile = "/tmp/healthy"
)
// ServiceConfig contains only foundation runtime settings. Infrastructure credentials and
// datasource settings are deliberately not accepted by the service skeletons yet.
type ServiceConfig struct {
ServiceName string
ListenAddress string
ShutdownAfter time.Duration
// HeartbeatFile is the liveness file a loop-driven service updates after
// every completed iteration, read by the container healthcheck.
HeartbeatFile string
}
func Load(serviceName string) (ServiceConfig, error) {
if serviceName == "" {
return ServiceConfig{}, fmt.Errorf("service name is required")
}
config := ServiceConfig{ServiceName: serviceName, ShutdownAfter: defaultShutdown}
envPrefix := "PULSE_" + strings.ToUpper(serviceName)
if serviceName == "api" {
config.ListenAddress = valueOrDefault("PULSE_API_ADDR", defaultAPIAddress)
} else {
config.ListenAddress = valueOrDefault(envPrefix+"_ADDR", "")
}
if raw := os.Getenv(envPrefix + "_SHUTDOWN_TIMEOUT"); raw != "" {
duration, err := time.ParseDuration(raw)
if err != nil || duration <= 0 || duration > time.Minute {
return ServiceConfig{}, fmt.Errorf("%s_SHUTDOWN_TIMEOUT must be a duration between 1ns and 1m", envPrefix)
}
config.ShutdownAfter = duration
}
if config.ListenAddress != "" {
if err := validateListenAddress(config.ListenAddress); err != nil {
return ServiceConfig{}, fmt.Errorf("%s_ADDR: %w", envPrefix, err)
}
}
config.HeartbeatFile = valueOrDefault("PULSE_HEARTBEAT_FILE", defaultHeartbeatFile)
if err := validateHeartbeatFile(config.HeartbeatFile); err != nil {
return ServiceConfig{}, fmt.Errorf("PULSE_HEARTBEAT_FILE: %w", err)
}
return config, nil
}
// validateHeartbeatFile keeps the heartbeat path an ordinary absolute file path
// so a misconfigured value fails at startup instead of silently never being
// written, which the healthcheck would eventually punish as a hang.
func validateHeartbeatFile(path string) error {
if !strings.HasPrefix(path, "/") || len(path) > 255 || strings.ContainsAny(path, "\x00\r\n") || strings.HasSuffix(path, "/") {
return fmt.Errorf("must be an absolute file path of at most 255 characters")
}
return nil
}
func valueOrDefault(key, fallback string) string {
if value := strings.TrimSpace(os.Getenv(key)); value != "" {
return value
}
return fallback
}
func validateListenAddress(address string) error {
host, portText, err := net.SplitHostPort(address)
if err != nil || host == "" {
return fmt.Errorf("must be host:port")
}
port, err := strconv.Atoi(portText)
if err != nil || port < 1 || port > 65535 {
return fmt.Errorf("port must be an integer between 1 and 65535")
}
return nil
}
func parseInt(value string) (int, error) { return strconv.Atoi(value) }
+28
View File
@@ -0,0 +1,28 @@
package runtimeconfig
import "testing"
func TestLoadRejectsInvalidAPIAddress(t *testing.T) {
t.Setenv("PULSE_API_ADDR", "not-an-address")
if _, err := Load("api"); err == nil {
t.Fatal("expected invalid address error")
}
}
func TestLoadRejectsInvalidShutdownTimeout(t *testing.T) {
t.Setenv("PULSE_WORKER_SHUTDOWN_TIMEOUT", "2h")
if _, err := Load("worker"); err == nil {
t.Fatal("expected invalid timeout error")
}
}
func TestLoadUsesSafeDefaults(t *testing.T) {
t.Setenv("PULSE_API_ADDR", "")
config, err := Load("api")
if err != nil {
t.Fatalf("Load returned error: %v", err)
}
if config.ListenAddress != defaultAPIAddress || config.ShutdownAfter != defaultShutdown {
t.Fatalf("unexpected defaults: %#v", config)
}
}