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.
exportResources()
Section titled “exportResources()”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>:
| Option | Flag | Notes |
|---|---|---|
environment | --from <env> | The one required argument. |
selector.type | --type <ResourceType> | |
selector.name | --name <name> | |
owned | --owned | |
verbatim | --verbatim | Default strips server-defaulted fields to the declared shape. |
stack | none | Core 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.
Pattern
Section titled “Pattern”Keep all I/O in the activity and the mapping pure, as the shipping lexicons do:
- Read live objects (
aws cloudformation get-template, a typedLIST, a RESTGET). - Strip to the declared shape by default. Remove server-written fields:
status, K8smanagedFields, server metadata, provider bookkeeping annotations. Keep them whenverbatimis set. - Map to
TemplateIRby reusing your import parser. - Apply the
selectorand theownedfilter, below.
Split the two halves across two files so the mapping tests without a transport. Seven lexicons implement this today:
| Lexicon | I/O entry point (what the plugin calls) | Pure mapping |
|---|---|---|
| aws | lexicons/aws/src/plugin.ts (inline) | lexicons/aws/src/import/live-export.ts |
| azure | lexicons/azure/src/export-resources.ts | lexicons/azure/src/import/live-export.ts |
| cedar | lexicons/cedar/src/avp/live-export.ts | same file |
| fly | lexicons/fly/src/export-resources.ts | lexicons/fly/src/import/live-export.ts |
| fountain | lexicons/fountain/src/export-resources.ts | same file |
| gcp | lexicons/gcp/src/export-resources.ts | lexicons/gcp/src/import/live-export.ts |
| k8s | lexicons/k8s/src/export-resources.ts | lexicons/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.
The ownership marker
Section titled “The ownership marker”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.
| Constant | Where | managedBy key |
|---|---|---|
LABEL_OWNERSHIP_KEYS | @intentius/chant/ownership (k8s, gcp, helm, k3d, k3s) | app.kubernetes.io/managed-by |
AWS_TAG_OWNERSHIP_KEYS | lexicons/aws/src/ownership.ts | chant:managed-by |
AZURE_TAG_OWNERSHIP_KEYS | lexicons/azure/src/ownership.ts | chant-managed-by |
FLY_METADATA_OWNERSHIP_KEYS | lexicons/fly/src/ownership.ts | managed-by |
CPLN_TAG_OWNERSHIP_KEYS | lexicons/cpln/src/ownership.ts | chant.intentius.io/managed-by |
RENDER_ENV_OWNERSHIP_KEYS | lexicons/render/src/ownership.ts | CHANT_MANAGED_BY |
AVP_OWNERSHIP_KEYS | lexicons/cedar/src/avp/ownership.ts | chant: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.
Declare the channel on the plugin
Section titled “Declare the channel on the plugin”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:
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.
Querying it for owned
Section titled “Querying it for owned”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 markerif (owned && !hasOwnershipMarker(labels, LABEL_OWNERSHIP_KEYS)) continue;
// describe: classify so the change set can gate deletemetadata.ownership = classifyOwnership(labels, LABEL_OWNERSHIP_KEYS);
// teardown: read the stack/env identity back verbatim, never inferredmetadata.marker = readOwnership(labels, LABEL_OWNERSHIP_KEYS); // undefined when unmarkedclassifyOwnership 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.
See also
Section titled “See also”- Implementing Observation — the
describeResources()/listArtifacts()side - Completeness Checklist — where these capabilities sit
- Live Import — the user-facing
chant import --from - Lifecycle Models — why ownership lives on the resource
- Observation Contract —
ExportedTemplate, the brand, and per-lexicon coverage