middleware-manager/services/config_generator.go
hhftechnologies 96b062dd94 Use middleware names for Traefik config
Key middleware entries in generated and proxied Traefik configs by middleware name (so chain references by name work) instead of by ID. Join middleware names in resource queries and scan them (with fallback to ID when name is not available), add Name fields to middleware structs, and use the extracted base name when building final middleware references. Also update log messages to include both name and ID. In the UI, enforce and auto-sanitize middleware names to lowercase a-z0-9- and hyphens only.
2026-02-17 16:37:02 +05:30

894 lines
28 KiB
Go

package services
import (
"database/sql"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"sync"
"time"
"github.com/hhftechnology/middleware-manager/database"
"github.com/hhftechnology/middleware-manager/models" // Correct import for your models
"gopkg.in/yaml.v3"
)
// ConfigGenerator generates Traefik configuration files
type ConfigGenerator struct {
db *database.DB
confDir string
configManager *ConfigManager
stopChan chan struct{}
isRunning bool
mutex sync.Mutex
lastConfig []byte
}
// TraefikConfig represents the structure of the Traefik configuration
type TraefikConfig struct {
HTTP struct {
Middlewares map[string]interface{} `yaml:"middlewares,omitempty"`
Routers map[string]interface{} `yaml:"routers,omitempty"`
Services map[string]interface{} `yaml:"services,omitempty"`
} `yaml:"http"`
TCP struct {
Routers map[string]interface{} `yaml:"routers,omitempty"`
Services map[string]interface{} `yaml:"services,omitempty"`
} `yaml:"tcp,omitempty"`
UDP struct {
Services map[string]interface{} `yaml:"services,omitempty"`
} `yaml:"udp,omitempty"`
TLS struct {
Options map[string]interface{} `yaml:"options,omitempty"`
} `yaml:"tls,omitempty"`
}
// Add this simple helper function at the top of config_generator.go
func shouldLog() bool {
logLevel := strings.ToLower(os.Getenv("LOG_LEVEL"))
return logLevel == "debug"
}
func shouldLogInfo() bool {
logLevel := strings.ToLower(os.Getenv("LOG_LEVEL"))
return logLevel == "debug" || logLevel == "info"
}
// NewConfigGenerator creates a new config generator
func NewConfigGenerator(db *database.DB, confDir string, configManager *ConfigManager) *ConfigGenerator {
return &ConfigGenerator{
db: db,
confDir: confDir,
configManager: configManager,
stopChan: make(chan struct{}),
isRunning: false,
lastConfig: nil,
}
}
// Start begins generating configuration files
func (cg *ConfigGenerator) Start(interval time.Duration) {
if strings.ToLower(os.Getenv("ENABLE_FILE_CONFIG")) != "true" {
if shouldLogInfo() {
log.Println("File config generator disabled (ENABLE_FILE_CONFIG != true); skipping resource-overrides.yml writes")
}
return
}
cg.mutex.Lock()
if cg.isRunning {
cg.mutex.Unlock()
return
}
cg.isRunning = true
cg.mutex.Unlock()
log.Printf("Config generator started, checking every %v", interval)
if err := os.MkdirAll(cg.confDir, 0755); err != nil {
log.Printf("Failed to create conf directory: %v", err)
return
}
ticker := time.NewTicker(interval)
defer ticker.Stop()
if err := cg.generateConfig(); err != nil {
log.Printf("Initial config generation failed: %v", err)
}
for {
select {
case <-ticker.C:
if err := cg.generateConfigWithRetry(); err != nil { // Use retry version
log.Printf("Config generation failed: %v", err)
}
case <-cg.stopChan:
log.Println("Config generator stopped")
return
}
}
}
// Add this helper function at the top of the file with other utility functions
func normalizeServiceID(id string) string {
// Extract the base name (everything before the first @)
baseName := id
if idx := strings.Index(id, "@"); idx > 0 {
baseName = id[:idx]
}
return baseName
}
// Stop stops the config generator
func (cg *ConfigGenerator) Stop() {
cg.mutex.Lock()
defer cg.mutex.Unlock()
if !cg.isRunning {
return
}
close(cg.stopChan)
cg.isRunning = false
}
func (cg *ConfigGenerator) generateConfigWithRetry() error {
maxRetries := 3
baseDelay := 1 * time.Second
for attempt := 0; attempt < maxRetries; attempt++ {
err := cg.generateConfig()
if err == nil {
return nil
}
// Check if it's a database locked error
if strings.Contains(strings.ToLower(err.Error()), "database is locked") {
if attempt < maxRetries-1 {
delay := baseDelay * time.Duration(1<<attempt) // Exponential backoff
log.Printf("⚠️ Database locked on attempt %d, retrying in %v", attempt+1, delay)
time.Sleep(delay)
continue
}
}
return err
}
return fmt.Errorf("config generation failed after %d attempts", maxRetries)
}
// generateConfig generates Traefik configuration files
// NOTE: Only middlewares are written to the override file.
// Routers and services come from Pangolin API, not the override file.
func (cg *ConfigGenerator) generateConfig() error {
if shouldLog() {
log.Println("Generating Traefik configuration...")
}
config := TraefikConfig{}
config.HTTP.Middlewares = make(map[string]interface{})
config.HTTP.Routers = make(map[string]interface{})
config.HTTP.Services = make(map[string]interface{})
config.TCP.Routers = make(map[string]interface{})
config.TCP.Services = make(map[string]interface{})
config.UDP.Services = make(map[string]interface{})
config.TLS.Options = make(map[string]interface{})
// Only process middlewares - routers and services come from Pangolin API
if err := cg.processMiddlewares(&config); err != nil {
return fmt.Errorf("failed to process middlewares: %w", err)
}
// Process mTLS options (adds TLS options and mtls-auth middleware)
if err := cg.processMTLSOptions(&config); err != nil {
return fmt.Errorf("failed to process mTLS options: %w", err)
}
// NOTE: We do NOT process services, HTTP routers, or TCP routers here.
// These are managed by Pangolin API and should not be in the override file.
processedConfig := preserveTraefikValues(config)
yamlNode := &yaml.Node{}
err := yamlNode.Encode(processedConfig)
if err != nil {
return fmt.Errorf("failed to encode config to YAML node: %w", err)
}
preserveStringsInYamlNode(yamlNode)
yamlData, err := yaml.Marshal(yamlNode)
if err != nil {
return fmt.Errorf("failed to marshal YAML node: %w", err)
}
if cg.hasConfigurationChanged(yamlData) {
if err := cg.writeConfigToFile(yamlData); err != nil {
return fmt.Errorf("failed to write config to file: %w", err)
}
// Keep this - user wants to know when config actually changes
log.Printf("Generated new Traefik configuration at %s", filepath.Join(cg.confDir, "resource-overrides.yml"))
} else {
// REPLACE: log.Println("Configuration unchanged, skipping file write")
if shouldLog() {
log.Println("Configuration unchanged, skipping file write")
}
}
return nil
}
func (cg *ConfigGenerator) processMiddlewares(config *TraefikConfig) error {
rows, err := cg.db.Query("SELECT id, name, type, config FROM middlewares")
if err != nil {
return fmt.Errorf("failed to fetch middlewares: %w", err)
}
defer rows.Close()
for rows.Next() {
var id, name, typ, configStr string
if err := rows.Scan(&id, &name, &typ, &configStr); err != nil {
// REPLACE: log.Printf("Failed to scan middleware: %v", err)
if shouldLog() {
log.Printf("Failed to scan middleware: %v", err)
}
continue
}
var middlewareConfig map[string]interface{}
if err := json.Unmarshal([]byte(configStr), &middlewareConfig); err != nil {
// REPLACE: log.Printf("Failed to parse middleware config for %s: %v", name, err)
if shouldLog() {
log.Printf("Failed to parse middleware config for %s: %v", name, err)
}
continue
}
// Use the centralized processing logic from models package
middlewareConfig = models.ProcessMiddlewareConfig(typ, middlewareConfig)
config.HTTP.Middlewares[name] = map[string]interface{}{
typ: middlewareConfig,
}
}
return rows.Err()
}
func (cg *ConfigGenerator) processServices(config *TraefikConfig) error {
rows, err := cg.db.Query("SELECT id, name, type, config FROM services")
if err != nil {
return fmt.Errorf("failed to fetch services: %w", err)
}
defer rows.Close()
for rows.Next() {
var id, name, typ, configStr string
if err := rows.Scan(&id, &name, &typ, &configStr); err != nil {
// REPLACE: log.Printf("Failed to scan service row: %v", err)
if shouldLog() {
log.Printf("Failed to scan service row: %v", err)
}
continue
}
var serviceConfig map[string]interface{}
if err := json.Unmarshal([]byte(configStr), &serviceConfig); err != nil {
// REPLACE: log.Printf("Failed to parse service config for %s: %v", name, err)
if shouldLog() {
log.Printf("Failed to parse service config for %s: %v", name, err)
}
continue
}
// Use the centralized processing logic from models package
serviceConfig = models.ProcessServiceConfig(typ, serviceConfig)
protocol := determineServiceProtocol(typ, serviceConfig)
serviceEntry := map[string]interface{}{typ: serviceConfig}
switch protocol {
case "http":
config.HTTP.Services[id] = serviceEntry
case "tcp":
config.TCP.Services[id] = serviceEntry
case "udp":
config.UDP.Services[id] = serviceEntry
}
}
return rows.Err()
}
// In services/config_generator.go
// processResourcesWithServices processes resources with their assigned services
// Helper function to extract the base name without provider suffixes
func extractBaseName(id string) string {
// If the ID contains @ character, extract the part before it
if idx := strings.Index(id, "@"); idx > 0 {
return id[:idx]
}
return id
}
// processResourcesWithServices processes resources with their assigned services
// processResourcesWithServices processes resources with their assigned services
func (cg *ConfigGenerator) processResourcesWithServices(config *TraefikConfig) error {
activeDSConfig, err := cg.configManager.GetActiveDataSourceConfig()
if err != nil {
if shouldLog() {
log.Printf("Warning: Could not get active data source config in ConfigGenerator: %v. Defaulting to Pangolin logic.", err)
}
activeDSConfig.Type = models.PangolinAPI
}
query := `
SELECT r.id, r.host, r.service_id, r.entrypoints, r.tls_domains,
r.custom_headers, r.router_priority, r.source_type, r.mtls_enabled,
rm.middleware_id, rm.priority, m.name as middleware_name,
rs.service_id as custom_service_id
FROM resources r
LEFT JOIN resource_middlewares rm ON r.id = rm.resource_id
LEFT JOIN middlewares m ON rm.middleware_id = m.id
LEFT JOIN resource_services rs ON r.id = rs.resource_id
WHERE r.status = 'active'
ORDER BY r.id, rm.priority DESC
`
rows, err := cg.db.Query(query)
if err != nil {
return fmt.Errorf("failed to fetch resources for HTTP routers: %w", err)
}
defer rows.Close()
type resourceProcessedData struct {
Info models.Resource
Middlewares []MiddlewareWithPriority
CustomServiceID sql.NullString
}
resourceDataMap := make(map[string]resourceProcessedData)
for rows.Next() {
var rID_db, host_db, serviceID_db, entrypoints_db, tlsDomains_db, customHeadersStr_db, sourceType_db string
var routerPriority_db sql.NullInt64
var mtlsEnabled_db int
var middlewareID_db sql.NullString
var middlewarePriority_db sql.NullInt64
var middlewareName_db sql.NullString
var customServiceID_db sql.NullString
err := rows.Scan(
&rID_db, &host_db, &serviceID_db, &entrypoints_db, &tlsDomains_db,
&customHeadersStr_db, &routerPriority_db, &sourceType_db, &mtlsEnabled_db,
&middlewareID_db, &middlewarePriority_db, &middlewareName_db, &customServiceID_db,
)
if err != nil {
log.Printf("Failed to scan resource data for HTTP router: %v", err)
continue
}
data, exists := resourceDataMap[rID_db]
if !exists {
data.Info = models.Resource{
ID: rID_db,
Host: host_db,
ServiceID: serviceID_db,
Entrypoints: entrypoints_db,
TLSDomains: tlsDomains_db,
CustomHeaders: customHeadersStr_db,
SourceType: sourceType_db,
MTLSEnabled: mtlsEnabled_db == 1,
}
if routerPriority_db.Valid {
data.Info.RouterPriority = int(routerPriority_db.Int64)
} else {
data.Info.RouterPriority = 100 // Default
}
data.CustomServiceID = customServiceID_db
}
if middlewareID_db.Valid {
mwPriority := 100
if middlewarePriority_db.Valid {
mwPriority = int(middlewarePriority_db.Int64)
}
mwName := middlewareID_db.String // fallback to ID if name not available
if middlewareName_db.Valid && middlewareName_db.String != "" {
mwName = middlewareName_db.String
}
data.Middlewares = append(data.Middlewares, MiddlewareWithPriority{
ID: middlewareID_db.String,
Name: mwName,
Priority: mwPriority,
})
}
resourceDataMap[rID_db] = data
}
if err = rows.Err(); err != nil {
return fmt.Errorf("error iterating resource rows for HTTP: %w", err)
}
for _, mapValueDataEntry := range resourceDataMap {
info := mapValueDataEntry.Info
assignedMiddlewares := mapValueDataEntry.Middlewares
sort.SliceStable(assignedMiddlewares, func(i, j int) bool {
return assignedMiddlewares[i].Priority > assignedMiddlewares[j].Priority
})
routerEntryPoints := strings.Split(strings.TrimSpace(info.Entrypoints), ",")
if len(routerEntryPoints) == 0 || (len(routerEntryPoints) == 1 && routerEntryPoints[0] == "") {
routerEntryPoints = []string{"websecure"}
}
var customHeadersMiddlewareID string
if info.CustomHeaders != "" && info.CustomHeaders != "{}" && info.CustomHeaders != "null" {
var headersMap map[string]string
if err := json.Unmarshal([]byte(info.CustomHeaders), &headersMap); err == nil && len(headersMap) > 0 {
middlewareName := fmt.Sprintf("%s-customheaders", info.ID)
customRequestHeadersMap := make(map[string]string)
for k, v := range headersMap {
customRequestHeadersMap[k] = v
}
config.HTTP.Middlewares[middlewareName] = map[string]interface{}{
"headers": map[string]interface{}{"customRequestHeaders": customRequestHeadersMap},
}
customHeadersMiddlewareID = fmt.Sprintf("%s@file", middlewareName)
} else if err != nil {
log.Printf("Failed to parse custom headers for resource %s: %v. Headers: %s", info.ID, err, info.CustomHeaders)
}
}
var finalMiddlewares []string
if customHeadersMiddlewareID != "" {
finalMiddlewares = append(finalMiddlewares, customHeadersMiddlewareID)
}
for _, mw := range assignedMiddlewares {
middlewareName := extractBaseName(mw.Name)
finalMiddlewares = append(finalMiddlewares, fmt.Sprintf("%s@file", middlewareName))
}
// Only add the badger middleware when using Pangolin data source
if activeDSConfig.Type == models.PangolinAPI {
isBadgerPresent := false
for _, m := range finalMiddlewares {
if m == "badger@http" {
isBadgerPresent = true
break
}
}
if !isBadgerPresent {
finalMiddlewares = append(finalMiddlewares, "badger@http")
}
}
var serviceReference string
if mapValueDataEntry.CustomServiceID.Valid && mapValueDataEntry.CustomServiceID.String != "" {
baseName := normalizeServiceID(mapValueDataEntry.CustomServiceID.String)
serviceReference = fmt.Sprintf("%s@file", baseName)
} else {
if strings.Contains(info.ServiceID, "@") {
serviceReference = info.ServiceID
} else {
providerSuffix := "docker"
if activeDSConfig.Type != models.TraefikAPI {
providerSuffix = "http"
}
baseName := normalizeServiceID(info.ServiceID)
serviceReference = fmt.Sprintf("%s@%s", baseName, providerSuffix)
}
}
log.Printf("Resource %s (HTTP): Router service set to %s. (SourceType: %s, ActiveDS: %s, CustomSvc: %s)",
info.ID,
serviceReference,
info.SourceType,
activeDSConfig.Type,
mapValueDataEntry.CustomServiceID.String)
routerIDBase := extractBaseName(info.ID)
var routerIDForTraefik string
if strings.HasSuffix(routerIDBase, "-auth") {
routerIDForTraefik = routerIDBase
} else {
routerIDForTraefik = fmt.Sprintf("%s-auth", routerIDBase)
}
routerConfig := map[string]interface{}{
"rule": fmt.Sprintf("Host(`%s`)", info.Host),
"service": serviceReference,
"entryPoints": routerEntryPoints,
"priority": info.RouterPriority,
}
if len(finalMiddlewares) > 0 {
routerConfig["middlewares"] = finalMiddlewares
}
tlsConfig := map[string]interface{}{"certResolver": "letsencrypt"}
if info.TLSDomains != "" {
sans := strings.Split(strings.TrimSpace(info.TLSDomains), ",")
var cleanSans []string
for _, s := range sans {
if trimmed := strings.TrimSpace(s); trimmed != "" {
cleanSans = append(cleanSans, trimmed)
}
}
if len(cleanSans) > 0 {
tlsConfig["domains"] = []map[string]interface{}{{"main": info.Host, "sans": cleanSans}}
}
}
// Add mTLS middleware and TLS options if enabled for this resource
if info.MTLSEnabled {
// Add TLS options reference for mTLS verification
tlsConfig["options"] = "mtls-verify@file"
// Prepend mTLS middleware to run first
finalMiddlewares = append([]string{"mtls-auth@file"}, finalMiddlewares...)
routerConfig["middlewares"] = finalMiddlewares
}
routerConfig["tls"] = tlsConfig
config.HTTP.Routers[routerIDForTraefik] = routerConfig
}
return nil
}
// processMTLSOptions adds TLS options and mtlswhitelist middleware for mTLS if globally enabled
func (cg *ConfigGenerator) processMTLSOptions(config *TraefikConfig) error {
// Check if mTLS is globally enabled and get all config values
var enabled int
var caCertPath string
var middlewareRules, middlewareRequestHeaders, middlewareRejectMessage sql.NullString
var middlewareRefreshInterval sql.NullInt64
err := cg.db.QueryRow(`
SELECT enabled, ca_cert_path, middleware_rules, middleware_request_headers,
middleware_reject_message, middleware_refresh_interval
FROM mtls_config WHERE id = 1
`).Scan(&enabled, &caCertPath, &middlewareRules, &middlewareRequestHeaders,
&middlewareRejectMessage, &middlewareRefreshInterval)
if err != nil {
if err == sql.ErrNoRows {
// No mTLS config, skip
return nil
}
return fmt.Errorf("failed to check mTLS config: %w", err)
}
// If mTLS is not enabled, don't add config
if enabled != 1 {
return nil
}
// If no CA cert path configured, skip
if caCertPath == "" {
log.Printf("Warning: mTLS enabled but no CA certificate path configured")
return nil
}
// 1. Add TLS options with VerifyClientCertIfGiven
// This allows clients WITHOUT certs to still connect (plugin handles validation)
config.TLS.Options["mtls-verify"] = map[string]interface{}{
"clientAuth": map[string]interface{}{
"caFiles": []string{caCertPath},
"clientAuthType": "VerifyClientCertIfGiven",
},
"minVersion": "VersionTLS12",
"sniStrict": true,
}
// 2. Add the mtls-auth middleware using mtlswhitelist plugin
pluginConfig := map[string]interface{}{
"caFiles": []string{caCertPath},
}
// Add optional plugin configuration if set
if middlewareRules.Valid && middlewareRules.String != "" {
// Parse rules JSON array
var rules []interface{}
if err := json.Unmarshal([]byte(middlewareRules.String), &rules); err == nil && len(rules) > 0 {
pluginConfig["rules"] = rules
}
}
if middlewareRequestHeaders.Valid && middlewareRequestHeaders.String != "" {
// Parse request headers JSON object
var headers map[string]interface{}
if err := json.Unmarshal([]byte(middlewareRequestHeaders.String), &headers); err == nil && len(headers) > 0 {
pluginConfig["requestHeaders"] = headers
}
}
if middlewareRejectMessage.Valid && middlewareRejectMessage.String != "" {
pluginConfig["rejectMessage"] = map[string]interface{}{
"message": middlewareRejectMessage.String,
"code": 403,
}
}
if middlewareRefreshInterval.Valid && middlewareRefreshInterval.Int64 > 0 {
pluginConfig["refreshInterval"] = fmt.Sprintf("%ds", middlewareRefreshInterval.Int64)
}
config.HTTP.Middlewares["mtls-auth"] = map[string]interface{}{
"plugin": map[string]interface{}{
"mtlswhitelist": pluginConfig,
},
}
if shouldLog() {
log.Printf("Added mTLS TLS options and mtls-auth middleware with CA cert: %s", caCertPath)
}
return nil
}
// Helper to fetch service names from Traefik API
func (cg *ConfigGenerator) fetchTraefikServiceNames() map[string]string {
serviceMap := make(map[string]string)
client := &http.Client{Timeout: 5 * time.Second}
// Get Traefik API URL from data source config
dsConfig, err := cg.configManager.GetActiveDataSourceConfig()
if err != nil {
log.Printf("Warning: Failed to get active data source config: %v", err)
return serviceMap
}
apiURL := dsConfig.URL
// Fetch HTTP services
resp, err := client.Get(apiURL + "/api/http/services")
if err != nil {
log.Printf("Warning: Failed to fetch services from Traefik API: %v", err)
return serviceMap
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
log.Printf("Warning: Traefik API returned status %d", resp.StatusCode)
return serviceMap
}
var services []struct {
Name string `json:"name"`
}
if err := json.NewDecoder(resp.Body).Decode(&services); err != nil {
log.Printf("Warning: Failed to decode Traefik API response: %v", err)
return serviceMap
}
// Build a map of base name -> full name with provider
for _, svc := range services {
baseName := normalizeServiceID(svc.Name)
serviceMap[baseName] = svc.Name
}
return serviceMap
}
// processTCPRouters processes TCP router resources
func (cg *ConfigGenerator) processTCPRouters(config *TraefikConfig) error {
activeDSConfig, err := cg.configManager.GetActiveDataSourceConfig()
if err != nil {
log.Printf("Warning: Could not get active data source config for TCP routers: %v. Defaulting to Pangolin logic.", err)
activeDSConfig.Type = models.PangolinAPI
}
query := `
SELECT r.id, r.host, r.service_id, r.tcp_entrypoints, r.tcp_sni_rule, r.router_priority, r.source_type,
rs.service_id as custom_service_id
FROM resources r
LEFT JOIN resource_services rs ON r.id = rs.resource_id
WHERE r.status = 'active' AND r.tcp_enabled = 1
`
rows, err := cg.db.Query(query)
if err != nil {
return fmt.Errorf("failed to fetch TCP resources: %w", err)
}
defer rows.Close()
for rows.Next() {
var id, host, serviceID, tcpEntrypointsStr, tcpSNIRule, sourceType string
var routerPriority sql.NullInt64
var customServiceID sql.NullString
if err := rows.Scan(&id, &host, &serviceID, &tcpEntrypointsStr, &tcpSNIRule, &routerPriority, &sourceType, &customServiceID); err != nil {
log.Printf("Failed to scan TCP resource: %v", err)
continue
}
priority := 100
if routerPriority.Valid {
priority = int(routerPriority.Int64)
}
entrypoints := strings.Split(strings.TrimSpace(tcpEntrypointsStr), ",")
if len(entrypoints) == 0 || entrypoints[0] == "" {
entrypoints = []string{"tcp"} // Default TCP entrypoint
}
rule := tcpSNIRule
if rule == "" { // Default SNI rule if not specified
rule = fmt.Sprintf("HostSNI(`%s`)", host)
}
var tcpServiceReference string
if customServiceID.Valid && customServiceID.String != "" {
// Extract base name without any suffixes
baseName := normalizeServiceID(customServiceID.String)
// Always add the file provider for custom services
tcpServiceReference = fmt.Sprintf("%s@file", baseName)
} else {
// Default provider suffix
providerSuffix := "http"
// If using Traefik API, consider using docker for appropriate sources
if activeDSConfig.Type == models.TraefikAPI {
if models.DataSourceType(sourceType) == models.TraefikAPI {
providerSuffix = "docker"
}
}
// Extract base name without any suffixes
baseName := normalizeServiceID(serviceID)
// Add the appropriate provider suffix
tcpServiceReference = fmt.Sprintf("%s@%s", baseName, providerSuffix)
}
log.Printf("Resource %s (TCP): Router service set to %s. (SourceType: %s, ActiveDS: %s, CustomSvc: %s)",
id, tcpServiceReference, sourceType, activeDSConfig.Type, customServiceID.String)
// Make sure we don't have duplicated suffixes in router ID
routerIDBase := extractBaseName(id)
tcpRouterID := fmt.Sprintf("%s-tcp", routerIDBase)
config.TCP.Routers[tcpRouterID] = map[string]interface{}{
"rule": rule,
"service": tcpServiceReference,
"entryPoints": entrypoints,
"priority": priority,
"tls": map[string]interface{}{}, // TCP routers with SNI usually involve TLS
}
}
return rows.Err()
}
// --- Helper functions (isNumeric, preserveStringsInYamlNode, preserveTraefikValues, etc.) ---
// These should be mostly the same as previously provided, ensure `models.ProcessMiddlewareConfig`
// and `models.ProcessServiceConfig` are used where appropriate for type-specific logic.
func (cg *ConfigGenerator) hasConfigurationChanged(newConfig []byte) bool {
if cg.lastConfig == nil || len(cg.lastConfig) != len(newConfig) || string(cg.lastConfig) != string(newConfig) {
cg.lastConfig = make([]byte, len(newConfig))
copy(cg.lastConfig, newConfig)
return true
}
return false
}
func (cg *ConfigGenerator) writeConfigToFile(yamlData []byte) error {
configFile := filepath.Join(cg.confDir, "resource-overrides.yml")
tempFile := configFile + ".tmp"
if err := os.WriteFile(tempFile, yamlData, 0644); err != nil {
return fmt.Errorf("failed to write temp config file: %w", err)
}
return os.Rename(tempFile, configFile)
}
// MiddlewareWithPriority represents a middleware with its priority value
type MiddlewareWithPriority struct {
ID string
Name string
Priority int
}
func stringSliceContains(slice []string, str string) bool {
for _, s := range slice {
if s == str {
return true
}
}
return false
}
func determineServiceProtocol(serviceType string, config map[string]interface{}) string {
if serviceType == string(models.LoadBalancerType) {
if servers, ok := config["servers"].([]interface{}); ok {
for _, s := range servers {
if serverMap, ok := s.(map[string]interface{}); ok {
if _, hasAddress := serverMap["address"]; hasAddress {
// Could be TCP or UDP. Default to TCP.
// UDP services might need more specific markers or be handled by a separate UDP services map in TraefikConfig
return "tcp"
}
if _, hasURL := serverMap["url"]; hasURL {
return "http"
}
}
}
}
}
// For weighted, mirroring, failover, they reference other services.
// The protocol is typically determined by the nature of those referenced services.
// For simplicity here, assume HTTP if not explicitly a loadbalancer with address.
return "http"
}
func preserveStringsInYamlNode(node *yaml.Node) {
if node == nil {
return
}
switch node.Kind {
case yaml.DocumentNode, yaml.SequenceNode:
for i := range node.Content {
preserveStringsInYamlNode(node.Content[i])
}
case yaml.MappingNode:
for i := 0; i < len(node.Content); i += 2 {
keyNode := node.Content[i]
valueNode := node.Content[i+1]
if (keyNode.Value == "Server" || keyNode.Value == "X-Powered-By" || strings.HasPrefix(keyNode.Value, "X-")) &&
valueNode.Kind == yaml.ScalarNode && valueNode.Value == "" {
valueNode.Style = yaml.DoubleQuotedStyle
}
if containsSpecialStringField(keyNode.Value) && valueNode.Kind == yaml.ScalarNode {
valueNode.Style = yaml.DoubleQuotedStyle
}
preserveStringsInYamlNode(keyNode) // Recursive call for key (though keys are usually simple strings)
preserveStringsInYamlNode(valueNode) // Recursive call for value
}
case yaml.ScalarNode:
if node.Value == "" {
node.Style = yaml.DoubleQuotedStyle
} else if isNumericString(node.Value) && len(node.Value) > 5 { // Example condition for large numbers
node.Tag = "!!str" // Force as string if it's a long number that might get scientific notation
}
}
}
func isNumericString(s string) bool {
_, err := strconv.ParseFloat(s, 64)
return err == nil
}
func containsSpecialStringField(fieldName string) bool {
specialFields := []string{
"key", "token", "secret", "apiKey", "Key", "Token", "Secret", "Password", "Pass", "User", "Users",
"regex", "replacement", "Regex", "Path", "path", "scheme", "url", "address",
"prefix", "prefixes", "expression", "rule", "certResolver", "address", "authResponseHeaders",
"customRequestHeaders", "customResponseHeaders", "customFrameOptionsValue", "contentSecurityPolicy",
"referrerPolicy", "permissionsPolicy", "stsSeconds", "excludedIPs", "sourceRange",
"query", "service", "fallback", "flushInterval", "interval", "timeout", // Some of these are durations but might be passed as strings
}
for _, field := range specialFields {
if strings.EqualFold(fieldName, field) || strings.Contains(strings.ToLower(fieldName), strings.ToLower(field)) {
return true
}
}
return false
}
func preserveTraefikValues(data interface{}) interface{} {
// This function is now more about structural integrity than type coercion,
// as specific type processing is handled by models.ProcessMiddlewareConfig and models.ProcessServiceConfig.
// It can still be useful for deeply nested generic maps or arrays if they occur outside of those.
if data == nil {
return nil
}
switch v := data.(type) {
case map[string]interface{}:
for key, val := range v {
v[key] = preserveTraefikValues(val)
}
return v
case []interface{}:
for i, item := range v {
v[i] = preserveTraefikValues(item)
}
return v
default:
return v // Primitives (string, int, bool, float64) are returned as is.
}
}