Plugins (Experimental)

OPA can be extended with custom built-in functions and plugins that implement functionality like support for new protocols.

This page focuses on how to build Go plugins that can be loaded when OPA starts however the steps are similar if you are embedding OPA as a library or building from source.

Building Go Plugins

At minimum, your Go plugin must implement the following:

  1. package main
  2. func Init() error {
  3. // your init function
  4. }

When OPA starts, it will invoke the Init 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

NOTE: You must build your plugin against the same version of the OPA that will eventually load the shared object file. If you build your plugin against a different version of the OPA source, the OPA will fail to start. You will see an error message like:

  1. Error: plugin.Open("plugin/logger"): plugin was built with a different version of package github.com/open-policy-agent/opa/ast

Built-in Functions

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:

  1. package main
  2. import (
  3. "github.com/open-policy-agent/opa/ast"
  4. "github.com/open-policy-agent/opa/types"
  5. "github.com/open-policy-agent/opa/topdown"
  6. "github.com/open-policy-agent/opa/topdown/builtins"
  7. )
  8. var HelloBuiltin = &ast.Builtin{
  9. Name: "hello",
  10. Decl: types.NewFunction(
  11. types.Args(types.S),
  12. types.S,
  13. ),
  14. }
  15. func HelloImpl(a ast.Value) (ast.Value, error) {
  16. s, err := builtins.StringOperand(a, 1)
  17. if err != nil {
  18. return nil, err
  19. }
  20. return ast.String("hello, " + string(s)), nil
  21. }
  22. func Init() error {
  23. ast.RegisterBuiltin(HelloBuiltin)
  24. topdown.RegisterFunctionalBuiltin1(HelloBuiltin.Name, HelloImpl)
  25. return nil
  26. }

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.

Custom Plugins

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 github.com/open-policy-agent/opa/runtime#RegisterPlugin inside your Init function.

Putting It Together

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. }
  8. func (p *PrintlnLogger) Start(ctx context.Context) error {
  9. // No-op.
  10. return nil
  11. }
  12. func (p *PrintlnLogger) Stop(ctx context.Context) {
  13. // No-op.
  14. }
  15. func (p *PrintlnLogger) Reconfigure(ctx context.Context, config interface{}) {
  16. p.mtx.Lock()
  17. defer p.mtx.Unlock()
  18. p.config = config.(Config)
  19. }
  20. func (p *PrintlnLogger) Log(ctx context.Context, event logs.EventV1) error {
  21. p.mtx.Lock()
  22. defer p.mtx.Unlock()
  23. w := os.Stdout
  24. if p.config.Stderr {
  25. w = os.Stderr
  26. }
  27. fmt.Fprintln(w, event) // ignoring errors!
  28. return nil
  29. }

Next, implement a factory function that instantiates your plugin:

  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:

  1. func Init() {
  2. runtime.RegisterPlugin("println_decision_logger", Factory{})
  3. }

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:

  1. opa --plugin-dir $PWD run --server --config-file config.yaml

Exercise the plugin via the OPA API:

  1. curl localhost:8181/v1/data

If everything worked you will see the Go struct representation of the decision log event written to stdout.

The source code for this example can be found here.

If there is a mask policy set (see Decision Logger for details) the Event received by the demo plugin will potentially be different than the example documented.