Sentinel: Introduction To Sentinel Language – Part-4

Share At:

HashiCorp DevOps tools add Sentinel for IT policy management

This Article only discusses about following topics in Sentinel Language:

  • Functions
  • Scope
  • Undefined
  • Logging and Errors

Sentinel policies are written using the Sentinel language. This language is easy to learn and easy to write. You can learn the Sentinel language and be productive within an hour. Learning Sentinel doesn’t require any formal programming experience.

Language: Functions

Functions allow you to create reusable code to perform computations.

Below is a trivial example function that doubles a number and shows the main rule using the function:

double = func(x) {
	return x * 2
}

main = rule { double(12) is 24 }

Functions may contain any expressions, conditionals, and loops. They must return a value at the end of their execution. If no reasonable return value exists, the function should return undefined.

Creating a Function

A function is created by assigning a variable to a func.

A function can have zero or more parameters. These parameters are specified within parentheses () separated by commas. If a function has no parameters, the parentheses must still be present.

A function must terminate with a return statement to return a value back to the caller. Only a single value can be returned. The type of a return can vary between return statements.

Example:

add1 = func(x) { return x + 1 }

This example creates a function that adds 1 to the parameter x. It is assigned to the variable add1.

Calling a Function

Functions are called by accessing their name followed by parentheses with a list of comma-separated values for the parameters. If the function takes no parameters, an empty set of parentheses must still be used to denote a function call.

To call the function in the example above:

x = 1
y = add1(x)

Scoping

The body of a func creates a new scope.

The parent scope is the scope in which the function is created. This allows for the creation of functions within functions that can access their outer scopes.

Example:

f = func() {
    a = 42
    print(a) // 42
    return undefined
}

print(a) // undefined
a = 18
f = func() {
    a = 42
    return undefined
}

print(a) // 18

And below is an example of creating a function in a function which uses outer values:

f = func() {
    a = 42
    double = func() { return a * 2 }
    return double()
}

print(f()) // 84

A more complex example below shows how scoping works when passing functions around as arguments or results:

f = func() {
    a = 42
    double = func() { return a * 2 }
    return double
}

double = f()
print(double()) // 84

Pass By Value

The parameters to a function are passed by value, but not deep copied. This means that elements of collections can still be modified but the root value cannot be changed.

Example:

f = func(x) {
    x = "value"
    return x
}

x = "outside"
f(x)
print(x) // "outside"
f = func(x) {
    append(x, "value")
    return x
}

x = []
f(x)
print(x) // ["value"]

Recursion

A function is allowed to call other functions, including itself. This can be used to implement recursion. For example, the fibonacci sequence is implemented below:

fib = func(x) {
    if x <= 0 {
        return undefined
    }

    if x == 1 {
        return 1
    } else {
        return x + fib(x - 1)
    }
}

Note that this example also shows using undefined as a return value in cases where a function has undefined behavior.


Language: Scope

Scope is the context in which variables are created and accessed. If a variable exists in a parent scope, it is accessed and can be modified. Otherwise, the variable is created in your current scope.

Scopes are created with blocks. A block is a possibly empty sequence of statements within matching brace brackets {}. You may nest blocks to create nested scopes.

In addition to explicitly created blocks, there are implicit blocks:

  1. The policy block encapsulates an entire policy.
  2. Each anyall, and for statement is considered to be in its own block. Note that if statements do not create their own block.

The example policy below shows various effects of scopes:

a = 1
print(a) // 1

f = func() {
    print(a) // 1
    a = 12
    b = 1
    return undefined
}
f()

print(a) // 12
print(b) // undefined

Language: Undefined

The value undefined is a special value representing undefined behavior or an unknown value.

Undefined is not an error because there are situations where the undefined value can be safely ignored and the language also provides construts for recovering from an undefined value.

The undefined value can be created manually with undefined. In most cases, undefined is created by accessing a non-existent list element or value. The undefine value is created in a number of other circumstances. The documentation will note when an undefined value may be created.

Almost any operation with undefined results in undefined. For example, performing math on undefined, attempting to call a function that is undefined, etc.

Main and Undefined

If the value undefined is returned from the top-level main rule, then the policy is considered false. However, the runtime provides mechanisms for the host application to determine the policy failure was caused by an undefined value and report that to the user.

The main rule returning the undefined value should be considered a logical error in most cases and should probably be corrected by the policy writer.

Debugging Undefined

When an undefined value is first created (either explicitly or implicitly), the runtime will record the position where the undefined was created. This location can be used to find logic that could be erroneous.

Boolean Logic and Undefined

Boolean logic is one way to safely recover from undefined. If boolean logic can safely determine a result despite an undefined value, that result will be returned.

For example: undefined or true will result in true, because no matter what actual value undefined could’ve been if the policy behaved properly, the boolean expression would still be true.

Another scenario where undefined can be safely ignored is short-circuit logic with andfalse and undefined will result in false.

The full table of logical operations on undefined is below:

undefined or true       = true
undefined or false      = undefined
undefined or undefined  = undefined
undefined and true      = undefined
undefined and false     = undefined
undefined and undefined = undefined
undefined xor true      = undefined
undefined xor false     = undefined
undefined xor undefined = undefined

// Short-circuit examples
false or true or undefined   = true
false or undefined or true   = true
true and false and undefined = false
true and undefined and false = undefined

Else Expressions

The else expression allows explicit recovery from undefined and is useful for setting default values.

else is an operator where the left and right hand side are values. If the left-hand value is undefined, the right-hand value is returned. Otherwise, the left-hand value is returned.

Examples:

foo() else 42
foo.bar else ""
config["bad-key"] else "default"

// In more complex scenarios

foo() else 42 == 12
a = config.value else "default"

Language: Logging and Errors

The built-in functions print and error can be used for logging and errors. Logging is helpful to understand how a policy is executing in development. And errors are useful to halting execution immediately in certain scenarios.

Print

The print function is used to output debug information. The exact location where this print statements go is dependent on the host application and the Sentinel runtime itself.

The print function takes one or more Sentinel values as arguments. Each value is formatted for human-friendly output and space-separated. Example:

value = 42
print("the value is", value) // the value is 42

map = { "foo": false }
print(map) // { "foo": false }

one_is_zero = rule { 1 == 0 }
print(one_is_zero) // false

Errors

Certain actions in Sentinel, such as accessing an undefined variable, attempting to add two incompatible types, etc. result in runtime errors. These errors halt the execution of a policy immediately. The pass/fail result of a policy is considered fail.

Runtime errors can be manually triggered using the error function. The error function behaves exactly like the print function except that execution is halted immediately.

This should only be used in scenarios where the policy must fail due to some unexpected or invalid condition. The line between error and undefined can sometimes be unclear. Undefined is recommended when a policy can conceivably recover or safely ignore the undefined behavior. An error should be used when the policy should fail and notify someone regardless.

Happy Learning !!!


Share At:
0 0 votes
Article Rating
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments
Back To Top

Contact Us