first commit
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user