154 lines
4.9 KiB
Go

package config
import (
"fmt"
"os"
"gopkg.in/yaml.v3"
)
type inventoryFragment struct {
SSHUser *string `yaml:"ssh_user"`
SSHPort *int `yaml:"ssh_port"`
SSHConfigFile *string `yaml:"ssh_config_file"`
SSHIdentityFile *string `yaml:"ssh_identity_file"`
SSHNodeHosts *map[string]string `yaml:"ssh_node_hosts"`
SSHNodeUsers *map[string]string `yaml:"ssh_node_users"`
SSHManagedNodes *[]string `yaml:"ssh_managed_nodes"`
ControlPlanes *[]string `yaml:"control_planes"`
Workers *[]string `yaml:"workers"`
Startup *struct {
RequiredNodeLabels *map[string]map[string]string `yaml:"required_node_labels"`
RequiredNodeLabelsMode *string `yaml:"required_node_labels_mode"`
IgnoreUnavailableNodes *[]string `yaml:"ignore_unavailable_nodes"`
} `yaml:"startup"`
}
// Load runs one orchestration or CLI step.
// Signature: Load(path string) (Config, error).
// Why: keeps behavior explicit so startup/shutdown workflows remain maintainable as services evolve.
func Load(path string) (Config, error) {
return LoadWithFragments(path)
}
// LoadWithFragments runs one orchestration or CLI step.
// Signature: LoadWithFragments(path string, fragments ...string) (Config, error).
// Why: Terraform can generate a narrow inventory fragment while Ananke keeps
// loading the ordinary host runtime config for imperative recovery behavior.
func LoadWithFragments(path string, fragments ...string) (Config, error) {
cfg := defaults()
b, err := os.ReadFile(path)
if err != nil {
return Config{}, fmt.Errorf("read config %s: %w", path, err)
}
startupProbeRepairConfigured, err := yamlPathExists(b, "startup", "critical_service_startup_probe_repair")
if err != nil {
return Config{}, fmt.Errorf("decode config %s: %w", path, err)
}
if err := yaml.Unmarshal(b, &cfg); err != nil {
return Config{}, fmt.Errorf("decode config %s: %w", path, err)
}
for _, fragment := range fragments {
fb, err := os.ReadFile(fragment)
if err != nil {
return Config{}, fmt.Errorf("read config fragment %s: %w", fragment, err)
}
if err := applyInventoryFragment(fb, &cfg); err != nil {
return Config{}, fmt.Errorf("decode config fragment %s: %w", fragment, err)
}
}
cfg.applyDefaults()
if !startupProbeRepairConfigured {
cfg.Startup.CriticalServiceStartupProbeRepair = defaults().Startup.CriticalServiceStartupProbeRepair
}
if err := cfg.Validate(); err != nil {
return Config{}, err
}
return cfg, nil
}
// applyInventoryFragment runs one orchestration or CLI step.
// Signature: applyInventoryFragment(raw []byte, cfg *Config) error.
// Why: restricts generated Terraform fragments to declarative inventory fields
// so they cannot accidentally take over Ananke's recovery behavior.
func applyInventoryFragment(raw []byte, cfg *Config) error {
var fragment inventoryFragment
if err := yaml.Unmarshal(raw, &fragment); err != nil {
return err
}
if fragment.SSHUser != nil {
cfg.SSHUser = *fragment.SSHUser
}
if fragment.SSHPort != nil {
cfg.SSHPort = *fragment.SSHPort
}
if fragment.SSHConfigFile != nil {
cfg.SSHConfigFile = *fragment.SSHConfigFile
}
if fragment.SSHIdentityFile != nil {
cfg.SSHIdentityFile = *fragment.SSHIdentityFile
}
if fragment.SSHNodeHosts != nil {
cfg.SSHNodeHosts = *fragment.SSHNodeHosts
}
if fragment.SSHNodeUsers != nil {
cfg.SSHNodeUsers = *fragment.SSHNodeUsers
}
if fragment.SSHManagedNodes != nil {
cfg.SSHManagedNodes = *fragment.SSHManagedNodes
}
if fragment.ControlPlanes != nil {
cfg.ControlPlanes = *fragment.ControlPlanes
}
if fragment.Workers != nil {
cfg.Workers = *fragment.Workers
}
if fragment.Startup == nil {
return nil
}
if fragment.Startup.RequiredNodeLabels != nil {
cfg.Startup.RequiredNodeLabels = *fragment.Startup.RequiredNodeLabels
}
if fragment.Startup.RequiredNodeLabelsMode != nil {
cfg.Startup.RequiredNodeLabelsMode = *fragment.Startup.RequiredNodeLabelsMode
}
if fragment.Startup.IgnoreUnavailableNodes != nil {
cfg.Startup.IgnoreUnavailableNodes = *fragment.Startup.IgnoreUnavailableNodes
}
return nil
}
// yamlPathExists runs one orchestration or CLI step.
// Signature: yamlPathExists(raw []byte, path ...string) (bool, error).
// Why: selected boolean defaults need to distinguish an omitted YAML key from an
// explicit false value after nested startup mappings are decoded.
func yamlPathExists(raw []byte, path ...string) (bool, error) {
var root yaml.Node
if err := yaml.Unmarshal(raw, &root); err != nil {
return false, err
}
node := &root
if node.Kind == yaml.DocumentNode && len(node.Content) > 0 {
node = node.Content[0]
}
for _, key := range path {
if node.Kind != yaml.MappingNode {
return false, nil
}
found := false
for i := 0; i+1 < len(node.Content); i += 2 {
if node.Content[i].Value == key {
node = node.Content[i+1]
found = true
break
}
}
if !found {
return false, nil
}
}
return true, nil
}