Add support for a default backend service

This commit is contained in:
Geoff Bourne 2019-04-19 20:27:14 -05:00
parent d76e5442da
commit 44a67dd359
5 changed files with 197 additions and 33 deletions

4
.gitignore vendored
View File

@ -1,4 +1,6 @@
/vendor/ /vendor/
/.idea/ /.idea/
/*.iml /*.iml
/mc-router.exe
/mc-router

View File

@ -25,16 +25,24 @@ Flags:
"backend": "HOST:PORT" "backend": "HOST:PORT"
} }
``` ```
* `POST /defaultRoute`
Registers a default route to the given backend. JSON body is structured as:
```json
{
"backend": "HOST:PORT"
}
```
* `DELETE /routes/{serverAddress}` * `DELETE /routes/{serverAddress}`
Deletes an existing route for the given `serverAddress` Deletes an existing route for the given `serverAddress`
## Using kubernetes service auto-discovery ## Using kubernetes service auto-discovery
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 `mc-router.itzg.me/externalServerName`. The value it will automatically watch for any services annotated with
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.
- `mc-router.itzg.me/defaultServer` : The service's clusterIP and target port are used as the default if
no other `externalServiceName` annotations applies.
For example, start `mc-router`'s container spec with For example, start `mc-router`'s container spec with

View File

@ -0,0 +1,94 @@
---
apiVersion: v1
kind: Service
metadata:
name: mc-stable
annotations:
"mc-router.itzg.me/defaultServer": "mc.your.domain"
spec:
ports:
- port: 25565
selector:
run: mc-stable
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
run: mc-stable
name: mc-stable
spec:
selector:
matchLabels:
run: mc-stable
template:
metadata:
labels:
run: mc-stable
spec:
securityContext:
runAsUser: 1000
fsGroup: 1000
containers:
- image: itzg/minecraft-server
name: mc-stable
env:
- name: EULA
value: "TRUE"
ports:
- containerPort: 25565
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
persistentVolumeClaim:
claimName: mc-stable
---
apiVersion: v1
kind: Service
metadata:
name: mc-snapshot
annotations:
"mc-router.itzg.me/externalServerName": "snapshot.your.domain"
spec:
ports:
- port: 25565
selector:
run: mc-snapshot
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
run: mc-snapshot
name: mc-snapshot
spec:
selector:
matchLabels:
run: mc-snapshot
template:
metadata:
labels:
run: mc-snapshot
spec:
securityContext:
runAsUser: 1000
fsGroup: 1000
containers:
- image: itzg/minecraft-server
name: mc-snapshot
env:
- name: EULA
value: "TRUE"
- name: VERSION
value: "SNAPSHOT"
ports:
- containerPort: 25565
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
persistentVolumeClaim:
claimName: mc-snapshot

View File

@ -12,6 +12,11 @@ import (
"net" "net"
) )
const (
AnnotationExternalServerName = "mc-router.itzg.me/externalServerName"
AnnotationDefaultServer = "mc-router.itzg.me/defaultServer"
)
type IK8sWatcher interface { type IK8sWatcher interface {
StartWithConfig(kubeConfigFile string) error StartWithConfig(kubeConfigFile string) error
StartInCluster() error StartInCluster() error
@ -65,7 +70,11 @@ func (w *k8sWatcherImpl) startWithLoadedConfig(config *rest.Config) error {
if routableService != nil { if routableService != nil {
logrus.WithField("routableService", routableService).Debug("ADD") logrus.WithField("routableService", routableService).Debug("ADD")
Routes.CreateMapping(routableService.externalServiceName, routableService.containerEndpoint) if routableService.externalServiceName != "" {
Routes.CreateMapping(routableService.externalServiceName, routableService.containerEndpoint)
} else {
Routes.SetDefaultRoute(routableService.containerEndpoint)
}
} }
}, },
DeleteFunc: func(obj interface{}) { DeleteFunc: func(obj interface{}) {
@ -73,7 +82,11 @@ func (w *k8sWatcherImpl) startWithLoadedConfig(config *rest.Config) error {
if routableService != nil { if routableService != nil {
logrus.WithField("routableService", routableService).Debug("DELETE") logrus.WithField("routableService", routableService).Debug("DELETE")
Routes.DeleteMapping(routableService.externalServiceName) if routableService.externalServiceName != "" {
Routes.DeleteMapping(routableService.externalServiceName)
} else {
Routes.SetDefaultRoute("")
}
} }
}, },
UpdateFunc: func(oldObj, newObj interface{}) { UpdateFunc: func(oldObj, newObj interface{}) {
@ -85,8 +98,12 @@ func (w *k8sWatcherImpl) startWithLoadedConfig(config *rest.Config) error {
"new": newRoutableService, "new": newRoutableService,
}).Debug("UPDATE") }).Debug("UPDATE")
Routes.DeleteMapping(oldRoutableService.externalServiceName) if oldRoutableService.externalServiceName != "" && newRoutableService.externalServiceName != "" {
Routes.CreateMapping(newRoutableService.externalServiceName, newRoutableService.containerEndpoint) Routes.DeleteMapping(oldRoutableService.externalServiceName)
Routes.CreateMapping(newRoutableService.externalServiceName, newRoutableService.containerEndpoint)
} else {
Routes.SetDefaultRoute(newRoutableService.containerEndpoint)
}
} }
}, },
}, },
@ -116,22 +133,28 @@ func extractRoutableService(obj interface{}) *routableService {
return nil return nil
} }
if externalServiceName, exists := service.Annotations["mc-router.itzg.me/externalServerName"]; exists { if externalServiceName, exists := service.Annotations[AnnotationExternalServerName]; exists {
clusterIp := service.Spec.ClusterIP return buildDetails(service, externalServiceName)
port := "25565" } else if _, exists := service.Annotations[AnnotationDefaultServer]; exists {
for _, p := range service.Spec.Ports { return buildDetails(service, "")
if p.Port == 25565 {
if p.TargetPort.String() != "" {
port = p.TargetPort.String()
}
}
}
rs := &routableService{
externalServiceName: externalServiceName,
containerEndpoint: net.JoinHostPort(clusterIp, port),
}
return rs
} }
return nil return nil
} }
func buildDetails(service *v1.Service, externalServiceName string) *routableService {
clusterIp := service.Spec.ClusterIP
port := "25565"
for _, p := range service.Spec.Ports {
if p.Port == 25565 {
if p.TargetPort.String() != "" {
port = p.TargetPort.String()
}
}
}
rs := &routableService{
externalServiceName: externalServiceName,
containerEndpoint: net.JoinHostPort(clusterIp, port),
}
return rs
}

View File

@ -1,11 +1,11 @@
package server package server
import ( import (
"sync"
"net/http"
"encoding/json" "encoding/json"
"github.com/sirupsen/logrus"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/sirupsen/logrus"
"net/http"
"sync"
) )
func init() { func init() {
@ -15,6 +15,9 @@ func init() {
apiRoutes.Path("/routes").Methods("POST"). apiRoutes.Path("/routes").Methods("POST").
Headers("Content-Type", "application/json"). Headers("Content-Type", "application/json").
HandlerFunc(routesCreateHandler) HandlerFunc(routesCreateHandler)
apiRoutes.Path("/defaultRoute").Methods("POST").
Headers("Content-Type", "application/json").
HandlerFunc(routesSetDefault)
apiRoutes.Path("/routes/{serverAddress}").Methods("DELETE").HandlerFunc(routesDeleteHandler) apiRoutes.Path("/routes/{serverAddress}").Methods("DELETE").HandlerFunc(routesDeleteHandler)
} }
@ -43,7 +46,7 @@ func routesDeleteHandler(writer http.ResponseWriter, request *http.Request) {
func routesCreateHandler(writer http.ResponseWriter, request *http.Request) { func routesCreateHandler(writer http.ResponseWriter, request *http.Request) {
var definition = struct { var definition = struct {
ServerAddress string ServerAddress string
Backend string Backend string
}{} }{}
defer request.Body.Close() defer request.Body.Close()
@ -60,6 +63,25 @@ func routesCreateHandler(writer http.ResponseWriter, request *http.Request) {
writer.WriteHeader(http.StatusCreated) writer.WriteHeader(http.StatusCreated)
} }
func routesSetDefault(writer http.ResponseWriter, request *http.Request) {
var body = struct {
Backend string
}{}
defer request.Body.Close()
decoder := json.NewDecoder(request.Body)
err := decoder.Decode(&body)
if err != nil {
logrus.WithError(err).Error("Unable to parse request")
writer.WriteHeader(http.StatusBadRequest)
return
}
Routes.SetDefaultRoute(body.Backend)
writer.WriteHeader(http.StatusOK)
}
type IRoutes interface { 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.
@ -68,6 +90,7 @@ type IRoutes interface {
GetMappings() map[string]string GetMappings() map[string]string
DeleteMapping(serverAddress string) bool DeleteMapping(serverAddress string) bool
CreateMapping(serverAddress string, backend string) CreateMapping(serverAddress string, backend string)
SetDefaultRoute(backend string)
} }
var Routes IRoutes = &routesImpl{} var Routes IRoutes = &routesImpl{}
@ -81,7 +104,16 @@ func (r *routesImpl) RegisterAll(mappings map[string]string) {
type routesImpl struct { type routesImpl struct {
sync.RWMutex sync.RWMutex
mappings map[string]string mappings map[string]string
defaultRoute string
}
func (r *routesImpl) SetDefaultRoute(backend string) {
r.defaultRoute = backend
logrus.WithFields(logrus.Fields{
"backend": backend,
}).Info("Using default route")
} }
func (r *routesImpl) FindBackendForServerAddress(serverAddress string) string { func (r *routesImpl) FindBackendForServerAddress(serverAddress string) string {
@ -89,9 +121,14 @@ func (r *routesImpl) FindBackendForServerAddress(serverAddress string) string {
defer r.RUnlock() defer r.RUnlock()
if r.mappings == nil { if r.mappings == nil {
return "" return r.defaultRoute
} else { } else {
return r.mappings[serverAddress]
if route, exists := r.mappings[serverAddress]; exists {
return route
} else {
return r.defaultRoute
}
} }
} }
@ -125,7 +162,7 @@ func (r *routesImpl) CreateMapping(serverAddress string, backend string) {
logrus.WithFields(logrus.Fields{ logrus.WithFields(logrus.Fields{
"serverAddress": serverAddress, "serverAddress": serverAddress,
"backend": backend, "backend": backend,
}).Info("Creating route") }).Info("Creating route")
r.mappings[serverAddress] = backend r.mappings[serverAddress] = backend
} }