Skip to content

Implementing Live Export

Two opt-in capabilities let a lexicon participate in the cloud → code direction: live export (regenerate TypeScript from live config) and the ownership marker (tell chant-owned resources from foreign ones). They pair with observation (the cloud → report direction) but are deliberately separate from it.

describeResources() returns scrubbed output metadata for diffing — you cannot regenerate a resource from it. exportResources() returns the full input config, suitable for regeneration:

exportResources?(options: {
environment: string;
stack?: string; // deployed stack to export from (#932)
region?: string; // region that stack is in
selector?: ResourceSelector; // { type?, name? }
owned?: boolean; // restrict to chant-owned resources
verbatim?: boolean; // keep server-defaulted fields
}): Promise<ExportedTemplate>;

Every option maps to a flag on chant import --from <env>:

OptionFlagNotes
environment--from <env>The one required argument.
selector.type--type <ResourceType>
selector.name--name <name>
owned--owned
verbatim--verbatimDefault strips server-defaulted fields to the declared shape.
stacknoneCore supplies it per declared stack when the project configures stacks; a single-stack project keeps the convention of the stack named after the environment.

-d, --lexicon <name> restricts the run to one lexicon and never reaches your implementation. chant import --from warns before writing that live config may carry secrets, so it is the caller’s review step rather than yours.

The result is the existing import IR (TemplateIR), branded as ExportedTemplate, so it feeds your lexicon’s templateGenerator() unchanged. The brand keeps a full-fidelity export — which may carry secrets — from flowing into the lifecycle code paths, which consume scrubbed metadata through the ObservationLexicon view.

Keep all I/O in the activity and the mapping pure, as the shipping lexicons do:

  1. Read live objects (aws cloudformation get-template, a typed LIST, a REST GET).
  2. Strip to the declared shape by default. Remove server-written fields: status, K8s managedFields, server metadata, provider bookkeeping annotations. Keep them when verbatim is set.
  3. Map to TemplateIR by reusing your import parser.
  4. Apply the selector and the owned filter, below.

Split the two halves across two files so the mapping tests without a transport. Seven lexicons implement this today:

LexiconI/O entry point (what the plugin calls)Pure mapping
awslexicons/aws/src/plugin.ts (inline)lexicons/aws/src/import/live-export.ts
azurelexicons/azure/src/export-resources.tslexicons/azure/src/import/live-export.ts
cedarlexicons/cedar/src/avp/live-export.tssame file
flylexicons/fly/src/export-resources.tslexicons/fly/src/import/live-export.ts
fountainlexicons/fountain/src/export-resources.tssame file
gcplexicons/gcp/src/export-resources.tslexicons/gcp/src/import/live-export.ts
k8slexicons/k8s/src/export-resources.tslexicons/k8s/src/import/live-export.ts

Wire it from plugin.ts behind a dynamic import, the way every one of them does, so chant build never loads a live transport.

Ownership is what later lets delete be precise without a hosted state file. The marker is stamped at synthesis time into the target’s native metadata channel:

Each lexicon exports its own ChannelKeys (a managedBy / stack / env triple). The label convention is shared as LABEL_OWNERSHIP_KEYS in @intentius/chant/ownership; tag-key syntax differs per provider, so tag-based lexicons declare their own.

ConstantWheremanagedBy key
LABEL_OWNERSHIP_KEYS@intentius/chant/ownership (k8s, gcp, helm, k3d, k3s)app.kubernetes.io/managed-by
AWS_TAG_OWNERSHIP_KEYSlexicons/aws/src/ownership.tschant:managed-by
AZURE_TAG_OWNERSHIP_KEYSlexicons/azure/src/ownership.tschant-managed-by
FLY_METADATA_OWNERSHIP_KEYSlexicons/fly/src/ownership.tsmanaged-by
CPLN_TAG_OWNERSHIP_KEYSlexicons/cpln/src/ownership.tschant.intentius.io/managed-by
RENDER_ENV_OWNERSHIP_KEYSlexicons/render/src/ownership.tsCHANT_MANAGED_BY
AVP_OWNERSHIP_KEYSlexicons/cedar/src/avp/ownership.tschant:managed-by

The value of the managed-by key is always chant (OWNERSHIP_MANAGED_BY_VALUE); the other two carry the stack name and, when the build has one, the environment.

Stamp it from your serializer by reading context.ownership and merging into your tag/label channel. The helper takes the keys, not a channel name:

import { ownershipEntries, LABEL_OWNERSHIP_KEYS } from "@intentius/chant/ownership";
serialize(entities, outputs, context) {
const labels = context?.ownership
? ownershipEntries(LABEL_OWNERSHIP_KEYS, context.ownership)
: {};
// merge `labels` into each resource's metadata.labels (explicit labels win)
}

That is lexicons/k8s/src/serializer.ts, which seeds its default-label map from the same call.

Stamping is half of it. Declare which read paths can resolve a verdict from the marker, so a caller knows the answer is available before asking:

plugin.ts
ownershipChannel: { keys: LABEL_OWNERSHIP_KEYS, reads: ["describeResources", "observeResourcesDeep", "exportResources"] },

chant dev check-lexicon fails a declaration naming a path the plugin does not implement, or whose keys are incomplete, and the observation conformance suite holds a declared path to a real verdict. Omit the field entirely when you have no marker channel anywhere; every verdict must then be unknown. See the observation contract.

Implement the owned filter on both describeResources and exportResources by reading the marker back:

import {
hasOwnershipMarker,
classifyOwnership,
readOwnership,
LABEL_OWNERSHIP_KEYS,
} from "@intentius/chant/ownership";
// export: drop objects without the marker
if (owned && !hasOwnershipMarker(labels, LABEL_OWNERSHIP_KEYS)) continue;
// describe: classify so the change set can gate delete
metadata.ownership = classifyOwnership(labels, LABEL_OWNERSHIP_KEYS);
// teardown: read the stack/env identity back verbatim, never inferred
metadata.marker = readOwnership(labels, LABEL_OWNERSHIP_KEYS); // undefined when unmarked

classifyOwnership returns only owned or foreign, so a path that could not read the channel must stamp "unknown" itself rather than calling it. Where tags arrive as a {Key, Value} array (CloudFormation’s form), tagArrayToMap(tags) converts to the map these expect.