diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 09f19263..87f75e80 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -34,3 +34,71 @@ jobs: # uses: shogo82148/actions-goveralls@v1 # with: # path-to-profile: profile.cov + + test-plugins: + name: Test GatewayD Plugins + runs-on: ubuntu-latest + services: + postgres: + image: postgres + env: + POSTGRES_HOST: postgres + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + steps: + - name: Checkout ๐Ÿ›Ž๏ธ + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Checkout test plugin ๐Ÿ›Ž๏ธ + uses: actions/checkout@v3 + with: + repository: gatewayd-io/gatewayd-plugin-test + path: gatewayd-plugin-test + token: ${{ secrets.GH_PLUGIN_TOKEN }} + + - name: Install Go ๐Ÿง‘โ€๐Ÿ’ป + uses: actions/setup-go@v3 + with: + go-version: '1.18' + + - name: Build test plugin ๐Ÿ—๏ธ + run: | + # Build GatewayD + make build + # Build test plugin + cd gatewayd-plugin-test && make build && cp gatewayd-plugin-test ../gdp-test && cd .. + export SHA256SUM=$(sha256sum gdp-test | awk '{print $1}') + cat < gatewayd_plugins.yaml + gatewayd-plugin-test: + enabled: True + localPath: ./gdp-test + checksum: ${SHA256SUM} + EOF + + - name: Run GatewayD with test plugin ๐Ÿš€ + run: | + ./gatewayd run & + + - name: Run a test with PSQL ๐Ÿงช + run: | + sudo apt-get update + sudo apt-get install --yes --no-install-recommends postgresql-client + psql -h ${PGHOST} -p ${PGPORT} -U ${PGUSER} -c "CREATE DATABASE gatewayd_test;" + psql -h ${PGHOST} -p ${PGPORT} -U ${PGUSER} -d ${DBNAME} -c "CREATE TABLE test_table (id serial PRIMARY KEY, name varchar(255));" + psql -h ${PGHOST} -p ${PGPORT} -U ${PGUSER} -d ${DBNAME} -c "INSERT INTO test_table (name) VALUES ('test');" + psql -h ${PGHOST} -p ${PGPORT} -U ${PGUSER} -d ${DBNAME} -c "SELECT * FROM test_table;" | grep test + env: + DBNAME: gatewayd_test + PGUSER: postgres + PGPASSWORD: postgres + PGHOST: localhost + PGPORT: 15432 diff --git a/cmd/run.go b/cmd/run.go index fe8a0f42..673d7bd9 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -96,8 +96,7 @@ var runCmd = &cobra.Command{ // This is a notification hook, so we don't care about the result. data, err := structpb.NewStruct(map[string]interface{}{ "timeFormat": loggerCfg.TimeFormat, - "level": loggerCfg.Level, - "output": loggerCfg.Output, + "level": loggerCfg.Level.String(), "noColor": loggerCfg.NoColor, }) if err != nil { @@ -179,7 +178,11 @@ var runCmd = &cobra.Command{ proxyCfg, err := structpb.NewStruct(map[string]interface{}{ "elastic": elastic, "reuseElasticClients": reuseElasticClients, - "clientConfig": elasticClientConfig, + "clientConfig": map[string]interface{}{ + "network": elasticClientConfig.Network, + "address": elasticClientConfig.Address, + "receiveBufferSize": elasticClientConfig.ReceiveBufferSize, + }, }) if err != nil { logger.Error().Err(err).Msg("Failed to convert proxy config to structpb") @@ -240,19 +243,19 @@ var runCmd = &cobra.Command{ "address": serverConfig.Address, "softLimit": serverConfig.SoftLimit, "hardLimit": serverConfig.HardLimit, - "tickInterval": serverConfig.TickInterval, + "tickInterval": serverConfig.TickInterval.Seconds(), "multiCore": serverConfig.MultiCore, "lockOSThread": serverConfig.LockOSThread, "enableTicker": serverConfig.EnableTicker, - "loadBalancer": serverConfig.LoadBalancer, + "loadBalancer": int(serverConfig.LoadBalancer), "readBufferCap": serverConfig.ReadBufferCap, "writeBufferCap": serverConfig.WriteBufferCap, "socketRecvBuffer": serverConfig.SocketRecvBuffer, "socketSendBuffer": serverConfig.SocketSendBuffer, "reuseAddress": serverConfig.ReuseAddress, "reusePort": serverConfig.ReusePort, - "tcpKeepAlive": serverConfig.TCPKeepAlive, - "tcpNoDelay": serverConfig.TCPNoDelay, + "tcpKeepAlive": serverConfig.TCPKeepAlive.Seconds(), + "tcpNoDelay": int(serverConfig.TCPNoDelay), }) if err != nil { logger.Error().Err(err).Msg("Failed to convert server config to structpb") @@ -281,7 +284,8 @@ var runCmd = &cobra.Command{ for _, s := range signals { if sig != s { // Notify the hooks that the server is shutting down - signalCfg, err := structpb.NewStruct(map[string]interface{}{"signal": sig}) + signalCfg, err := structpb.NewStruct( + map[string]interface{}{"signal": sig.String()}) if err != nil { logger.Error().Err(err).Msg( "Failed to convert signal config to structpb") diff --git a/logging/logging_test.go b/logging/logging_test.go new file mode 100644 index 00000000..0776e470 --- /dev/null +++ b/logging/logging_test.go @@ -0,0 +1,51 @@ +package logging + +import ( + "bytes" + "encoding/json" + "testing" + + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" +) + +func TestNewLogger(t *testing.T) { + var buffer bytes.Buffer + logger := NewLogger( + LoggerConfig{ + Output: &buffer, + Level: zerolog.DebugLevel, + TimeFormat: zerolog.TimeFormatUnix, + NoColor: true, + }, + ) + assert.NotNil(t, logger) + + var msg interface{} + err := json.Unmarshal(buffer.Bytes(), &msg) + assert.NoError(t, err) + + if jsonMsg, ok := msg.(map[string]interface{}); ok { + // This is created when the logger is created and + // is used to test that the logger is working. + assert.Equal(t, "Created a new logger", jsonMsg["message"]) + assert.Equal(t, "debug", jsonMsg["level"]) + } else { + t.Fail() + } + + buffer.Reset() + + logger.Error().Str("key", "key").Msg("This is an error") + var msg2 interface{} + err = json.Unmarshal(buffer.Bytes(), &msg2) + assert.NoError(t, err) + + if jsonMsg, ok := msg2.(map[string]interface{}); ok { + assert.Equal(t, "This is an error", jsonMsg["message"]) + assert.Equal(t, "error", jsonMsg["level"]) + assert.Equal(t, "key", jsonMsg["key"]) + } else { + t.Fail() + } +} diff --git a/network/server.go b/network/server.go index c3e71f55..88940e7f 100644 --- a/network/server.go +++ b/network/server.go @@ -44,8 +44,7 @@ func (s *Server) OnBoot(engine gnet.Engine) gnet.Action { s.logger.Debug().Msg("GatewayD is booting...") onBootingData, err := structpb.NewStruct(map[string]interface{}{ - "server": s, - "engine": engine, + "status": string(s.Status), }) if err != nil { s.logger.Error().Err(err).Msg("Failed to create structpb") @@ -63,8 +62,7 @@ func (s *Server) OnBoot(engine gnet.Engine) gnet.Action { s.Status = Running onBootedData, err := structpb.NewStruct(map[string]interface{}{ - "server": s, - "engine": engine, + "status": string(s.Status), }) if err != nil { s.logger.Error().Err(err).Msg("Failed to create structpb") @@ -85,8 +83,10 @@ func (s *Server) OnOpen(gconn gnet.Conn) ([]byte, gnet.Action) { s.logger.Debug().Msgf("GatewayD is opening a connection from %s", gconn.RemoteAddr().String()) onOpeningData, err := structpb.NewStruct(map[string]interface{}{ - "server": s, - "gconn": gconn, + "client": map[string]interface{}{ + "local": gconn.LocalAddr().String(), + "remote": gconn.RemoteAddr().String(), + }, }) if err != nil { s.logger.Error().Err(err).Msg("Failed to create structpb") @@ -117,8 +117,10 @@ func (s *Server) OnOpen(gconn gnet.Conn) ([]byte, gnet.Action) { } onOpenedData, err := structpb.NewStruct(map[string]interface{}{ - "server": s, - "gconn": gconn, + "client": map[string]interface{}{ + "local": gconn.LocalAddr().String(), + "remote": gconn.RemoteAddr().String(), + }, }) if err != nil { s.logger.Error().Err(err).Msg("Failed to create structpb") @@ -137,9 +139,11 @@ func (s *Server) OnClose(gconn gnet.Conn, err error) gnet.Action { s.logger.Debug().Msgf("GatewayD is closing a connection from %s", gconn.RemoteAddr().String()) onClosingData, err := structpb.NewStruct(map[string]interface{}{ - "server": s, - "gconn": gconn, - "error": err, + "client": map[string]interface{}{ + "local": gconn.LocalAddr().String(), + "remote": gconn.RemoteAddr().String(), + }, + "error": err, }) if err != nil { s.logger.Error().Err(err).Msg("Failed to create structpb") @@ -160,9 +164,11 @@ func (s *Server) OnClose(gconn gnet.Conn, err error) gnet.Action { } onClosedData, err := structpb.NewStruct(map[string]interface{}{ - "server": s, - "gconn": gconn, - "error": err, + "client": map[string]interface{}{ + "local": gconn.LocalAddr().String(), + "remote": gconn.RemoteAddr().String(), + }, + "error": err, }) if err != nil { s.logger.Error().Err(err).Msg("Failed to create structpb") @@ -179,8 +185,10 @@ func (s *Server) OnClose(gconn gnet.Conn, err error) gnet.Action { func (s *Server) OnTraffic(gconn gnet.Conn) gnet.Action { onTrafficData, err := structpb.NewStruct(map[string]interface{}{ - "server": s, - "gconn": gconn, + "client": map[string]interface{}{ + "local": gconn.LocalAddr().String(), + "remote": gconn.RemoteAddr().String(), + }, }) if err != nil { s.logger.Error().Err(err).Msg("Failed to create structpb") @@ -205,8 +213,7 @@ func (s *Server) OnShutdown(engine gnet.Engine) { s.logger.Debug().Msg("GatewayD is shutting down...") onShutdownData, err := structpb.NewStruct(map[string]interface{}{ - "server": s, - "engine": engine, + "connections": s.engine.CountConnections(), }) if err != nil { s.logger.Error().Err(err).Msg("Failed to create structpb") @@ -227,7 +234,7 @@ func (s *Server) OnTick() (time.Duration, gnet.Action) { s.logger.Info().Msgf("Active connections: %d", s.engine.CountConnections()) onTickData, err := structpb.NewStruct(map[string]interface{}{ - "server": s, + "connections": s.engine.CountConnections(), }) if err != nil { s.logger.Error().Err(err).Msg("Failed to create structpb") @@ -254,7 +261,6 @@ func (s *Server) Run() error { // Since gnet.Run is blocking, we need to run OnRun before it //nolint:nestif if onRunData, err := structpb.NewStruct(map[string]interface{}{ - "server": s, "address": addr, "error": err, }); err != nil { diff --git a/plugin/hooks_test.go b/plugin/hooks_test.go index 14fe6b12..84a12c40 100644 --- a/plugin/hooks_test.go +++ b/plugin/hooks_test.go @@ -77,65 +77,6 @@ func Test_HookConfig_Run(t *testing.T) { assert.Nil(t, err) } -func Test_Verify(t *testing.T) { - params, err := structpb.NewStruct( - map[string]interface{}{ - "test": "test", - }, - ) - assert.Nil(t, err) - - returnVal, err := structpb.NewStruct( - map[string]interface{}{ - "test": "test", - }, - ) - assert.Nil(t, err) - - assert.True(t, Verify(params, returnVal)) -} - -func Test_Verify_fail(t *testing.T) { - data := [][]map[string]interface{}{ - { - { - "test": "test", - }, - { - "test": "test", - "test2": "test2", - }, - }, - { - { - "test": "test", - "test2": "test2", - }, - { - "test": "test", - }, - }, - { - { - "test": "test", - "test2": "test2", - }, - { - "test": "test", - "test3": "test3", - }, - }, - } - - for _, d := range data { - params, err := structpb.NewStruct(d[0]) - assert.Nil(t, err) - returnVal, err := structpb.NewStruct(d[1]) - assert.Nil(t, err) - assert.False(t, Verify(params, returnVal)) - } -} - func Test_HookConfig_Run_PassDown(t *testing.T) { hooks := NewHookConfig() // The result of the hook will be nil and will be passed down to the next hook. diff --git a/plugin/registry.go b/plugin/registry.go index fcfb7ea6..d35bc0c3 100644 --- a/plugin/registry.go +++ b/plugin/registry.go @@ -152,6 +152,7 @@ func (reg *RegistryImpl) LoadPlugins(pluginConfig *koanf.Koanf) { Managed: true, MinPort: DefaultMinPort, MaxPort: DefaultMaxPort, + // TODO: Enable GRPC DialOptions // GRPCDialOptions: []grpc.DialOption{ // grpc.WithInsecure(), // }, @@ -189,9 +190,14 @@ func (reg *RegistryImpl) LoadPlugins(pluginConfig *koanf.Koanf) { &plugin.Hooks); err != nil { reg.hooksConfig.Logger.Debug().Err(err).Msg("Failed to decode plugin hooks") } - if err := mapstructure.Decode(metadata.Fields["config"].GetStructValue().AsMap(), - &plugin.Config); err != nil { - reg.hooksConfig.Logger.Debug().Err(err).Msg("Failed to decode plugin config") + + plugin.Config = make(map[string]string) + for key, value := range metadata.Fields["config"].GetStructValue().AsMap() { + if val, ok := value.(string); ok { + plugin.Config[key] = val + } else { + reg.hooksConfig.Logger.Debug().Msgf("Failed to decode plugin config: %s", key) + } } reg.Add(plugin) diff --git a/plugin/registry_test.go b/plugin/registry_test.go new file mode 100644 index 00000000..318edb92 --- /dev/null +++ b/plugin/registry_test.go @@ -0,0 +1,36 @@ +package plugin + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestPluginRegistry(t *testing.T) { + hooksConfig := NewHookConfig() + assert.NotNil(t, hooksConfig) + reg := NewRegistry(hooksConfig) + assert.NotNil(t, reg) + assert.NotNil(t, reg.plugins) + assert.NotNil(t, reg.hooksConfig) + assert.Equal(t, 0, len(reg.List())) + + ident := Identifier{ + Name: "test", + Version: "1.0.0", + RemoteURL: "github.com/remote/test", + } + impl := &Impl{ + ID: ident, + } + reg.Add(impl) + assert.Equal(t, 1, len(reg.List())) + + instance := reg.Get(ident) + assert.Equal(t, instance, impl) + + reg.Remove(ident) + assert.Equal(t, 0, len(reg.List())) + + reg.Shutdown() +} diff --git a/plugin/utils.go b/plugin/utils.go index 53a31b77..dafe9799 100644 --- a/plugin/utils.go +++ b/plugin/utils.go @@ -15,6 +15,9 @@ import ( const bufferSize = 65536 +// sha256sum returns the sha256 checksum of a file. +// Ref: https://github.com/codingsince1985/checksum +// A little copying is better than a little dependency. func sha256sum(filename string) (string, error) { if info, err := os.Stat(filename); err != nil || info.IsDir() { return "", err //nolint:wrapcheck @@ -42,6 +45,7 @@ func sha256sum(filename string) (string, error) { } } +// Verify compares two structs and returns true if they are equal. func Verify(params, returnVal *structpb.Struct) bool { return cmp.Equal(params.AsMap(), returnVal.AsMap(), cmp.Options{ cmpopts.SortMaps(func(a, b string) bool { diff --git a/plugin/utils_test.go b/plugin/utils_test.go new file mode 100644 index 00000000..9ae59c9b --- /dev/null +++ b/plugin/utils_test.go @@ -0,0 +1,85 @@ +package plugin + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "google.golang.org/protobuf/types/known/structpb" +) + +func Test_sha256sum(t *testing.T) { + checksum, err := sha256sum("../LICENSE") + assert.Nil(t, err) + assert.Equal(t, + "8486a10c4393cee1c25392769ddd3b2d6c242d6ec7928e1414efff7dfb2f07ef", + checksum, + ) +} + +func Test_sha256sum_fail(t *testing.T) { + _, err := sha256sum("not_a_file") + assert.NotNil(t, err) +} + +func Test_Verify(t *testing.T) { + params, err := structpb.NewStruct( + map[string]interface{}{ + "test": "test", + }, + ) + assert.Nil(t, err) + + returnVal, err := structpb.NewStruct( + map[string]interface{}{ + "test": "test", + }, + ) + assert.Nil(t, err) + + assert.True(t, Verify(params, returnVal)) +} + +func Test_Verify_fail(t *testing.T) { + data := [][]map[string]interface{}{ + { + { + "test": "test", + }, + { + "test": "test", + "test2": "test2", + }, + }, + { + { + "test": "test", + "test2": "test2", + }, + { + "test": "test", + }, + }, + { + { + "test": "test", + "test2": "test2", + }, + { + "test": "test", + "test3": "test3", + }, + }, + } + + for _, d := range data { + params, err := structpb.NewStruct(d[0]) + assert.Nil(t, err) + returnVal, err := structpb.NewStruct(d[1]) + assert.Nil(t, err) + assert.False(t, Verify(params, returnVal)) + } +} + +func Test_Verify_nil(t *testing.T) { + assert.True(t, Verify(nil, nil)) +} diff --git a/pool/pool_test.go b/pool/pool_test.go index dab38909..34a8ac0e 100644 --- a/pool/pool_test.go +++ b/pool/pool_test.go @@ -26,6 +26,7 @@ func TestPool_Put(t *testing.T) { assert.Equal(t, 2, pool.Size()) } +//nolint:dupl func TestPool_Pop(t *testing.T) { pool := NewPool() defer pool.Clear() @@ -82,6 +83,75 @@ func TestPool_ForEach(t *testing.T) { }) } +//nolint:dupl +func TestPool_Get(t *testing.T) { + pool := NewPool() + defer pool.Clear() + assert.NotNil(t, pool) + assert.NotNil(t, pool.Pool()) + assert.Equal(t, 0, pool.Size()) + pool.Put("client1.ID", "client1") + assert.Equal(t, 1, pool.Size()) + pool.Put("client2.ID", "client2") + assert.Equal(t, 2, pool.Size()) + if c1, ok := pool.Get("client1.ID").(string); !ok { + assert.Equal(t, c1, "client1") + } else { + assert.Equal(t, "client1", c1) + assert.Equal(t, 2, pool.Size()) + } + if c2, ok := pool.Get("client2.ID").(string); !ok { + assert.Equal(t, c2, "client2") + } else { + assert.Equal(t, "client2", c2) + assert.Equal(t, 2, pool.Size()) + } +} + +func TestPool_GetOrPut(t *testing.T) { + pool := NewPool() + defer pool.Clear() + assert.NotNil(t, pool) + assert.NotNil(t, pool.Pool()) + assert.Equal(t, 0, pool.Size()) + pool.Put("client1.ID", "client1") + assert.Equal(t, 1, pool.Size()) + pool.Put("client2.ID", "client2") + assert.Equal(t, 2, pool.Size()) + c1, loaded := pool.GetOrPut("client1.ID", "client1") + assert.True(t, loaded) + if c1, ok := c1.(string); !ok { + assert.Equal(t, c1, "client1") + } else { + assert.Equal(t, "client1", c1) + assert.Equal(t, 2, pool.Size()) + } + c2, loaded := pool.GetOrPut("client2.ID", "client2") + assert.True(t, loaded) + if c2, ok := c2.(string); !ok { + assert.Equal(t, c2, "client2") + } else { + assert.Equal(t, "client2", c2) + assert.Equal(t, 2, pool.Size()) + } +} + +func TestPool_Remove(t *testing.T) { + pool := NewPool() + defer pool.Clear() + assert.NotNil(t, pool) + assert.NotNil(t, pool.Pool()) + assert.Equal(t, 0, pool.Size()) + pool.Put("client1.ID", "client1") + assert.Equal(t, 1, pool.Size()) + pool.Put("client2.ID", "client2") + assert.Equal(t, 2, pool.Size()) + pool.Remove("client1.ID") + assert.Equal(t, 1, pool.Size()) + pool.Remove("client2.ID") + assert.Equal(t, 0, pool.Size()) +} + func TestPool_GetClientIDs(t *testing.T) { pool := NewPool() defer pool.Clear()