160 lines
6.2 KiB
Go
160 lines
6.2 KiB
Go
// 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
|
||
}
|