Architectural Models

Functional Models with Jsonnet

Author declarative, reusable, and parameterized graph rules using Jsonnet.

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: .jsonnet and .toml files 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.libsonnet builders default to singleton execution (origin=''). To iterate over graph vertices, specify origin='servers' or wrap rules in rescile.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 from lib/.
  • Pure Functions: Author business logic, naming functions, data normalization, and property calculation in lib/*.libsonnet.
  • Keep Models Clean: Models in models/*.jsonnet focus on declarative graph bindings (createResource, linkResources) while delegating complex calculations to lib/.

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:

  1. Load Time (Jsonnet): Jsonnet evaluates functions, list comprehensions, and configurations, emitting canonical rule definitions.
  2. 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:

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:

  1. Path Confinement: Imports are restricted to the local module root and declared module dependencies (import 'lib/utils.libsonnet').
  2. No Traversal Escape: Attempts to import outside the module root (../../...) fail.
  3. No Absolute Paths: Absolute file imports are rejected.
  4. Deterministic Limits: Evaluator enforces recursion depth caps and execution timeouts.
  5. 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.