diff --git a/cmd/config_parser.go b/cmd/config_parser.go index 75892c29..f1768830 100644 --- a/cmd/config_parser.go +++ b/cmd/config_parser.go @@ -36,9 +36,9 @@ func getPath(path string) string { // return nil // } -// verificationPolicy returns the verification policy for the plugin from config file. +// verificationPolicy returns the hook verification policy from plugin config file. func verificationPolicy() plugin.Policy { - vPolicy := globalConfig.String("plugins.verificationPolicy") + vPolicy := pluginConfig.String("plugins.verificationPolicy") verificationPolicy := plugin.PassDown // default switch vPolicy { case "ignore": @@ -52,6 +52,20 @@ func verificationPolicy() plugin.Policy { return verificationPolicy } +// pluginCompatPolicy returns the plugin compatibility policy from plugin config file. +func pluginCompatPolicy() plugin.CompatPolicy { + vPolicy := pluginConfig.String("plugins.compatibilityPolicy") + compatPolicy := plugin.Strict // default + switch vPolicy { + case "strict": + compatPolicy = plugin.Strict + case "loose": + compatPolicy = plugin.Loose + } + + return compatPolicy +} + // loggerConfig returns the logger config from config file. func loggerConfig() logging.LoggerConfig { cfg := logging.LoggerConfig{StartupMsg: true} diff --git a/cmd/run.go b/cmd/run.go index f3fac2e6..01563af4 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -57,7 +57,10 @@ var runCmd = &cobra.Command{ } } - // Load plugins and register their hooks + // Set the plugin compatibility policy. + pluginRegistry.CompatPolicy = pluginCompatPolicy() + + // Load plugins and register their hooks. pluginRegistry.LoadPlugins(pluginConfig) if f, err := cmd.Flags().GetString("config"); err == nil { @@ -68,7 +71,7 @@ var runCmd = &cobra.Command{ } } - // Get hooks signature verification policy + // Get hooks signature verification policy. hooksConfig.Verification = verificationPolicy() // The config will be passed to the hooks, and in turn to the plugins that @@ -91,11 +94,11 @@ var runCmd = &cobra.Command{ } } - // Create a new logger from the config + // Create a new logger from the config. loggerCfg := loggerConfig() logger := logging.NewLogger(loggerCfg) - // Replace the default logger with the new one from the config + // Replace the default logger with the new one from the config. hooksConfig.Logger = logger // This is a notification hook, so we don't care about the result. @@ -111,7 +114,7 @@ var runCmd = &cobra.Command{ logger.Error().Err(err).Msg("Failed to run OnNewLogger hooks") } - // Create and initialize a pool of connections + // Create and initialize a pool of connections. poolSize, clientConfig := poolConfig() pool := pool.NewPool(poolSize) @@ -136,10 +139,10 @@ var runCmd = &cobra.Command{ "address": clientConfig.Address, "receiveBufferSize": clientConfig.ReceiveBufferSize, "receiveChunkSize": clientConfig.ReceiveChunkSize, - "receiveDeadline": clientConfig.ReceiveDeadline, - "sendDeadline": clientConfig.SendDeadline, + "receiveDeadline": clientConfig.ReceiveDeadline.Seconds(), + "sendDeadline": clientConfig.SendDeadline.Seconds(), "tcpKeepAlive": clientConfig.TCPKeepAlive, - "tcpKeepAlivePeriod": clientConfig.TCPKeepAlivePeriod, + "tcpKeepAlivePeriod": clientConfig.TCPKeepAlivePeriod.Seconds(), } _, err := hooksConfig.Run( context.Background(), @@ -178,7 +181,7 @@ var runCmd = &cobra.Command{ logger.Error().Err(err).Msg("Failed to run OnNewPool hooks") } - // Create a prefork proxy with the pool of clients + // Create a prefork proxy with the pool of clients. elastic, reuseElasticClients, elasticClientConfig := proxyConfig() proxy := network.NewProxy( pool, hooksConfig, elastic, reuseElasticClients, elasticClientConfig, logger) @@ -266,7 +269,7 @@ var runCmd = &cobra.Command{ logger.Error().Err(err).Msg("Failed to run OnNewServer hooks") } - // Shutdown the server gracefully + // Shutdown the server gracefully. var signals []os.Signal signals = append(signals, os.Interrupt, @@ -283,7 +286,7 @@ var runCmd = &cobra.Command{ for sig := range signalsCh { for _, s := range signals { if sig != s { - // Notify the hooks that the server is shutting down + // Notify the hooks that the server is shutting down. _, err := hooksConfig.Run( context.Background(), map[string]interface{}{"signal": sig.String()}, @@ -302,7 +305,7 @@ var runCmd = &cobra.Command{ } }(hooksConfig) - // Run the server + // Run the server. if err := server.Run(); err != nil { logger.Error().Err(err).Msg("Failed to start server") } diff --git a/gatewayd.yaml b/gatewayd.yaml index 27ddba4f..295089dc 100644 --- a/gatewayd.yaml +++ b/gatewayd.yaml @@ -65,6 +65,3 @@ server: reusePort: True tcpKeepAlive: 3s # duration tcpNoDelay: True - -plugins: - verificationPolicy: "passdown" diff --git a/gatewayd_plugins.yaml b/gatewayd_plugins.yaml index 42e5ec4e..c175f664 100644 --- a/gatewayd_plugins.yaml +++ b/gatewayd_plugins.yaml @@ -1,5 +1,11 @@ # Plugin configuration file for GatewayD +plugins: + # Possible values: "passdown" (default), "ignore", "abort" and "remove" + verificationPolicy: "passdown" + # Possible values: "strict" (default) and "loose" + compatibilityPolicy: "strict" + # Plugin name gatewayd-plugin-test: # whether to enable or disable the plugin on the next run @@ -17,4 +23,4 @@ gatewayd-plugin-test: - MAGIC_COOKIE_KEY=GATEWAYD_PLUGIN - MAGIC_COOKIE_VALUE=5712b87aa5d7e9f9e9ab643e6603181c5b796015cb1c09d6f5ada882bf2a1872 # Checksum hash to verify the binary before loading - checksum: 006e19bcfd1951e077746143590934470e8b1d67a3904036603013e150fcb708 + checksum: 09bce5ad90a36a6f3ec0804023098519424289f3d68679159d68e0b08ce70c89 diff --git a/go.mod b/go.mod index 95a36d20..8aa44d8a 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/gatewayd-io/gatewayd go 1.19 require ( + github.com/Masterminds/semver/v3 v3.2.0 github.com/fergusstrange/embedded-postgres v1.19.0 github.com/google/go-cmp v0.5.9 github.com/hashicorp/go-hclog v1.4.0 diff --git a/go.sum b/go.sum index 27623ff7..d39b3823 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g= +github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= diff --git a/plugin/registry.go b/plugin/registry.go index dadba103..159ba0b9 100644 --- a/plugin/registry.go +++ b/plugin/registry.go @@ -3,6 +3,7 @@ package plugin import ( "context" + semver "github.com/Masterminds/semver/v3" gerr "github.com/gatewayd-io/gatewayd/errors" "github.com/gatewayd-io/gatewayd/logging" pluginV1 "github.com/gatewayd-io/gatewayd/plugin/v1" @@ -13,6 +14,8 @@ import ( "google.golang.org/protobuf/types/known/structpb" ) +type CompatPolicy uint + const ( DefaultMinPort uint = 50000 DefaultMaxPort uint = 60000 @@ -21,10 +24,16 @@ const ( LoggerName string = "plugin" ) +const ( + Strict CompatPolicy = iota + Loose +) + type Registry interface { Add(plugin *Impl) bool Get(id Identifier) *Impl List() []Identifier + Exists(name, version, remoteURL string) bool Remove(id Identifier) Shutdown() LoadPlugins(pluginConfig *koanf.Koanf) @@ -32,8 +41,9 @@ type Registry interface { } type RegistryImpl struct { - plugins pool.Pool - hooksConfig *HookConfig + plugins pool.Pool + hooksConfig *HookConfig + CompatPolicy CompatPolicy } var _ Registry = &RegistryImpl{} @@ -74,6 +84,40 @@ func (reg *RegistryImpl) List() []Identifier { return plugins } +// Exists checks if a plugin exists in the registry. +func (reg *RegistryImpl) Exists(name, version, remoteURL string) bool { + for _, plugin := range reg.List() { + if plugin.Name == name && plugin.RemoteURL == remoteURL { + // Parse the supplied version and the version in the registry. + suppliedVer, err := semver.NewVersion(version) + if err != nil { + reg.hooksConfig.Logger.Error().Err(err).Msg( + "Failed to parse supplied plugin version") + return false + } + + registryVer, err := semver.NewVersion(plugin.Version) + if err != nil { + reg.hooksConfig.Logger.Error().Err(err).Msg( + "Failed to parse plugin version in registry") + return false + } + + // Check if the version of the plugin is less than or equal to + // the version in the registry. + if suppliedVer.LessThan(registryVer) || suppliedVer.Equal(registryVer) { + return true + } + + reg.hooksConfig.Logger.Debug().Str("name", name).Str("version", version).Msg( + "Supplied plugin version is greater than the version in registry") + return false + } + } + + return false +} + // Remove removes a plugin from the registry. func (reg *RegistryImpl) Remove(id Identifier) { reg.plugins.Remove(id) @@ -105,6 +149,11 @@ func (reg *RegistryImpl) LoadPlugins(pluginConfig *koanf.Koanf) { // Add each plugin to the registry. for priority, name := range plugins { + // Skip the top-level "plugins" key. + if name == "plugins" { + continue + } + reg.hooksConfig.Logger.Debug().Str("name", name).Msg("Loading plugin") plugin := &Impl{ ID: Identifier{ @@ -209,24 +258,62 @@ func (reg *RegistryImpl) LoadPlugins(pluginConfig *koanf.Koanf) { } } + // Retrieve plugin requirements. + if err := mapstructure.Decode(metadata.Fields["requires"].GetListValue().AsSlice(), + &plugin.Requires); err != nil { + reg.hooksConfig.Logger.Debug().Err(err).Msg("Failed to decode plugin requirements") + } + + // Too many requirements or not enough plugins loaded. + if len(plugin.Requires) > reg.plugins.Size() { + reg.hooksConfig.Logger.Debug().Msg( + "The plugin has too many requirements, " + + "and not enough of them exist in the registry, so it won't work properly") + } + + // Check if the plugin requirements are met. + for _, req := range plugin.Requires { + if !reg.Exists(req.Name, req.Version, req.RemoteURL) { + reg.hooksConfig.Logger.Debug().Fields( + map[string]interface{}{ + "name": plugin.ID.Name, + "requirement": req.Name, + }, + ).Msg("The plugin requirement is not met, so it won't work properly") + if reg.CompatPolicy == Strict { + reg.hooksConfig.Logger.Debug().Str("name", plugin.ID.Name).Msg( + "Registry is in strict compatibility mode, so the plugin won't be loaded") + plugin.Stop() // Stop the plugin. + continue + } else { + reg.hooksConfig.Logger.Debug().Fields( + map[string]interface{}{ + "name": plugin.ID.Name, + "requirement": req.Name, + }, + ).Msg("Registry is in loose compatibility mode, " + + "so the plugin will be loaded anyway") + } + } + } + plugin.ID.RemoteURL = metadata.Fields["id"].GetStructValue().Fields["remoteUrl"].GetStringValue() plugin.ID.Version = metadata.Fields["id"].GetStructValue().Fields["version"].GetStringValue() plugin.Description = metadata.Fields["description"].GetStringValue() plugin.License = metadata.Fields["license"].GetStringValue() plugin.ProjectURL = metadata.Fields["projectUrl"].GetStringValue() - if err := mapstructure.Decode(metadata.Fields["requires"].GetListValue().AsSlice(), - &plugin.Requires); err != nil { - reg.hooksConfig.Logger.Debug().Err(err).Msg("Failed to decode plugin requirements") - } + // Retrieve authors. if err := mapstructure.Decode(metadata.Fields["authors"].GetListValue().AsSlice(), &plugin.Authors); err != nil { reg.hooksConfig.Logger.Debug().Err(err).Msg("Failed to decode plugin authors") } + // Retrieve hooks. if err := mapstructure.Decode(metadata.Fields["hooks"].GetListValue().AsSlice(), &plugin.Hooks); err != nil { reg.hooksConfig.Logger.Debug().Err(err).Msg("Failed to decode plugin hooks") } + // Retrieve plugin config. plugin.Config = make(map[string]string) for key, value := range metadata.Fields["config"].GetStructValue().AsMap() { if val, ok := value.(string); ok { diff --git a/plugin/v1/plugin.pb.go b/plugin/v1/plugin.pb.go index 83999d16..e8e83776 100644 --- a/plugin/v1/plugin.pb.go +++ b/plugin/v1/plugin.pb.go @@ -186,9 +186,9 @@ type PluginConfig struct { Id *PluginID `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` + Authors []string `protobuf:"bytes,3,rep,name=authors,proto3" json:"authors,omitempty"` License string `protobuf:"bytes,4,opt,name=license,proto3" json:"license,omitempty"` ProjectUrl string `protobuf:"bytes,5,opt,name=project_url,json=projectUrl,proto3" json:"project_url,omitempty"` - Authors []string `protobuf:"bytes,3,rep,name=authors,proto3" json:"authors,omitempty"` // internal and external config options Config map[string]string `protobuf:"bytes,6,rep,name=config,proto3" json:"config,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` // hooks it attaches to @@ -245,6 +245,13 @@ func (x *PluginConfig) GetDescription() string { return "" } +func (x *PluginConfig) GetAuthors() []string { + if x != nil { + return x.Authors + } + return nil +} + func (x *PluginConfig) GetLicense() string { if x != nil { return x.License @@ -259,13 +266,6 @@ func (x *PluginConfig) GetProjectUrl() string { return "" } -func (x *PluginConfig) GetAuthors() []string { - if x != nil { - return x.Authors - } - return nil -} - func (x *PluginConfig) GetConfig() map[string]string { if x != nil { return x.Config @@ -330,12 +330,12 @@ var file_plugin_v1_plugin_proto_rawDesc = []byte{ 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x49, 0x44, 0x52, 0x02, 0x69, 0x64, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, - 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x6c, 0x69, 0x63, 0x65, 0x6e, 0x73, - 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, - 0x12, 0x1f, 0x0a, 0x0b, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x75, 0x72, 0x6c, 0x18, - 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x55, 0x72, - 0x6c, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x73, 0x18, 0x03, 0x20, 0x03, - 0x28, 0x09, 0x52, 0x07, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x73, 0x12, 0x3b, 0x0a, 0x06, 0x63, + 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, + 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x73, + 0x12, 0x18, 0x0a, 0x07, 0x6c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x07, 0x6c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x70, 0x72, + 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0a, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x55, 0x72, 0x6c, 0x12, 0x3b, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x45, 0x6e, 0x74, 0x72, 0x79, diff --git a/plugin/v1/plugin.proto b/plugin/v1/plugin.proto index cda0ef7a..cef887e0 100644 --- a/plugin/v1/plugin.proto +++ b/plugin/v1/plugin.proto @@ -51,9 +51,9 @@ message PluginID { message PluginConfig { PluginID id = 1; string description = 2; + repeated string authors = 3; string license = 4; string project_url = 5; - repeated string authors = 3; // internal and external config options map config = 6; // hooks it attaches to