159 lines
4.7 KiB
Python
159 lines
4.7 KiB
Python
"""Configuration loading from TOML with environment variable overrides."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import tomllib
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
|
|
|
|
@dataclass
|
|
class ServerConfig:
|
|
"""[server] section."""
|
|
|
|
socket_path: str = "/run/nix-builder-autoscaler/daemon.sock"
|
|
log_level: str = "info"
|
|
db_path: str = "/var/lib/nix-builder-autoscaler/state.db"
|
|
|
|
|
|
@dataclass
|
|
class AwsConfig:
|
|
"""[aws] section."""
|
|
|
|
region: str = "us-east-1"
|
|
launch_template_id: str = ""
|
|
subnet_ids: list[str] = field(default_factory=list)
|
|
security_group_ids: list[str] = field(default_factory=list)
|
|
instance_profile_arn: str = ""
|
|
|
|
|
|
@dataclass
|
|
class HaproxyConfig:
|
|
"""[haproxy] section."""
|
|
|
|
runtime_socket: str = "/run/haproxy/admin.sock"
|
|
backend: str = "all"
|
|
slot_prefix: str = "slot"
|
|
slot_count: int = 8
|
|
check_ready_up_count: int = 2
|
|
|
|
|
|
@dataclass
|
|
class SystemConfig:
|
|
"""[[systems]] entry for per-architecture capacity policy."""
|
|
|
|
name: str = "x86_64-linux"
|
|
min_slots: int = 0
|
|
max_slots: int = 8
|
|
target_warm_slots: int = 0
|
|
max_leases_per_slot: int = 1
|
|
launch_batch_size: int = 1
|
|
scale_down_idle_seconds: int = 900
|
|
|
|
|
|
@dataclass
|
|
class CapacityConfig:
|
|
"""[capacity] section — global defaults."""
|
|
|
|
default_system: str = "x86_64-linux"
|
|
min_slots: int = 0
|
|
max_slots: int = 8
|
|
target_warm_slots: int = 0
|
|
max_leases_per_slot: int = 1
|
|
reservation_ttl_seconds: int = 1200
|
|
idle_scale_down_seconds: int = 900
|
|
drain_timeout_seconds: int = 120
|
|
launch_timeout_seconds: int = 300
|
|
boot_timeout_seconds: int = 300
|
|
binding_timeout_seconds: int = 180
|
|
terminating_timeout_seconds: int = 300
|
|
|
|
|
|
@dataclass
|
|
class SecurityConfig:
|
|
"""[security] section."""
|
|
|
|
socket_mode: str = "0660"
|
|
socket_owner: str = "buildbot"
|
|
socket_group: str = "buildbot"
|
|
|
|
|
|
@dataclass
|
|
class SchedulerConfig:
|
|
"""[scheduler] section."""
|
|
|
|
tick_seconds: float = 3.0
|
|
reconcile_seconds: float = 15.0
|
|
|
|
|
|
@dataclass
|
|
class AppConfig:
|
|
"""Top-level application configuration."""
|
|
|
|
server: ServerConfig = field(default_factory=ServerConfig)
|
|
aws: AwsConfig = field(default_factory=AwsConfig)
|
|
haproxy: HaproxyConfig = field(default_factory=HaproxyConfig)
|
|
capacity: CapacityConfig = field(default_factory=CapacityConfig)
|
|
security: SecurityConfig = field(default_factory=SecurityConfig)
|
|
scheduler: SchedulerConfig = field(default_factory=SchedulerConfig)
|
|
systems: list[SystemConfig] = field(default_factory=list)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Environment variable overrides
|
|
# ---------------------------------------------------------------------------
|
|
# AUTOSCALER_TAILSCALE_API_TOKEN — Tailscale API token for IP discovery
|
|
# AWS_REGION — override aws.region
|
|
# AWS_ACCESS_KEY_ID — explicit AWS credential
|
|
# AWS_SECRET_ACCESS_KEY — explicit AWS credential
|
|
|
|
|
|
def _apply_env_overrides(cfg: AppConfig) -> None:
|
|
"""Apply environment variable overrides for secrets and region."""
|
|
region = os.environ.get("AWS_REGION")
|
|
if region:
|
|
cfg.aws.region = region
|
|
|
|
|
|
def _build_dataclass(cls: type, data: dict) -> object: # noqa: ANN001
|
|
"""Construct a dataclass from a dict, ignoring unknown keys."""
|
|
valid = {f.name for f in cls.__dataclass_fields__.values()} # type: ignore[attr-defined]
|
|
return cls(**{k: v for k, v in data.items() if k in valid})
|
|
|
|
|
|
def load_config(path: Path) -> AppConfig:
|
|
"""Load configuration from a TOML file.
|
|
|
|
Args:
|
|
path: Path to the TOML config file.
|
|
|
|
Returns:
|
|
Validated AppConfig instance.
|
|
"""
|
|
with open(path, "rb") as f:
|
|
raw = tomllib.load(f)
|
|
|
|
cfg = AppConfig()
|
|
|
|
if "server" in raw:
|
|
cfg.server = _build_dataclass(ServerConfig, raw["server"]) # type: ignore[assignment]
|
|
if "aws" in raw:
|
|
cfg.aws = _build_dataclass(AwsConfig, raw["aws"]) # type: ignore[assignment]
|
|
if "haproxy" in raw:
|
|
cfg.haproxy = _build_dataclass(HaproxyConfig, raw["haproxy"]) # type: ignore[assignment]
|
|
if "capacity" in raw:
|
|
cfg.capacity = _build_dataclass(CapacityConfig, raw["capacity"]) # type: ignore[assignment]
|
|
if "security" in raw:
|
|
cfg.security = _build_dataclass(SecurityConfig, raw["security"]) # type: ignore[assignment]
|
|
if "scheduler" in raw:
|
|
cfg.scheduler = _build_dataclass(SchedulerConfig, raw["scheduler"]) # type: ignore[assignment]
|
|
|
|
if "systems" in raw:
|
|
cfg.systems = list[SystemConfig](
|
|
_build_dataclass(SystemConfig, s) # type: ignore[list-item]
|
|
for s in raw["systems"]
|
|
)
|
|
|
|
_apply_env_overrides(cfg)
|
|
return cfg
|