Skip to content

Commit

Permalink
Allow endpoints to be bound to alternative HTTP interfaces
Browse files Browse the repository at this point in the history
  • Loading branch information
reinkrul committed Feb 11, 2021
1 parent f40e63e commit dd7fca6
Show file tree
Hide file tree
Showing 15 changed files with 1,240 additions and 802 deletions.
1 change: 1 addition & 0 deletions .codeclimate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ exclude_patterns:
- "**/*_test.go"
- "**/mock/**/*.go"
- "**/mock.go"
- "**/*_mock.go"
- "docs/**/*.go"
- "**/*.pb.go"
# Until network protocol implementation has been spec and optionally replace, ignore those sources
Expand Down
64 changes: 38 additions & 26 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,34 +72,9 @@ func createServerCommand(system *core.System) *cobra.Command {
Run: func(cmd *cobra.Command, args []string) {
// Load all config and add generic options
if err := system.Load(cmd); err != nil {
panic(err)
}

logrus.Info("Starting server with config:")
logrus.Info(system.Config.PrintConfig())

// check config on all engines
if err := system.Configure(); err != nil {
logrus.Fatal(err)
}

// start engines
if err := system.Start(); err != nil {
logrus.Fatal(err)
}

// add routes
echoServer := system.EchoCreator()
for _, r := range system.Routers {
r.Routes(echoServer)
}

defer func() {
if err := system.Shutdown(); err != nil {
logrus.Fatal(err)
}
}()
if err := echoServer.Start(system.Config.Address); err != nil {
if err := startServer(system); err != nil {
logrus.Fatal(err)
}
},
Expand All @@ -108,6 +83,43 @@ func createServerCommand(system *core.System) *cobra.Command {
return cmd
}

func startServer(system *core.System) error {
logrus.Info("Starting server with config:")
logrus.Info(system.Config.PrintConfig())

// check config on all engines
if err := system.Configure(); err != nil {
return err
}

// start engines
if err := system.Start(); err != nil {
return err
}

// init HTTP interfaces and routes
echoServer := core.NewMultiEcho(system.EchoCreator, system.Config.HTTP.HTTPConfig)
for httpGroup, httpConfig := range system.Config.HTTP.AltBinds {
logrus.Infof("Binding /%s -> %s", httpGroup, httpConfig.Address)
if err := echoServer.Bind(httpGroup, httpConfig); err != nil {
return err
}
}
for _, r := range system.Routers {
r.Routes(echoServer)
}

defer func() {
if err := system.Shutdown(); err != nil {
logrus.Fatal(err)
}
}()
if err := echoServer.Start(); err != nil {
return err
}
return nil
}

// CreateCommand creates the command with all subcommands to run the system.
func CreateCommand(system *core.System) *cobra.Command {
command := createRootCommand()
Expand Down
94 changes: 94 additions & 0 deletions cmd/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package cmd

import (
"bytes"
"errors"
"github.com/labstack/echo/v4"
"os"
"testing"

Expand Down Expand Up @@ -38,7 +40,10 @@ func Test_rootCmd(t *testing.T) {
assert.Contains(t, actual, "Current system config")
assert.Contains(t, actual, "address")
})
}


func Test_serverCmd(t *testing.T) {
t.Run("start in server mode", func(t *testing.T) {
ctrl := gomock.NewController(t)
echoServer := core.NewMockEchoServer(ctrl)
Expand Down Expand Up @@ -66,6 +71,49 @@ func Test_rootCmd(t *testing.T) {
// Assert engine config is injected
assert.Equal(t, testDirectory, m.TestConfig.Datadir)
})
t.Run("defaults and alt binds are used", func(t *testing.T) {
var echoServers []*stubEchoServer
system := CreateSystem()
system.EchoCreator = func() core.EchoServer {
s := &stubEchoServer{}
echoServers = append(echoServers, s)
return s
}
system.Config = core.NewServerConfig()
system.Config.Datadir = io.TestDirectory(t)
system.Config.HTTP.AltBinds["internal"] = core.HTTPConfig{Address: "localhost:7642"}
err := startServer(system)
if !assert.NoError(t, err) {
return
}
assert.Len(t, echoServers, 2)
assert.Equal(t, system.Config.HTTP.Address, echoServers[0].address)
assert.Equal(t, "localhost:7642", echoServers[1].address)
})
t.Run("unable to configure system", func(t *testing.T) {
system := core.NewSystem()
system.Config = core.NewServerConfig()
system.Config.Datadir = "root_test.go"
err := startServer(system)
assert.Error(t, err, "unable to start")
})
t.Run("alt binds error", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()

echoServer := core.NewMockEchoServer(ctrl)
echoServer.EXPECT().Start(gomock.Any()).Return(errors.New("unable to start"))

system := core.NewSystem()
system.EchoCreator = func() core.EchoServer {
return echoServer
}
system.Config = core.NewServerConfig()
system.Config.Datadir = io.TestDirectory(t)
system.Config.HTTP.AltBinds["internal"] = core.HTTPConfig{Address: "localhost:7642"}
err := startServer(system)
assert.EqualError(t, err, "unable to start")
})
}

func Test_CreateSystem(t *testing.T) {
Expand All @@ -77,3 +125,49 @@ func Test_CreateSystem(t *testing.T) {
})
assert.Equal(t, 5, numEngines)
}

type stubEchoServer struct {
address string
}

func (s stubEchoServer) CONNECT(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route {
return nil
}

func (s stubEchoServer) DELETE(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route {
return nil
}

func (s stubEchoServer) GET(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route {
return nil
}

func (s stubEchoServer) HEAD(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route {
return nil
}

func (s stubEchoServer) OPTIONS(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route {
return nil
}

func (s stubEchoServer) PATCH(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route {
return nil
}

func (s stubEchoServer) POST(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route {
return nil
}

func (s stubEchoServer) PUT(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route {
return nil
}

func (s stubEchoServer) TRACE(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route {
return nil
}

func (s *stubEchoServer) Start(address string) error {
s.address = address
return nil
}

1 change: 1 addition & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ ignore:
- "**/*_test.go"
- "mock/**/*.go"
- "**/mock.go"
- "**/*_mock.go"
- "docs/**/*.go"
- "**/*.pb.go"
# Until network protocol implementation has been spec and optionally replace, ignore those sources
Expand Down
154 changes: 154 additions & 0 deletions core/echo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package core

import (
"fmt"
"github.com/labstack/echo/v4"
"strings"
)

// EchoServer implements both the EchoRouter interface and Start function to aid testing.
type EchoServer interface {
EchoRouter
Start(address string) error
}

// EchoRouter is the interface the generated server API's will require as the Routes func argument
type EchoRouter interface {
CONNECT(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
DELETE(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
GET(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
HEAD(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
OPTIONS(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
PATCH(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
POST(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
PUT(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
TRACE(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route
}

const defaultEchoGroup = ""

// NewMultiEcho creates a new MultiEcho which uses the given function to create EchoServers. If a route is registered
// for an unknown group is is bound to the given defaultInterface.
func NewMultiEcho(creatorFn func() EchoServer, defaultInterface HTTPConfig) *MultiEcho {
instance := &MultiEcho{
interfaces: map[string]EchoServer{},
groups: map[string]string{},
creatorFn: creatorFn,
}
_ = instance.Bind(defaultEchoGroup, defaultInterface)
return instance
}

// MultiEcho allows to bind specific URLs to specific HTTP interfaces
type MultiEcho struct {
interfaces map[string]EchoServer
groups map[string]string
creatorFn func() EchoServer
}

// Bind binds the given group (first part of the URL) to the given HTTP interface. Calling Bind for the same group twice
// results in an error being returned.
func (c *MultiEcho) Bind(group string, interfaceConfig HTTPConfig) error {
normGroup := strings.ToLower(group)
if _, groupExists := c.groups[normGroup]; groupExists {
return fmt.Errorf("http bind group already exists: %s", group)
}
c.groups[group] = interfaceConfig.Address
if _, addressBound := c.interfaces[interfaceConfig.Address]; !addressBound {
c.interfaces[interfaceConfig.Address] = c.creatorFn()
}
return nil
}

// Start starts all Echo servers.
func (c MultiEcho) Start() error {
for address, echoServer := range c.interfaces {
if err := echoServer.Start(address); err != nil {
return err
}
}
return nil
}

func (c *MultiEcho) register(path string, registerFn func(router EchoRouter) *echo.Route) {
group := getGroup(path)
groupAddress := c.groups[group]
if groupAddress != "" {
registerFn(c.interfaces[groupAddress])
} else {
registerFn(c.interfaces[c.groups[defaultEchoGroup]])
}
}

func (c MultiEcho) CONNECT(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route {
c.register(path, func(router EchoRouter) *echo.Route {
return router.CONNECT(path, h, m...)
})
return nil
}

func (c MultiEcho) DELETE(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route {
c.register(path, func(router EchoRouter) *echo.Route {
return router.DELETE(path, h, m...)
})
return nil
}

func (c MultiEcho) GET(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route {
c.register(path, func(router EchoRouter) *echo.Route {
return router.GET(path, h, m...)
})
return nil
}

func (c MultiEcho) HEAD(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route {
c.register(path, func(router EchoRouter) *echo.Route {
return router.HEAD(path, h, m...)
})
return nil
}

func (c MultiEcho) OPTIONS(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route {
c.register(path, func(router EchoRouter) *echo.Route {
return router.OPTIONS(path, h, m...)
})
return nil
}

func (c MultiEcho) PATCH(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route {
c.register(path, func(router EchoRouter) *echo.Route {
return router.PATCH(path, h, m...)
})
return nil
}

func (c MultiEcho) POST(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route {
c.register(path, func(router EchoRouter) *echo.Route {
return router.POST(path, h, m...)
})
return nil
}

func (c MultiEcho) PUT(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route {
c.register(path, func(router EchoRouter) *echo.Route {
return router.PUT(path, h, m...)
})
return nil
}

func (c MultiEcho) TRACE(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route {
c.register(path, func(router EchoRouter) *echo.Route {
return router.TRACE(path, h, m...)
})
return nil
}

func getGroup(path string) string {
parts := strings.Split(path, "/")
for _, part := range parts {
if strings.TrimSpace(part) != "" {
return strings.ToLower(part)
}
}
return ""
}
Loading

0 comments on commit dd7fca6

Please sign in to comment.