... | @@ -63,7 +63,7 @@ Opened question is how to exchange data between hooks registered on the same cal |
... | @@ -63,7 +63,7 @@ Opened question is how to exchange data between hooks registered on the same cal |
|
|
|
|
|
We probably need to implement something similar to the `LibraryManager` from Kea to manage the hooks. We also need the `HooksManager` to register and call the hook callouts.
|
|
We probably need to implement something similar to the `LibraryManager` from Kea to manage the hooks. We also need the `HooksManager` to register and call the hook callouts.
|
|
|
|
|
|
### Technical details
|
|
## Technical details
|
|
|
|
|
|
```mermaid
|
|
```mermaid
|
|
classDiagram
|
|
classDiagram
|
... | @@ -180,20 +180,200 @@ classDiagram |
... | @@ -180,20 +180,200 @@ classDiagram |
|
BarBazImpl ..|> BazCallout
|
|
BarBazImpl ..|> BazCallout
|
|
```
|
|
```
|
|
|
|
|
|
The hook module will contain three elements:
|
|
The code is distributed over four packages:
|
|
|
|
|
|
- Stork version
|
|
- hook module - it contains the interfaces and is shared between core and hook libraries
|
|
- Hook interface
|
|
- plugin package - the hook library implementations
|
|
- Callout types
|
|
- support package - utilities to search, load, and handle hook libraries
|
|
|
|
- agent/server package - the core package that calls the hook callout points
|
|
|
|
|
|
The Stork version is forwarded from the `version.go` file and is used to check the compatibility between the core and hook. Hook interface is a Go interface that contains the definitions of primary functions (`load`, `unload`, `version`) and `callouts` object. The `callouts` object implements a variety set of callouts functions. The hook module defines the callouts as Go interfaces. The hook's author can use the interface checking technique to ensure that the callout signatures are correct.
|
|
The hook module [`HookModule`] defines the primary hook interface [`IHook`]. It specifies that each hook should provide the `Version` and `Load` functions. The `Version` function returns the application name and version with which the plugin is compatible. The `Load` function produces an object that implements the callout points. The supported callout points are defined in the hook module as Go interfaces [`FooCallout`/`BarCallout`/`BazCallout`].
|
|
|
|
|
|
On the top level, the hooks are managed by the hook loader. It is responsible for searching for the hook libraries, creating the library manager instances, and calling them. The loader works on the server's startup for loading all possible hooks and on the graceful shutdown to unload them. It should skip and log the libraries that report incompatible versions.
|
|
The hook creator must pick one or more callout interfaces and implement them in a single structure [`FooImpl`/`BarBazImpl`]. The structure may provide the `Close` function. It will be called during a graceful shutdown. The structure instance must be created by the top-level `Load` function. The creator also must implement the top-level `Version` function using the current version [`CurrentVersion'] constant provided by the hook module.
|
|
|
|
|
|
The library manager is a wrapper around the plugin module. It is responsible for retrieving and calling the primary functions and the callouts object. It can have a short lifetime because each Go plugin is opened only once and never closed. The library manager functions are called by the hook loader. The hook loader passes the callouts object to the hook manager.
|
|
The core needs to provide the hook manager [`AgentHookManager`/`ServerHookManager`] as a facade for the hooks. It is responsible for calling the callout points. The hook manager implements all supported callout points and defers calls to the executor [`HookExecutor`]. Executor contains all callout implementations from hooks and provides methods for calling them. The callouts can be called sequentially "one-by-one," to first success, to first fail, etc. The calling order is defined by the hook manager depending on the characteristics of the callout point. The construction of the executor has two phases. First, the hook manager creates an instance and registers all support callouts' types (interfaces). Next, the hook loader [`HookLoader`] retrieves the callout point objects and registers them in the executor. Now, the executor contains the mapping between a callout type (interface) and its implementations.
|
|
|
|
|
|
The hook manager is a facade for all hooks. It implements all hook callouts. The server's function that needs to pass data to the hooks doesn't need to check if a specific hook is registered or not. They just call the hook manager methods. After loading a hook, the callout object is passed to the manager. The manager enumerates all callout types and checks if a given callout object implements them. If yes, it registers a given callout object as a hook handler. The hook manager's callout implementations redirect the calls to a specific handler or handlers or do nothing if no handler was registered.
|
|
The hook loader searches for the hook libraries in the directory specified by the configuration. It opens the files using the library manager `LibraryManager` which knows how to retrieve the expected members. The loader is responsible for checking the application name and version compatibility between core and hook.
|
|
|
|
|
|
The hook's author needs to prepare a structure that implements a set of chosen callouts. The interface checks should be used to ensure that the function signatures are valid. The hook must export the `Callouts` variable with an instance of this structure. The instance may be created in the `Load` function and destroyed in the `Unload` function. Additionally, the hook requires the `Version` function to be provided.
|
|
The hook manager hides all details of the hook solution. The code that calls the callout point doesn't matter if a specific hook is registered or how many hooks implement it.
|
|
|
|
|
|
The hook loader searches for hook libraries in a specific directory. Users can provide a path by a flag or environment variable or use a default value. |
|
## Steps to implement hook
|
|
|
|
|
|
|
|
1. Look for needed callout points in the hook module
|
|
|
|
|
|
|
|
```go
|
|
|
|
type Foo interface {
|
|
|
|
int Foo(x int)
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
2. Prepare a structure that will implement the callouts
|
|
|
|
|
|
|
|
```go
|
|
|
|
type Callouts struct {}
|
|
|
|
```
|
|
|
|
|
|
|
|
3. Write interface checks to ensure that the callouts will have a correct signature. It would cause compilation errors if the callout point changed.
|
|
|
|
|
|
|
|
```go
|
|
|
|
var _ hooks.Foo = (*Callouts)(nil)
|
|
|
|
```
|
|
|
|
|
|
|
|
4. Implement callout function
|
|
|
|
|
|
|
|
```go
|
|
|
|
func (c *Callouts) Foo(x int) int {
|
|
|
|
return 42
|
|
|
|
}
|
|
|
|
|
|
|
|
5. Prepare top-level version function using the constants from the shared module
|
|
|
|
|
|
|
|
```go
|
|
|
|
func Version() (string, string) {
|
|
|
|
return hooks.AgentName, hooks.CurrentVersion
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
6. Prepare top-level load function
|
|
|
|
|
|
|
|
```go
|
|
|
|
func Load() (interface{}, error) {
|
|
|
|
return &Callouts{}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
7. Compile to a plugin file
|
|
|
|
|
|
|
|
```go
|
|
|
|
To be done...
|
|
|
|
go build --plugin ...
|
|
|
|
```
|
|
|
|
|
|
|
|
8. Copy the plugin file to the hook directory
|
|
|
|
|
|
|
|
```sh
|
|
|
|
cp foo-hook.go /var/lib/stork-server/hooks
|
|
|
|
```
|
|
|
|
|
|
|
|
9. Run the Stork. Enjoy!
|
|
|
|
|
|
|
|
## Implementation examples
|
|
|
|
|
|
|
|
There is an initial proposal for implementation. You can find the current development in #779.
|
|
|
|
|
|
|
|
### Hook Manager implementation
|
|
|
|
|
|
|
|
We need to prepare the list of callouts supported by a given application. They are specified using `reflect.Type` objects. It is helpful first to assign them into variables because the syntax is a little complex.
|
|
|
|
|
|
|
|
```go
|
|
|
|
var (
|
|
|
|
BeforeForwardToKeaOverHTTPCalloutType reflect.Type = reflect.TypeOf((*hooks.BeforeForwardToKeaOverHTTPCallout)(nil)).Elem()
|
|
|
|
)
|
|
|
|
```
|
|
|
|
|
|
|
|
You need to be aware of two traps that aren't detected by the linting system:
|
|
|
|
|
|
|
|
- The pointer type must be used. Miss of an asterisk causes nil.
|
|
|
|
- The proper type must be extracted by the `.Elem()` call. Otherwise, check if a specific callout is implemented panics with the `non-interface type` message.
|
|
|
|
|
|
|
|
The hook module provides an implementation of the hook executor. The constructor accepts the list of the supported callouts. It's convenient to prepare a specialized constructor for a specific application (e.g. agent or server):
|
|
|
|
|
|
|
|
```go
|
|
|
|
func newHookExecutor() *hooks.HookExecutor {
|
|
|
|
executor := hooks.NewHookExecutor([]reflect.Type{
|
|
|
|
BeforeForwardToKeaOverHTTPCalloutType,
|
|
|
|
})
|
|
|
|
return executor
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
We need to prepare the hook manager. It is a facade for hooks. It will be used in the core to call the callouts.
|
|
|
|
|
|
|
|
```go
|
|
|
|
type HookManager struct {
|
|
|
|
executor *hooks.HookExecutor
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
The hook manager must implement all supported hooks. The interface checks should ensure that the proper signatures are used.
|
|
|
|
|
|
|
|
```go
|
|
|
|
var _ hooks.BeforeForwardToKeaOverHTTPCallout = (*HookManager)(nil)
|
|
|
|
```
|
|
|
|
|
|
|
|
The hook manager doesn't provide the implementation of the callouts. It only defers the calls to hook executor. The hook executor contains the functions to walk through the hooks in some orders. We can call all hooks or call until to specific condition occurs. The iteration method should be matched to the specificity of the callout. The executor doesn't know the callout signature; it needs a caller function that passes the arguments to the hook.
|
|
|
|
|
|
|
|
```go
|
|
|
|
func (hm *HookManager) OnBeforeForwardToKeaOverHTTP(in *agentapi.ForwardToKeaOverHTTPReq) {
|
|
|
|
hm.executor.CallSequential(BeforeForwardToKeaOverHTTPCalloutType, func(rawCallout interface{}) {
|
|
|
|
callout := rawCallout.(hooks.BeforeForwardToKeaOverHTTPCallout)
|
|
|
|
callout.OnBeforeForwardToKeaOverHTTP(in)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
The hook manager should implement the `Close` function to clean up all hooks. It just calls the unregister function of the executor.
|
|
|
|
|
|
|
|
```go
|
|
|
|
func (hm *HookManager) Close() {
|
|
|
|
hm.executor.UnregisterAllCallouts()
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
The hook manager needs a constructor that must prepare an executor, create an instance and load the callouts. The hook module provides a utility function to perform the loading. It's convenient for writing unit tests to define an additional constructor that accepts already loaded callouts objects.
|
|
|
|
|
|
|
|
```go
|
|
|
|
func newHookManager(executor *hooks.HookExecutor) *HookManager {
|
|
|
|
return &HookManager{
|
|
|
|
executor: executor,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func NewHookManagerFromDirectory(directory string) *HookManager {
|
|
|
|
allCallouts := hooks.LoadAllHooks(hooks.HookProgramAgent, directory)
|
|
|
|
return NewHookManagerFromCallouts(allCallouts)
|
|
|
|
}
|
|
|
|
|
|
|
|
func NewHookManagerFromCallouts(allCallouts []interface{}) *HookManager {
|
|
|
|
executor := newHookExecutor()
|
|
|
|
for _, callouts := range allCallouts {
|
|
|
|
executor.RegisterCallouts(callouts)
|
|
|
|
}
|
|
|
|
return newHookManager(executor)
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
### Using hooks in core
|
|
|
|
|
|
|
|
The core uses only the hook manager object. The core setup creates the manager instance and assigns it to the main application object.
|
|
|
|
|
|
|
|
```go
|
|
|
|
func NewStorkAgent(settings *cli.Context, appMonitor AppMonitor) *StorkAgent {
|
|
|
|
...
|
|
|
|
sa := &StorkAgent{
|
|
|
|
...
|
|
|
|
hookManager: NewHookManagerFromDirectory(settings.Path("hook-directory")),
|
|
|
|
}
|
|
|
|
...
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
Any core function may use the hook manager to call a specific hook.
|
|
|
|
|
|
|
|
```go
|
|
|
|
func (sa *StorkAgent) ForwardToKeaOverHTTP(ctx context.Context, in *agentapi.ForwardToKeaOverHTTPReq) (*agentapi.ForwardToKeaOverHTTPRsp, error) {
|
|
|
|
|
|
|
|
sa.hookManager.OnBeforeForwardToKeaOverHTTP(in)
|
|
|
|
|
|
|
|
...
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
The application calls the stop method of the hook manager during the graceful shutdown. It cleans up all hooks (if applicable).
|
|
|
|
|
|
|
|
```go
|
|
|
|
func (sa *StorkAgent) Shutdown() {
|
|
|
|
...
|
|
|
|
sa.hookManager.Close()
|
|
|
|
...
|
|
|
|
}
|
|
|
|
``` |