273 lines
8.3 KiB
Go
273 lines
8.3 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"`
|
|
}
|
|
|
|
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"`
|
|
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.Monitoring.HealthchecksBase, "https://status.felhom.eu")
|
|
d(&cfg.Monitoring.HealthCheckSchedule, "06:00")
|
|
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")
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
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)
|
|
}
|