Files
releases/docs/content/plugins.md
T
Tim Hinrichs 6760303a47 docs: Reorganize the docs navigation
The docs have grown organically and it was time for a reorganization, specifically
helping people understand at a single glance the information
that is available in the docs.

The new docs are organized as follows:
- Core docs (policy and data)
- Use-case specific docs
- Operations
- Support

As part of the reorganization, several files were renamed and/or combined.

Also made an editing pass over what was previously the user-attribute docs
and generalized them to focus on data-replication, an issue that is
now promoted to the core docs.  As part of that, updated the images
and removed the old ones; new image sources are in google slides.

Tweaked the navigation bar to give more space on the left-hand column
so that more of the topics are visible without scrolling.

Signed-off-by: Tim Hinrichs <tim@styra.com>
2019-08-29 09:29:08 -07:00

5.9 KiB

title, navtitle, kind, weight
title navtitle kind weight
Plugins (Experimental) Extensions operations 1

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:

package main

func Init() error {
    // your init function
}

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:

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

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:

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:

package main

import (
	"github.com/open-policy-agent/opa/ast"
	"github.com/open-policy-agent/opa/types"
	"github.com/open-policy-agent/opa/topdown"
	"github.com/open-policy-agent/opa/topdown/builtins"
)

var HelloBuiltin = &ast.Builtin{
	Name: "hello",
	Decl: types.NewFunction(
		types.Args(types.S),
		types.S,
	),
}

func HelloImpl(a ast.Value) (ast.Value, error) {
	s, err := builtins.StringOperand(a, 1)
	if err != nil {
		return nil, err
	}
	return ast.String("hello, " + string(s)), nil
}

func Init() error {
	ast.RegisterBuiltin(HelloBuiltin)
	topdown.RegisterFunctionalBuiltin1(HelloBuiltin.Name, HelloImpl)
	return nil
}

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.

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

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
}

Next, implement a factory function that instantiates your plugin:

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:

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

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:

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

Exercise the plugin via the OPA API:

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.