Files
ITWorx-Pulse-Public/internal/runtimeconfig/config.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

92 lines
3.0 KiB
Go

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