116 lines
3.3 KiB
Go
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, "-")
|
|
}
|