Add DNSRegistrar that creates/updates/deletes A records in MCNS during deploy and stop. When a service has routes, the agent ensures an A record exists in the configured zone pointing to the node's address. On stop, the record is removed. - Add MCNSConfig to agent config (server_url, ca_cert, token_path, zone, node_addr) with defaults and env overrides - Add DNSRegistrar (internal/agent/dns.go): REST client for MCNS record CRUD, nil-receiver safe - Wire into deploy flow (EnsureRecord after route registration) - Wire into stop flow (RemoveRecord before container stop) - 7 new tests, make all passes with 0 issues Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
275 lines
7.7 KiB
Go
275 lines
7.7 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"time"
|
|
|
|
toml "github.com/pelletier/go-toml/v2"
|
|
)
|
|
|
|
// AgentConfig is the configuration for the mcp-agent daemon.
|
|
type AgentConfig struct {
|
|
Server ServerConfig `toml:"server"`
|
|
Database DatabaseConfig `toml:"database"`
|
|
MCIAS MCIASConfig `toml:"mcias"`
|
|
Agent AgentSettings `toml:"agent"`
|
|
MCProxy MCProxyConfig `toml:"mcproxy"`
|
|
Metacrypt MetacryptConfig `toml:"metacrypt"`
|
|
MCNS MCNSConfig `toml:"mcns"`
|
|
Monitor MonitorConfig `toml:"monitor"`
|
|
Log LogConfig `toml:"log"`
|
|
}
|
|
|
|
// MetacryptConfig holds the Metacrypt CA integration settings for
|
|
// automated TLS cert provisioning. If ServerURL is empty, cert
|
|
// provisioning is disabled.
|
|
type MetacryptConfig struct {
|
|
// ServerURL is the Metacrypt API base URL (e.g. "https://metacrypt:8443").
|
|
ServerURL string `toml:"server_url"`
|
|
|
|
// CACert is the path to the CA certificate for verifying Metacrypt's TLS.
|
|
CACert string `toml:"ca_cert"`
|
|
|
|
// Mount is the CA engine mount name. Defaults to "pki".
|
|
Mount string `toml:"mount"`
|
|
|
|
// Issuer is the intermediate CA issuer name. Defaults to "infra".
|
|
Issuer string `toml:"issuer"`
|
|
|
|
// TokenPath is the path to the MCIAS service token file.
|
|
TokenPath string `toml:"token_path"`
|
|
}
|
|
|
|
// MCNSConfig holds the MCNS DNS integration settings for automated
|
|
// DNS record registration. If ServerURL is empty, DNS registration
|
|
// is disabled.
|
|
type MCNSConfig struct {
|
|
// ServerURL is the MCNS API base URL (e.g. "https://localhost:28443").
|
|
ServerURL string `toml:"server_url"`
|
|
|
|
// CACert is the path to the CA certificate for verifying MCNS's TLS.
|
|
CACert string `toml:"ca_cert"`
|
|
|
|
// TokenPath is the path to the MCIAS service token file.
|
|
TokenPath string `toml:"token_path"`
|
|
|
|
// Zone is the DNS zone for service records. Defaults to "svc.mcp.metacircular.net".
|
|
Zone string `toml:"zone"`
|
|
|
|
// NodeAddr is the IP address to register as the A record value.
|
|
NodeAddr string `toml:"node_addr"`
|
|
}
|
|
|
|
// MCProxyConfig holds the mc-proxy connection settings.
|
|
type MCProxyConfig struct {
|
|
// Socket is the path to the mc-proxy gRPC admin API Unix socket.
|
|
// If empty, route registration is disabled.
|
|
Socket string `toml:"socket"`
|
|
|
|
// CertDir is the directory containing TLS certificates for routes.
|
|
// Convention: <service>.pem and <service>.key per service.
|
|
// Defaults to /srv/mc-proxy/certs.
|
|
CertDir string `toml:"cert_dir"`
|
|
}
|
|
|
|
// ServerConfig holds gRPC server listen address and TLS paths.
|
|
type ServerConfig struct {
|
|
GRPCAddr string `toml:"grpc_addr"`
|
|
TLSCert string `toml:"tls_cert"`
|
|
TLSKey string `toml:"tls_key"`
|
|
}
|
|
|
|
// DatabaseConfig holds the SQLite database path.
|
|
type DatabaseConfig struct {
|
|
Path string `toml:"path"`
|
|
}
|
|
|
|
// AgentSettings holds agent-specific settings.
|
|
type AgentSettings struct {
|
|
NodeName string `toml:"node_name"`
|
|
ContainerRuntime string `toml:"container_runtime"`
|
|
}
|
|
|
|
// MonitorConfig holds monitoring loop parameters.
|
|
type MonitorConfig struct {
|
|
Interval Duration `toml:"interval"`
|
|
AlertCommand []string `toml:"alert_command"`
|
|
Cooldown Duration `toml:"cooldown"`
|
|
FlapThreshold int `toml:"flap_threshold"`
|
|
FlapWindow Duration `toml:"flap_window"`
|
|
Retention Duration `toml:"retention"`
|
|
}
|
|
|
|
// LogConfig holds logging settings.
|
|
type LogConfig struct {
|
|
Level string `toml:"level"`
|
|
}
|
|
|
|
// Duration wraps time.Duration to support TOML string unmarshaling.
|
|
// It accepts Go duration strings (e.g. "60s", "15m", "24h") plus a
|
|
// "d" suffix for days (e.g. "30d" becomes 30*24h).
|
|
type Duration struct {
|
|
time.Duration
|
|
}
|
|
|
|
// UnmarshalText implements encoding.TextUnmarshaler for TOML parsing.
|
|
func (d *Duration) UnmarshalText(text []byte) error {
|
|
s := string(text)
|
|
if s == "" {
|
|
d.Duration = 0
|
|
return nil
|
|
}
|
|
|
|
// Handle "d" suffix for days.
|
|
if len(s) > 1 && s[len(s)-1] == 'd' {
|
|
var days float64
|
|
if _, err := fmt.Sscanf(s[:len(s)-1], "%f", &days); err != nil {
|
|
return fmt.Errorf("parse duration %q: %w", s, err)
|
|
}
|
|
d.Duration = time.Duration(days * float64(24*time.Hour))
|
|
return nil
|
|
}
|
|
|
|
dur, err := time.ParseDuration(s)
|
|
if err != nil {
|
|
return fmt.Errorf("parse duration %q: %w", s, err)
|
|
}
|
|
d.Duration = dur
|
|
return nil
|
|
}
|
|
|
|
// MarshalText implements encoding.TextMarshaler for TOML serialization.
|
|
func (d Duration) MarshalText() ([]byte, error) {
|
|
return []byte(d.String()), nil
|
|
}
|
|
|
|
// LoadAgentConfig reads and validates an agent configuration file.
|
|
// Environment variables override file values for select fields.
|
|
func LoadAgentConfig(path string) (*AgentConfig, error) {
|
|
data, err := os.ReadFile(path) //nolint:gosec // config path from trusted CLI flag
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read config %q: %w", path, err)
|
|
}
|
|
|
|
var cfg AgentConfig
|
|
if err := toml.Unmarshal(data, &cfg); err != nil {
|
|
return nil, fmt.Errorf("parse config %q: %w", path, err)
|
|
}
|
|
|
|
applyAgentDefaults(&cfg)
|
|
applyAgentEnvOverrides(&cfg)
|
|
|
|
if err := validateAgentConfig(&cfg); err != nil {
|
|
return nil, fmt.Errorf("validate config: %w", err)
|
|
}
|
|
|
|
return &cfg, nil
|
|
}
|
|
|
|
func applyAgentDefaults(cfg *AgentConfig) {
|
|
if cfg.Monitor.Interval.Duration == 0 {
|
|
cfg.Monitor.Interval.Duration = 60 * time.Second
|
|
}
|
|
if cfg.Monitor.Cooldown.Duration == 0 {
|
|
cfg.Monitor.Cooldown.Duration = 15 * time.Minute
|
|
}
|
|
if cfg.Monitor.FlapThreshold == 0 {
|
|
cfg.Monitor.FlapThreshold = 3
|
|
}
|
|
if cfg.Monitor.FlapWindow.Duration == 0 {
|
|
cfg.Monitor.FlapWindow.Duration = 10 * time.Minute
|
|
}
|
|
if cfg.Monitor.Retention.Duration == 0 {
|
|
cfg.Monitor.Retention.Duration = 30 * 24 * time.Hour // 30 days
|
|
}
|
|
if cfg.Log.Level == "" {
|
|
cfg.Log.Level = "info"
|
|
}
|
|
if cfg.Agent.ContainerRuntime == "" {
|
|
cfg.Agent.ContainerRuntime = "podman"
|
|
}
|
|
if cfg.MCProxy.CertDir == "" {
|
|
cfg.MCProxy.CertDir = "/srv/mc-proxy/certs"
|
|
}
|
|
if cfg.Metacrypt.Mount == "" {
|
|
cfg.Metacrypt.Mount = "pki"
|
|
}
|
|
if cfg.Metacrypt.Issuer == "" {
|
|
cfg.Metacrypt.Issuer = "infra"
|
|
}
|
|
if cfg.MCNS.Zone == "" {
|
|
cfg.MCNS.Zone = "svc.mcp.metacircular.net"
|
|
}
|
|
}
|
|
|
|
func applyAgentEnvOverrides(cfg *AgentConfig) {
|
|
if v := os.Getenv("MCP_AGENT_SERVER_GRPC_ADDR"); v != "" {
|
|
cfg.Server.GRPCAddr = v
|
|
}
|
|
if v := os.Getenv("MCP_AGENT_SERVER_TLS_CERT"); v != "" {
|
|
cfg.Server.TLSCert = v
|
|
}
|
|
if v := os.Getenv("MCP_AGENT_SERVER_TLS_KEY"); v != "" {
|
|
cfg.Server.TLSKey = v
|
|
}
|
|
if v := os.Getenv("MCP_AGENT_DATABASE_PATH"); v != "" {
|
|
cfg.Database.Path = v
|
|
}
|
|
if v := os.Getenv("MCP_AGENT_NODE_NAME"); v != "" {
|
|
cfg.Agent.NodeName = v
|
|
}
|
|
if v := os.Getenv("MCP_AGENT_CONTAINER_RUNTIME"); v != "" {
|
|
cfg.Agent.ContainerRuntime = v
|
|
}
|
|
if v := os.Getenv("MCP_AGENT_LOG_LEVEL"); v != "" {
|
|
cfg.Log.Level = v
|
|
}
|
|
if v := os.Getenv("MCP_AGENT_MCPROXY_SOCKET"); v != "" {
|
|
cfg.MCProxy.Socket = v
|
|
}
|
|
if v := os.Getenv("MCP_AGENT_MCPROXY_CERT_DIR"); v != "" {
|
|
cfg.MCProxy.CertDir = v
|
|
}
|
|
if v := os.Getenv("MCP_AGENT_METACRYPT_SERVER_URL"); v != "" {
|
|
cfg.Metacrypt.ServerURL = v
|
|
}
|
|
if v := os.Getenv("MCP_AGENT_METACRYPT_TOKEN_PATH"); v != "" {
|
|
cfg.Metacrypt.TokenPath = v
|
|
}
|
|
if v := os.Getenv("MCP_AGENT_MCNS_SERVER_URL"); v != "" {
|
|
cfg.MCNS.ServerURL = v
|
|
}
|
|
if v := os.Getenv("MCP_AGENT_MCNS_TOKEN_PATH"); v != "" {
|
|
cfg.MCNS.TokenPath = v
|
|
}
|
|
if v := os.Getenv("MCP_AGENT_MCNS_NODE_ADDR"); v != "" {
|
|
cfg.MCNS.NodeAddr = v
|
|
}
|
|
}
|
|
|
|
func validateAgentConfig(cfg *AgentConfig) error {
|
|
if cfg.Server.GRPCAddr == "" {
|
|
return fmt.Errorf("server.grpc_addr is required")
|
|
}
|
|
if cfg.Server.TLSCert == "" {
|
|
return fmt.Errorf("server.tls_cert is required")
|
|
}
|
|
if cfg.Server.TLSKey == "" {
|
|
return fmt.Errorf("server.tls_key is required")
|
|
}
|
|
if cfg.Database.Path == "" {
|
|
return fmt.Errorf("database.path is required")
|
|
}
|
|
if cfg.MCIAS.ServerURL == "" {
|
|
return fmt.Errorf("mcias.server_url is required")
|
|
}
|
|
if cfg.MCIAS.ServiceName == "" {
|
|
return fmt.Errorf("mcias.service_name is required")
|
|
}
|
|
if cfg.Agent.NodeName == "" {
|
|
return fmt.Errorf("agent.node_name is required")
|
|
}
|
|
return nil
|
|
}
|