4c8bf63ce3
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>
114 lines
2.8 KiB
Go
114 lines
2.8 KiB
Go
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
|
|
}
|