// Package config loads the agent's configuration from environment variables. package config import ( "encoding/json" "errors" "fmt" "os" "path/filepath" "regexp" "strings" "time" ) var masterKeyPattern = regexp.MustCompile(`^sha256:[0-9a-f]{64}$`) // Config holds the agent's runtime configuration. type Config struct { MasterURL string // MASTER_URL, wss://host:port/agent MasterKey string // MASTER_KEY, fingerprint shown in the master UI Token string // AGENT_TOKEN, shown once when the agent was added SelfID string // SELF_ID, defaults to the hostname (= container ID prefix) LogLevel string // LOG_LEVEL, default info // UpdateTimeout bounds one update job (UPDATE_TIMEOUT, default 5m). UpdateTimeout time.Duration // DockerConfig is the directory holding config.json with registry // credentials (DOCKER_CONFIG). Empty means anonymous registry access. DockerConfig string } // defaultUpdateTimeout is used when UPDATE_TIMEOUT is unset. const defaultUpdateTimeout = 5 * time.Minute // Load reads and validates the agent configuration. It fails fast so a // misconfigured agent never starts dialling. func Load() (*Config, error) { cfg := &Config{ MasterURL: os.Getenv("MASTER_URL"), MasterKey: os.Getenv("MASTER_KEY"), Token: os.Getenv("AGENT_TOKEN"), SelfID: os.Getenv("SELF_ID"), LogLevel: os.Getenv("LOG_LEVEL"), UpdateTimeout: defaultUpdateTimeout, DockerConfig: os.Getenv("DOCKER_CONFIG"), } switch { case cfg.MasterURL == "": return nil, errors.New("MASTER_URL is required") case !strings.HasPrefix(cfg.MasterURL, "wss://"): return nil, errors.New("MASTER_URL must start with wss://") case cfg.MasterKey == "": return nil, errors.New("MASTER_KEY is required") case !masterKeyPattern.MatchString(cfg.MasterKey): return nil, errors.New("MASTER_KEY must look like sha256:<64 hex>") case cfg.Token == "": return nil, errors.New("AGENT_TOKEN is required") } if v := os.Getenv("UPDATE_TIMEOUT"); v != "" { d, err := time.ParseDuration(v) if err != nil || d <= 0 { return nil, fmt.Errorf("UPDATE_TIMEOUT must be a positive duration such as 5m, got %q", v) } cfg.UpdateTimeout = d } if cfg.DockerConfig != "" { if err := checkDockerConfig(cfg.DockerConfig); err != nil { return nil, err } } if cfg.SelfID == "" { cfg.SelfID, _ = os.Hostname() } if cfg.LogLevel == "" { cfg.LogLevel = "info" } return cfg, nil } // checkDockerConfig fails when DOCKER_CONFIG is set but its config.json is // missing or not JSON, so a typo does not silently turn into anonymous pulls. func checkDockerConfig(dir string) error { path := filepath.Join(dir, "config.json") raw, err := os.ReadFile(path) if err != nil { return fmt.Errorf("DOCKER_CONFIG: read %s: %w", path, err) } if !json.Valid(raw) { return fmt.Errorf("DOCKER_CONFIG: %s is not valid JSON", path) } return nil }