Files
tiny-forge/internal/config/config.go
T
alexei.dolgolyov cdf21682d6 feat(docker-watcher): phase 2 - crypto & config seed loader
AES-256-GCM encryption for credential storage, YAML seed config
parser with validation, and transactional import into SQLite.
Credentials (registry tokens, NPM password) encrypted before storage.
2026-03-27 21:01:16 +03:00

113 lines
3.2 KiB
Go

package config
import (
"fmt"
"os"
"gopkg.in/yaml.v3"
)
// SeedConfig represents the top-level YAML seed configuration.
type SeedConfig struct {
Global GlobalConfig `yaml:"global"`
Registries map[string]RegistryDef `yaml:"registries"`
Projects map[string]ProjectDef `yaml:"projects"`
}
// GlobalConfig holds domain-wide settings from the seed file.
type GlobalConfig struct {
Domain string `yaml:"domain"`
ServerIP string `yaml:"server_ip"`
Network string `yaml:"network"`
SubdomainPattern string `yaml:"subdomain_pattern"`
NotificationURL string `yaml:"notification_url"`
Npm NpmConfig `yaml:"npm"`
}
// NpmConfig holds Nginx Proxy Manager connection details.
type NpmConfig struct {
URL string `yaml:"url"`
Email string `yaml:"email"`
Password string `yaml:"password"`
}
// RegistryDef defines a container registry from the seed file.
type RegistryDef struct {
URL string `yaml:"url"`
Type string `yaml:"type"`
Token string `yaml:"token"`
}
// ProjectDef defines a project from the seed file.
type ProjectDef struct {
Registry string `yaml:"registry"`
Image string `yaml:"image"`
Port int `yaml:"port"`
Healthcheck string `yaml:"healthcheck"`
Env map[string]string `yaml:"env"`
Volumes map[string]string `yaml:"volumes"`
Stages map[string]StageDef `yaml:"stages"`
}
// StageDef defines a deployment stage from the seed file.
type StageDef struct {
TagPattern string `yaml:"tag_pattern"`
AutoDeploy bool `yaml:"auto_deploy"`
MaxInstances int `yaml:"max_instances"`
Confirm bool `yaml:"confirm"`
PromoteFrom string `yaml:"promote_from"`
Subdomain string `yaml:"subdomain"`
}
// LoadSeedFile reads and parses the YAML seed config from the given path.
func LoadSeedFile(path string) (SeedConfig, error) {
data, err := os.ReadFile(path)
if err != nil {
return SeedConfig{}, fmt.Errorf("read seed file: %w", err)
}
return ParseSeed(data)
}
// ParseSeed parses raw YAML bytes into a SeedConfig.
func ParseSeed(data []byte) (SeedConfig, error) {
var cfg SeedConfig
if err := yaml.Unmarshal(data, &cfg); err != nil {
return SeedConfig{}, fmt.Errorf("parse yaml: %w", err)
}
if err := validate(cfg); err != nil {
return SeedConfig{}, fmt.Errorf("validate seed config: %w", err)
}
return cfg, nil
}
// validate checks that required fields are present in the seed config.
func validate(cfg SeedConfig) error {
if cfg.Global.Domain == "" {
return fmt.Errorf("global.domain is required")
}
for name, proj := range cfg.Projects {
if proj.Image == "" {
return fmt.Errorf("project %q: image is required", name)
}
if proj.Registry != "" {
if _, ok := cfg.Registries[proj.Registry]; !ok {
return fmt.Errorf("project %q: references unknown registry %q", name, proj.Registry)
}
}
for stageName, stage := range proj.Stages {
if stage.TagPattern == "" {
return fmt.Errorf("project %q stage %q: tag_pattern is required", name, stageName)
}
if stage.MaxInstances < 0 {
return fmt.Errorf("project %q stage %q: max_instances must be >= 0", name, stageName)
}
}
}
return nil
}