package app import ( "encoding/json" "fmt" "os" "path/filepath" "strings" "time" "github.com/example/glpi-env-controller/internal/dockerctl" ) type Config struct { ListenAddr string `json:"listen_addr"` Username string `json:"username"` PasswordEnv string `json:"password_env"` PasswordFile string `json:"password_file"` AutoImportMissing bool `json:"auto_import_missing"` MaxBackups int `json:"max_backups"` DockerTimeout string `json:"docker_timeout"` Projects []ProjectConfig `json:"projects"` } type ProjectConfig struct { ID string `json:"id"` Title string `json:"title"` EnvFile string `json:"env_file"` ExampleFile string `json:"example_file"` BackupDir string `json:"backup_dir"` Targets []dockerctl.Target `json:"targets"` } func LoadConfig(path string) (Config, string, error) { data, err := os.ReadFile(path) if err != nil { return Config{}, "", err } var cfg Config if err := json.Unmarshal(data, &cfg); err != nil { return Config{}, "", fmt.Errorf("decode config: %w", err) } if cfg.ListenAddr == "" { cfg.ListenAddr = ":8090" } if cfg.Username == "" { cfg.Username = "admin" } if cfg.PasswordEnv == "" && cfg.PasswordFile == "" { cfg.PasswordEnv = "ENV_CONTROLLER_PASSWORD" } if cfg.MaxBackups <= 0 { cfg.MaxBackups = 100 } if cfg.DockerTimeout == "" { cfg.DockerTimeout = "3m" } if _, err := time.ParseDuration(cfg.DockerTimeout); err != nil { return Config{}, "", fmt.Errorf("docker_timeout: %w", err) } password := "" if cfg.PasswordFile != "" { data, err := os.ReadFile(cfg.PasswordFile) if err != nil { return Config{}, "", fmt.Errorf("read password_file: %w", err) } password = strings.TrimSpace(string(data)) } else { password = os.Getenv(cfg.PasswordEnv) } if password == "" { return Config{}, "", fmt.Errorf("controller password is empty") } if len(cfg.Projects) == 0 { return Config{}, "", fmt.Errorf("at least one project is required") } seen := map[string]struct{}{} for i := range cfg.Projects { p := &cfg.Projects[i] if p.ID == "" || !safeID(p.ID) { return Config{}, "", fmt.Errorf("project %d has invalid id", i) } if _, ok := seen[p.ID]; ok { return Config{}, "", fmt.Errorf("duplicate project id %q", p.ID) } seen[p.ID] = struct{}{} if p.Title == "" { p.Title = p.ID } for label, value := range map[string]string{"env_file": p.EnvFile, "example_file": p.ExampleFile, "backup_dir": p.BackupDir} { if value == "" || !filepath.IsAbs(value) { return Config{}, "", fmt.Errorf("project %q %s must be an absolute path", p.ID, label) } } if filepath.Clean(p.EnvFile) == filepath.Clean(p.ExampleFile) { return Config{}, "", fmt.Errorf("project %q env_file and example_file must differ", p.ID) } if err := dockerctl.ValidateTargets(p.Targets); err != nil { return Config{}, "", fmt.Errorf("project %q targets: %w", p.ID, err) } } return cfg, password, nil } func safeID(value string) bool { if value == "" { return false } for _, r := range value { if !((r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '-' || r == '_') { return false } } return !strings.HasPrefix(value, "-") }