Part I · Getting started · Chapter 2

Your first spell

Writing a manifest that discovers something real about your account, and understanding what each part of it does.

A spell is a project SpellCraft renders. Its entry point is manifest.jsonnet, and the whole model fits in one sentence: top-level keys are filenames, and their values are the file contents.

The smallest useful manifest

local spellcraft = import "spellcraft";

{
  "config.json": {
    renderedFrom: spellcraft.path(),
  },
}

Render it:

npx spellcraft generate manifest.jsonnet

SpellCraft evaluates the file and writes render/config.json. The key became the filename; the value became the contents, serialised as JSON because the filename ends in .json.

Making it discover something

The point of SpellCraft is that evaluation can reach out. Install a provider plugin:

npm install --save @c6fc/spellcraft-plugins
local plugins = import "@c6fc/spellcraft-plugins/module.libsonnet";

local identity = plugins.aws.auth.getCallerIdentity();

{
  "account.json": {
    account: identity.Account,
    principal: identity.Arn,
  },
}

Render that and render/account.json contains your real account number, fetched from STS while the Jsonnet was being evaluated. Nobody typed it, and nobody has to update it when you switch accounts.

This is the whole idea
Configuration that asks, rather than configuration that is told

The account number above is not a variable you set, a tfvars file you maintain, or a value your CI injects. It is a question the configuration asks at the moment it renders. Everything else in SpellCraft exists to make that pattern practical: plugins to supply the questions, events to order them, and memoisation to keep them cheap.

Three kinds of import

local spellcraft = import "spellcraft";                              // built-ins
local modules = import "modules";                                    // spellcraft_modules/
local plugins = import "@c6fc/spellcraft-plugins/module.libsonnet";  // installed plugins

{
  "context.json": {
    renderedFrom: spellcraft.path(),
    name: modules.util.slug("My First Spell"),
    account: plugins.aws.auth.getCallerIdentity().Account,
  },
}
  • "spellcraft" is the built-in library. It is small on purpose: envvar(name) and path().
  • "modules" is generated from your project’s spellcraft_modules/ directory, and only exists when that directory does. See Local modules.
  • A package path imports an installed plugin’s Jsonnet facade.

Producing more than one file

Because the keys are filenames, a manifest can emit an entire directory:

{
  "main.tf.json": { /* ... */ },
  "variables.tf.json": { /* ... */ },
  "README.md": "# Generated. Do not edit.\n",
}

Each file is serialised according to its extension. Rendering output covers how that dispatch works and how to add your own formats.