159 lines
4.1 KiB
Go
159 lines
4.1 KiB
Go
package config
|
|
|
|
import (
|
|
"bufio"
|
|
"errors"
|
|
"fmt"
|
|
"net/url"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
DefaultHeartbeatIntervalSeconds = 30
|
|
MinHeartbeatIntervalSeconds = 5
|
|
MaxHeartbeatIntervalSeconds = 3600
|
|
)
|
|
|
|
type Config struct {
|
|
PanelURL string
|
|
AgentID string
|
|
AgentSecret string
|
|
HeartbeatInterval time.Duration
|
|
RequestTimeout time.Duration
|
|
HeartbeatIntervalSeconds int
|
|
RequestTimeoutSeconds int
|
|
}
|
|
|
|
func Load(path string) (Config, error) {
|
|
values := map[string]string{}
|
|
if path != "" {
|
|
fileValues, err := readKeyValueFile(path)
|
|
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
|
return Config{}, err
|
|
}
|
|
for key, value := range fileValues {
|
|
values[key] = value
|
|
}
|
|
}
|
|
|
|
overlayEnv(values, "panel_url", "INCUDAL_PANEL_URL")
|
|
overlayEnv(values, "agent_id", "INCUDAL_AGENT_ID")
|
|
overlayEnv(values, "agent_secret", "INCUDAL_AGENT_SECRET")
|
|
overlayEnv(values, "heartbeat_interval_seconds", "INCUDAL_HEARTBEAT_INTERVAL_SECONDS")
|
|
overlayEnv(values, "request_timeout_seconds", "INCUDAL_REQUEST_TIMEOUT_SECONDS")
|
|
|
|
heartbeatSeconds := clampInt(
|
|
parsePositiveInt(values["heartbeat_interval_seconds"], DefaultHeartbeatIntervalSeconds),
|
|
MinHeartbeatIntervalSeconds,
|
|
MaxHeartbeatIntervalSeconds,
|
|
)
|
|
timeoutSeconds := parsePositiveInt(values["request_timeout_seconds"], 10)
|
|
cfg := Config{
|
|
PanelURL: strings.TrimRight(values["panel_url"], "/"),
|
|
AgentID: values["agent_id"],
|
|
AgentSecret: values["agent_secret"],
|
|
HeartbeatIntervalSeconds: heartbeatSeconds,
|
|
RequestTimeoutSeconds: timeoutSeconds,
|
|
HeartbeatInterval: time.Duration(heartbeatSeconds) * time.Second,
|
|
RequestTimeout: time.Duration(timeoutSeconds) * time.Second,
|
|
}
|
|
|
|
if err := cfg.Validate(); err != nil {
|
|
return Config{}, err
|
|
}
|
|
return cfg, nil
|
|
}
|
|
|
|
func (cfg Config) Validate() error {
|
|
if cfg.PanelURL == "" {
|
|
return errors.New("panel_url is required")
|
|
}
|
|
parsed, err := url.Parse(cfg.PanelURL)
|
|
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
|
|
return fmt.Errorf("panel_url is invalid: %s", cfg.PanelURL)
|
|
}
|
|
if parsed.Scheme != "http" && parsed.Scheme != "https" {
|
|
return fmt.Errorf("panel_url scheme must be http or https: %s", parsed.Scheme)
|
|
}
|
|
if cfg.AgentID == "" {
|
|
return errors.New("agent_id is required")
|
|
}
|
|
if cfg.AgentSecret == "" {
|
|
return errors.New("agent_secret is required")
|
|
}
|
|
if cfg.HeartbeatInterval < time.Duration(MinHeartbeatIntervalSeconds)*time.Second {
|
|
return fmt.Errorf("heartbeat interval must be at least %d seconds", MinHeartbeatIntervalSeconds)
|
|
}
|
|
if cfg.RequestTimeout < time.Second {
|
|
return errors.New("request timeout must be at least 1 second")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func readKeyValueFile(path string) (map[string]string, error) {
|
|
file, err := os.Open(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer file.Close()
|
|
|
|
values := map[string]string{}
|
|
scanner := bufio.NewScanner(file)
|
|
lineNumber := 0
|
|
for scanner.Scan() {
|
|
lineNumber++
|
|
line := strings.TrimSpace(scanner.Text())
|
|
if line == "" || strings.HasPrefix(line, "#") {
|
|
continue
|
|
}
|
|
key, value, ok := strings.Cut(line, ":")
|
|
if !ok {
|
|
return nil, fmt.Errorf("invalid config line %d: expected key: value", lineNumber)
|
|
}
|
|
key = strings.TrimSpace(key)
|
|
value = trimConfigValue(value)
|
|
if key == "" {
|
|
return nil, fmt.Errorf("invalid config line %d: empty key", lineNumber)
|
|
}
|
|
values[key] = value
|
|
}
|
|
if err := scanner.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return values, nil
|
|
}
|
|
|
|
func trimConfigValue(value string) string {
|
|
trimmed := strings.TrimSpace(value)
|
|
trimmed = strings.Trim(trimmed, `"`)
|
|
trimmed = strings.Trim(trimmed, `'`)
|
|
return trimmed
|
|
}
|
|
|
|
func overlayEnv(values map[string]string, key string, envName string) {
|
|
if value := strings.TrimSpace(os.Getenv(envName)); value != "" {
|
|
values[key] = value
|
|
}
|
|
}
|
|
|
|
func parsePositiveInt(value string, fallback int) int {
|
|
parsed, err := strconv.Atoi(strings.TrimSpace(value))
|
|
if err != nil || parsed <= 0 {
|
|
return fallback
|
|
}
|
|
return parsed
|
|
}
|
|
|
|
func clampInt(value int, min int, max int) int {
|
|
if value < min {
|
|
return min
|
|
}
|
|
if value > max {
|
|
return max
|
|
}
|
|
return value
|
|
}
|