service-mismatch

This commit is contained in:
hhftechnologies 2026-01-23 13:37:23 +05:30
parent e578ec6a81
commit 84101c4018
6 changed files with 1554 additions and 1508 deletions

View file

@ -6,10 +6,12 @@ import (
"fmt"
"log"
"net/http"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/hhftechnology/middleware-manager/models"
"github.com/hhftechnology/middleware-manager/util"
)
// ServiceHandler handles service-related requests
@ -149,7 +151,7 @@ func (h *ServiceHandler) CreateService(c *gin.Context) {
ResponseWithError(c, http.StatusInternalServerError, "Database error")
return
}
// If something goes wrong, rollback
var txErr error
defer func() {
@ -158,21 +160,21 @@ func (h *ServiceHandler) CreateService(c *gin.Context) {
log.Printf("Transaction rolled back due to error: %v", txErr)
}
}()
log.Printf("Attempting to insert service with ID=%s, name=%s, type=%s",
log.Printf("Attempting to insert service with ID=%s, name=%s, type=%s",
id, service.Name, service.Type)
result, txErr := tx.Exec(
"INSERT INTO services (id, name, type, config, status, source_type) VALUES (?, ?, ?, ?, 'active', 'manual')",
id, service.Name, service.Type, string(configJSON),
)
if txErr != nil {
log.Printf("Error inserting service: %v", txErr)
ResponseWithError(c, http.StatusInternalServerError, "Failed to save service")
return
}
rowsAffected, err := result.RowsAffected()
if err == nil {
log.Printf("Insert affected %d rows", rowsAffected)
@ -205,11 +207,7 @@ func (h *ServiceHandler) GetService(c *gin.Context) {
return
}
var name, typ, configStr, status, sourceType string
err := h.DB.QueryRow(
"SELECT name, type, config, COALESCE(status, 'active'), COALESCE(source_type, '') FROM services WHERE id = ?",
id,
).Scan(&name, &typ, &configStr, &status, &sourceType)
rec, err := h.findServiceByID(id)
if err == sql.ErrNoRows {
ResponseWithError(c, http.StatusNotFound, "Service not found")
return
@ -220,18 +218,18 @@ func (h *ServiceHandler) GetService(c *gin.Context) {
}
var config map[string]interface{}
if err := json.Unmarshal([]byte(configStr), &config); err != nil {
if err := json.Unmarshal([]byte(rec.Config), &config); err != nil {
log.Printf("Error parsing service config: %v", err)
config = map[string]interface{}{}
}
c.JSON(http.StatusOK, gin.H{
"id": id,
"name": name,
"type": typ,
"id": rec.ID,
"name": rec.Name,
"type": rec.Type,
"config": config,
"status": status,
"source_type": sourceType,
"status": rec.Status,
"source_type": rec.SourceType,
})
}
@ -260,9 +258,7 @@ func (h *ServiceHandler) UpdateService(c *gin.Context) {
return
}
// Check if service exists
var exists int
err := h.DB.QueryRow("SELECT 1 FROM services WHERE id = ?", id).Scan(&exists)
rec, err := h.findServiceByID(id)
if err == sql.ErrNoRows {
ResponseWithError(c, http.StatusNotFound, "Service not found")
return
@ -290,7 +286,7 @@ func (h *ServiceHandler) UpdateService(c *gin.Context) {
ResponseWithError(c, http.StatusInternalServerError, "Database error")
return
}
// If something goes wrong, rollback
var txErr error
defer func() {
@ -299,21 +295,21 @@ func (h *ServiceHandler) UpdateService(c *gin.Context) {
log.Printf("Transaction rolled back due to error: %v", txErr)
}
}()
log.Printf("Attempting to update service %s with name=%s, type=%s",
log.Printf("Attempting to update service %s with name=%s, type=%s",
id, service.Name, service.Type)
result, txErr := tx.Exec(
"UPDATE services SET name = ?, type = ?, config = ?, updated_at = ? WHERE id = ?",
service.Name, service.Type, string(configJSON), time.Now(), id,
service.Name, service.Type, string(configJSON), time.Now(), rec.ID,
)
if txErr != nil {
log.Printf("Error updating service: %v", txErr)
ResponseWithError(c, http.StatusInternalServerError, "Failed to update service")
return
}
rowsAffected, err := result.RowsAffected()
if err == nil {
log.Printf("Update affected %d rows", rowsAffected)
@ -321,7 +317,7 @@ func (h *ServiceHandler) UpdateService(c *gin.Context) {
log.Printf("Warning: Update query succeeded but no rows were affected")
}
}
// Commit the transaction
if txErr = tx.Commit(); txErr != nil {
log.Printf("Error committing transaction: %v", txErr)
@ -331,18 +327,18 @@ func (h *ServiceHandler) UpdateService(c *gin.Context) {
// Double-check that the service was updated
var updatedName string
err = h.DB.QueryRow("SELECT name FROM services WHERE id = ?", id).Scan(&updatedName)
err = h.DB.QueryRow("SELECT name FROM services WHERE id = ?", rec.ID).Scan(&updatedName)
if err != nil {
log.Printf("Warning: Could not verify service update: %v", err)
} else if updatedName != service.Name {
log.Printf("Warning: Name mismatch after update. Expected '%s', got '%s'", service.Name, updatedName)
} else {
log.Printf("Successfully verified service update for %s", id)
log.Printf("Successfully verified service update for %s", rec.ID)
}
// Return the updated service
c.JSON(http.StatusOK, gin.H{
"id": id,
"id": rec.ID,
"name": service.Name,
"type": service.Type,
"config": service.Config,
@ -357,9 +353,19 @@ func (h *ServiceHandler) DeleteService(c *gin.Context) {
return
}
rec, err := h.findServiceByID(id)
if err == sql.ErrNoRows {
ResponseWithError(c, http.StatusNotFound, "Service not found")
return
} else if err != nil {
log.Printf("Error fetching service for delete: %v", err)
ResponseWithError(c, http.StatusInternalServerError, "Database error")
return
}
// Check for dependencies first - resources using this service
var count int
err := h.DB.QueryRow("SELECT COUNT(*) FROM resource_services WHERE service_id = ?", id).Scan(&count)
err = h.DB.QueryRow("SELECT COUNT(*) FROM resource_services WHERE service_id = ?", rec.ID).Scan(&count)
if err != nil {
log.Printf("Error checking service dependencies: %v", err)
ResponseWithError(c, http.StatusInternalServerError, "Database error")
@ -378,7 +384,7 @@ func (h *ServiceHandler) DeleteService(c *gin.Context) {
ResponseWithError(c, http.StatusInternalServerError, "Database error")
return
}
// If something goes wrong, rollback
var txErr error
defer func() {
@ -387,10 +393,10 @@ func (h *ServiceHandler) DeleteService(c *gin.Context) {
log.Printf("Transaction rolled back due to error: %v", txErr)
}
}()
log.Printf("Attempting to delete service %s", id)
result, txErr := tx.Exec("DELETE FROM services WHERE id = ?", id)
log.Printf("Attempting to delete service %s", rec.ID)
result, txErr := tx.Exec("DELETE FROM services WHERE id = ?", rec.ID)
if txErr != nil {
log.Printf("Error deleting service: %v", txErr)
ResponseWithError(c, http.StatusInternalServerError, "Failed to delete service")
@ -410,7 +416,7 @@ func (h *ServiceHandler) DeleteService(c *gin.Context) {
}
// Track deletion to prevent template from being re-created on restart
_, txErr = tx.Exec("INSERT OR REPLACE INTO deleted_templates (id, type) VALUES (?, 'service')", id)
_, txErr = tx.Exec("INSERT OR REPLACE INTO deleted_templates (id, type) VALUES (?, 'service')", rec.ID)
if txErr != nil {
log.Printf("Warning: Failed to track deleted template: %v", txErr)
// Continue anyway - this is not critical
@ -425,7 +431,7 @@ func (h *ServiceHandler) DeleteService(c *gin.Context) {
return
}
log.Printf("Successfully deleted service %s", id)
log.Printf("Successfully deleted service %s", rec.ID)
c.JSON(http.StatusOK, gin.H{"message": "Service deleted successfully"})
}
@ -458,15 +464,15 @@ func (h *ServiceHandler) AssignServiceToResource(c *gin.Context) {
ResponseWithError(c, http.StatusInternalServerError, "Database error")
return
}
// Don't allow attaching services to disabled resources
if status == "disabled" {
ResponseWithError(c, http.StatusBadRequest, "Cannot assign service to a disabled resource")
return
}
// Verify service exists
err = h.DB.QueryRow("SELECT 1 FROM services WHERE id = ?", input.ServiceID).Scan(&exists)
// Verify service exists (supports normalized IDs / provider suffixes)
serviceRec, err := h.findServiceByID(input.ServiceID)
if err == sql.ErrNoRows {
ResponseWithError(c, http.StatusNotFound, "Service not found")
return
@ -483,7 +489,7 @@ func (h *ServiceHandler) AssignServiceToResource(c *gin.Context) {
ResponseWithError(c, http.StatusInternalServerError, "Database error")
return
}
// If something goes wrong, rollback
var txErr error
defer func() {
@ -492,7 +498,7 @@ func (h *ServiceHandler) AssignServiceToResource(c *gin.Context) {
log.Printf("Transaction rolled back due to error: %v", txErr)
}
}()
// First delete any existing relationship
log.Printf("Removing existing service relationship: resource=%s", resourceID)
_, txErr = tx.Exec(
@ -504,26 +510,26 @@ func (h *ServiceHandler) AssignServiceToResource(c *gin.Context) {
ResponseWithError(c, http.StatusInternalServerError, "Database error")
return
}
// Then insert the new relationship
log.Printf("Creating new service relationship: resource=%s, service=%s",
resourceID, input.ServiceID)
resourceID, serviceRec.ID)
result, txErr := tx.Exec(
"INSERT INTO resource_services (resource_id, service_id) VALUES (?, ?)",
resourceID, input.ServiceID,
resourceID, serviceRec.ID,
)
if txErr != nil {
log.Printf("Error assigning service: %v", txErr)
ResponseWithError(c, http.StatusInternalServerError, "Failed to assign service")
return
}
rowsAffected, err := result.RowsAffected()
if err == nil {
log.Printf("Insert affected %d rows", rowsAffected)
}
// Commit the transaction
if txErr = tx.Commit(); txErr != nil {
log.Printf("Error committing transaction: %v", txErr)
@ -532,10 +538,10 @@ func (h *ServiceHandler) AssignServiceToResource(c *gin.Context) {
}
log.Printf("Successfully assigned service %s to resource %s",
input.ServiceID, resourceID)
serviceRec.ID, resourceID)
c.JSON(http.StatusOK, gin.H{
"resource_id": resourceID,
"service_id": input.ServiceID,
"service_id": serviceRec.ID,
})
}
@ -556,7 +562,7 @@ func (h *ServiceHandler) RemoveServiceFromResource(c *gin.Context) {
ResponseWithError(c, http.StatusInternalServerError, "Database error")
return
}
// If something goes wrong, rollback
var txErr error
defer func() {
@ -565,12 +571,12 @@ func (h *ServiceHandler) RemoveServiceFromResource(c *gin.Context) {
log.Printf("Transaction rolled back due to error: %v", txErr)
}
}()
result, txErr := tx.Exec(
"DELETE FROM resource_services WHERE resource_id = ?",
resourceID,
)
if txErr != nil {
log.Printf("Error removing service: %v", txErr)
ResponseWithError(c, http.StatusInternalServerError, "Failed to remove service")
@ -583,15 +589,15 @@ func (h *ServiceHandler) RemoveServiceFromResource(c *gin.Context) {
ResponseWithError(c, http.StatusInternalServerError, "Database error")
return
}
if rowsAffected == 0 {
log.Printf("No service assignment found for resource %s", resourceID)
ResponseWithError(c, http.StatusNotFound, "Resource service relationship not found")
return
}
log.Printf("Delete affected %d rows", rowsAffected)
// Commit the transaction
if txErr = tx.Commit(); txErr != nil {
log.Printf("Error committing transaction: %v", txErr)
@ -649,4 +655,50 @@ func (h *ServiceHandler) GetResourceService(c *gin.Context) {
"config": config,
},
})
}
}
type serviceRecord struct {
ID string
Name string
Type string
Config string
Status string
SourceType string
}
// findServiceByID resolves a service by exact ID, normalized ID, or provider-suffixed variants.
func (h *ServiceHandler) findServiceByID(id string) (serviceRecord, error) {
candidates := []string{id}
normalized := util.NormalizeID(id)
if normalized != id {
candidates = append(candidates, normalized)
}
if !strings.Contains(normalized, "@") {
candidates = append(candidates, normalized+"@%")
}
var rec serviceRecord
for _, candidate := range candidates {
var err error
if strings.Contains(candidate, "%") {
err = h.DB.QueryRow(
"SELECT id, name, type, config, COALESCE(status, 'active'), COALESCE(source_type, '') FROM services WHERE id LIKE ? LIMIT 1",
candidate,
).Scan(&rec.ID, &rec.Name, &rec.Type, &rec.Config, &rec.Status, &rec.SourceType)
} else {
err = h.DB.QueryRow(
"SELECT id, name, type, config, COALESCE(status, 'active'), COALESCE(source_type, '') FROM services WHERE id = ?",
candidate,
).Scan(&rec.ID, &rec.Name, &rec.Type, &rec.Config, &rec.Status, &rec.SourceType)
}
if err == nil {
return rec, nil
}
if err != sql.ErrNoRows {
return serviceRecord{}, err
}
}
return serviceRecord{}, sql.ErrNoRows
}

View file

@ -218,8 +218,9 @@ func loadConfiguration(debug bool) Configuration {
}
return Configuration{
PangolinAPIURL: getEnv("PANGOLIN_API_URL", "http://pangolin:3001/api/v1"),
TraefikAPIURL: getEnv("TRAEFIK_API_URL", "http://host.docker.internal:8080"),
PangolinAPIURL: getEnv("PANGOLIN_API_URL", "http://pangolin:3001/api/v1"),
// Default to in-network Traefik service; host.docker.internal often fails inside containers
TraefikAPIURL: getEnv("TRAEFIK_API_URL", "http://traefik:8080"),
TraefikConfDir: getEnv("TRAEFIK_CONF_DIR", "/conf"),
DBPath: getEnv("DB_PATH", "/data/middleware.db"),
Port: getEnv("PORT", "3456"),

View file

@ -1,328 +1,329 @@
package services
import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"time"
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/hhftechnology/middleware-manager/models"
"github.com/hhftechnology/middleware-manager/models"
)
// ConfigManager manages system configuration
type ConfigManager struct {
configPath string
config models.SystemConfig
mu sync.RWMutex
configPath string
config models.SystemConfig
mu sync.RWMutex
}
// NewConfigManager creates a new config manager
func NewConfigManager(configPath string) (*ConfigManager, error) {
cm := &ConfigManager{
configPath: configPath,
}
if err := cm.loadConfig(); err != nil {
return nil, err
}
return cm, nil
cm := &ConfigManager{
configPath: configPath,
}
if err := cm.loadConfig(); err != nil {
return nil, err
}
return cm, nil
}
// loadConfig loads configuration from file
func (cm *ConfigManager) loadConfig() error {
cm.mu.Lock()
defer cm.mu.Unlock()
// Check if config file exists
if _, err := os.Stat(cm.configPath); os.IsNotExist(err) {
// Create default config
cm.config = models.SystemConfig{
ActiveDataSource: "pangolin",
DataSources: map[string]models.DataSourceConfig{
"pangolin": {
Type: models.PangolinAPI,
URL: "http://pangolin:3001/api/v1",
},
"traefik": {
Type: models.TraefikAPI,
URL: "http://host.docker.internal:8080",
},
},
}
// Save default config
return cm.saveConfig()
}
// Read config file
data, err := os.ReadFile(cm.configPath)
if err != nil {
return fmt.Errorf("failed to read config file: %w", err)
}
// Parse config
if err := json.Unmarshal(data, &cm.config); err != nil {
return fmt.Errorf("failed to parse config: %w", err)
}
return nil
cm.mu.Lock()
defer cm.mu.Unlock()
// Check if config file exists
if _, err := os.Stat(cm.configPath); os.IsNotExist(err) {
// Create default config
cm.config = models.SystemConfig{
ActiveDataSource: "pangolin",
DataSources: map[string]models.DataSourceConfig{
"pangolin": {
Type: models.PangolinAPI,
URL: "http://pangolin:3001/api/v1",
},
"traefik": {
Type: models.TraefikAPI,
URL: "http://host.docker.internal:8080",
},
},
}
// Save default config
return cm.saveConfig()
}
// Read config file
data, err := os.ReadFile(cm.configPath)
if err != nil {
return fmt.Errorf("failed to read config file: %w", err)
}
// Parse config
if err := json.Unmarshal(data, &cm.config); err != nil {
return fmt.Errorf("failed to parse config: %w", err)
}
return nil
}
// EnsureDefaultDataSources ensures default data sources are configured
func (cm *ConfigManager) EnsureDefaultDataSources(pangolinURL, traefikURL string) error {
cm.mu.Lock()
defer cm.mu.Unlock()
cm.mu.Lock()
defer cm.mu.Unlock()
// Ensure data sources map exists
if cm.config.DataSources == nil {
cm.config.DataSources = make(map[string]models.DataSourceConfig)
}
// Ensure data sources map exists
if cm.config.DataSources == nil {
cm.config.DataSources = make(map[string]models.DataSourceConfig)
}
// Add default Pangolin data source if not present
if _, exists := cm.config.DataSources["pangolin"]; !exists {
cm.config.DataSources["pangolin"] = models.DataSourceConfig{
Type: models.PangolinAPI,
URL: pangolinURL,
}
} else {
// Ensure Type is set for existing Pangolin config (fix for old configs)
pConfig := cm.config.DataSources["pangolin"]
if pConfig.Type == "" {
pConfig.Type = models.PangolinAPI
if pConfig.URL == "" {
pConfig.URL = pangolinURL
}
cm.config.DataSources["pangolin"] = pConfig
log.Printf("Fixed missing Type for pangolin data source")
}
}
// Add default Pangolin data source if not present
if _, exists := cm.config.DataSources["pangolin"]; !exists {
cm.config.DataSources["pangolin"] = models.DataSourceConfig{
Type: models.PangolinAPI,
URL: pangolinURL,
}
} else {
// Ensure Type is set for existing Pangolin config (fix for old configs)
pConfig := cm.config.DataSources["pangolin"]
if pConfig.Type == "" {
pConfig.Type = models.PangolinAPI
if pConfig.URL == "" {
pConfig.URL = pangolinURL
}
cm.config.DataSources["pangolin"] = pConfig
log.Printf("Fixed missing Type for pangolin data source")
}
}
// Add default Traefik data source if not present
if _, exists := cm.config.DataSources["traefik"]; !exists {
cm.config.DataSources["traefik"] = models.DataSourceConfig{
Type: models.TraefikAPI,
URL: traefikURL,
}
} else {
// Ensure Type is set for existing Traefik config (fix for old configs)
tConfig := cm.config.DataSources["traefik"]
if tConfig.Type == "" {
tConfig.Type = models.TraefikAPI
log.Printf("Fixed missing Type for traefik data source")
}
// Update Traefik URL if provided (could be auto-discovered)
if traefikURL != "" && tConfig.URL != traefikURL {
log.Printf("Updating Traefik URL from %s to %s", tConfig.URL, traefikURL)
tConfig.URL = traefikURL
}
cm.config.DataSources["traefik"] = tConfig
}
// Ensure there's an active data source
if cm.config.ActiveDataSource == "" {
cm.config.ActiveDataSource = "pangolin"
}
// Try to determine if Traefik is available
if cm.config.ActiveDataSource == "pangolin" {
client := &http.Client{Timeout: 2 * time.Second}
traefikConfig := cm.config.DataSources["traefik"]
// Try the Traefik URL
resp, err := client.Get(traefikConfig.URL + "/api/version")
if err == nil && resp.StatusCode == http.StatusOK {
resp.Body.Close()
// Traefik is available, but not active - log a message
log.Printf("Note: Traefik API appears to be available at %s but is not the active source", traefikConfig.URL)
}
if resp != nil {
resp.Body.Close()
}
}
// Save the updated configuration
return cm.saveConfig()
// Add default Traefik data source if not present
if _, exists := cm.config.DataSources["traefik"]; !exists {
cm.config.DataSources["traefik"] = models.DataSourceConfig{
Type: models.TraefikAPI,
URL: traefikURL,
}
} else {
// Ensure Type is set for existing Traefik config (fix for old configs)
tConfig := cm.config.DataSources["traefik"]
if tConfig.Type == "" {
tConfig.Type = models.TraefikAPI
log.Printf("Fixed missing Type for traefik data source")
}
// Update Traefik URL if provided (could be auto-discovered)
if traefikURL != "" && tConfig.URL != traefikURL {
log.Printf("Updating Traefik URL from %s to %s", tConfig.URL, traefikURL)
tConfig.URL = traefikURL
}
cm.config.DataSources["traefik"] = tConfig
}
// Ensure there's an active data source
if cm.config.ActiveDataSource == "" {
cm.config.ActiveDataSource = "pangolin"
}
// Try to determine if Traefik is available
if cm.config.ActiveDataSource == "pangolin" {
client := &http.Client{Timeout: 2 * time.Second}
traefikConfig := cm.config.DataSources["traefik"]
// Try the Traefik URL
resp, err := client.Get(traefikConfig.URL + "/api/version")
if err == nil && resp.StatusCode == http.StatusOK {
resp.Body.Close()
// Traefik is available, but not active - log a message
log.Printf("Note: Traefik API appears to be available at %s but is not the active source", traefikConfig.URL)
}
if resp != nil {
resp.Body.Close()
}
}
// Save the updated configuration
return cm.saveConfig()
}
// saveConfig saves configuration to file
func (cm *ConfigManager) saveConfig() error {
// Create directory if it doesn't exist
dir := filepath.Dir(cm.configPath)
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("failed to create config directory: %w", err)
}
// Marshal config to JSON
data, err := json.MarshalIndent(cm.config, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal config: %w", err)
}
// Write config file
if err := os.WriteFile(cm.configPath, data, 0644); err != nil {
return fmt.Errorf("failed to write config file: %w", err)
}
return nil
// Create directory if it doesn't exist
dir := filepath.Dir(cm.configPath)
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("failed to create config directory: %w", err)
}
// Marshal config to JSON
data, err := json.MarshalIndent(cm.config, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal config: %w", err)
}
// Write config file
if err := os.WriteFile(cm.configPath, data, 0644); err != nil {
return fmt.Errorf("failed to write config file: %w", err)
}
return nil
}
// GetActiveDataSourceConfig returns the active data source configuration
func (cm *ConfigManager) GetActiveDataSourceConfig() (models.DataSourceConfig, error) {
cm.mu.RLock()
defer cm.mu.RUnlock()
cm.mu.RLock()
defer cm.mu.RUnlock()
dsName := cm.config.ActiveDataSource
ds, ok := cm.config.DataSources[dsName]
if !ok {
return models.DataSourceConfig{}, fmt.Errorf("active data source not found: %s", dsName)
}
dsName := cm.config.ActiveDataSource
ds, ok := cm.config.DataSources[dsName]
if !ok {
return models.DataSourceConfig{}, fmt.Errorf("active data source not found: %s", dsName)
}
// Fallback: infer Type from name if empty (for old configs)
if ds.Type == "" {
switch dsName {
case "pangolin":
ds.Type = models.PangolinAPI
case "traefik":
ds.Type = models.TraefikAPI
default:
return models.DataSourceConfig{}, fmt.Errorf("unknown data source type for: %s", dsName)
}
}
// Fallback: infer Type from name if empty (for old configs)
if ds.Type == "" {
switch dsName {
case "pangolin":
ds.Type = models.PangolinAPI
case "traefik":
ds.Type = models.TraefikAPI
default:
return models.DataSourceConfig{}, fmt.Errorf("unknown data source type for: %s", dsName)
}
}
return ds, nil
return ds, nil
}
// GetActiveSourceName returns the name of the active data source
func (cm *ConfigManager) GetActiveSourceName() string {
cm.mu.RLock()
defer cm.mu.RUnlock()
return cm.config.ActiveDataSource
cm.mu.RLock()
defer cm.mu.RUnlock()
return cm.config.ActiveDataSource
}
// SetActiveDataSource sets the active data source
func (cm *ConfigManager) SetActiveDataSource(name string) error {
cm.mu.Lock()
defer cm.mu.Unlock()
if _, ok := cm.config.DataSources[name]; !ok {
return fmt.Errorf("data source not found: %s", name)
}
// Skip if already active
if cm.config.ActiveDataSource == name {
return nil
}
// Store the previous active source for logging
oldSource := cm.config.ActiveDataSource
// Update active source
cm.config.ActiveDataSource = name
// Log the change
log.Printf("Changed active data source from %s to %s", oldSource, name)
return cm.saveConfig()
cm.mu.Lock()
defer cm.mu.Unlock()
if _, ok := cm.config.DataSources[name]; !ok {
return fmt.Errorf("data source not found: %s", name)
}
// Skip if already active
if cm.config.ActiveDataSource == name {
return nil
}
// Store the previous active source for logging
oldSource := cm.config.ActiveDataSource
// Update active source
cm.config.ActiveDataSource = name
// Log the change
log.Printf("Changed active data source from %s to %s", oldSource, name)
return cm.saveConfig()
}
// GetDataSources returns all configured data sources
func (cm *ConfigManager) GetDataSources() map[string]models.DataSourceConfig {
cm.mu.RLock()
defer cm.mu.RUnlock()
// Return a copy to prevent map mutation
sources := make(map[string]models.DataSourceConfig)
for k, v := range cm.config.DataSources {
sources[k] = v
}
return sources
cm.mu.RLock()
defer cm.mu.RUnlock()
// Return a copy to prevent map mutation
sources := make(map[string]models.DataSourceConfig)
for k, v := range cm.config.DataSources {
sources[k] = v
}
return sources
}
// UpdateDataSource updates a data source configuration
func (cm *ConfigManager) UpdateDataSource(name string, config models.DataSourceConfig) error {
cm.mu.Lock()
defer cm.mu.Unlock()
// Create a copy to avoid reference issues
newConfig := config
// Ensure URL doesn't end with a slash
if newConfig.URL != "" && strings.HasSuffix(newConfig.URL, "/") {
newConfig.URL = strings.TrimSuffix(newConfig.URL, "/")
}
// Test the connection before saving
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := cm.testDataSourceConnection(ctx, newConfig); err != nil {
log.Printf("Warning: Data source connection test failed: %v", err)
// Continue anyway but log the warning
}
// Update the config
cm.config.DataSources[name] = newConfig
// If this is the active data source, log a special message
if cm.config.ActiveDataSource == name {
log.Printf("Updated active data source '%s'", name)
}
return cm.saveConfig()
cm.mu.Lock()
defer cm.mu.Unlock()
// Create a copy to avoid reference issues
newConfig := config
// Ensure URL doesn't end with a slash
if newConfig.URL != "" && strings.HasSuffix(newConfig.URL, "/") {
newConfig.URL = strings.TrimSuffix(newConfig.URL, "/")
}
// Test the connection before saving
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := cm.testDataSourceConnection(ctx, newConfig); err != nil {
log.Printf("Warning: Data source connection test failed: %v", err)
// Continue anyway but log the warning
}
// Update the config
cm.config.DataSources[name] = newConfig
// If this is the active data source, log a special message
if cm.config.ActiveDataSource == name {
log.Printf("Updated active data source '%s'", name)
}
return cm.saveConfig()
}
// testDataSourceConnection tests the connection to a data source
func (cm *ConfigManager) testDataSourceConnection(ctx context.Context, config models.DataSourceConfig) error {
client := &http.Client{
Timeout: 5 * time.Second,
}
var url string
switch config.Type {
case models.PangolinAPI:
url = config.URL + "/status"
case models.TraefikAPI:
url = config.URL + "/api/version"
default:
return fmt.Errorf("unsupported data source type: %s", config.Type)
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
// Add basic auth if configured
if config.BasicAuth.Username != "" {
req.SetBasicAuth(config.BasicAuth.Username, config.BasicAuth.Password)
}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("connection test failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return fmt.Errorf("connection test failed with status code: %d", resp.StatusCode)
}
return nil
client := &http.Client{
Timeout: 5 * time.Second,
}
var url string
switch config.Type {
case models.PangolinAPI:
// Use the same health-check endpoint as API handler; Pangolin does not expose /status
url = config.URL + "/traefik-config"
case models.TraefikAPI:
url = config.URL + "/api/version"
default:
return fmt.Errorf("unsupported data source type: %s", config.Type)
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
// Add basic auth if configured
if config.BasicAuth.Username != "" {
req.SetBasicAuth(config.BasicAuth.Username, config.BasicAuth.Password)
}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("connection test failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return fmt.Errorf("connection test failed with status code: %d", resp.StatusCode)
}
return nil
}
// TestDataSourceConnection is a public method to test a connection
func (cm *ConfigManager) TestDataSourceConnection(config models.DataSourceConfig) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
return cm.testDataSourceConnection(ctx, config)
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
return cm.testDataSourceConnection(ctx, config)
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -130,10 +130,10 @@ func (f *TraefikFetcher) fetchResourcesInternal(ctx context.Context) (*models.Re
// Try common fallback URLs
fallbackURLs := []string{
"http://host.docker.internal:8080",
"http://traefik:8080",
"http://localhost:8080",
"http://127.0.0.1:8080",
"http://traefik:8080",
"http://host.docker.internal:8080",
}
// Don't try the same URL twice