Add possibility to use multiple names for one service (#31)

Co-authored-by: Geoff Bourne <itzgeoff@gmail.com>
This commit is contained in:
Bartosz Stefańczyk
2021-12-04 07:25:28 -08:00
committed by GitHub
parent 6bf14043bb
commit 1b1f8e5f22
5 changed files with 257 additions and 46 deletions
+68 -44
View File
@@ -11,6 +11,7 @@ import (
"k8s.io/client-go/tools/clientcmd"
"net"
"strconv"
"strings"
)
const (
@@ -66,47 +67,9 @@ func (w *k8sWatcherImpl) startWithLoadedConfig(config *rest.Config) error {
&v1.Service{},
0,
cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) {
routableService := extractRoutableService(obj)
if routableService != nil {
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)
}
}
},
AddFunc: w.handleAdd,
DeleteFunc: w.handleDelete,
UpdateFunc: w.handleUpdate,
},
)
@@ -117,6 +80,61 @@ func (w *k8sWatcherImpl) startWithLoadedConfig(config *rest.Config) error {
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() {
if w.stop != nil {
w.stop <- struct{}{}
@@ -128,16 +146,22 @@ type routableService struct {
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)
if !ok {
return nil
}
routableServices := make([]*routableService, 0)
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 {
return buildDetails(service, "")
return []*routableService{buildDetails(service, "")}
}
return nil
+172
View 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)
}
})
}
}
+1
View File
@@ -87,6 +87,7 @@ type IRoutes interface {
RegisterAll(mappings map[string]string)
// FindBackendForServerAddress returns the host:port for the external server address, if registered.
// Otherwise, an empty string is returned
// Also returns the normalized version of the given serverAddress
FindBackendForServerAddress(serverAddress string) (string, string)
GetMappings() map[string]string
DeleteMapping(serverAddress string) bool