diff --git a/README.md b/README.md index 7ee0c34..53feead 100644 --- a/README.md +++ b/README.md @@ -161,7 +161,7 @@ When using in Docker, make sure to volume mount the Docker socket into the conta These are the labels scanned: -- `mc-router.host`: Used to configure the hostname the Minecraft clients would use to connect to the server. The container/service endpoint will be used as the routed backend. You can use more than one hostname by splitting it with a comma. +- `mc-router.host`: Used to configure the hostname the Minecraft clients would use to connect to the server. The container/service endpoint will be used as the routed backend. You can use more than one hostname by splitting it with a comma or newline. Whitespace around commas is automatically trimmed. For example: `"host1.com,host2.com"`, `"host1.com, host2.com"`, or `"host1.com\nhost2.com"`. - `mc-router.port`: This value must be set to the port the Minecraft server is listening on. The default value is 25565. - `mc-router.default`: Set this to a truthy value to make this server the default backend. Please note that `mc-router.host` is still required to be set. - `mc-router.network`: Specify the network you are using for the router if multiple are present in the container/service. You can either use the network ID, it's full name or an alias. @@ -234,7 +234,7 @@ For more information on the allow/deny list configuration, see the [json schema] ### Using Kubernetes Service auto-discovery When running `mc-router` as a Kubernetes Pod and you pass the `--in-kube-cluster` command-line argument, then it will automatically watch for any services annotated with -- `mc-router.itzg.me/externalServerName` : The value of the annotation will be registered as the external hostname Minecraft clients would used to connect to the routed service. The service is used as the routed backend. You can use more hostnames by splitting them with comma. +- `mc-router.itzg.me/externalServerName` : The value of the annotation will be registered as the external hostname Minecraft clients would used to connect to the routed service. The service is used as the routed backend. You can use more hostnames by splitting them with comma or newline. Whitespace around commas is automatically trimmed. For example: `"host1.com,host2.com"`, `"host1.com, host2.com"`, or multi-line values. - `mc-router.itzg.me/defaultServer` : When set to "true", the service is used as the default if no other `externalServiceName` annotations applies. By default, the router will watch all namespaces for those services; however, a specific namespace can be specified using the `KUBE_NAMESPACE` environment variable. The pod's own namespace could be set using: @@ -275,6 +275,18 @@ metadata: annotations: "mc-router.itzg.me/externalServerName": "external.host.name,other.host.name" ``` +or with newlines and optional whitespace: + +```yaml +apiVersion: v1 +kind: Service +metadata: + name: mc-forge + annotations: + "mc-router.itzg.me/externalServerName": | + external.host.name + other.host.name +``` The `Role` or `ClusterRole` bound to the service account should have the rules: @@ -625,4 +637,4 @@ docker run -it --rm \ ## Related Projects -* https://github.com/haveachin/infrared +* https://github.com/haveachin/infrared \ No newline at end of file diff --git a/server/docker.go b/server/docker.go index 84b03c1..f3765f6 100644 --- a/server/docker.go +++ b/server/docker.go @@ -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 { diff --git a/server/docker_swarm.go b/server/docker_swarm.go index 3d4265a..8ddf053 100644 --- a/server/docker_swarm.go +++ b/server/docker_swarm.go @@ -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 { diff --git a/server/k8s.go b/server/k8s.go index 3eae748..be32640 100644 --- a/server/k8s.go +++ b/server/k8s.go @@ -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)) } diff --git a/server/k8s_test.go b/server/k8s_test.go index 7ec48a5..4dc1a48 100644 --- a/server/k8s_test.go +++ b/server/k8s_test.go @@ -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) { diff --git a/server/utils.go b/server/utils.go new file mode 100644 index 0000000..7638cd8 --- /dev/null +++ b/server/utils.go @@ -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 +}