diff --git a/models/resource.go b/models/resource.go
index 1949211..62f5a47 100644
--- a/models/resource.go
+++ b/models/resource.go
@@ -6,36 +6,36 @@ import (
// Resource represents a Pangolin resource
type Resource struct {
- ID string `json:"id"` // Internal UUID (stable, never changes)
- PangolinRouterID string `json:"pangolin_router_id"` // Pangolin's router ID (can change)
- Host string `json:"host"`
- ServiceID string `json:"service_id"`
- OrgID string `json:"org_id"`
- SiteID string `json:"site_id"`
- Status string `json:"status"`
+ ID string `json:"id"` // Internal UUID (stable, never changes)
+ PangolinRouterID string `json:"pangolin_router_id"` // Pangolin's router ID (can change)
+ Host string `json:"host"`
+ ServiceID string `json:"service_id"`
+ OrgID string `json:"org_id"`
+ SiteID string `json:"site_id"`
+ Status string `json:"status"`
// HTTP router configuration
- Entrypoints string `json:"entrypoints"`
-
+ Entrypoints string `json:"entrypoints"`
+
// TLS certificate configuration
- TLSDomains string `json:"tls_domains"`
-
+ TLSDomains string `json:"tls_domains"`
+
// TCP SNI routing configuration
- TCPEnabled bool `json:"tcp_enabled"`
- TCPEntrypoints string `json:"tcp_entrypoints"`
- TCPSNIRule string `json:"tcp_sni_rule"`
-
+ TCPEnabled bool `json:"tcp_enabled"`
+ TCPEntrypoints string `json:"tcp_entrypoints"`
+ TCPSNIRule string `json:"tcp_sni_rule"`
+
// Custom headers configuration
- CustomHeaders string `json:"custom_headers"`
-
+ CustomHeaders string `json:"custom_headers"`
+
// Router priority configuration
- RouterPriority int `json:"router_priority"`
+ RouterPriority int `json:"router_priority"`
// Source type for tracking data origin
- SourceType string `json:"source_type"`
+ SourceType string `json:"source_type"`
// mTLS configuration
- MTLSEnabled bool `json:"mtls_enabled"`
+ MTLSEnabled bool `json:"mtls_enabled"`
// TLS Hardening configuration (standalone, disabled when mTLS is active)
TLSHardeningEnabled bool `json:"tls_hardening_enabled"`
@@ -43,8 +43,8 @@ type Resource struct {
// Secure Headers configuration
SecureHeadersEnabled bool `json:"secure_headers_enabled"`
- CreatedAt time.Time `json:"created_at"`
- UpdatedAt time.Time `json:"updated_at"`
+ CreatedAt time.Time `json:"created_at"`
+ UpdatedAt time.Time `json:"updated_at"`
}
// PangolinResource represents the format of a resource from Pangolin API
@@ -58,9 +58,10 @@ type PangolinResource struct {
// PangolinTraefikConfig represents the Traefik configuration from Pangolin API
type PangolinTraefikConfig struct {
HTTP struct {
- Routers map[string]PangolinRouter `json:"routers"`
- Services map[string]PangolinService `json:"services"`
- Middlewares map[string]map[string]interface{} `json:"middlewares"`
+ Routers map[string]PangolinRouter `json:"routers"`
+ Services map[string]PangolinService `json:"services"`
+ Middlewares map[string]map[string]interface{} `json:"middlewares"`
+ ServersTransports map[string]interface{} `json:"serversTransports"`
} `json:"http"`
}
@@ -96,7 +97,7 @@ type PangolinServiceConfig struct {
type TraefikService struct {
Name string `json:"name"`
Provider string `json:"provider"`
-
+
// Service types - only one will be populated based on service type
LoadBalancer *struct {
Servers []struct {
@@ -108,7 +109,7 @@ type TraefikService struct {
Sticky interface{} `json:"sticky,omitempty"`
HealthCheck interface{} `json:"healthCheck,omitempty"`
} `json:"loadBalancer,omitempty"`
-
+
Weighted *struct {
Services []struct {
Name string `json:"name"`
@@ -117,10 +118,10 @@ type TraefikService struct {
Sticky interface{} `json:"sticky,omitempty"`
HealthCheck interface{} `json:"healthCheck,omitempty"`
} `json:"weighted,omitempty"`
-
+
Mirroring *struct {
- Service string `json:"service"`
- Mirrors []struct {
+ Service string `json:"service"`
+ Mirrors []struct {
Name string `json:"name"`
Percent int `json:"percent"`
} `json:"mirrors,omitempty"`
@@ -128,10 +129,10 @@ type TraefikService struct {
MirrorBody *bool `json:"mirrorBody,omitempty"`
HealthCheck interface{} `json:"healthCheck,omitempty"`
} `json:"mirroring,omitempty"`
-
+
Failover *struct {
Service string `json:"service"`
Fallback string `json:"fallback"`
HealthCheck interface{} `json:"healthCheck,omitempty"`
} `json:"failover,omitempty"`
-}
\ No newline at end of file
+}
diff --git a/services/config_proxy.go b/services/config_proxy.go
index d7037ea..abfcb3c 100644
--- a/services/config_proxy.go
+++ b/services/config_proxy.go
@@ -28,9 +28,10 @@ type ProxiedTraefikConfig struct {
// HTTPConfig represents HTTP configuration section
type HTTPConfig struct {
- Middlewares map[string]interface{} `json:"middlewares,omitempty"`
- Routers map[string]interface{} `json:"routers,omitempty"`
- Services map[string]interface{} `json:"services,omitempty"`
+ Middlewares map[string]interface{} `json:"middlewares,omitempty"`
+ Routers map[string]interface{} `json:"routers,omitempty"`
+ Services map[string]interface{} `json:"services,omitempty"`
+ ServersTransports map[string]interface{} `json:"serversTransports,omitempty"`
}
// TCPConfig represents TCP configuration section
@@ -53,12 +54,12 @@ type TLSConfig struct {
// OrderedRouter represents a Traefik HTTP router with fields in Pangolin's order.
// The JSON field order matches Pangolin API output for consistency.
type OrderedRouter struct {
- EntryPoints []string `json:"entryPoints,omitempty"`
- Middlewares []string `json:"middlewares,omitempty"`
- Service string `json:"service,omitempty"`
- Rule string `json:"rule,omitempty"`
- Priority int `json:"priority,omitempty"`
- TLS *OrderedTLSConfig `json:"tls,omitempty"`
+ EntryPoints []string `json:"entryPoints,omitempty"`
+ Middlewares []string `json:"middlewares,omitempty"`
+ Service string `json:"service,omitempty"`
+ Rule string `json:"rule,omitempty"`
+ Priority int `json:"priority,omitempty"`
+ TLS *OrderedTLSConfig `json:"tls,omitempty"`
}
// OrderedTLSConfig represents TLS config for a router with Pangolin's field order.
@@ -146,7 +147,7 @@ func NewConfigProxy(db *database.DB, configManager *ConfigManager, pangolinURL s
db: db,
configManager: configManager,
pangolinURL: pangolinURL,
- httpClient: HTTPClientWithTimeout(10 * time.Second),
+ httpClient: HTTPClientWithTimeout(10 * time.Second),
cacheDuration: 5 * time.Second, // Match typical Traefik poll interval
}
}
@@ -266,6 +267,9 @@ func (cp *ConfigProxy) initializeConfigMaps(config *ProxiedTraefikConfig) {
if config.HTTP.Services == nil {
config.HTTP.Services = make(map[string]interface{})
}
+ if config.HTTP.ServersTransports == nil {
+ config.HTTP.ServersTransports = make(map[string]interface{})
+ }
if config.TCP == nil {
config.TCP = &TCPConfig{}
diff --git a/services/config_proxy_test.go b/services/config_proxy_test.go
index e2d0fb1..629c9f9 100644
--- a/services/config_proxy_test.go
+++ b/services/config_proxy_test.go
@@ -55,6 +55,89 @@ func TestConfigProxyCachesAndInvalidates(t *testing.T) {
}
}
+func TestConfigProxyPreservesServersTransports(t *testing.T) {
+ db := newTestDB(t)
+ cm := newTestConfigManager(t)
+
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ _ = json.NewEncoder(w).Encode(map[string]interface{}{
+ "http": map[string]interface{}{
+ "middlewares": map[string]interface{}{},
+ "routers": map[string]interface{}{},
+ "services": map[string]interface{}{
+ "14-example-service": map[string]interface{}{
+ "loadBalancer": map[string]interface{}{
+ "servers": []map[string]interface{}{
+ {"url": "https://10.0.0.1:12345"},
+ },
+ "serversTransport": "14-transport",
+ },
+ },
+ },
+ "serversTransports": map[string]interface{}{
+ "14-transport": map[string]interface{}{
+ "serverName": "example.com",
+ "insecureSkipVerify": true,
+ },
+ },
+ },
+ })
+ }))
+ defer server.Close()
+
+ cp := NewConfigProxy(db, cm, server.URL)
+ cp.httpClient = server.Client()
+
+ config, err := cp.GetMergedConfig()
+ if err != nil {
+ t.Fatalf("GetMergedConfig() error = %v", err)
+ }
+
+ if config.HTTP == nil {
+ t.Fatal("config.HTTP is nil")
+ }
+
+ transportRaw, exists := config.HTTP.ServersTransports["14-transport"]
+ if !exists {
+ t.Fatalf("serversTransport %q was not preserved", "14-transport")
+ }
+
+ transport, ok := transportRaw.(map[string]interface{})
+ if !ok {
+ t.Fatalf("serversTransport has type %T, want map[string]interface{}", transportRaw)
+ }
+
+ if got, ok := transport["serverName"].(string); !ok || got != "example.com" {
+ t.Fatalf("serverName = %#v, want %q", transport["serverName"], "example.com")
+ }
+ if got, ok := transport["insecureSkipVerify"].(bool); !ok || !got {
+ t.Fatalf("insecureSkipVerify = %#v, want true", transport["insecureSkipVerify"])
+ }
+
+ serviceRaw, exists := config.HTTP.Services["14-example-service"]
+ if !exists {
+ t.Fatalf("service %q not found", "14-example-service")
+ }
+ service, ok := serviceRaw.(map[string]interface{})
+ if !ok {
+ t.Fatalf("service has type %T, want map[string]interface{}", serviceRaw)
+ }
+
+ loadBalancerRaw, exists := service["loadBalancer"]
+ if !exists {
+ t.Fatalf("service %q missing loadBalancer", "14-example-service")
+ }
+ loadBalancer, ok := loadBalancerRaw.(map[string]interface{})
+ if !ok {
+ t.Fatalf("loadBalancer has type %T, want map[string]interface{}", loadBalancerRaw)
+ }
+
+ if got, ok := loadBalancer["serversTransport"].(string); !ok || got != "14-transport" {
+ t.Fatalf("loadBalancer.serversTransport = %#v, want %q", loadBalancer["serversTransport"], "14-transport")
+ }
+}
+
func TestConfigGeneratorWritesConfigFile(t *testing.T) {
db := newTestDB(t)
cm := newTestConfigManager(t)
diff --git a/ui/src/components/settings/DataSourceSettings.tsx b/ui/src/components/settings/DataSourceSettings.tsx
index 72970b2..4e5dbae 100644
--- a/ui/src/components/settings/DataSourceSettings.tsx
+++ b/ui/src/components/settings/DataSourceSettings.tsx
@@ -101,7 +101,7 @@ export function DataSourceSettings() {
Data Source Settings