// Package config читает файл настроек goherence (по умолчанию // goherence.yml в текущей директории) — аналог ansible.cfg, но с // явными Go-типами вместо INI-секций. CLI-флаги имеют приоритет над // файлом: Load возвращает Config с дефолтами, а cmd/goherence поверх // него применяет флаги, которые пользователь указал явно. package config import ( "fmt" "os" "time" "gopkg.in/yaml.v3" ) // Config — то, что чаще всего настраивают в ansible.cfg: параллелизм, // become, факты, таймауты, дефолтные SSH-параметры. type Config struct { // Forks — сколько хостов внутри одного батча обрабатывать // одновременно (аналог ansible.cfg forks). 1 — строго последовательно. Forks int `yaml:"forks"` // Serial — размер батча rolling-обновления: сначала обновляется // Serial хостов, потом (если MaxFails не превышен) следующие Serial, // и так далее. 1 — классическое "один хост за раз". 0/не задано — // один батч на всех сразу (Forks по-прежнему ограничивает // параллелизм внутри этого единственного батча). Serial int `yaml:"serial"` // MaxFails — после скольких упавших хостов прервать оставшиеся // батчи rolling-обновления. 0 — без ограничения (прогонять все // батчи независимо от числа ошибок, как было по умолчанию раньше). MaxFails int `yaml:"max_fails"` // Become — глобально поднимать привилегии (sudo) для всех команд. // Гранулярнее, per-task become в первой версии не реализовано — // это включатель на весь прогон, как ansible.cfg become=true // без per-play override. Become bool `yaml:"become"` // GatherFacts — собирать ли internal/facts перед тасками. false // экономит одну SSH-сессию на хост и полезно, если os_family // и подобные факты плейбуку не нужны. GatherFacts bool `yaml:"gather_facts"` // Timeout — таймаut на установление SSH-соединения к одному хосту. Timeout time.Duration `yaml:"timeout"` // SSHUser / SSHKeyPath / SSHPort — дефолты для подключения, // используются, если соответствующий CLI-флаг не передан явно. SSHUser string `yaml:"ssh_user"` SSHKeyPath string `yaml:"ssh_key_path"` SSHPort int `yaml:"ssh_port"` // ModulesDir — путь к modules.d/ по умолчанию. ModulesDir string `yaml:"modules_dir"` } // Default — значения, которые действуют, если ни в файле, ни в CLI // ничего не указано. Forks=5 — тот же дефолт, что у самого Ansible. func Default() Config { return Config{ Forks: 5, Serial: 0, MaxFails: 0, Become: false, GatherFacts: true, Timeout: 10 * time.Second, SSHUser: "root", SSHPort: 22, } } // yamlConfig — то же самое, что Config, но Timeout — строка ("10s"), // потому что time.Duration не умеет сам себя парсить из YAML-скаляра // без кастомного UnmarshalYAML; отдельный тип проще, чем городить его // прямо на Config. type yamlConfig struct { Forks *int `yaml:"forks"` Serial *int `yaml:"serial"` MaxFails *int `yaml:"max_fails"` Become *bool `yaml:"become"` GatherFacts *bool `yaml:"gather_facts"` Timeout string `yaml:"timeout"` SSHUser string `yaml:"ssh_user"` SSHKeyPath string `yaml:"ssh_key_path"` SSHPort *int `yaml:"ssh_port"` ModulesDir string `yaml:"modules_dir"` } // Load читает YAML-файл конфигурации и накладывает его поверх Default(). // Отсутствие файла — не ошибка (Load(path) с несуществующим path // возвращает Default() как есть) — файл конфигурации в goherence // полностью опционален, все параметры дублируются флагами CLI. func Load(path string) (Config, error) { cfg := Default() if path == "" { return cfg, nil } data, err := os.ReadFile(path) if os.IsNotExist(err) { return cfg, nil } if err != nil { return cfg, fmt.Errorf("читаю конфиг %s: %w", path, err) } var y yamlConfig if err := yaml.Unmarshal(data, &y); err != nil { return cfg, fmt.Errorf("парсинг конфига %s: %w", path, err) } if y.Forks != nil { cfg.Forks = *y.Forks } if y.Serial != nil { cfg.Serial = *y.Serial } if y.MaxFails != nil { cfg.MaxFails = *y.MaxFails } if y.Become != nil { cfg.Become = *y.Become } if y.GatherFacts != nil { cfg.GatherFacts = *y.GatherFacts } if y.Timeout != "" { d, err := time.ParseDuration(y.Timeout) if err != nil { return cfg, fmt.Errorf("конфиг %s: timeout: %w", path, err) } cfg.Timeout = d } if y.SSHUser != "" { cfg.SSHUser = y.SSHUser } if y.SSHKeyPath != "" { cfg.SSHKeyPath = y.SSHKeyPath } if y.SSHPort != nil { cfg.SSHPort = *y.SSHPort } if y.ModulesDir != "" { cfg.ModulesDir = y.ModulesDir } if cfg.Forks < 1 { return cfg, fmt.Errorf("конфиг %s: forks должен быть >= 1", path) } if cfg.Serial < 0 { return cfg, fmt.Errorf("конфиг %s: serial не может быть отрицательным", path) } if cfg.MaxFails < 0 { return cfg, fmt.Errorf("конфиг %s: max_fails не может быть отрицательным", path) } return cfg, nil }