feat: support newline and comma-whitespace external host lists for auto-discovery (#468)

This commit is contained in:
Supratim Ghose
2025-10-25 03:18:34 +05:30
committed by GitHub
parent 4b5cb125e4
commit 22ec39b805
6 changed files with 113 additions and 10 deletions
+1 -1
View File
@@ -199,7 +199,7 @@ func (w *dockerWatcherImpl) parseContainerData(container *dockertypes.Container)
Warnf("ignoring container with duplicate %s label", DockerRouterLabelHost)
return
}
data.hosts = strings.Split(value, ",")
data.hosts = SplitExternalHosts(value)
}
if key == DockerRouterLabelPort {
+1 -1
View File
@@ -247,7 +247,7 @@ func (w *dockerSwarmWatcherImpl) parseServiceData(service *swarm.Service, networ
Warnf("ignoring service with duplicate %s", DockerRouterLabelHost)
return
}
data.hosts = strings.Split(value, ",")
data.hosts = SplitExternalHosts(value)
}
if key == DockerRouterLabelPort {
if data.port != 0 {
+5 -5
View File
@@ -3,6 +3,10 @@ package server
import (
"context"
"fmt"
"net"
"strconv"
"sync"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
apps "k8s.io/api/apps/v1"
@@ -14,10 +18,6 @@ import (
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/tools/clientcmd"
"net"
"strconv"
"strings"
"sync"
)
const (
@@ -238,7 +238,7 @@ func (w *K8sWatcher) extractRoutableServices(obj interface{}) []*routableService
routableServices := make([]*routableService, 0)
if externalServiceName, exists := service.Annotations[AnnotationExternalServerName]; exists {
serviceNames := strings.Split(externalServiceName, ",")
serviceNames := SplitExternalHosts(externalServiceName)
for _, serviceName := range serviceNames {
routableServices = append(routableServices, w.buildDetails(service, serviceName))
}
+53
View File
@@ -117,6 +117,59 @@ func TestK8sWatcherImpl_handleAddThenUpdate(t *testing.T) {
},
},
},
{
name: "comma with spaces",
initial: svcAndScenarios{
svc: ` {"metadata": {"annotations": {"mc-router.itzg.me/externalServerName": "a.com, b.com"}}, "spec":{"clusterIP": "1.1.1.1"}}`,
scenarios: []scenario{
{server: "a.com", backend: "1.1.1.1:25565"},
{server: "b.com", backend: "1.1.1.1:25565"},
},
},
update: svcAndScenarios{
svc: ` {"metadata": {"annotations": {"mc-router.itzg.me/externalServerName": "b.com"}}, "spec":{"clusterIP": "1.1.1.1"}}`,
scenarios: []scenario{
{server: "a.com", backend: ""},
{server: "b.com", backend: "1.1.1.1:25565"},
},
},
},
{
name: "newline separated",
initial: svcAndScenarios{
svc: ` {"metadata": {"annotations": {"mc-router.itzg.me/externalServerName": "a.com\nb.com"}}, "spec":{"clusterIP": "1.1.1.1"}}`,
scenarios: []scenario{
{server: "a.com", backend: "1.1.1.1:25565"},
{server: "b.com", backend: "1.1.1.1:25565"},
},
},
update: svcAndScenarios{
svc: ` {"metadata": {"annotations": {"mc-router.itzg.me/externalServerName": "b.com"}}, "spec":{"clusterIP": "1.1.1.1"}}`,
scenarios: []scenario{
{server: "a.com", backend: ""},
{server: "b.com", backend: "1.1.1.1:25565"},
},
},
},
{
name: "mixed comma and newline with spaces",
initial: svcAndScenarios{
svc: ` {"metadata": {"annotations": {"mc-router.itzg.me/externalServerName": "a.com, \nb.com, c.com"}}, "spec":{"clusterIP": "1.1.1.1"}}`,
scenarios: []scenario{
{server: "a.com", backend: "1.1.1.1:25565"},
{server: "b.com", backend: "1.1.1.1:25565"},
{server: "c.com", backend: "1.1.1.1:25565"},
},
},
update: svcAndScenarios{
svc: ` {"metadata": {"annotations": {"mc-router.itzg.me/externalServerName": "b.com"}}, "spec":{"clusterIP": "1.1.1.1"}}`,
scenarios: []scenario{
{server: "a.com", backend: ""},
{server: "b.com", backend: "1.1.1.1:25565"},
{server: "c.com", backend: ""},
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
+38
View File
@@ -0,0 +1,38 @@
package server
import (
"regexp"
"strings"
)
// splitPattern is the regex pattern used to split external host definitions.
// kept as a const so the literal is easy to see and reuse in tests or docs.
const splitPattern = ",|\n"
// splitRe is the compiled regexp for splitPattern. Compiling once at package
// initialization avoids repeated compilation on every call to
// SplitExternalHosts and is slightly more efficient.
var splitRe = regexp.MustCompile(splitPattern)
// SplitExternalHosts splits a string containing external hostnames by comma and/or newline delimiters.
// It trims whitespace around each hostname and filters out empty strings.
// Examples:
// - "host1.com,host2.com" -> ["host1.com", "host2.com"]
// - "host1.com, host2.com" -> ["host1.com", "host2.com"]
// - "host1.com\nhost2.com" -> ["host1.com", "host2.com"]
// - "host1.com,\nhost2.com" -> ["host1.com", "host2.com"]
func SplitExternalHosts(s string) []string {
// Use regexp to split on either comma or newline
parts := splitRe.Split(s, -1)
// Trim whitespace and filter out empty strings
result := make([]string, 0, len(parts))
for _, part := range parts {
trimmed := strings.TrimSpace(part)
if trimmed != "" {
result = append(result, trimmed)
}
}
return result
}