package models import ( "encoding/json" "fmt" "os" "path/filepath" "sync" ) var ( mu sync.RWMutex cfg *AppConfig cfgPath string ) func init() { cfgPath = resolveCfgPath() hostname, _ := os.Hostname() cfg = &AppConfig{ Node: NodeInfo{ Hostname: hostname, UpdateHistory: []UpdateRecord{}, Backup: BackupConfig{ Targets: []BackupTarget{}, History: []BackupExecution{}, }, }, Nodes: []RemoteNode{}, Services: []Service{}, } if data, err := os.ReadFile(cfgPath); err == nil { _ = json.Unmarshal(data, cfg) } ensureSlices() } func ensureSlices() { if cfg.Node.UpdateHistory == nil { cfg.Node.UpdateHistory = []UpdateRecord{} } if cfg.Node.Backup.Targets == nil { cfg.Node.Backup.Targets = []BackupTarget{} } if cfg.Node.Backup.History == nil { cfg.Node.Backup.History = []BackupExecution{} } if cfg.Nodes == nil { cfg.Nodes = []RemoteNode{} } if cfg.Services == nil { cfg.Services = []Service{} } } func resolveCfgPath() string { const etcPath = "/etc/nodemaster/nodemaster.conf" if err := os.MkdirAll(filepath.Dir(etcPath), 0755); err == nil { if f, err := os.OpenFile(etcPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600); err == nil { f.Close() return etcPath } } home, err := os.UserHomeDir() if err != nil { home = "." } return filepath.Join(home, ".nodemaster.conf") } func persist() error { data, err := json.MarshalIndent(cfg, "", " ") if err != nil { return err } if err := os.MkdirAll(filepath.Dir(cfgPath), 0755); err != nil { return fmt.Errorf("mkdir: %w", err) } // 0600: the config now embeds the restic repository password, so it must // not be group/world-readable. Chmod covers pre-existing files that were // created before this field existed (WriteFile's mode only applies on // create, not to files that already exist with looser permissions). if err := os.WriteFile(cfgPath, data, 0600); err != nil { return err } _ = os.Chmod(cfgPath, 0600) return nil } // ConfigPath returns the resolved config file path. func ConfigPath() string { return cfgPath }