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.
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user