Files
cloud-ip-validator/internal/config/config.go
T
2026-08-21 07:34:45 +03:00

237 lines
6.7 KiB
Go

// Package config defines the YAML configuration structures for all three
// binaries and loads them from disk. OpenStack credentials are deliberately
// never part of these structs — only the *names* of environment variables
// to read them from — so secrets never land in a config file on disk.
package config
import (
"fmt"
"os"
"gopkg.in/yaml.v3"
)
// ---- control-api ----
type ControlAPI struct {
Server ServerConfig `yaml:"server"`
Database DatabaseConfig `yaml:"database"`
OpenStack OpenStackConfig `yaml:"openstack"`
Orchestrator OrchestratorConfig `yaml:"orchestrator"`
Aggregation AggregationConfig `yaml:"aggregation"`
Validators []ValidatorConfig `yaml:"validators"`
Sites []SiteConfig `yaml:"sites"`
CheckTypes []CheckTypeConfig `yaml:"check_types"`
Targets map[string][]string `yaml:"targets"`
Inbound InboundConfig `yaml:"inbound_checks"`
IPAddresses []string `yaml:"ip_addresses"`
}
type ServerConfig struct {
ListenAddr string `yaml:"listen_addr"`
}
type DatabaseConfig struct {
Path string `yaml:"path"`
}
// OpenStackConfig names the environment variables control-api reads its
// OpenStack admin credential from at startup. Mode "mock" skips all of this
// and uses an in-memory FloatingIPClient instead — used for local dev and
// the offline end-to-end harness.
type OpenStackConfig struct {
Mode string `yaml:"mode"` // "mock" | "real"
AuthURLEnv string `yaml:"auth_url_env"`
TokenEnv string `yaml:"token_env"`
ProjectIDEnv string `yaml:"project_id_env"`
ProjectNameEnv string `yaml:"project_name_env"`
ProjectDomainEnv string `yaml:"project_domain_env"`
RegionEnv string `yaml:"region_env"`
}
type OrchestratorConfig struct {
PollIntervalSeconds int `yaml:"poll_interval_seconds"`
SelfCheckTimeoutSeconds int `yaml:"self_check_timeout_seconds"`
MaxSelfCheckRetries int `yaml:"max_self_check_retries"`
CheckingWindowSeconds int `yaml:"checking_window_seconds"`
MaxRetries int `yaml:"max_retries"`
LeaseTTLSeconds int `yaml:"lease_ttl_seconds"`
HeartbeatTimeoutSeconds int `yaml:"heartbeat_timeout_seconds"`
}
type AggregationConfig struct {
MissingCountsAsFail bool `yaml:"missing_counts_as_fail"`
}
type ValidatorConfig struct {
ValidatorID string `yaml:"validator_id"`
OSPortID string `yaml:"os_port_id"`
}
type SiteConfig struct {
SiteID string `yaml:"site_id"`
Index int `yaml:"index"` // 1, 2, or 3 — maps to ip_queue.siteN_complete
}
// CheckTypeConfig maps a check type (https, icmp, ssh) to the named target
// groups (keys into ControlAPI.Targets) it should run against. This drives
// the egress/outbound checks the validator-agent performs.
type CheckTypeConfig struct {
Name string `yaml:"name"`
Enabled bool `yaml:"enabled"`
Targets []string `yaml:"targets"`
}
type InboundConfig struct {
Ports []int `yaml:"ports"`
ICMP bool `yaml:"icmp"`
}
func LoadControlAPI(path string) (*ControlAPI, error) {
var c ControlAPI
if err := loadYAML(path, &c); err != nil {
return nil, err
}
if c.Server.ListenAddr == "" {
c.Server.ListenAddr = ":8080"
}
if c.Database.Path == "" {
c.Database.Path = "control-api.db"
}
if c.OpenStack.Mode == "" {
c.OpenStack.Mode = "mock"
}
if c.Orchestrator.PollIntervalSeconds == 0 {
c.Orchestrator.PollIntervalSeconds = 5
}
if c.Orchestrator.SelfCheckTimeoutSeconds == 0 {
c.Orchestrator.SelfCheckTimeoutSeconds = 60
}
if c.Orchestrator.MaxSelfCheckRetries == 0 {
c.Orchestrator.MaxSelfCheckRetries = 3
}
if c.Orchestrator.CheckingWindowSeconds == 0 {
c.Orchestrator.CheckingWindowSeconds = 120
}
if c.Orchestrator.MaxRetries == 0 {
c.Orchestrator.MaxRetries = 3
}
if c.Orchestrator.LeaseTTLSeconds == 0 {
c.Orchestrator.LeaseTTLSeconds = 180
}
if c.Orchestrator.HeartbeatTimeoutSeconds == 0 {
c.Orchestrator.HeartbeatTimeoutSeconds = 30
}
return &c, nil
}
// ---- validator-agent ----
type ValidatorAgent struct {
ValidatorID string `yaml:"validator_id"`
ControlAPIURL string `yaml:"control_api_url"`
PollIntervalSeconds int `yaml:"poll_interval_seconds"`
SelfCheck SelfCheckCfg `yaml:"self_check"`
Checks AgentChecks `yaml:"checks"`
}
type SelfCheckCfg struct {
TimeoutSeconds int `yaml:"timeout_seconds"`
}
type AgentChecks struct {
HTTPSTimeoutSeconds int `yaml:"https_timeout_seconds"`
ICMPTimeoutSeconds int `yaml:"icmp_timeout_seconds"`
ICMPCount int `yaml:"icmp_count"`
SSH SSHCfg `yaml:"ssh"`
}
type SSHCfg struct {
Enabled bool `yaml:"enabled"`
TimeoutSeconds int `yaml:"timeout_seconds"`
}
func LoadValidatorAgent(path string) (*ValidatorAgent, error) {
var c ValidatorAgent
if err := loadYAML(path, &c); err != nil {
return nil, err
}
if c.PollIntervalSeconds == 0 {
c.PollIntervalSeconds = 5
}
if c.SelfCheck.TimeoutSeconds == 0 {
c.SelfCheck.TimeoutSeconds = 10
}
if c.Checks.HTTPSTimeoutSeconds == 0 {
c.Checks.HTTPSTimeoutSeconds = 10
}
if c.Checks.ICMPTimeoutSeconds == 0 {
c.Checks.ICMPTimeoutSeconds = 5
}
if c.Checks.ICMPCount == 0 {
c.Checks.ICMPCount = 3
}
if c.Checks.SSH.TimeoutSeconds == 0 {
c.Checks.SSH.TimeoutSeconds = 5
}
if c.ValidatorID == "" {
return nil, fmt.Errorf("validator_id is required")
}
if c.ControlAPIURL == "" {
return nil, fmt.Errorf("control_api_url is required")
}
return &c, nil
}
// ---- prober ----
type Prober struct {
SiteID string `yaml:"site_id"`
ControlAPIURL string `yaml:"control_api_url"`
PollIntervalSeconds int `yaml:"poll_interval_seconds"`
Checks ProberChecks `yaml:"checks"`
}
type ProberChecks struct {
TCPTimeoutSeconds int `yaml:"tcp_timeout_seconds"`
ICMPTimeoutSeconds int `yaml:"icmp_timeout_seconds"`
ICMPCount int `yaml:"icmp_count"`
}
func LoadProber(path string) (*Prober, error) {
var c Prober
if err := loadYAML(path, &c); err != nil {
return nil, err
}
if c.PollIntervalSeconds == 0 {
c.PollIntervalSeconds = 5
}
if c.Checks.TCPTimeoutSeconds == 0 {
c.Checks.TCPTimeoutSeconds = 5
}
if c.Checks.ICMPTimeoutSeconds == 0 {
c.Checks.ICMPTimeoutSeconds = 5
}
if c.Checks.ICMPCount == 0 {
c.Checks.ICMPCount = 3
}
if c.SiteID == "" {
return nil, fmt.Errorf("site_id is required")
}
if c.ControlAPIURL == "" {
return nil, fmt.Errorf("control_api_url is required")
}
return &c, nil
}
func loadYAML(path string, out interface{}) error {
data, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("read config %s: %w", path, err)
}
if err := yaml.Unmarshal(data, out); err != nil {
return fmt.Errorf("parse config %s: %w", path, err)
}
return nil
}