Part II · Concepts · Chapter 1

Manifests

The filename-to-contents model, why Jsonnet was chosen for it, and the composition patterns that keep large configurations readable.

A manifest is an ordinary Jsonnet file whose evaluated result is an object. SpellCraft imposes exactly one convention on it: the top-level keys are filenames.

Why Jsonnet

Jsonnet is a configuration language that produces JSON, with the features that HCL keeps almost having: real functions, real imports, object inheritance, and comprehensions. Two properties matter most here.

Composition is native. The + operator merges two objects, and +: on a field merges into the inherited one rather than replacing it. A plugin can hand you a complete resource definition and you can adjust one nested field of it without the plugin having exposed a parameter for that field:

local s3 = (import "@c6fc/spellcraft-plugins/module.libsonnet").aws.terraform.s3;

s3.bucket("artifacts", "us-west-2") + {
  resource+: {
    aws_s3_bucket+: {
      artifacts+: { force_destroy: true },
    },
  },
}

Nothing in the plugin anticipated force_destroy. It did not need to.

Note every level of that override carrying its own +:. + on its own is a shallow merge — the right-hand side replaces a key outright rather than descending into it — which is why { resource+: ... } alone would have thrown away every resource in the bucket definition and kept only aws_s3_bucket. The rule generalises: when a plugin hands you a whole file’s worth of resources, give it its own manifest key rather than adding it to another one.

Evaluation is lazy and pure. Nothing is computed until something needs it, and Jsonnet itself cannot perform side effects. That purity is what makes SpellCraft’s escape hatch safe to reason about: every impure thing in a render happens in a native function, and those are the only places to look when output surprises you.

Hidden fields

A field declared with :: is hidden: it participates in inheritance and computation but never appears in the output.

{
  local region = "us-west-2",

  defaults:: {
    tags: { managed_by: "spellcraft" },
  },

  "main.tf.json": self.defaults + {
    provider: { aws: { region: region } },
  },
}

defaults shapes the output without becoming a file called defaults. This is the idiomatic way to share structure between output files in one manifest, and it is why plugins declare their internal helpers with :: too.

Parameterising a render

There is deliberately no --var flag. Configuration that varies comes from the environment, discovered at render time:

local spellcraft = import "spellcraft";

local environment =
  local declared = spellcraft.envvar("ENVIRONMENT");
  if declared == false then "dev" else declared;

{
  "config.json": { environment: environment },
}

envvar returns false — not null, not an error — when a variable is unset, which makes defaulting a plain conditional.

On the missing --var

A flag would be a second, weaker source of truth competing with the one SpellCraft is built around. If a value is knowable from your cloud account, discover it; if it is genuinely an operator's choice, an environment variable is already the portable way to express that across shells, CI and local runs.