mirror of
https://github.com/itzg/mc-router.git
synced 2024-11-30 12:53:53 +01:00
Add possibility to use multiple names for one service (#31)
Co-authored-by: Geoff Bourne <itzgeoff@gmail.com>
This commit is contained in:
parent
6bf14043bb
commit
1b1f8e5f22
3
.github/workflows/test.yml
vendored
3
.github/workflows/test.yml
vendored
@ -21,6 +21,9 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
go-version: 1.17
|
go-version: 1.17
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: go test ./...
|
||||||
|
|
||||||
- name: Run GoReleaser Snapshot
|
- name: Run GoReleaser Snapshot
|
||||||
uses: goreleaser/goreleaser-action@v2
|
uses: goreleaser/goreleaser-action@v2
|
||||||
with:
|
with:
|
||||||
|
15
README.md
15
README.md
@ -106,7 +106,7 @@ To test out this example, I added these two entries to my "hosts" file:
|
|||||||
When running `mc-router` as a kubernetes pod and you pass the `--in-kube-cluster` command-line argument, then
|
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
|
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
|
- `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's clusterIP and target port are used as the routed backend.
|
routed service. The service's clusterIP and target port are used as the routed backend. You can use more hostnames by splitting them with comma.
|
||||||
- `mc-router.itzg.me/defaultServer` : The service's clusterIP and target port are used as the default if
|
- `mc-router.itzg.me/defaultServer` : The service's clusterIP and target port are used as the default if
|
||||||
no other `externalServiceName` annotations applies.
|
no other `externalServiceName` annotations applies.
|
||||||
|
|
||||||
@ -129,6 +129,17 @@ metadata:
|
|||||||
"mc-router.itzg.me/externalServerName": "external.host.name"
|
"mc-router.itzg.me/externalServerName": "external.host.name"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
you can use multiple host names:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: mc-forge
|
||||||
|
annotations:
|
||||||
|
"mc-router.itzg.me/externalServerName": "external.host.name,other.host.name"
|
||||||
|
```
|
||||||
|
|
||||||
## Example kubernetes deployment
|
## Example kubernetes deployment
|
||||||
|
|
||||||
[This example deployment](docs/k8s-example-auto.yaml)
|
[This example deployment](docs/k8s-example-auto.yaml)
|
||||||
@ -136,7 +147,7 @@ metadata:
|
|||||||
* Declares a service account with access to watch and list services
|
* Declares a service account with access to watch and list services
|
||||||
* Declares `--in-kube-cluster` in the `mc-router` container arguments
|
* Declares `--in-kube-cluster` in the `mc-router` container arguments
|
||||||
* Two "backend" Minecraft servers are declared each with an
|
* Two "backend" Minecraft servers are declared each with an
|
||||||
`"mc-router.itzg.me/externalServerName"` annotation that declares their external server name
|
`"mc-router.itzg.me/externalServerName"` annotation that declares their external server name(s)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
kubectl apply -f https://raw.githubusercontent.com/itzg/mc-router/master/docs/k8s-example-auto.yaml
|
kubectl apply -f https://raw.githubusercontent.com/itzg/mc-router/master/docs/k8s-example-auto.yaml
|
||||||
|
112
server/k8s.go
112
server/k8s.go
@ -11,6 +11,7 @@ import (
|
|||||||
"k8s.io/client-go/tools/clientcmd"
|
"k8s.io/client-go/tools/clientcmd"
|
||||||
"net"
|
"net"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -66,47 +67,9 @@ func (w *k8sWatcherImpl) startWithLoadedConfig(config *rest.Config) error {
|
|||||||
&v1.Service{},
|
&v1.Service{},
|
||||||
0,
|
0,
|
||||||
cache.ResourceEventHandlerFuncs{
|
cache.ResourceEventHandlerFuncs{
|
||||||
AddFunc: func(obj interface{}) {
|
AddFunc: w.handleAdd,
|
||||||
routableService := extractRoutableService(obj)
|
DeleteFunc: w.handleDelete,
|
||||||
if routableService != nil {
|
UpdateFunc: w.handleUpdate,
|
||||||
logrus.WithField("routableService", routableService).Debug("ADD")
|
|
||||||
|
|
||||||
if routableService.externalServiceName != "" {
|
|
||||||
Routes.CreateMapping(routableService.externalServiceName, routableService.containerEndpoint)
|
|
||||||
} else {
|
|
||||||
Routes.SetDefaultRoute(routableService.containerEndpoint)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
DeleteFunc: func(obj interface{}) {
|
|
||||||
routableService := extractRoutableService(obj)
|
|
||||||
if routableService != nil {
|
|
||||||
logrus.WithField("routableService", routableService).Debug("DELETE")
|
|
||||||
|
|
||||||
if routableService.externalServiceName != "" {
|
|
||||||
Routes.DeleteMapping(routableService.externalServiceName)
|
|
||||||
} else {
|
|
||||||
Routes.SetDefaultRoute("")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
UpdateFunc: func(oldObj, newObj interface{}) {
|
|
||||||
oldRoutableService := extractRoutableService(oldObj)
|
|
||||||
newRoutableService := extractRoutableService(newObj)
|
|
||||||
if oldRoutableService != nil && newRoutableService != nil {
|
|
||||||
logrus.WithFields(logrus.Fields{
|
|
||||||
"old": oldRoutableService,
|
|
||||||
"new": newRoutableService,
|
|
||||||
}).Debug("UPDATE")
|
|
||||||
|
|
||||||
if oldRoutableService.externalServiceName != "" && newRoutableService.externalServiceName != "" {
|
|
||||||
Routes.DeleteMapping(oldRoutableService.externalServiceName)
|
|
||||||
Routes.CreateMapping(newRoutableService.externalServiceName, newRoutableService.containerEndpoint)
|
|
||||||
} else {
|
|
||||||
Routes.SetDefaultRoute(newRoutableService.containerEndpoint)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -117,6 +80,61 @@ func (w *k8sWatcherImpl) startWithLoadedConfig(config *rest.Config) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// oldObj and newObj are expected to be *v1.Service
|
||||||
|
func (w *k8sWatcherImpl) handleUpdate(oldObj interface{}, newObj interface{}) {
|
||||||
|
for _, oldRoutableService := range extractRoutableServices(oldObj) {
|
||||||
|
logrus.WithFields(logrus.Fields{
|
||||||
|
"old": oldRoutableService,
|
||||||
|
}).Debug("UPDATE")
|
||||||
|
if oldRoutableService.externalServiceName != "" {
|
||||||
|
Routes.DeleteMapping(oldRoutableService.externalServiceName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, newRoutableService := range extractRoutableServices(newObj) {
|
||||||
|
logrus.WithFields(logrus.Fields{
|
||||||
|
"new": newRoutableService,
|
||||||
|
}).Debug("UPDATE")
|
||||||
|
if newRoutableService.externalServiceName != "" {
|
||||||
|
Routes.CreateMapping(newRoutableService.externalServiceName, newRoutableService.containerEndpoint)
|
||||||
|
} else {
|
||||||
|
Routes.SetDefaultRoute(newRoutableService.containerEndpoint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// obj is expected to be a *v1.Service
|
||||||
|
func (w *k8sWatcherImpl) handleDelete(obj interface{}) {
|
||||||
|
routableServices := extractRoutableServices(obj)
|
||||||
|
for _, routableService := range routableServices {
|
||||||
|
if routableService != nil {
|
||||||
|
logrus.WithField("routableService", routableService).Debug("DELETE")
|
||||||
|
|
||||||
|
if routableService.externalServiceName != "" {
|
||||||
|
Routes.DeleteMapping(routableService.externalServiceName)
|
||||||
|
} else {
|
||||||
|
Routes.SetDefaultRoute("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// obj is expected to be a *v1.Service
|
||||||
|
func (w *k8sWatcherImpl) handleAdd(obj interface{}) {
|
||||||
|
routableServices := extractRoutableServices(obj)
|
||||||
|
for _, routableService := range routableServices {
|
||||||
|
if routableService != nil {
|
||||||
|
logrus.WithField("routableService", routableService).Debug("ADD")
|
||||||
|
|
||||||
|
if routableService.externalServiceName != "" {
|
||||||
|
Routes.CreateMapping(routableService.externalServiceName, routableService.containerEndpoint)
|
||||||
|
} else {
|
||||||
|
Routes.SetDefaultRoute(routableService.containerEndpoint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (w *k8sWatcherImpl) Stop() {
|
func (w *k8sWatcherImpl) Stop() {
|
||||||
if w.stop != nil {
|
if w.stop != nil {
|
||||||
w.stop <- struct{}{}
|
w.stop <- struct{}{}
|
||||||
@ -128,16 +146,22 @@ type routableService struct {
|
|||||||
containerEndpoint string
|
containerEndpoint string
|
||||||
}
|
}
|
||||||
|
|
||||||
func extractRoutableService(obj interface{}) *routableService {
|
// obj is expected to be a *v1.Service
|
||||||
|
func extractRoutableServices(obj interface{}) []*routableService {
|
||||||
service, ok := obj.(*v1.Service)
|
service, ok := obj.(*v1.Service)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
routableServices := make([]*routableService, 0)
|
||||||
if externalServiceName, exists := service.Annotations[AnnotationExternalServerName]; exists {
|
if externalServiceName, exists := service.Annotations[AnnotationExternalServerName]; exists {
|
||||||
return buildDetails(service, externalServiceName)
|
serviceNames := strings.Split(externalServiceName, ",")
|
||||||
|
for _, serviceName := range serviceNames {
|
||||||
|
routableServices = append(routableServices, buildDetails(service, serviceName))
|
||||||
|
}
|
||||||
|
return routableServices
|
||||||
} else if _, exists := service.Annotations[AnnotationDefaultServer]; exists {
|
} else if _, exists := service.Annotations[AnnotationDefaultServer]; exists {
|
||||||
return buildDetails(service, "")
|
return []*routableService{buildDetails(service, "")}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
172
server/k8s_test.go
Normal file
172
server/k8s_test.go
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
v1 "k8s.io/api/core/v1"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestK8sWatcherImpl_handleAddThenUpdate(t *testing.T) {
|
||||||
|
type scenario struct {
|
||||||
|
given string
|
||||||
|
expect string
|
||||||
|
}
|
||||||
|
type svcAndScenarios struct {
|
||||||
|
svc string
|
||||||
|
scenarios []scenario
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
initial svcAndScenarios
|
||||||
|
update svcAndScenarios
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "a to b",
|
||||||
|
initial: svcAndScenarios{
|
||||||
|
svc: ` {"metadata": {"annotations": {"mc-router.itzg.me/externalServerName": "a.com"}}, "spec":{"clusterIP": "1.1.1.1"}}`,
|
||||||
|
scenarios: []scenario{
|
||||||
|
{given: "a.com", expect: "1.1.1.1:25565"},
|
||||||
|
{given: "b.com", expect: ""},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
update: svcAndScenarios{
|
||||||
|
svc: ` {"metadata": {"annotations": {"mc-router.itzg.me/externalServerName": "b.com"}}, "spec":{"clusterIP": "1.1.1.1"}}`,
|
||||||
|
scenarios: []scenario{
|
||||||
|
{given: "a.com", expect: ""},
|
||||||
|
{given: "b.com", expect: "1.1.1.1:25565"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "a to a,b",
|
||||||
|
initial: svcAndScenarios{
|
||||||
|
svc: ` {"metadata": {"annotations": {"mc-router.itzg.me/externalServerName": "a.com"}}, "spec":{"clusterIP": "1.1.1.1"}}`,
|
||||||
|
scenarios: []scenario{
|
||||||
|
{given: "a.com", expect: "1.1.1.1:25565"},
|
||||||
|
{given: "b.com", expect: ""},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
update: svcAndScenarios{
|
||||||
|
svc: ` {"metadata": {"annotations": {"mc-router.itzg.me/externalServerName": "a.com,b.com"}}, "spec":{"clusterIP": "1.1.1.1"}}`,
|
||||||
|
scenarios: []scenario{
|
||||||
|
{given: "a.com", expect: "1.1.1.1:25565"},
|
||||||
|
{given: "b.com", expect: "1.1.1.1:25565"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "a,b to b",
|
||||||
|
initial: svcAndScenarios{
|
||||||
|
svc: ` {"metadata": {"annotations": {"mc-router.itzg.me/externalServerName": "a.com,b.com"}}, "spec":{"clusterIP": "1.1.1.1"}}`,
|
||||||
|
scenarios: []scenario{
|
||||||
|
{given: "a.com", expect: "1.1.1.1:25565"},
|
||||||
|
{given: "b.com", expect: "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{
|
||||||
|
{given: "a.com", expect: ""},
|
||||||
|
{given: "b.com", expect: "1.1.1.1:25565"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
// reset the routes
|
||||||
|
Routes.RegisterAll(map[string]string{})
|
||||||
|
|
||||||
|
watcher := &k8sWatcherImpl{}
|
||||||
|
initialSvc := v1.Service{}
|
||||||
|
err := json.Unmarshal([]byte(test.initial.svc), &initialSvc)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
watcher.handleAdd(&initialSvc)
|
||||||
|
for _, s := range test.initial.scenarios {
|
||||||
|
backend, _ := Routes.FindBackendForServerAddress(s.given)
|
||||||
|
assert.Equal(t, s.expect, backend, "initial: given=%s", s.given)
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedSvc := v1.Service{}
|
||||||
|
err = json.Unmarshal([]byte(test.update.svc), &updatedSvc)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
watcher.handleUpdate(&initialSvc, &updatedSvc)
|
||||||
|
for _, s := range test.update.scenarios {
|
||||||
|
backend, _ := Routes.FindBackendForServerAddress(s.given)
|
||||||
|
assert.Equal(t, s.expect, backend, "update: given=%s", s.given)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestK8sWatcherImpl_handleAddThenDelete(t *testing.T) {
|
||||||
|
type scenario struct {
|
||||||
|
given string
|
||||||
|
expect string
|
||||||
|
}
|
||||||
|
type svcAndScenarios struct {
|
||||||
|
svc string
|
||||||
|
scenarios []scenario
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
initial svcAndScenarios
|
||||||
|
delete []scenario
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "single",
|
||||||
|
initial: svcAndScenarios{
|
||||||
|
svc: ` {"metadata": {"annotations": {"mc-router.itzg.me/externalServerName": "a.com"}}, "spec":{"clusterIP": "1.1.1.1"}}`,
|
||||||
|
scenarios: []scenario{
|
||||||
|
{given: "a.com", expect: "1.1.1.1:25565"},
|
||||||
|
{given: "b.com", expect: ""},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
delete: []scenario{
|
||||||
|
{given: "a.com", expect: ""},
|
||||||
|
{given: "b.com", expect: ""},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multi",
|
||||||
|
initial: svcAndScenarios{
|
||||||
|
svc: ` {"metadata": {"annotations": {"mc-router.itzg.me/externalServerName": "a.com,b.com"}}, "spec":{"clusterIP": "1.1.1.1"}}`,
|
||||||
|
scenarios: []scenario{
|
||||||
|
{given: "a.com", expect: "1.1.1.1:25565"},
|
||||||
|
{given: "b.com", expect: "1.1.1.1:25565"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
delete: []scenario{
|
||||||
|
{given: "a.com", expect: ""},
|
||||||
|
{given: "b.com", expect: ""},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
// reset the routes
|
||||||
|
Routes.RegisterAll(map[string]string{})
|
||||||
|
|
||||||
|
watcher := &k8sWatcherImpl{}
|
||||||
|
initialSvc := v1.Service{}
|
||||||
|
err := json.Unmarshal([]byte(test.initial.svc), &initialSvc)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
watcher.handleAdd(&initialSvc)
|
||||||
|
for _, s := range test.initial.scenarios {
|
||||||
|
backend, _ := Routes.FindBackendForServerAddress(s.given)
|
||||||
|
assert.Equal(t, s.expect, backend, "initial: given=%s", s.given)
|
||||||
|
}
|
||||||
|
|
||||||
|
watcher.handleDelete(&initialSvc)
|
||||||
|
for _, s := range test.delete {
|
||||||
|
backend, _ := Routes.FindBackendForServerAddress(s.given)
|
||||||
|
assert.Equal(t, s.expect, backend, "update: given=%s", s.given)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -87,6 +87,7 @@ type IRoutes interface {
|
|||||||
RegisterAll(mappings map[string]string)
|
RegisterAll(mappings map[string]string)
|
||||||
// FindBackendForServerAddress returns the host:port for the external server address, if registered.
|
// FindBackendForServerAddress returns the host:port for the external server address, if registered.
|
||||||
// Otherwise, an empty string is returned
|
// Otherwise, an empty string is returned
|
||||||
|
// Also returns the normalized version of the given serverAddress
|
||||||
FindBackendForServerAddress(serverAddress string) (string, string)
|
FindBackendForServerAddress(serverAddress string) (string, string)
|
||||||
GetMappings() map[string]string
|
GetMappings() map[string]string
|
||||||
DeleteMapping(serverAddress string) bool
|
DeleteMapping(serverAddress string) bool
|
||||||
|
Loading…
Reference in New Issue
Block a user