Functional Models with Jsonnet
Rescile supports Jsonnet as a parallel, first-class authoring language for models (models/*.jsonnet), compliance (compliance/*.jsonnet), and outputs (output/*.jsonnet).
Jsonnet files evaluate into canonical Rescile rule structures at build time, executing alongside existing TOML rules in the property graph stabilization loop.
Why Jsonnet?
- Reusability: Define helper functions and shared configurations instead of repeating TOML blocks.
- Comprehensions: Generate rule sets from lists or data structures using
[rule(x) for x in list]. - Pure & Deterministic: Pure functional configuration evaluated within a confined sandbox.
- Gradual Adoption:
.jsonnetand.tomlfiles coexist seamlessly in the same directory.
Decision Rubric: When to Use TOML vs. Jsonnet
| Pattern | Recommended Format | Rationale |
|---|---|---|
| Simple Static Singleton (e.g. creating 1 fixed boundary node) | TOML | Keep in TOML (omit origin_resource). 5 lines of declarative fields; Jsonnet adds unnecessary indirection. |
| Parameterized Rule Family (e.g. multiple rules differing only by env/tier) | Jsonnet | Array comprehension [rule(x) for x in list] eliminates boilerplate. |
Global Input Iteration (e.g. creating nodes from input/data.json) |
Jsonnet | Native import 'data.json' and comprehension is cleaner than TOML create_from. Rules default to singleton (origin=''). |
| Global Aggregation / Calculation | Jsonnet | Pure functions, math, and filtering in Jsonnet or lib/*.libsonnet. Rules default to singleton (origin=''). |
| Per-Resource Graph Traversal | TOML or Jsonnet | Use TOML if 1 direct rule; use Jsonnet (origin='servers' or rescile.forOrigin('servers', [...])) if generating multiple variants. |
Natural Defaults: Like TOML, all
rescile.libsonnetbuilders default to singleton execution (origin=''). To iterate over graph vertices, specifyorigin='servers'or wrap rules inrescile.forOrigin('servers', [...]).
Embedded Standard Library (rescile.libsonnet)
Rescile embeds a standard helper library accessible via virtual import:
local rescile = import 'rescile/v1/rescile.libsonnet';
Core Builders
| Helper | Description |
|---|---|
rescile.eq(property, value) |
Equality matcher { property: "...", value: "..." } |
rescile.neq(property, value) |
Inequality matcher { property: "...", value: "...", op: "neq" } |
rescile.inList(property, values) |
List membership matcher |
rescile.forOrigin(originResource, rules) |
Scopes a rule or list of rules to an origin resource |
rescile.createFromList(listName, asName) |
create_from list expansion helper |
rescile.createFromProperty(propName, asName) |
create_from origin property expansion helper |
rescile.createResource(...) |
Resource creation rule (create_resource) |
rescile.linkResources(...) |
Resource link & property copy rule (link_resources) |
rescile.copyProperty(...) |
Property copy rule (copy_property) |
rescile.retypeRelation(...) |
Relation retyping rule (retype_relation) |
rescile.audit(...) |
Compliance audit definition |
rescile.control(...) |
Compliance control definition |
rescile.target(...) |
Compliance control target |
Reusable Libraries and the lib/ Directory
Store shared, reusable .libsonnet files in the dedicated lib/ directory.
my-project/
└── data/ (or module root)
├── lib/
│ ├── network_helpers.libsonnet <-- Reusable functions
│ └── common_tags.libsonnet
├── input/
│ └── providers.json
└── models/
└── vpc_network.jsonnet <-- Model rules
Auto-Discovery from lib/
Rescile’s import resolver automatically searches the lib/ directory in the module root. You can import libraries directly by filename without relative directory paths (../lib/):
// models/vpc_network.jsonnet
local rescile = import 'rescile/v1/rescile.libsonnet';
local net = import 'network_helpers.libsonnet'; // <-- Auto-resolved from lib/
rescile.createResource(
origin='vpcs',
resourceType='subnet',
relationType='CONTAINS_SUBNET',
name=net.makeSubnetName('{{ origin_resource.name }}', 'public'),
properties={ cidr: net.calcCidr('10.0.0.0/16', 1) },
id='vpc-subnet'
)
Purpose of lib/
- Exclusively for Jsonnet: The
lib/directory is exclusively scanned by the Jsonnet resolver. TOML models, CSV assets, and action runtimes do not read fromlib/. - Pure Functions: Author business logic, naming functions, data normalization, and property calculation in
lib/*.libsonnet. - Keep Models Clean: Models in
models/*.jsonnetfocus on declarative graph bindings (createResource,linkResources) while delegating complex calculations tolib/.
Setting origin_resource to Iterate Over Graph Vertices
Every model rule specifies which graph resources it consumes:
1. Iterating Over Graph Vertices
Setting origin='servers' evaluates the rule once for every servers vertex in the graph. In template strings, {{ origin_resource.property }} accesses the properties of the current vertex:
local rescile = import 'rescile/v1/rescile.libsonnet';
rescile.createResource(
origin='servers', // <-- Iterates over every 'servers' vertex in graph
resourceType='active_server',
relationType='IS_ACTIVE',
name='{{ origin_resource.name }}-active',
properties={
ip: '{{ origin_resource.ip_address }}',
status: 'active',
},
match=[rescile.eq('status', 'online')],
id='active-servers'
)
2. Global / Singleton Execution (No Graph Iteration)
Setting origin='' (empty string) executes the rule once globally without binding to a vertex:
local rescile = import 'rescile/v1/rescile.libsonnet';
rescile.createResource(
origin='', // <-- Runs once globally
resourceType='security_boundary',
relationType='DEFINES',
name='global-perimeter',
properties={ level: 'pci-dss' },
id='global-boundary'
)
3. Multiple origin_resource Blocks in One File
In TOML, files are split at origin_resource = "..." boundaries. In Jsonnet, simply return an array of model rule objects targeting different origins:
local rescile = import 'rescile/v1/rescile.libsonnet';
[
// Iterates over all 'servers'
rescile.createResource(
origin='servers',
resourceType='backup_job',
relationType='HAS_BACKUP',
name='backup-{{ origin_resource.name }}',
id='server-backups'
),
// Iterates over all 'databases'
rescile.createResource(
origin='databases',
resourceType='backup_job',
relationType='HAS_BACKUP',
name='backup-{{ origin_resource.name }}',
id='db-backups'
),
]
4. Scoping Rule Groups with rescile.forOrigin
Instead of repeating origin='servers' in every rule call, wrap a group of rules in rescile.forOrigin:
local rescile = import 'rescile/v1/rescile.libsonnet';
rescile.forOrigin('servers', [
rescile.createResource(
resourceType='active_server',
relationType='IS_ACTIVE',
name='{{ origin_resource.name }}-active',
),
rescile.linkResources(
withResource='clusters',
joinLocal='cluster_id',
joinRemote='id',
relationType='PART_OF',
),
])
Using Tera Templates and Custom Filters in Generated Rules
Rescile uses a two-stage evaluation pipeline:
- Load Time (Jsonnet): Jsonnet evaluates functions, list comprehensions, and configurations, emitting canonical rule definitions.
- Runtime (Tera & Graph Engine): The importer engine applies these generated rules across vertices in the graph, evaluating all embedded
{{ ... }}strings with the full Tera template engine.
Any string value in name, properties, or match_on can use the full power of Tera expressions and Rescile’s custom filters.
1. CIDR Calculations and Subnetting
Rescile’s custom CIDR filters are fully available inside template strings emitted by Jsonnet:
// models/network_topology.jsonnet
local rescile = import 'rescile/v1/rescile.libsonnet';
rescile.createResource(
origin='vpcs',
resourceType='subnet',
relationType='CONTAINS_SUBNET',
// Calculate specific nth /24 subnet from origin VPC CIDR:
name='subnet-{{ origin_resource.name }}-{{ origin_resource.cidr | cidr_nth_subnet(prefix=24, nth=2) }}',
properties={
// Subnet calculation filter executed dynamically per vertex:
allocated_cidr: '{{ origin_resource.cidr | cidr_nth_subnet(prefix=24, nth=2) }}',
// Split origin CIDR into subnets:
all_subnets: '{{ origin_resource.cidr | cidr_split_n(n=4) }}',
},
id='vpc-subnets'
)
Available CIDR filters in templates:
cidr_nth_subnet(prefix=N, nth=M)— Extracts the M-th subnet of size /N.cidr_split_n(n=N)— Divides a CIDR into N equal subnets.calculate_cidr(ips=[...])— Computes the smallest enclosing CIDR.allocate_subnets(cidr="...", host_map={...})— Allocates subnets sized by required host counts.
2. Cryptographic and Hashing Filters
// models/secure_tokens.jsonnet
local rescile = import 'rescile/v1/rescile.libsonnet';
rescile.createResource(
origin='services',
resourceType='service_identity',
relationType='IDENTIFIED_BY',
name='ident-{{ origin_resource.name }}',
properties={
// Deterministic SHA-256 hash of service name and environment:
token_hash: '{{ (origin_resource.name ~ "-" ~ origin_resource.env) | sha256 }}',
// Base64 encoding:
encoded_id: '{{ origin_resource.name | base64_encode }}',
},
id='service-ident'
)
3. Summary: Jsonnet vs. Tera Roles
- Jsonnet: Generates the rules, handles author-time parameters, loops over static config, imports libraries from
lib/. - Tera: Evaluates per-vertex expressions, runs custom filters (
cidr_*,sha256, JMESPath) against runtime graph data.
Iterating Over Input JSON and Lists (create_from)
Rescile supports two complementary patterns for iterating over data lists:
Pattern A: Native Jsonnet Load-Time Comprehension (Recommended)
Import JSON files or declare lists directly in Jsonnet. Rescile automatically includes the input/ and lib/ directories in the search path (just like TOML’s "json!" = "filename.json"):
// models/cloud_providers.jsonnet
local rescile = import 'rescile/v1/rescile.libsonnet';
// Auto-resolved from input/ directory:
local providers = import 'providers.json';
// 2. Expand into one rule per item:
[
rescile.createResource(
origin='', // Global execution
resourceType='cloud_provider',
relationType='DEFINES',
name='provider-' + p.name,
properties={ region: p.region, enabled: p.enabled },
id='provider-' + p.name
)
for p in providers
]
Pattern B: Runtime create_from Origin Property Expansion
When an origin_resource has a comma-separated or array property (e.g. services = "ui,api,worker"), generate one resource per value at graph runtime using createFromProperty:
// models/app_services.jsonnet
local rescile = import 'rescile/v1/rescile.libsonnet';
rescile.createResource(
origin='applications', // Iterates over each application
createFrom=rescile.createFromProperty('services', asName='service'),
resourceType='service',
relationType='PROVIDES',
name='svc-{{ value | upper }}', // 'value' is each comma-separated entry
properties={
application: '{{ origin_resource.name }}',
},
id='expand-services'
)
Pattern C: Runtime create_from Header / Dynamic Input List
When iterating over an input list loaded by the importer engine:
// models/regions.jsonnet
local rescile = import 'rescile/v1/rescile.libsonnet';
// 1. Declare the list field in the model object:
{
region_list: ['eu-west-1', 'eu-central-1', 'us-east-1'],
} + rescile.createResource(
origin='',
createFrom=rescile.createFromList('region_list'),
resourceType='cloud_region',
relationType='REGION_IN',
name='region-{{ value }}',
id='expand-regions'
)
Linking and Foreign Key Join (linkResources)
// models/application_ownership.jsonnet
local rescile = import 'rescile/v1/rescile.libsonnet';
rescile.linkResources(
origin='applications', // Origin resource
withResource='teams', // Remote resource to link with
joinLocal='owner_team', // Local key on application
joinRemote='name', // Remote key on team
relationType='OWNED_BY',
direction='local_to_remote',
copyProperties=[
{ from: 'lead', as: 'team_lead' },
{ from: 'slack_channel', as: 'contact_channel' },
],
id='app-team-link'
)
Complex Architecture Example: Multi-Tier Network & Security Matrix
Here is an example that would require dozens of repetitive tables in TOML, but is expressed cleanly in Jsonnet using lib/.
1. Library: lib/network_matrix.libsonnet
Defines tiers, default flows, and generates subnets and firewall rules:
// lib/network_matrix.libsonnet
local rescile = import 'rescile/v1/rescile.libsonnet';
{
tiers:: [
{ name: 'public', subnetNth: 1, publicAccess: true },
{ name: 'private', subnetNth: 2, publicAccess: false },
{ name: 'database', subnetNth: 3, publicAccess: false },
],
flows:: [
{ from: 'public', to: 'private', port: 443, proto: 'tcp' },
{ from: 'private', to: 'database', port: 5432, proto: 'tcp' },
],
makeSubnetRules():: [
rescile.createResource(
resourceType='subnet',
relationType='CONTAINS_SUBNET',
name='subnet-{{ origin_resource.name }}-' + tier.name,
properties={
tier: tier.name,
cidr: '{{ origin_resource.cidr | cidr_nth_subnet(prefix=24, nth=' + tier.subnetNth + ') }}',
public: tier.publicAccess,
},
id='subnet-' + tier.name
)
for tier in $.tiers
],
makeFlowRules():: [
rescile.createResource(
resourceType='firewall_rule',
relationType='ENFORCES_RULE',
name='fw-{{ origin_resource.name }}-' + flow.from + '-to-' + flow.to,
properties={
source_tier: flow.from,
target_tier: flow.to,
port: flow.port,
protocol: flow.proto,
action: 'allow',
},
id='fw-' + flow.from + '-' + flow.to
)
for flow in $.flows
],
}
2. Model: models/vpc_topology.jsonnet
The actual model file requires only 4 lines of code:
// models/vpc_topology.jsonnet
local rescile = import 'rescile/v1/rescile.libsonnet';
local net = import 'network_matrix.libsonnet';
rescile.forOrigin('vpcs', net.makeSubnetRules() + net.makeFlowRules())
Stage B: Pure Graph Transformations (Direct Operations)
In addition to Stage A (generating declarative rules with Tera template strings), Rescile supports Stage B Pure Graph Transformations.
Instead of emitting rule definitions, Stage B functional models receive an immutable JSON projection of the graph and return an array of concrete graph mutation operations. This eliminates embedded Tera strings entirely.
The Execution Model
Graph Projection (std.extVar('graph'))
│
▼
Jsonnet Pure Function: fn(graph) -> [operations]
│
▼
[
{ "op": "upsertResource", "type": "server", "name": "srv-billing", "properties": {...} },
{ "op": "ensureRelation", "from": {...}, "type": "HOSTED_ON", "to": {...} },
{ "op": "enrichProperty", "target": {...}, "properties": {...} }
]
│
▼
Rust Engine: applies operations, updates graph, checks convergence
Input: std.extVar('graph')
The host provides a bounded, read-only graph projection:
local graph = std.extVar('graph');
// graph.resources.<label> -> Array of vertices of that type
// graph.params -> Resolved module parameters
// graph.iteration -> Current stabilization iteration (1, 2, ...)
Emitted Graph Operations
Operation (op) |
Fields | Description |
|---|---|---|
upsertResource |
type, name, properties |
Creates resource if missing, or merges properties if exists. |
ensureRelation |
from: { type, name }, type, to: { type, name }, properties |
Idempotently creates an edge between two resources. |
enrichProperty |
target: { type, name }, properties |
Updates or sets properties on an existing resource. |
Complete Stage B Example
Notice there are no Tera {{ ... }} strings; all logic uses native Jsonnet expressions:
// Pure graph transformation: Maps applications to servers and links them
local graph = std.extVar('graph');
local apps = graph.resources.application;
local makeServer(app) = [
// 1. Create or update the target server
{
op: 'upsertResource',
type: 'server',
name: 'srv-' + app.name,
properties: {
environment: app.env,
tier: if app.env == 'prod' then 'critical' else 'standard',
cores: if app.env == 'prod' then 8 else 2,
},
},
// 2. Ensure relationship between application and server
{
op: 'ensureRelation',
from: { type: 'application', name: app.name },
type: 'HOSTED_ON',
to: { type: 'server', name: 'srv-' + app.name },
properties: { auto_wired: true },
}
];
// Flatten and return the list of operations
std.flattenArrays([makeServer(app) for app in apps])
Sandbox & Confinement Rules
To ensure reproducible, safe evaluation:
- Path Confinement: Imports are restricted to the local module root and declared module dependencies (
import 'lib/utils.libsonnet'). - No Traversal Escape: Attempts to import outside the module root (
../../...) fail. - No Absolute Paths: Absolute file imports are rejected.
- Deterministic Limits: Evaluator enforces recursion depth caps and execution timeouts.
- No Ambient I/O: Jsonnet cannot execute shell processes, read environment variables, or perform network requests.
CLI Validation and Inspection
Render Intermediate Representation (IR)
Validate and render normalized rule structures as JSON. Rendering uses the same production loaders as graph imports, so TOML chunking, Jsonnet imports, module parameters, and semantic checks match normal execution.
# Render every model, compliance rule, and output definition
rescile-ce validate --render-ir
# Render one file as a ModelSet, ComplianceSet, or OutputSet envelope
rescile-ce validate --render-ir --target models/service_profiles.jsonnet
# Write workspace IR to a file
rescile-ce validate --render-ir --output workspace.ir.json
# Inspect the Canonical Rule IR JSON Schema
rescile-ce validate --render-ir-schema
IR modes always emit JSON. Diagnostics go to stderr, which keeps stdout safe for shell pipelines.
Validate Rules
Validate TOML and Jsonnet rules against syntax, sandbox, shape, and semantic constraints:
rescile-ce validate --only rules
Run full static workspace validation with bare rescile-ce validate. Add --build to construct and validate the graph in memory. Graph validation does not run data generators; materialize required generator targets first.