Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow API calls to be bound to specific HTTP interfaces #56

Merged
merged 4 commits into from
Feb 12, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 {
reinkrul marked this conversation as resolved.
Show resolved Hide resolved
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 {
reinkrul marked this conversation as resolved.
Show resolved Hide resolved
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