»Extending Sentinel: Modules

Modules allow you to re-use Sentinel code as an import. This allows for the export of any general construct that Sentinel supports, including values, functions, and even rules. As such, it's a great way to package code that is useful across multiple policies, reducing boilerplate and making final policy code simpler.

You may want to use modules for:

  • Writing a simple re-usable helper library. If you mostly know Sentinel or have helpers that you have written in Sentinel, moving these helpers to a module will offer a low-friction way of facilitating their re-use.
  • Writing re-usable rules. If you have a set of rules you know you want to use across multiple policies, bundling them into a module is a great way of abstracting the logic away.
  • Abstracting existing Sentinel imports. Using a module is a great way to extend existing Sentinel imports by abstracting the common functionality that you use within an import to your own workflow.

This page describes how you can use modules within the context of the Sentinel CLI. For instructions for a particular integration, see the documentation for that product.

»Configuring a Module

A module is configured within the Sentinel CLI by adding an entry for it in within the modules section of the CLI configuration file. The key for each entry specifies the path that the module is imported as. The path value within each entry specifies the path to the module file on the local filesystem.

{
  "modules": {
    "foo": {
      "path": "modules/foo.sentinel"
    },
    "bar": {
      "path": "modules/bar.sentinel"
    }
  }
}

In this example, we have two modules:

  • Import path foo, being loaded from the code within modules/foo.sentinel.
  • Import path bar, being loaded from the code within modules/bar.sentinel.

See the modules section within the CLI configuration file syntax page for more details.

»Testing Using Modules

As with mocks, modules are loaded relative to the configuration file location when using sentinel test. As such, you need to account for this in your test configuration files:

{
  "modules": {
    "foo": {
      "path": "../../modules/foo.sentinel"
    },
    "bar": {
      "path": "../../modules/bar.sentinel"
    }
  }
}

This could be a test file for a policy (example: policy.sentinel), residing under the correct test file directory for this policy (example: test/policy/pass.json).

»Writing and Using a Module

Writing a module is nearly the same as writing a Sentinel policy, except for the following two exceptions:

  • Modules do not require main to be present. It can be, but it will not carry any extra significance as it does within a policy.
  • param declarations cannot be present in a module. If they are encountered, a runtime error is given.

Once you have a complete module ready for use and configured, using the module is as simple as importing it.

Building on the above configuration example, we can export a simple test function in the module foo by adding this code to modules/foo.sentinel:

// modules/foo.sentinel
hello = func() {
  print("hello world!")
  return undefined
}

We can then import it within our policy and use it:

// policy.sentinel
import "foo"

// prints "hello world" in the trace
foo.hello()

main = true

Note that modules are considered imports by the Sentinel runtime and as such conform to the specification. This means that you cannot directly assign values to anything behind a module scope. You can, however, use functions to change state.