feat: customer config management — CRUD, API retrieval, per-customer auth (v0.2.0)
New "Configurations" section lets operators pre-configure customer settings
in the Hub, then docker-setup.sh can download a ready-made controller.yaml
using just a customer ID and retrieval password.
- Store: customer_configs table with CRUD + per-customer API key lookup
- API: GET /api/v1/config/{id} with X-Retrieval-Password auth
- Auth: per-customer API keys alongside existing global key (backward compatible)
- Web UI: /configs list, create, edit, delete, YAML preview, copy-to-clipboard
- YAML gen: deep-merge controller.yaml.example template with customer overrides
- Template fetcher: background goroutine refreshing template from Gitea repo
- Navigation: Dashboard / Configurations tabs on all pages
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+31
-10
@@ -43,10 +43,11 @@ type Config struct {
|
||||
StaleThreshold string `yaml:"stale_threshold"`
|
||||
} `yaml:"alerting"`
|
||||
Registry struct {
|
||||
Image string `yaml:"image"`
|
||||
Username string `yaml:"username"`
|
||||
Token string `yaml:"token"`
|
||||
CheckInterval string `yaml:"check_interval"`
|
||||
Image string `yaml:"image"`
|
||||
Username string `yaml:"username"`
|
||||
Token string `yaml:"token"`
|
||||
CheckInterval string `yaml:"check_interval"`
|
||||
TemplateInterval string `yaml:"template_interval"`
|
||||
} `yaml:"registry"`
|
||||
Server struct {
|
||||
Listen string `yaml:"listen"`
|
||||
@@ -88,9 +89,30 @@ func main() {
|
||||
staleThreshold = 30 * time.Minute
|
||||
}
|
||||
|
||||
// Initialize handlers
|
||||
apiHandler := api.New(dataStore, cfg.API.ReportAPIKey, cfg.Notifications.ResendAPIKey, cfg.Notifications.FromEmail, logger)
|
||||
// Background context for all goroutines
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
// Initialize template fetcher for customer config generation
|
||||
var templateFetcher *web.TemplateFetcher
|
||||
if cfg.Registry.Username != "" && cfg.Registry.Token != "" {
|
||||
templateInterval, err := time.ParseDuration(cfg.Registry.TemplateInterval)
|
||||
if err != nil {
|
||||
templateInterval = 1 * time.Hour
|
||||
}
|
||||
templateFetcher = web.NewTemplateFetcher(cfg.Registry.Username, cfg.Registry.Token, templateInterval, logger)
|
||||
go templateFetcher.Run(ctx)
|
||||
logger.Printf("[INFO] Template fetcher started (every %s)", cfg.Registry.TemplateInterval)
|
||||
}
|
||||
|
||||
// Initialize handlers — pass templateFetcher as interface (nil-safe)
|
||||
var templateProvider api.ConfigTemplateProvider
|
||||
if templateFetcher != nil {
|
||||
templateProvider = templateFetcher
|
||||
}
|
||||
apiHandler := api.New(dataStore, cfg.API.ReportAPIKey, cfg.Notifications.ResendAPIKey, cfg.Notifications.FromEmail, templateProvider, logger)
|
||||
webServer := web.New(dataStore, cfg.Auth.PasswordHash, cfg.API.ReportAPIKey, staleThreshold, logger)
|
||||
webServer.SetTemplateFetcher(templateFetcher)
|
||||
|
||||
// Build HTTP mux
|
||||
mux := http.NewServeMux()
|
||||
@@ -120,10 +142,6 @@ func main() {
|
||||
IdleTimeout: 120 * time.Second,
|
||||
}
|
||||
|
||||
// Background: daily prune + version checker
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
// Initialize version checker for controller image registry
|
||||
var versionChecker *web.VersionChecker
|
||||
if cfg.Registry.Username != "" && cfg.Registry.Token != "" {
|
||||
@@ -211,6 +229,9 @@ func loadConfig(path string, logger *log.Logger) *Config {
|
||||
if cfg.Registry.CheckInterval == "" {
|
||||
cfg.Registry.CheckInterval = "6h"
|
||||
}
|
||||
if cfg.Registry.TemplateInterval == "" {
|
||||
cfg.Registry.TemplateInterval = "1h"
|
||||
}
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user