goherence/internal/config/config.go
2026-09-11 10:17:25 +03:00

160 lines
6.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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
}