From 000115ceb051a67d8d1eeadca9e5809a37ce0768 Mon Sep 17 00:00:00 2001 From: Kelvin Clement Mwinuka Date: Tue, 30 Apr 2024 12:17:30 +0800 Subject: [PATCH] Created "loadmodules" args for loading ".so" modules into EchoVault. Moved types associated with embedded commands extension into api_admin.go file and deleted types.go file as it's no longer necessary. Updated docker-compose and Dockerfile.dev to pass .so modules to load on startup. Volumes folder is no longer ignores except for the nodes subfolder. --- .gitignore | 4 +- Dockerfile.dev | 3 ++ docker-compose.yaml | 60 ++++++++++++++++++-------- echovault/api_admin.go | 66 +++++++++++++++++++++++++++++ echovault/types.go | 84 ------------------------------------- internal/config/config.go | 14 +++++++ internal/config/default.go | 1 + volumes/config/acl.yml | 0 volumes/modules/module_1.go | 1 + volumes/modules/module_2.go | 1 + 10 files changed, 129 insertions(+), 105 deletions(-) delete mode 100644 echovault/types.go create mode 100644 volumes/config/acl.yml create mode 100644 volumes/modules/module_1.go create mode 100644 volumes/modules/module_2.go diff --git a/.gitignore b/.gitignore index f444be1..7c4c130 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,6 @@ .idea bin -volumes -/config/ - +volumes/nodes dist/ pkg/modules/*/aof pkg/echovault/aof diff --git a/Dockerfile.dev b/Dockerfile.dev index ed353d4..9dc85eb 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -41,3 +41,6 @@ CMD "./server" \ "--cert-key-pair=${CERT_KEY_PAIR_2}" \ # List of client certs "--client-ca=${CLIENT_CA_1}" \ + # List of plugins to load on startup + "--loadmodule=${MODULE_1}" \ + "--loadmodule=${MODULE_2}" \ diff --git a/docker-compose.yaml b/docker-compose.yaml index cdecdb0..6ca6c93 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -21,7 +21,7 @@ services: - TLS=false - MTLS=false - BOOTSTRAP_CLUSTER=false - # - ACL_CONFIG=/etc/config/echovault/acl.yml + # - ACL_CONFIG=/etc/echovault/config/acl.yml - REQUIRE_PASS=false - PASSWORD=password1 - FORWARD_COMMAND=false @@ -39,13 +39,17 @@ services: - CERT_KEY_PAIR_2=/etc/ssl/certs/echovault/echovault/server2.crt,/etc/ssl/certs/echovault/echovault/server2.key # List of client certificate authorities - CLIENT_CA_1=/etc/ssl/certs/echovault/client/rootCA.crt + # List of shared object plugins to load on startup + - MODULE_1=/lib/echovault/modules/module_1.so + - MODULE_2=/lib/echovault/modules/module_2.so ports: - "7480:7480" - "7946:7946" - "7999:8000" volumes: - - ./volumes/config/acl.yml:/etc/config/echovault/acl.yml - - ./volumes/standalone_node:/var/lib/echovault + - ./volumes/config:/etc/echovault/config + - ./volumes/plugins:/lib/echovault/plugins + - ./volumes/nodes/standalone_node:/var/lib/echovault networks: - testnet @@ -65,7 +69,7 @@ services: - TLS=false - MTLS=false - BOOTSTRAP_CLUSTER=true - # - ACL_CONFIG=/etc/config/echovault/acl.yml + # - ACL_CONFIG=/etc/echovault/config/acl.yml - REQUIRE_PASS=false - FORWARD_COMMAND=true - SNAPSHOT_THRESHOLD=1000 @@ -82,13 +86,17 @@ services: - CERT_KEY_PAIR_2=/etc/ssl/certs/echovault/echovault/server2.crt,/etc/ssl/certs/echovault/echovault/server2.key # List of client certificate authorities - CLIENT_CA_1=/etc/ssl/certs/echovault/client/rootCA.crt + # List of shared object plugins to load on startup + - MODULE_1=/lib/echovault/modules/module_1.so + - MODULE_2=/lib/echovault/modules/module_2.so ports: - "7481:7480" - "7945:7946" - "8000:8000" volumes: - - ./volumes/config/acl.yml:/etc/config/echovault/acl.yml - - ./volumes/cluster_node_1:/var/lib/echovault + - ./volumes/config:/etc/echovault/config + - ./volumes/plugins:/lib/echovault/plugins + - ./volumes/nodes/cluster_node_1:/var/lib/echovault networks: - testnet @@ -108,7 +116,7 @@ services: - TLS=false - MTLS=false - BOOTSTRAP_CLUSTER=false - # - ACL_CONFIG=/etc/config/echovault/acl.yml + # - ACL_CONFIG=/etc/echovault/config/acl.yml - REQUIRE_PASS=false - FORWARD_COMMAND=true - SNAPSHOT_THRESHOLD=1000 @@ -125,13 +133,17 @@ services: - CERT_KEY_PAIR_2=/etc/ssl/certs/echovault/echovault/server2.crt,/etc/ssl/certs/echovault/echovault/server2.key # List of client certificate authorities - CLIENT_CA_1=/etc/ssl/certs/echovault/client/rootCA.crt + # List of shared object plugins to load on startup + - MODULE_1=/lib/echovault/modules/module_1.so + - MODULE_2=/lib/echovault/modules/module_2.so ports: - "7482:7480" - "7947:7946" - "8001:8000" volumes: - - ./volumes/config/acl.yml:/etc/config/echovault/acl.yml - - ./volumes/cluster_node_2:/var/lib/echovault + - ./volumes/config:/etc/echovault/config + - ./volumes/plugins:/lib/echovault/plugins + - ./volumes/nodes/cluster_node_2:/var/lib/echovault networks: - testnet @@ -151,7 +163,7 @@ services: - TLS=false - MTLS=false - BOOTSTRAP_CLUSTER=false - # - ACL_CONFIG=/etc/config/echovault/acl.yml + # - ACL_CONFIG=/etc/echovault/config/acl.yml - REQUIRE_PASS=false - FORWARD_COMMAND=true - SNAPSHOT_THRESHOLD=1000 @@ -168,13 +180,17 @@ services: - CERT_KEY_PAIR_2=/etc/ssl/certs/echovault/echovault/server2.crt,/etc/ssl/certs/echovault/echovault/server2.key # List of client certificate authorities - CLIENT_CA_1=/etc/ssl/certs/echovault/client/rootCA.crt + # List of shared object plugins to load on startup + - MODULE_1=/lib/echovault/modules/module_1.so + - MODULE_2=/lib/echovault/modules/module_2.so ports: - "7483:7480" - "7948:7946" - "8002:8000" volumes: - - ./volumes/config/acl.yml:/etc/config/echovault/acl.yml - - ./volumes/cluster_node_3:/var/lib/echovault + - ./volumes/config:/etc/echovault/config + - ./volumes/plugins:/lib/echovault/plugins + - ./volumes/nodes/cluster_node_3:/var/lib/echovault networks: - testnet @@ -194,7 +210,7 @@ services: - TLS=false - MTLS=false - BOOTSTRAP_CLUSTER=false - # - ACL_CONFIG=/etc/config/echovault/acl.yml + # - ACL_CONFIG=/etc/echovault/config/acl.yml - REQUIRE_PASS=false - FORWARD_COMMAND=true - SNAPSHOT_THRESHOLD=1000 @@ -211,13 +227,17 @@ services: - CERT_KEY_PAIR_2=/etc/ssl/certs/echovault/echovault/server2.crt,/etc/ssl/certs/echovault/echovault/server2.key # List of client certificate authorities - CLIENT_CA_1=/etc/ssl/certs/echovault/client/rootCA.crt + # List of shared object plugins to load on startup + - MODULE_1=/lib/echovault/modules/module_1.so + - MODULE_2=/lib/echovault/modules/module_2.so ports: - "7484:7480" - "7949:7946" - "8003:8000" volumes: - - ./volumes/config/acl.yml:/etc/config/echovault/acl.yml - - ./volumes/cluster_node_4:/var/lib/echovault + - ./volumes/config:/etc/echovault/config + - ./volumes/plugins:/lib/echovault/plugins + - ./volumes/nodes/cluster_node_4:/var/lib/echovault networks: - testnet @@ -237,7 +257,7 @@ services: - TLS=false - MTLS=false - BOOTSTRAP_CLUSTER=false - # - ACL_CONFIG=/etc/config/echovault/acl.yml + # - ACL_CONFIG=/etc/echovault/config/acl.yml - REQUIRE_PASS=false - FORWARD_COMMAND=true - SNAPSHOT_THRESHOLD=1000 @@ -254,12 +274,16 @@ services: - CERT_KEY_PAIR_2=/etc/ssl/certs/echovault/echovault/server2.crt,/etc/ssl/certs/echovault/echovault/server2.key # List of client certificate authorities - CLIENT_CA_1=/etc/ssl/certs/echovault/client/rootCA.crt + # List of shared object plugins to load on startup + - MODULE_1=/lib/echovault/modules/module_1.so + - MODULE_2=/lib/echovault/modules/module_2.so ports: - "7485:7480" - "7950:7946" - "8004:8000" volumes: - - ./volumes/config/acl.yml:/etc/config/echovault/acl.yml - - ./volumes/cluster_node_5:/var/lib/echovault + - ./volumes/config:/etc/echovault/config + - ./volumes/plugins:/lib/echovault/plugins + - ./volumes/nodes/cluster_node_5:/var/lib/echovault networks: - testnet \ No newline at end of file diff --git a/echovault/api_admin.go b/echovault/api_admin.go index dd7e6ea..4ea074b 100644 --- a/echovault/api_admin.go +++ b/echovault/api_admin.go @@ -15,8 +15,10 @@ package echovault import ( + "context" "fmt" "github.com/echovault/echovault/internal" + "net" "slices" "strings" ) @@ -34,6 +36,70 @@ type CommandListOptions struct { MODULE string } +// CommandKeyExtractionFuncResult specifies the keys accessed by the associated command or subcommand. +// ReadKeys is a string slice containing the keys that the commands read from. +// WriteKeys is a string slice containing the keys that the command writes to. +// +// These keys will typically be extracted from the command slice, but they can also be hardcoded. +type CommandKeyExtractionFuncResult struct { + ReadKeys []string + WriteKeys []string +} + +// CommandKeyExtractionFunc if the function that extracts the keys accessed by the command or subcommand. +type CommandKeyExtractionFunc func(cmd []string) (CommandKeyExtractionFuncResult, error) + +// CommandHandlerFunc is the handler function for the command or subcommand. +// +// This function must return a byte slice containing a valid RESP2 response, or an error. +type CommandHandlerFunc func(params CommandHandlerFuncParams) ([]byte, error) + +// CommandHandlerFuncParams contains the helper parameters passed to the command's handler by EchoVault. +// +// Command is the string slice command containing the command that triggered this handler. +// +// Connection is the TCP connection that triggered this command. In embedded mode, this will always be nil. +// Any TCP client that trigger the custom command will have its connection passed to the handler here. +// +// KeyExists returns true if the key passed to it exists in the store. +// +// CreateKeyAndLock creates the new key and immediately write locks it. If the key already exists, then +// it is simply write locked which makes this function safe to call even if the key already exists. Always call +// KeyUnlock when done after CreateKeyAndLock. +// +// KeyLock acquires a write lock for the specified key. If the lock is successfully acquired, the function will return +// (true, nil). Otherwise, it will return false and an error describing why the locking failed. Always call KeyUnlock +// when done after KeyLock. +// +// KeyUnlock releases the write lock for the specified key. Always call this after KeyLock otherwise the key will not be +// lockable by any future invocations of this command or other commands. +// +// KeyRLock acquires a read lock for the specified key. If the lock is successfully acquired, the function will return +// (true, nil). Otherwise, it will return false and an error describing why the locking failed. Always call KeyRUnlock +// when done after KeyRLock. +// +// KeyRUnlock releases the real lock for the specified key. Always call this after KeyRLock otherwise the key will not be +// write-lockable by any future invocations of this command or other commands. +// +// GetValue returns the value held at the specified key as an interface{}. Make sure to invoke KeyLock or KeyRLock on the +// key before GetValue to ensure thread safety. +// +// SetValue sets the value at the specified key. Make sure to invoke KeyLock on the key before +// SetValue to ensure thread safety. +type CommandHandlerFuncParams struct { + Context context.Context + Command []string + Connection *net.Conn + KeyExists func(ctx context.Context, key string) bool + CreateKeyAndLock func(ctx context.Context, key string) (bool, error) + KeyLock func(ctx context.Context, key string) (bool, error) + KeyUnlock func(ctx context.Context, key string) + KeyRLock func(ctx context.Context, key string) (bool, error) + KeyRUnlock func(ctx context.Context, key string) + GetValue func(ctx context.Context, key string) interface{} + SetValue func(ctx context.Context, key string, value interface{}) error +} + // CommandOptions provides the specification of the command to be added to the EchoVault instance. // // Command is the keyword used to trigger this command (e.g. LPUSH, ZADD, ACL ...). diff --git a/echovault/types.go b/echovault/types.go deleted file mode 100644 index 992ba3d..0000000 --- a/echovault/types.go +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright 2024 Kelvin Clement Mwinuka -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package echovault - -import ( - "context" - "net" -) - -// CommandKeyExtractionFuncResult specifies the keys accessed by the associated command or subcommand. -// ReadKeys is a string slice containing the keys that the commands read from. -// WriteKeys is a string slice containing the keys that the command writes to. -// -// These keys will typically be extracted from the command slice, but they can also be hardcoded. -type CommandKeyExtractionFuncResult struct { - ReadKeys []string - WriteKeys []string -} - -// CommandKeyExtractionFunc if the function that extracts the keys accessed by the command or subcommand. -type CommandKeyExtractionFunc func(cmd []string) (CommandKeyExtractionFuncResult, error) - -// CommandHandlerFunc is the handler function for the command or subcommand. -// -// This function must return a byte slice containing a valid RESP2 response, or an error. -type CommandHandlerFunc func(params CommandHandlerFuncParams) ([]byte, error) - -// CommandHandlerFuncParams contains the helper parameters passed to the command's handler by EchoVault. -// -// Command is the string slice command containing the command that triggered this handler. -// -// Connection is the TCP connection that triggered this command. In embedded mode, this will always be nil. -// Any TCP client that trigger the custom command will have its connection passed to the handler here. -// -// KeyExists returns true if the key passed to it exists in the store. -// -// CreateKeyAndLock creates the new key and immediately write locks it. If the key already exists, then -// it is simply write locked which makes this function safe to call even if the key already exists. Always call -// KeyUnlock when done after CreateKeyAndLock. -// -// KeyLock acquires a write lock for the specified key. If the lock is successfully acquired, the function will return -// (true, nil). Otherwise, it will return false and an error describing why the locking failed. Always call KeyUnlock -// when done after KeyLock. -// -// KeyUnlock releases the write lock for the specified key. Always call this after KeyLock otherwise the key will not be -// lockable by any future invocations of this command or other commands. -// -// KeyRLock acquires a read lock for the specified key. If the lock is successfully acquired, the function will return -// (true, nil). Otherwise, it will return false and an error describing why the locking failed. Always call KeyRUnlock -// when done after KeyRLock. -// -// KeyRUnlock releases the real lock for the specified key. Always call this after KeyRLock otherwise the key will not be -// write-lockable by any future invocations of this command or other commands. -// -// GetValue returns the value held at the specified key as an interface{}. Make sure to invoke KeyLock or KeyRLock on the -// key before GetValue to ensure thread safety. -// -// SetValue sets the value at the specified key. Make sure to invoke KeyLock on the key before -// SetValue to ensure thread safety. -type CommandHandlerFuncParams struct { - Context context.Context - Command []string - Connection *net.Conn - KeyExists func(ctx context.Context, key string) bool - CreateKeyAndLock func(ctx context.Context, key string) (bool, error) - KeyLock func(ctx context.Context, key string) (bool, error) - KeyUnlock func(ctx context.Context, key string) - KeyRLock func(ctx context.Context, key string) (bool, error) - KeyRUnlock func(ctx context.Context, key string) - GetValue func(ctx context.Context, key string) interface{} - SetValue func(ctx context.Context, key string, value interface{}) error -} diff --git a/internal/config/config.go b/internal/config/config.go index 205b047..8e3860f 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -58,6 +58,7 @@ type Config struct { EvictionPolicy string `json:"EvictionPolicy" yaml:"EvictionPolicy"` EvictionSample uint `json:"EvictionSample" yaml:"EvictionSample"` EvictionInterval time.Duration `json:"EvictionInterval" yaml:"EvictionInterval"` + Modules []string `json:"Plugins" yaml:"Plugins"` } func GetConfig() (Config, error) { @@ -131,6 +132,18 @@ There is no limit by default.`, func(memory string) error { return nil }) + var modules []string + flag.Func( + "loadmodule", + `Path to shared object library to extend EchoVault commands (e.g. /path/to/plugin.so)`, + func(p string) error { + if !strings.HasSuffix(p, ".so") { + return fmt.Errorf("\"%s\" is not a .so file", p) + } + modules = append(modules, p) + return nil + }) + tls := flag.Bool("tls", false, "Start the echovault in TLS mode. Default is false.") mtls := flag.Bool("mtls", false, "Use mTLS to verify the client.") port := flag.Int("port", 7480, "Port to use. Default is 7480") @@ -200,6 +213,7 @@ It is a plain text value by default but you can provide a SHA256 hash by adding EvictionPolicy: evictionPolicy, EvictionSample: *evictionSample, EvictionInterval: *evictionInterval, + Modules: modules, } if len(*config) > 0 { diff --git a/internal/config/default.go b/internal/config/default.go index e50c217..c8c344c 100644 --- a/internal/config/default.go +++ b/internal/config/default.go @@ -33,5 +33,6 @@ func DefaultConfig() Config { EvictionPolicy: constants.NoEviction, EvictionSample: 20, EvictionInterval: 100 * time.Millisecond, + Modules: make([]string, 0), } } diff --git a/volumes/config/acl.yml b/volumes/config/acl.yml new file mode 100644 index 0000000..e69de29 diff --git a/volumes/modules/module_1.go b/volumes/modules/module_1.go new file mode 100644 index 0000000..11174d7 --- /dev/null +++ b/volumes/modules/module_1.go @@ -0,0 +1 @@ +package modules diff --git a/volumes/modules/module_2.go b/volumes/modules/module_2.go new file mode 100644 index 0000000..11174d7 --- /dev/null +++ b/volumes/modules/module_2.go @@ -0,0 +1 @@ +package modules