feat(docker-watcher): phase 12 - hardening
Blue-green zero-downtime deploys, promote flow validation. Dual auth: local (bcrypt + JWT) and OAuth2/OIDC (any provider). Auth middleware, login page, auth settings UI. Structured logging (slog JSON), config export to YAML. Graceful shutdown with deploy draining. Multi-stage Dockerfile and production docker-compose.yml. Swap phase order: Volumes & Env before UI Polish.
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/alexei/docker-watcher/internal/store"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// ExportConfig reads the current database state and produces a SeedConfig YAML
|
||||
// representation. Credential fields (tokens, passwords) are exported as placeholder
|
||||
// strings since they are encrypted in the database.
|
||||
func ExportConfig(db *store.Store) ([]byte, error) {
|
||||
cfg, err := buildSeedConfig(db)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("build seed config: %w", err)
|
||||
}
|
||||
|
||||
data, err := yaml.Marshal(cfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal yaml: %w", err)
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// buildSeedConfig constructs a SeedConfig from the current database state.
|
||||
func buildSeedConfig(db *store.Store) (SeedConfig, error) {
|
||||
settings, err := db.GetSettings()
|
||||
if err != nil {
|
||||
return SeedConfig{}, fmt.Errorf("get settings: %w", err)
|
||||
}
|
||||
|
||||
registries, err := db.GetAllRegistries()
|
||||
if err != nil {
|
||||
return SeedConfig{}, fmt.Errorf("get registries: %w", err)
|
||||
}
|
||||
|
||||
projects, err := db.GetAllProjects()
|
||||
if err != nil {
|
||||
return SeedConfig{}, fmt.Errorf("get projects: %w", err)
|
||||
}
|
||||
|
||||
cfg := SeedConfig{
|
||||
Global: GlobalConfig{
|
||||
Domain: settings.Domain,
|
||||
ServerIP: settings.ServerIP,
|
||||
Network: settings.Network,
|
||||
SubdomainPattern: settings.SubdomainPattern,
|
||||
NotificationURL: settings.NotificationURL,
|
||||
Npm: NpmConfig{
|
||||
URL: settings.NpmURL,
|
||||
Email: settings.NpmEmail,
|
||||
Password: "CHANGE_ME", // Encrypted value, export placeholder.
|
||||
},
|
||||
},
|
||||
Registries: make(map[string]RegistryDef),
|
||||
Projects: make(map[string]ProjectDef),
|
||||
}
|
||||
|
||||
for _, reg := range registries {
|
||||
cfg.Registries[reg.Name] = RegistryDef{
|
||||
URL: reg.URL,
|
||||
Type: reg.Type,
|
||||
Token: "CHANGE_ME", // Encrypted value, export placeholder.
|
||||
}
|
||||
}
|
||||
|
||||
for _, proj := range projects {
|
||||
stages, err := db.GetStagesByProjectID(proj.ID)
|
||||
if err != nil {
|
||||
return SeedConfig{}, fmt.Errorf("get stages for project %s: %w", proj.Name, err)
|
||||
}
|
||||
|
||||
stageDefs := make(map[string]StageDef)
|
||||
for _, st := range stages {
|
||||
stageDefs[st.Name] = StageDef{
|
||||
TagPattern: st.TagPattern,
|
||||
AutoDeploy: st.AutoDeploy,
|
||||
MaxInstances: st.MaxInstances,
|
||||
Confirm: st.Confirm,
|
||||
PromoteFrom: st.PromoteFrom,
|
||||
Subdomain: st.Subdomain,
|
||||
}
|
||||
}
|
||||
|
||||
envMap := parseJSONMap(proj.Env)
|
||||
volMap := parseJSONMap(proj.Volumes)
|
||||
|
||||
cfg.Projects[proj.Name] = ProjectDef{
|
||||
Registry: proj.Registry,
|
||||
Image: proj.Image,
|
||||
Port: proj.Port,
|
||||
Healthcheck: proj.Healthcheck,
|
||||
Env: envMap,
|
||||
Volumes: volMap,
|
||||
Stages: stageDefs,
|
||||
}
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// parseJSONMap safely parses a JSON-encoded map string. Returns nil on failure.
|
||||
func parseJSONMap(jsonStr string) map[string]string {
|
||||
if jsonStr == "" || jsonStr == "{}" {
|
||||
return nil
|
||||
}
|
||||
var m map[string]string
|
||||
if err := json.Unmarshal([]byte(jsonStr), &m); err != nil {
|
||||
return nil
|
||||
}
|
||||
if len(m) == 0 {
|
||||
return nil
|
||||
}
|
||||
return m
|
||||
}
|
||||
Reference in New Issue
Block a user