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:

  1. 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.

  1. opa --plugin-dir=/path/to/plugins run
  1. 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:

  1. > hello("bob")
  2. "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:

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).

  1. type Config struct {
  2. Stderr bool `json:"stderr"` // false => stdout, true => stderr
  3. }
  4. type PrintlnLogger struct {
  5. mtx sync.Mutex
  6. config Config
  7. func (p *PrintlnLogger) Start(ctx context.Context) error {
  8. // No-op.
  9. return nil
  10. }
  11. func (p *PrintlnLogger) Stop(ctx context.Context) {
  12. // No-op.
  13. }
  14. func (p *PrintlnLogger) Reconfigure(ctx context.Context, config interface{}) {
  15. p.mtx.Lock()
  16. defer p.mtx.Unlock()
  17. p.config = config.(Config)
  18. }
  19. func (p *PrintlnLogger) Log(ctx context.Context, event logs.EventV1) error {
  20. p.mtx.Lock()
  21. defer p.mtx.Unlock()
  22. w := os.Stdout
  23. if p.config.Stderr {
  24. w = os.Stderr
  25. fmt.Fprintln(w, event) // ignoring errors!
  26. return nil
  27. }
  1. type Factory struct{}
  2. func (Factory) New(_ *plugins.Manager, config interface{}) plugins.Plugin {
  3. return &PrintlnLogger{
  4. config: config.(Config),
  5. }
  6. }
  7. func (Factory) Validate(_ *plugins.Manager, config []byte) (interface{}, error) {
  8. parsedConfig := Config{}
  9. return parsedConfig, util.Unmarshal(config, &parsedConfig)
  10. }

Finally, register your factory with OPA:

To test your plugin, build a shared object file:

  1. go build -buildmode=plugin -o=plugin.so main.go

Define an OPA configuration file that will use your plugin:

config.yaml:

  1. decision_logs:
  2. plugin: println_decision_logger
  3. plugins:
  4. println_decision_logger:
  5. 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.