// Package config parses /etc/kubesolo/update.conf — the persistent // configuration for the update agent. Each line is "key = value"; blank // lines and "#"-prefixed comments are ignored. Unknown keys are tolerated // (forward compatibility). // // Example: // // # Where to look for updates // server = https://updates.kubesolo.example.com // channel = stable // // # Only apply between 03:00 and 05:00 local time // maintenance_window = 03:00-05:00 // // pubkey = /etc/kubesolo/update-pubkey.hex // // The file is populated on first boot by cloud-init (see the cloud-init // updates: block) and can be hand-edited afterwards. package config import ( "bufio" "fmt" "os" "strings" ) // DefaultPath is where update.conf lives on a live system. const DefaultPath = "/etc/kubesolo/update.conf" // Config holds the parsed update.conf values. Empty fields mean "not set" — // the caller's defaults apply. type Config struct { Server string Channel string MaintenanceWindow string PubKey string // HealthcheckURL is an optional URL the healthcheck command will GET; // 200 = pass, anything else = fail. HealthcheckURL string // AutoRollbackAfter is the number of consecutive post-boot healthcheck // failures after which the agent will call Rollback automatically. // 0 = disabled (default). AutoRollbackAfter int } // Load reads and parses update.conf. A missing file returns an empty Config // (not an error) — fresh systems before cloud-init has run. func Load(path string) (*Config, error) { f, err := os.Open(path) if err != nil { if os.IsNotExist(err) { return &Config{}, nil } return nil, fmt.Errorf("open %s: %w", path, err) } defer f.Close() c := &Config{} scanner := bufio.NewScanner(f) lineNo := 0 for scanner.Scan() { lineNo++ line := strings.TrimSpace(scanner.Text()) if line == "" || strings.HasPrefix(line, "#") { continue } eq := strings.IndexByte(line, '=') if eq < 0 { return nil, fmt.Errorf("%s:%d: missing '=' in line: %q", path, lineNo, line) } key := strings.TrimSpace(line[:eq]) value := strings.TrimSpace(line[eq+1:]) switch key { case "server": c.Server = value case "channel": c.Channel = value case "maintenance_window": c.MaintenanceWindow = value case "pubkey": c.PubKey = value case "healthcheck_url": c.HealthcheckURL = value case "auto_rollback_after": // Parse a small integer. Non-numeric values are silently // ignored (forward compat); zero disables the feature. n := 0 for _, ch := range value { if ch >= '0' && ch <= '9' { n = n*10 + int(ch-'0') } else { n = 0 break } } c.AutoRollbackAfter = n } // Unknown keys are silently ignored for forward compatibility. } if err := scanner.Err(); err != nil { return nil, fmt.Errorf("read %s: %w", path, err) } return c, nil }