package configgen import ( "crypto/rand" "encoding/hex" "fmt" "time" "gitea.dooplex.hu/admin/felhom-hub/internal/store" "gopkg.in/yaml.v3" ) // Generate takes the template YAML and a customer config, // then produces a complete controller.yaml with customer-specific values // merged in. The returned string is valid YAML ready for deployment. func Generate(templateYAML string, cfg *store.CustomerConfig) (string, error) { // Parse template into generic map var base map[string]interface{} if err := yaml.Unmarshal([]byte(templateYAML), &base); err != nil { return "", fmt.Errorf("parsing template YAML: %w", err) } if base == nil { base = make(map[string]interface{}) } // Parse customer config_json overrides var overrides map[string]interface{} if cfg.ConfigJSON != "" && cfg.ConfigJSON != "{}" { if err := yaml.Unmarshal([]byte(cfg.ConfigJSON), &overrides); err != nil { return "", fmt.Errorf("parsing config overrides: %w", err) } } // Apply config_json overrides first (deep merge) if len(overrides) > 0 { base = deepMerge(base, overrides) } // Apply programmatic overrides — these always win over config_json setNested(base, []string{"customer", "id"}, cfg.CustomerID) setNested(base, []string{"customer", "name"}, cfg.CustomerName) setNested(base, []string{"customer", "domain"}, cfg.Domain) setNested(base, []string{"customer", "email"}, cfg.Email) setNested(base, []string{"hub", "enabled"}, true) setNested(base, []string{"hub", "url"}, "https://hub.felhom.eu") setNested(base, []string{"hub", "api_key"}, cfg.APIKey) // Generate session secret sessionSecret, err := RandomHex(32) if err != nil { return "", fmt.Errorf("generating session secret: %w", err) } setNested(base, []string{"web", "session_secret"}, sessionSecret) // Marshal back to YAML out, err := yaml.Marshal(base) if err != nil { return "", fmt.Errorf("marshaling YAML: %w", err) } // Add header comment header := fmt.Sprintf( "# Felhom Controller Configuration\n# Generated by Felhom Hub for %q on %s\n# Download URL: https://hub.felhom.eu/api/v1/config/%s\n\n", cfg.CustomerID, time.Now().UTC().Format(time.RFC3339), cfg.CustomerID, ) return header + string(out), nil } // deepMerge recursively merges overlay into base. // When both base and overlay have a map at the same key, they are merged recursively. // Otherwise, the overlay value wins. func deepMerge(base, overlay map[string]interface{}) map[string]interface{} { result := make(map[string]interface{}, len(base)) for k, v := range base { result[k] = v } for k, v := range overlay { if baseMap, ok := result[k].(map[string]interface{}); ok { if overlayMap, ok := v.(map[string]interface{}); ok { result[k] = deepMerge(baseMap, overlayMap) continue } } result[k] = v } return result } // setNested sets a value at a nested path in a map, creating intermediate maps as needed. func setNested(m map[string]interface{}, path []string, value interface{}) { for i, key := range path { if i == len(path)-1 { m[key] = value return } sub, ok := m[key].(map[string]interface{}) if !ok { sub = make(map[string]interface{}) m[key] = sub } m = sub } } // RandomHex generates n random bytes and returns them as a hex string. func RandomHex(n int) (string, error) { b := make([]byte, n) if _, err := rand.Read(b); err != nil { return "", err } return hex.EncodeToString(b), nil }