Files
deploy-felhom-compose/controller/internal/config/config.go
T
admin d32d9fb44b v0.4.0: monitoring & backup — scheduler, CPU/temp metrics, healthchecks, restic backups
Phase 2 (Monitoring & Health):
- Central job scheduler replacing ad-hoc goroutines (internal/scheduler)
- CPU usage collector via /proc/stat background sampling (internal/system/cpu_linux.go)
- Temperature reading from /sys/class/thermal + /host/sys (Docker mount)
- Load average from /proc/loadavg
- Healthchecks.io-compatible HTTP pinger (internal/monitor/pinger.go)
- System health checks: disk, memory, CPU, temp, Docker, protected containers (internal/monitor/healthcheck.go)

Phase 3 (Backups):
- Database auto-discovery via docker ps + docker inspect (internal/backup/dbdump.go)
- Database dumping via docker exec (pg_dump / mariadb-dump) with atomic writes
- Restic backup integration with auto-password generation (internal/backup/restic.go)
- Backup orchestrator: DB dumps + restic snapshots + weekly prune (internal/backup/backup.go)
- Manual backup trigger via dashboard button and POST /api/backup/run

Dashboard UI:
- CPU usage bar with load average display
- Temperature with colored indicator dot
- Backup status card with last run time, DB count, repo stats
- "Mentés most" button for manual backup trigger

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 11:17:10 +01:00

283 lines
8.8 KiB
Go

