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:
@@ -0,0 +1,113 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const templateRawURL = "https://gitea.dooplex.hu/admin/deploy-felhom-compose/raw/branch/main/controller/configs/controller.yaml.example"
|
||||
|
||||
// TemplateFetcher periodically fetches controller.yaml.example from the Gitea
|
||||
// repo and caches it for config generation. Falls back to go:embed default.
|
||||
type TemplateFetcher struct {
|
||||
username string
|
||||
token string
|
||||
fetchInterval time.Duration
|
||||
logger *log.Logger
|
||||
|
||||
mu sync.RWMutex
|
||||
cachedTemplate string
|
||||
lastFetch time.Time
|
||||
lastError string
|
||||
}
|
||||
|
||||
// NewTemplateFetcher creates a new TemplateFetcher.
|
||||
func NewTemplateFetcher(username, token string, fetchInterval time.Duration, logger *log.Logger) *TemplateFetcher {
|
||||
return &TemplateFetcher{
|
||||
username: username,
|
||||
token: token,
|
||||
fetchInterval: fetchInterval,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Run starts the periodic fetch loop. Call in a goroutine.
|
||||
// It fetches immediately on start, then every fetchInterval.
|
||||
func (tf *TemplateFetcher) Run(ctx context.Context) {
|
||||
tf.fetch()
|
||||
ticker := time.NewTicker(tf.fetchInterval)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
tf.fetch()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (tf *TemplateFetcher) fetch() {
|
||||
req, err := http.NewRequest("GET", templateRawURL, nil)
|
||||
if err != nil {
|
||||
tf.mu.Lock()
|
||||
tf.lastError = err.Error()
|
||||
tf.mu.Unlock()
|
||||
tf.logger.Printf("[WARN] Template fetch: failed to create request: %v", err)
|
||||
return
|
||||
}
|
||||
if tf.username != "" && tf.token != "" {
|
||||
req.SetBasicAuth(tf.username, tf.token)
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: 15 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
tf.mu.Lock()
|
||||
tf.lastError = err.Error()
|
||||
tf.mu.Unlock()
|
||||
tf.logger.Printf("[WARN] Template fetch: HTTP request failed: %v", err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
tf.mu.Lock()
|
||||
tf.lastError = fmt.Sprintf("HTTP %d", resp.StatusCode)
|
||||
tf.mu.Unlock()
|
||||
tf.logger.Printf("[WARN] Template fetch: unexpected status %d", resp.StatusCode)
|
||||
return
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 64*1024)) // 64KB max
|
||||
if err != nil {
|
||||
tf.mu.Lock()
|
||||
tf.lastError = err.Error()
|
||||
tf.mu.Unlock()
|
||||
tf.logger.Printf("[WARN] Template fetch: read error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
tf.mu.Lock()
|
||||
tf.cachedTemplate = string(body)
|
||||
tf.lastFetch = time.Now()
|
||||
tf.lastError = ""
|
||||
tf.mu.Unlock()
|
||||
tf.logger.Printf("[DEBUG] Template fetched (%d bytes)", len(body))
|
||||
}
|
||||
|
||||
// Template returns the cached controller.yaml template.
|
||||
// If the cache is empty (never fetched or all failures), returns the go:embed fallback.
|
||||
func (tf *TemplateFetcher) Template() string {
|
||||
tf.mu.RLock()
|
||||
defer tf.mu.RUnlock()
|
||||
if tf.cachedTemplate != "" {
|
||||
return tf.cachedTemplate
|
||||
}
|
||||
return defaultControllerTemplate
|
||||
}
|
||||
Reference in New Issue
Block a user