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 - Configure your data source connections for Pangolin or Traefik API + Configure your data source connections for Pangolin or Traefik API --Traefik api will not function on pangolin stack/api,dont switch @@ -178,7 +178,7 @@ export function DataSourceSettings() { setEditUrl(e.target.value)} - placeholder="http://localhost:8080" + placeholder="http://gerbil:8080" />