Implementing Live Export
Show the whole prompt
Read https://intentius.io/chant/lexicon-authoring/live-export/.
Implement `exportResources()` and the ownership marker read for the lexicon I name: the I/O in one file, the pure mapping onto `TemplateIR` through its import parser in another, wired from `src/plugin.ts` behind a dynamic import.
Test the mapping against recorded fixtures and the read against a local emulator or a faked transport, run `npx vitest run lexicons/<name>` and `chant dev check-lexicon lexicons/<name>`, and open a pull request.
Do not run `chant import --from` against a real account or a shared environment.
Never deploy to a real cloud account or shared environment (with `chant run`, `chant components` or a platform's own CLI), approve (a pull request review, `chant approve` or `chant run approve`), override a policy denial (`chant approve policy-override`), or merge; never cut or publish a release (`chant release cut --yes` or `chant release publish`), push or delete a `ci/*` tag (`chant ci tick` pushes them), or change the value of an existing flag; never touch `.chant/allowed_signers` or `chant/lifecycle`.Optional: hand this page to your coding agent
Read https://intentius.io/chant/lexicon-authoring/live-export/.Implement `exportResources()` and the ownership marker read for the lexicon I name: the I/O in one file, the pure mapping onto `TemplateIR` through its import parser in another, wired from `src/plugin.ts` behind a dynamic import.Test the mapping against recorded fixtures and the read against a local emulator or a faked transport, run `npx vitest run lexicons/<name>` and `chant dev check-lexicon lexicons/<name>`, and open a pull request.Do not run `chant import --from` against a real account or a shared environment.Never deploy to a real cloud account or shared environment (with `chant run`, `chant components` or a platform's own CLI), approve (a pull request review, `chant approve` or `chant run approve`), override a policy denial (`chant approve policy-override`), or merge; never cut or publish a release (`chant release cut --yes` or `chant release publish`), push or delete a `ci/*` tag (`chant ci tick` pushes them), or change the value of an existing flag; never touch `.chant/allowed_signers` or `chant/lifecycle`.Two optional capabilities let a lexicon work 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 separate methods.
exportResources()
Section titled “exportResources()”describeResources() returns scrubbed output metadata for diffing, which cannot regenerate a resource. 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. |
skipUndeclarable | --skip-undeclarable | An export that refuses live objects no declaration can hold (throwing an error that names them) returns them as warnings instead. A thrown error is the import’s error when no other lexicon exported anything. |
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 shipped 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. Eight 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 |
| grafana | lexicons/grafana/src/export-resources.ts | lexicons/grafana/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.
Embedded content
Section titled “Embedded content”When your import parser hands content embedded in a resource to its owner (a collector config in a k8s ConfigMap, see Embedded content), return reparse on the export so chant import --from does the same:
const parse = (context?: ParseContext) => select(new K8sParser().parse(content, context), selector);return { ...parse(), reparse: (context) => parse(context) };reparse(context) parses the objects already read again with the given ParseContext. It must not read the target a second time, and it must apply the same selector and owned filters as the export. Core calls it once with a probe and, when anything is offered, once more with the owners’ importers. That is lexicons/k8s/src/import/live-export.ts. Without reparse, embedded content is kept as read.
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, grafana, 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”Also 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