From 44a67dd359f003767d4e82306f9d8d14821aefc3 Mon Sep 17 00:00:00 2001 From: Geoff Bourne Date: Fri, 19 Apr 2019 20:27:14 -0500 Subject: [PATCH] Add support for a default backend service --- .gitignore | 4 +- README.md | 16 ++++-- docs/k8s-mc-with-default.yaml | 94 +++++++++++++++++++++++++++++++++++ server/k8s.go | 61 ++++++++++++++++------- server/routes.go | 55 ++++++++++++++++---- 5 files changed, 197 insertions(+), 33 deletions(-) create mode 100644 docs/k8s-mc-with-default.yaml diff --git a/.gitignore b/.gitignore index 3b0a980..d9d44aa 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ /vendor/ /.idea/ -/*.iml \ No newline at end of file +/*.iml +/mc-router.exe +/mc-router diff --git a/README.md b/README.md index 35b94f9..775c733 100644 --- a/README.md +++ b/README.md @@ -25,16 +25,24 @@ Flags: "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}` Deletes an existing route for the given `serverAddress` ## 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's clusterIP and target port are used as the routed backend. +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'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 diff --git a/docs/k8s-mc-with-default.yaml b/docs/k8s-mc-with-default.yaml new file mode 100644 index 0000000..92ae741 --- /dev/null +++ b/docs/k8s-mc-with-default.yaml @@ -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 diff --git a/server/k8s.go b/server/k8s.go index 691e45b..6d342f9 100644 --- a/server/k8s.go +++ b/server/k8s.go @@ -12,6 +12,11 @@ import ( "net" ) +const ( + AnnotationExternalServerName = "mc-router.itzg.me/externalServerName" + AnnotationDefaultServer = "mc-router.itzg.me/defaultServer" +) + type IK8sWatcher interface { StartWithConfig(kubeConfigFile string) error StartInCluster() error @@ -65,7 +70,11 @@ func (w *k8sWatcherImpl) startWithLoadedConfig(config *rest.Config) error { if routableService != nil { 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{}) { @@ -73,7 +82,11 @@ func (w *k8sWatcherImpl) startWithLoadedConfig(config *rest.Config) error { if routableService != nil { 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{}) { @@ -85,8 +98,12 @@ func (w *k8sWatcherImpl) startWithLoadedConfig(config *rest.Config) error { "new": newRoutableService, }).Debug("UPDATE") - Routes.DeleteMapping(oldRoutableService.externalServiceName) - Routes.CreateMapping(newRoutableService.externalServiceName, newRoutableService.containerEndpoint) + if oldRoutableService.externalServiceName != "" && newRoutableService.externalServiceName != "" { + 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 } - if externalServiceName, exists := service.Annotations["mc-router.itzg.me/externalServerName"]; exists { - 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 + if externalServiceName, exists := service.Annotations[AnnotationExternalServerName]; exists { + return buildDetails(service, externalServiceName) + } else if _, exists := service.Annotations[AnnotationDefaultServer]; exists { + return buildDetails(service, "") } 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 +} diff --git a/server/routes.go b/server/routes.go index e3081da..4f64db2 100644 --- a/server/routes.go +++ b/server/routes.go @@ -1,11 +1,11 @@ package server import ( - "sync" - "net/http" "encoding/json" - "github.com/sirupsen/logrus" "github.com/gorilla/mux" + "github.com/sirupsen/logrus" + "net/http" + "sync" ) func init() { @@ -15,6 +15,9 @@ func init() { apiRoutes.Path("/routes").Methods("POST"). Headers("Content-Type", "application/json"). HandlerFunc(routesCreateHandler) + apiRoutes.Path("/defaultRoute").Methods("POST"). + Headers("Content-Type", "application/json"). + HandlerFunc(routesSetDefault) 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) { var definition = struct { ServerAddress string - Backend string + Backend string }{} defer request.Body.Close() @@ -60,6 +63,25 @@ func routesCreateHandler(writer http.ResponseWriter, request *http.Request) { 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 { RegisterAll(mappings map[string]string) // FindBackendForServerAddress returns the host:port for the external server address, if registered. @@ -68,6 +90,7 @@ type IRoutes interface { GetMappings() map[string]string DeleteMapping(serverAddress string) bool CreateMapping(serverAddress string, backend string) + SetDefaultRoute(backend string) } var Routes IRoutes = &routesImpl{} @@ -81,7 +104,16 @@ func (r *routesImpl) RegisterAll(mappings map[string]string) { type routesImpl struct { 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 { @@ -89,9 +121,14 @@ func (r *routesImpl) FindBackendForServerAddress(serverAddress string) string { defer r.RUnlock() if r.mappings == nil { - return "" + return r.defaultRoute } 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{ "serverAddress": serverAddress, - "backend": backend, + "backend": backend, }).Info("Creating route") r.mappings[serverAddress] = backend -} \ No newline at end of file +}