Files
jbergner fe32431d21
Some checks failed
release-tag / release-image (push) Failing after 55s
init
2026-08-01 23:54:45 +02:00

116 lines
3.3 KiB
Go

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, "-")
}