154 lines
4.9 KiB
Go
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
|
|
}
|