mirror of
https://github.com/hhftechnology/middleware-manager.git
synced 2026-04-28 03:29:42 +00:00
service-mismatch
This commit is contained in:
parent
e578ec6a81
commit
84101c4018
6 changed files with 1554 additions and 1508 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
5
main.go
5
main.go
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue