@@ -810,6 +810,135 @@ func TestGenerateConfig_PatternHostname(t *testing.T) {
810810 assert .Contains (t , configStr , "http.request.host.labels" )
811811}
812812
813+ func TestGenerateConfig_TLSHostnameDeduplication (t * testing.T ) {
814+ // Test that duplicate TLS hostnames (same hostname on different ports) don't cause
815+ // Caddy to fail with "cannot apply more than one automation policy to host"
816+ tmpDir , err := os .MkdirTemp ("" , "ingress-config-tls-dedup-test-*" )
817+ require .NoError (t , err )
818+ defer os .RemoveAll (tmpDir )
819+
820+ p := paths .New (tmpDir )
821+ require .NoError (t , os .MkdirAll (p .CaddyDir (), 0755 ))
822+ require .NoError (t , os .MkdirAll (p .CaddyDataDir (), 0755 ))
823+
824+ // Create generator with ACME configured
825+ acmeConfig := ACMEConfig {
826+ Email : "admin@example.com" ,
827+ DNSProvider : DNSProviderCloudflare ,
828+ CloudflareAPIToken : "test-token" ,
829+ AllowedDomains : "*.example.com" ,
830+ }
831+ generator := NewCaddyConfigGenerator (p , "0.0.0.0" , "127.0.0.1" , 2019 , acmeConfig , APIIngressConfig {}, 5353 )
832+
833+ ctx := context .Background ()
834+ // Create two ingresses with the same wildcard hostname pattern on different ports
835+ ingresses := []Ingress {
836+ {
837+ ID : "ing-port-443" ,
838+ Name : "wildcard-443" ,
839+ Rules : []IngressRule {
840+ {
841+ Match : IngressMatch {Hostname : "{instance}.example.com" , Port : 443 },
842+ Target : IngressTarget {Instance : "{instance}" , Port : 80 },
843+ TLS : true ,
844+ },
845+ },
846+ },
847+ {
848+ ID : "ing-port-3000" ,
849+ Name : "wildcard-3000" ,
850+ Rules : []IngressRule {
851+ {
852+ Match : IngressMatch {Hostname : "{instance}.example.com" , Port : 3000 },
853+ Target : IngressTarget {Instance : "{instance}" , Port : 3000 },
854+ TLS : true ,
855+ },
856+ },
857+ },
858+ {
859+ ID : "ing-port-8080" ,
860+ Name : "wildcard-8080" ,
861+ Rules : []IngressRule {
862+ {
863+ Match : IngressMatch {Hostname : "{instance}.example.com" , Port : 8080 },
864+ Target : IngressTarget {Instance : "{instance}" , Port : 8080 },
865+ TLS : true ,
866+ },
867+ },
868+ },
869+ }
870+
871+ data , err := generator .GenerateConfig (ctx , ingresses )
872+ require .NoError (t , err )
873+
874+ // Parse the config to verify TLS subjects are deduplicated
875+ var config map [string ]interface {}
876+ err = json .Unmarshal (data , & config )
877+ require .NoError (t , err )
878+
879+ // Navigate to TLS automation policies
880+ apps := config ["apps" ].(map [string ]interface {})
881+ tlsApp := apps ["tls" ].(map [string ]interface {})
882+ automation := tlsApp ["automation" ].(map [string ]interface {})
883+ policies := automation ["policies" ].([]interface {})
884+
885+ require .Len (t , policies , 1 , "should have exactly one policy" )
886+
887+ policy := policies [0 ].(map [string ]interface {})
888+ subjects := policy ["subjects" ].([]interface {})
889+
890+ // Should have only ONE entry for *.example.com (deduplicated)
891+ assert .Len (t , subjects , 1 , "TLS subjects should be deduplicated" )
892+ assert .Equal (t , "*.example.com" , subjects [0 ].(string ))
893+
894+ // Verify all three ports are in listen addresses
895+ configStr := string (data )
896+ assert .Contains (t , configStr , ":443" )
897+ assert .Contains (t , configStr , ":3000" )
898+ assert .Contains (t , configStr , ":8080" )
899+ }
900+
901+ func TestDeduplicateStrings (t * testing.T ) {
902+ tests := []struct {
903+ name string
904+ input []string
905+ expected []string
906+ }{
907+ {
908+ name : "empty" ,
909+ input : []string {},
910+ expected : []string {},
911+ },
912+ {
913+ name : "no duplicates" ,
914+ input : []string {"a" , "b" , "c" },
915+ expected : []string {"a" , "b" , "c" },
916+ },
917+ {
918+ name : "with duplicates" ,
919+ input : []string {"a" , "b" , "a" , "c" , "b" },
920+ expected : []string {"a" , "b" , "c" },
921+ },
922+ {
923+ name : "all same" ,
924+ input : []string {"x" , "x" , "x" },
925+ expected : []string {"x" },
926+ },
927+ {
928+ name : "preserves order" ,
929+ input : []string {"c" , "a" , "b" , "a" , "c" },
930+ expected : []string {"c" , "a" , "b" },
931+ },
932+ }
933+
934+ for _ , tc := range tests {
935+ t .Run (tc .name , func (t * testing.T ) {
936+ result := deduplicateStrings (tc .input )
937+ assert .Equal (t , tc .expected , result )
938+ })
939+ }
940+ }
941+
813942func TestGenerateConfig_DynamicUpstreams (t * testing.T ) {
814943 // Create temp dir
815944 tmpDir , err := os .MkdirTemp ("" , "ingress-config-dynamic-test-*" )
0 commit comments