OPA can be extended with custom built-in functions and plugins that implement functionality like support for new protocols.
At minimum, your Go plugin must implement the following:
When OPA starts, it will invoke the function which can:
- Register custom built-in functions.
- Register custom OPA plugins (e.g., decision loggers, servers, etc.)
- …or do anything else.
See the sections below for examples.
To build your plugin into a shared object file (.so
), you will (minimally) run the following command:
go build -buildmode=plugin -o=plugin.so plugin.go
This will produce a file named plugin.so
that you can pass to OPA with the --plugin-dir
flag. OPA will load all of the .so
files out of the directory you give it.
opa --plugin-dir=/path/to/plugins run
Error: plugin.Open("plugin/logger"): plugin was built with a different version of package github.com/open-policy-agent/opa/ast
To implement custom built-in functions your Init
function should call:
ast.RegisterBuiltin
to declare the built-in function.topdown.RegisterFunctionalBuiltin[X]
to register the built-in function implementation (where X is replaced by the number of parameters your function receives.)
For example:
If you build this file into a shared object and start OPA with it you can call it like other built-in functions:
> hello("bob")
"hello, bob"
For more details on implementing built-in functions, see the OPA Go Documentation.
OPA defines a plugin interface that allows you to customize certain behaviour like decision logging or add new behaviour like different query APIs. To implement a custom plugin you must implement two interfaces:
- to instantiate your plugin.
- github.com/open-policy-agent/opa/plugins#Plugin to provide your plugin behavior.
You can register your factory with OPA by calling inside your Init
function.
The example below shows how you can implement a custom Decision Logger that writes events to a stream (e.g., stdout/stderr).
type Config struct {
Stderr bool `json:"stderr"` // false => stdout, true => stderr
}
type PrintlnLogger struct {
mtx sync.Mutex
config Config
func (p *PrintlnLogger) Start(ctx context.Context) error {
// No-op.
return nil
}
func (p *PrintlnLogger) Stop(ctx context.Context) {
// No-op.
}
func (p *PrintlnLogger) Reconfigure(ctx context.Context, config interface{}) {
p.mtx.Lock()
defer p.mtx.Unlock()
p.config = config.(Config)
}
func (p *PrintlnLogger) Log(ctx context.Context, event logs.EventV1) error {
p.mtx.Lock()
defer p.mtx.Unlock()
w := os.Stdout
if p.config.Stderr {
w = os.Stderr
fmt.Fprintln(w, event) // ignoring errors!
return nil
}
type Factory struct{}
func (Factory) New(_ *plugins.Manager, config interface{}) plugins.Plugin {
return &PrintlnLogger{
config: config.(Config),
}
}
func (Factory) Validate(_ *plugins.Manager, config []byte) (interface{}, error) {
parsedConfig := Config{}
return parsedConfig, util.Unmarshal(config, &parsedConfig)
}
Finally, register your factory with OPA:
To test your plugin, build a shared object file:
go build -buildmode=plugin -o=plugin.so main.go
Define an OPA configuration file that will use your plugin:
config.yaml:
decision_logs:
plugin: println_decision_logger
plugins:
println_decision_logger:
stderr: false
Start OPA with the plugin directory and configuration file:
Exercise the plugin via the OPA API:
If everything worked you will see the Go struct representation of the decision log event written to stdout.