package config
import (
"fmt"
"os"
"strings"
"gopkg.in/yaml.v3"
)
// Config is the top-level configuration structure.
// Contains ONLY infrastructure/customer identity.
// App-specific config lives in per-app app.yaml files.
type Config struct {
Customer CustomerConfig `yaml:"customer"`
Infrastructure InfrastructureConfig `yaml:"infrastructure"`
Paths PathsConfig `yaml:"paths"`
Web WebConfig `yaml:"web"`
Git GitConfig `yaml:"git"`
Stacks StacksConfig `yaml:"stacks"`
Backup BackupConfig `yaml:"backup"`
Monitoring MonitoringConfig `yaml:"monitoring"`
SelfUpdate SelfUpdateConfig `yaml:"self_update"`
Notifications NotificationsConfig `yaml:"notifications"`
Logging LoggingConfig `yaml:"logging"`
Assets AssetsConfig `yaml:"assets"`
System SystemConfig `yaml:"system"`
}
type SystemConfig struct {
ReservedMemoryMB int `yaml:"reserved_memory_mb"`
}
type CustomerConfig struct {
ID string `yaml:"id"`
Name string `yaml:"name"`
Domain string `yaml:"domain"`
Email string `yaml:"email"`
TelegramChatID string `yaml:"telegram_chat_id"`
}
type InfrastructureConfig struct {
CFTunnelToken string `yaml:"cf_tunnel_token"`
CFAPIToken string `yaml:"cf_api_token"`
}
type PathsConfig struct {
StacksDir string `yaml:"stacks_dir"`
DataDir string `yaml:"data_dir"`
BackupDir string `yaml:"backup_dir"`
DBDumpDir string `yaml:"db_dump_dir"`
HDDPath string `yaml:"hdd_path"`
}
type WebConfig struct {
Listen string `yaml:"listen"`
PasswordHash string `yaml:"password_hash"`
SessionSecret string `yaml:"session_secret"`
}
type GitConfig struct {
RepoURL string `yaml:"repo_url"`
Branch string `yaml:"branch"`
SyncInterval string `yaml:"sync_interval"`
Username string `yaml:"username"`
Token string `yaml:"token"`
}
type StacksConfig struct {
Protected []string `yaml:"protected"`
UpdateWindow string `yaml:"update_window"`
ComposeCommand string `yaml:"compose_command"`
}
type BackupConfig struct {
Enabled bool `yaml:"enabled"`
ResticRepo string `yaml:"restic_repo"`
ResticPasswordFile string `yaml:"restic_password_file"`
DBDumpSchedule string `yaml:"db_dump_schedule"`
ResticSchedule string `yaml:"restic_schedule"`
Retention RetentionConfig `yaml:"retention"`
PruneSchedule string `yaml:"prune_schedule"`
}
type RetentionConfig struct {
KeepDaily int `yaml:"keep_daily"`
KeepWeekly int `yaml:"keep_weekly"`
KeepMonthly int `yaml:"keep_monthly"`
}
type MonitoringConfig struct {
Enabled bool `yaml:"enabled"`
HealthchecksBase string `yaml:"healthchecks_base"`
PingUUIDs PingUUIDsConfig `yaml:"ping_uuids"`
HealthCheckSchedule string `yaml:"health_check_schedule"`
SystemHealthInterval string `yaml:"system_health_interval"`
Thresholds ThresholdsConfig `yaml:"thresholds"`
}
type PingUUIDsConfig struct {
DBDump string `yaml:"db_dump"`
Backup string `yaml:"backup"`
SystemHealth string `yaml:"system_health"`
}
type ThresholdsConfig struct {
DiskWarnPercent int `yaml:"disk_warn_percent"`
DiskCritPercent int `yaml:"disk_crit_percent"`
BackupMaxAgeHours int `yaml:"backup_max_age_hours"`
CPUWarnPercent int `yaml:"cpu_warn_percent"`
MemoryWarnPercent int `yaml:"memory_warn_percent"`
TemperatureWarnCelsius int `yaml:"temperature_warn_celsius"`
}
type SelfUpdateConfig struct {
Enabled bool `yaml:"enabled"`
CheckInterval string `yaml:"check_interval"`
Image string `yaml:"image"`
AutoUpdate bool `yaml:"auto_update"`
HealthTimeoutSeconds int `yaml:"health_timeout_seconds"`
}
type NotificationsConfig struct {
CustomerEvents []string `yaml:"customer_events"`
OperatorEvents []string `yaml:"operator_events"`
}
type LoggingConfig struct {
Level string `yaml:"level"`
File string `yaml:"file"`
MaxSizeMB int `yaml:"max_size_mb"`
MaxFiles int `yaml:"max_files"`
}
type AssetsConfig struct {
SourceURL string `yaml:"source_url"` // Only used during build, not runtime
}
// Load reads and parses the config file, applies defaults, and validates.
func Load(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("reading config file: %w", err)
}
// Expand environment variables in the YAML
expanded := os.ExpandEnv(string(data))
cfg := &Config{}
if err := yaml.Unmarshal([]byte(expanded), cfg); err != nil {
return nil, fmt.Errorf("parsing config file: %w", err)
}
applyDefaults(cfg)
applyEnvOverrides(cfg)
if err := validate(cfg); err != nil {
return nil, fmt.Errorf("config validation: %w", err)
}
return cfg, nil
}
func applyDefaults(cfg *Config) {
d := func(val *string, def string) {
if *val == "" {
*val = def
}
}
di := func(val *int, def int) {
if *val == 0 {
*val = def
}
}
d(&cfg.Paths.StacksDir, "/opt/docker/stacks")
d(&cfg.Paths.DataDir, "/opt/docker/felhom-controller/data")
d(&cfg.Paths.BackupDir, "/srv/backups")
d(&cfg.Paths.DBDumpDir, "/srv/backups/db-dumps")
d(&cfg.Web.Listen, ":8080")
d(&cfg.Git.Branch, "main")
d(&cfg.Git.SyncInterval, "15m")
d(&cfg.Stacks.UpdateWindow, "03:00-05:00")
d(&cfg.Backup.ResticRepo, "/srv/backups/restic-repo")
d(&cfg.Backup.DBDumpSchedule, "02:30")
d(&cfg.Backup.ResticSchedule, "03:00")
d(&cfg.Backup.PruneSchedule, "weekly")
di(&cfg.Backup.Retention.KeepDaily, 7)
di(&cfg.Backup.Retention.KeepWeekly, 4)
di(&cfg.Backup.Retention.KeepMonthly, 6)
d(&cfg.Backup.ResticPasswordFile, "/opt/docker/felhom-controller/data/restic-password")
d(&cfg.Monitoring.HealthchecksBase, "https://status.felhom.eu")
d(&cfg.Monitoring.HealthCheckSchedule, "06:00")
d(&cfg.Monitoring.SystemHealthInterval, "5m")
di(&cfg.Monitoring.Thresholds.DiskWarnPercent, 80)
di(&cfg.Monitoring.Thresholds.DiskCritPercent, 90)
di(&cfg.Monitoring.Thresholds.BackupMaxAgeHours, 36)
di(&cfg.Monitoring.Thresholds.CPUWarnPercent, 90)
di(&cfg.Monitoring.Thresholds.MemoryWarnPercent, 85)
di(&cfg.Monitoring.Thresholds.TemperatureWarnCelsius, 75)
d(&cfg.SelfUpdate.CheckInterval, "6h")
di(&cfg.SelfUpdate.HealthTimeoutSeconds, 60)
d(&cfg.Logging.Level, "info")
di(&cfg.Logging.MaxSizeMB, 10)
di(&cfg.Logging.MaxFiles, 3)
d(&cfg.Assets.SourceURL, "https://felhom.eu")
di(&cfg.System.ReservedMemoryMB, 384)
}
func applyEnvOverrides(cfg *Config) {
envStr := func(key string, target *string) {
if v := os.Getenv(key); v != "" {
*target = v
}
}
envStr("FELHOM_CUSTOMER_ID", &cfg.Customer.ID)
envStr("FELHOM_CUSTOMER_DOMAIN", &cfg.Customer.Domain)
envStr("FELHOM_WEB_LISTEN", &cfg.Web.Listen)
envStr("FELHOM_WEB_PASSWORD_HASH", &cfg.Web.PasswordHash)
envStr("FELHOM_PATHS_STACKS_DIR", &cfg.Paths.StacksDir)
envStr("FELHOM_PATHS_HDD_PATH", &cfg.Paths.HDDPath)
envStr("FELHOM_LOGGING_LEVEL", &cfg.Logging.Level)
envStr("FELHOM_MONITORING_SYSTEM_HEALTH_INTERVAL", &cfg.Monitoring.SystemHealthInterval)
}
func validate(cfg *Config) error {
var errs []string
if cfg.Customer.ID == "" {
errs = append(errs, "customer.id is required")
}
if cfg.Customer.Domain == "" {
errs = append(errs, "customer.domain is required")
}
switch cfg.Logging.Level {
case "debug", "info", "warn", "error":
default:
errs = append(errs, fmt.Sprintf("logging.level must be debug|info|warn|error, got %q", cfg.Logging.Level))
}
if cfg.Monitoring.Thresholds.DiskWarnPercent >= cfg.Monitoring.Thresholds.DiskCritPercent {
errs = append(errs, "disk_warn_percent must be less than disk_crit_percent")
}
if len(errs) > 0 {
return fmt.Errorf("validation errors:\n - %s", strings.Join(errs, "\n - "))
}
return nil
}
// IsProtectedStack checks if a stack name is in the protected list.
func (cfg *Config) IsProtectedStack(name string) bool {
for _, p := range cfg.Stacks.Protected {
if strings.EqualFold(p, name) {
return true
}
}
return false
}
// AppLogoURL returns the primary logo URL (SVG). Use AppLogoPNGURL as fallback.
func (cfg *Config) AppLogoURL(slug string) string {
return fmt.Sprintf("/static/assets/%s-logo.svg", slug)
}
// AppLogoPNGURL returns the PNG fallback logo URL.
func (cfg *Config) AppLogoPNGURL(slug string) string {
return fmt.Sprintf("/static/assets/%s-logo.png", slug)
}
// AppScreenshotURL returns the local URL for an app's screenshot.
func (cfg *Config) AppScreenshotURL(slug string, index int) string {
return fmt.Sprintf("/static/assets/%s-screenshot-%d.webp", slug, index)
}
// AppPageURL returns the URL for an app's detail page.
// This links to the local controller-hosted app detail page.
func (cfg *Config) AppPageURL(slug string) string {
return fmt.Sprintf("/apps/%s", slug)
}