94 lines
2.8 KiB
Go
94 lines
2.8 KiB
Go
// 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
|
|
}
|