Files
mcp/internal/config/cli.go
Kyle Isom f9f6f339f4 Add multi-address fallback for node connectivity
NodeConfig and MasterNodeConfig gain an optional addresses[] field
for fallback addresses tried in order after the primary address.
Provides resilience when Tailscale DNS is down or a node is only
reachable via LAN.

- dialAgentMulti: tries each address with a 3s health check, returns
  first success
- forEachNode: uses multi-address dialing
- AgentPool.AddNodeMulti: master tries all addresses when connecting
- AllAddresses(): deduplicates primary + fallback addresses

Config example:
  [[nodes]]
  name = "rift"
  address = "rift.scylla-hammerhead.ts.net:9444"
  addresses = ["100.95.252.120:9444", "192.168.88.181:9444"]

Existing configs without addresses[] work unchanged.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 09:45:50 -07:00

145 lines
4.0 KiB
Go

package config
import (
"fmt"
"os"
"strings"
toml "github.com/pelletier/go-toml/v2"
)
// CLIConfig is the configuration for the mcp CLI binary.
type CLIConfig struct {
Services ServicesConfig `toml:"services"`
Build BuildConfig `toml:"build"`
MCIAS MCIASConfig `toml:"mcias"`
Auth AuthConfig `toml:"auth"`
Nodes []NodeConfig `toml:"nodes"`
Master *CLIMasterConfig `toml:"master,omitempty"`
}
// CLIMasterConfig holds the optional master connection settings.
// When configured, deploy/undeploy/status go through the master
// instead of directly to agents.
type CLIMasterConfig struct {
Address string `toml:"address"` // master gRPC address (e.g. "100.95.252.120:9555")
}
// BuildConfig holds settings for building container images.
type BuildConfig struct {
Workspace string `toml:"workspace"`
}
// ServicesConfig defines where service definition files live.
type ServicesConfig struct {
Dir string `toml:"dir"`
}
// MCIASConfig holds MCIAS connection settings, shared by CLI and agent.
type MCIASConfig struct {
ServerURL string `toml:"server_url"`
CACert string `toml:"ca_cert"`
ServiceName string `toml:"service_name"`
}
// AuthConfig holds authentication settings for the CLI.
type AuthConfig struct {
TokenPath string `toml:"token_path"`
Username string `toml:"username"` // optional, for unattended operation
PasswordFile string `toml:"password_file"` // optional, for unattended operation
}
// NodeConfig defines a managed node that the CLI connects to.
// Address is the primary address. Addresses is an optional list of
// fallback addresses tried in order if the primary fails. This
// provides resilience when Tailscale DNS is down or a node is
// reachable via LAN but not Tailnet.
type NodeConfig struct {
Name string `toml:"name"`
Address string `toml:"address"`
Addresses []string `toml:"addresses,omitempty"`
}
// AllAddresses returns the node's primary address followed by any
// fallback addresses, deduplicated.
func (n NodeConfig) AllAddresses() []string {
seen := make(map[string]bool)
var addrs []string
for _, a := range append([]string{n.Address}, n.Addresses...) {
if a != "" && !seen[a] {
seen[a] = true
addrs = append(addrs, a)
}
}
return addrs
}
// LoadCLIConfig reads and validates a CLI configuration file.
// Environment variables override file values for select fields.
func LoadCLIConfig(path string) (*CLIConfig, 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 CLIConfig
if err := toml.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("parse config %q: %w", path, err)
}
applyCLIEnvOverrides(&cfg)
if err := validateCLIConfig(&cfg); err != nil {
return nil, fmt.Errorf("validate config: %w", err)
}
return &cfg, nil
}
func applyCLIEnvOverrides(cfg *CLIConfig) {
if v := os.Getenv("MCP_SERVICES_DIR"); v != "" {
cfg.Services.Dir = v
}
if v := os.Getenv("MCP_BUILD_WORKSPACE"); v != "" {
cfg.Build.Workspace = v
}
if v := os.Getenv("MCP_MCIAS_SERVER_URL"); v != "" {
cfg.MCIAS.ServerURL = v
}
if v := os.Getenv("MCP_MCIAS_CA_CERT"); v != "" {
cfg.MCIAS.CACert = v
}
if v := os.Getenv("MCP_MCIAS_SERVICE_NAME"); v != "" {
cfg.MCIAS.ServiceName = v
}
if v := os.Getenv("MCP_AUTH_TOKEN_PATH"); v != "" {
cfg.Auth.TokenPath = v
}
}
func validateCLIConfig(cfg *CLIConfig) error {
if cfg.Services.Dir == "" {
return fmt.Errorf("services.dir 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.Auth.TokenPath == "" {
return fmt.Errorf("auth.token_path is required")
}
// Expand ~ in workspace path.
if strings.HasPrefix(cfg.Build.Workspace, "~/") {
home, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("expand workspace path: %w", err)
}
cfg.Build.Workspace = home + cfg.Build.Workspace[1:]
}
return nil
}