@intentius/chant-lexicon-aws 0.124.51 → 0.124.54

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/dist/codegen/generate-lexicon.d.ts +2 -0
  2. package/dist/codegen/generate-lexicon.d.ts.map +1 -1
  3. package/dist/codegen/generate-lexicon.js +1 -0
  4. package/dist/components/__tests__/mock-cloud-executor.d.ts +4 -0
  5. package/dist/components/__tests__/mock-cloud-executor.d.ts.map +1 -1
  6. package/dist/components/__tests__/mock-cloud-executor.js +18 -1
  7. package/dist/components/apply.d.ts +76 -3
  8. package/dist/components/apply.d.ts.map +1 -1
  9. package/dist/components/apply.js +488 -32
  10. package/dist/components/cfn-adopt.d.ts +99 -0
  11. package/dist/components/cfn-adopt.d.ts.map +1 -0
  12. package/dist/components/cfn-adopt.js +278 -0
  13. package/dist/components/cfn-imports.d.ts +84 -0
  14. package/dist/components/cfn-imports.d.ts.map +1 -0
  15. package/dist/components/cfn-imports.js +239 -0
  16. package/dist/components/cloud-executor.d.ts +44 -2
  17. package/dist/components/cloud-executor.d.ts.map +1 -1
  18. package/dist/components/cloud-executor.js +73 -2
  19. package/dist/importable.d.ts +30 -0
  20. package/dist/importable.d.ts.map +1 -0
  21. package/dist/importable.js +72 -0
  22. package/dist/index-identity.d.ts +19 -0
  23. package/dist/index-identity.d.ts.map +1 -0
  24. package/dist/index-identity.js +79 -0
  25. package/dist/index-import.d.ts +17 -0
  26. package/dist/index-import.d.ts.map +1 -0
  27. package/dist/index-import.js +115 -0
  28. package/dist/integrity.json +3 -3
  29. package/dist/listed-types.d.ts +11 -0
  30. package/dist/listed-types.d.ts.map +1 -0
  31. package/dist/listed-types.js +52 -0
  32. package/dist/manifest.json +1 -1
  33. package/dist/meta.json +13206 -1237
  34. package/dist/plugin.d.ts.map +1 -1
  35. package/dist/plugin.js +9 -0
  36. package/dist/reference-catalog.d.ts.map +1 -1
  37. package/dist/reference-catalog.js +62 -20
  38. package/dist/spec/fetch.d.ts +5 -0
  39. package/dist/spec/fetch.d.ts.map +1 -1
  40. package/dist/spec/parse.d.ts +8 -0
  41. package/dist/spec/parse.d.ts.map +1 -1
  42. package/dist/spec/parse.js +3 -0
  43. package/package.json +2 -2
  44. package/src/codegen/generate-lexicon.ts +3 -0
  45. package/src/components/__tests__/mock-cloud-executor.ts +20 -1
  46. package/src/components/apply.ts +546 -33
  47. package/src/components/cfn-adopt.test.ts +284 -0
  48. package/src/components/cfn-adopt.ts +340 -0
  49. package/src/components/cfn-exports-in-use.test.ts +146 -0
  50. package/src/components/cfn-imports-preview.test.ts +178 -0
  51. package/src/components/cfn-imports-three-stacks.test.ts +107 -0
  52. package/src/components/cfn-imports.test.ts +213 -0
  53. package/src/components/cfn-imports.ts +246 -0
  54. package/src/components/cfn-plan-floci.e2e.test.ts +53 -0
  55. package/src/components/cfn-plan-shape-conformance.test.ts +83 -0
  56. package/src/components/cfn-plan.test.ts +328 -9
  57. package/src/components/cloud-executor.ts +119 -6
  58. package/src/components/demo-composed-stacks-floci.e2e.test.ts +235 -0
  59. package/src/derived-edges.test.ts +142 -0
  60. package/src/generated/lexicon-aws.json +13206 -1237
  61. package/src/importable.test.ts +86 -0
  62. package/src/importable.ts +83 -0
  63. package/src/index-identity.test.ts +134 -0
  64. package/src/index-identity.ts +98 -0
  65. package/src/index-import.test.ts +242 -0
  66. package/src/index-import.ts +140 -0
  67. package/src/index-search.test.ts +87 -0
  68. package/src/listed-types.test.ts +97 -0
  69. package/src/listed-types.ts +56 -0
  70. package/src/plugin.ts +10 -0
  71. package/src/reference-catalog.ts +63 -20
  72. package/src/spec/fetch.ts +5 -0
  73. package/src/spec/parse.test.ts +15 -0
  74. package/src/spec/parse.ts +12 -0
  75. package/src/testdata/indexed/identity.listing.json +65 -0
  76. package/src/testdata/indexed/import.listing.json +126 -0
  77. package/src/testdata/indexed/listing.json +69 -0
  78. package/src/testdata/indexed/topology.listing.json +350 -0
  79. package/src/testdata/three-cloud/dns.graph.json +336 -0
  80. package/src/three-cloud-parity.test.ts +49 -0
@@ -0,0 +1,99 @@
1
+ import { type AdoptionFile } from "@intentius/chant/lifecycle/adoption-file";
2
+ import type { IndexDocument } from "@intentius/chant/lifecycle/index-document";
3
+ import { type ImportabilityManifestEntry } from "../importable.js";
4
+ import type { CfnChangeSet, CloudFormationClient } from "./cloud-executor.js";
5
+ /** A CloudFormation template as chant builds it. */
6
+ export interface CfnTemplate {
7
+ Metadata?: Record<string, unknown>;
8
+ Resources: Record<string, {
9
+ Type: string;
10
+ Properties?: Record<string, unknown>;
11
+ DeletionPolicy?: string;
12
+ UpdateReplacePolicy?: string;
13
+ [key: string]: unknown;
14
+ }>;
15
+ [key: string]: unknown;
16
+ }
17
+ /** One resource the adoption brings under the stack. */
18
+ export interface CfnAdoptedResource {
19
+ node: string;
20
+ logicalId: string;
21
+ type: string;
22
+ identifier: Record<string, string>;
23
+ deletionPolicy: string;
24
+ /** The property the markers are written to, when the type has one chant can stamp. */
25
+ tagProperty?: string;
26
+ /** The marker tags the UPDATE writes. Empty for a type chant cannot tag. */
27
+ markers: Record<string, string>;
28
+ }
29
+ export interface CfnAdoptionPlan {
30
+ /** Why the adoption cannot run, each by name. Empty when it can. */
31
+ refusals: string[];
32
+ resources: CfnAdoptedResource[];
33
+ /** The IMPORT change set's template: the built one without markers or stack tags. */
34
+ importTemplate: CfnTemplate;
35
+ /** The UPDATE change set's template: the built one. */
36
+ markerTemplate: CfnTemplate;
37
+ }
38
+ export interface PlanCfnAdoptionInput {
39
+ /** The built template of the stack the resources join, from the source `chant import --index` wrote. */
40
+ template: CfnTemplate;
41
+ adoption: AdoptionFile;
42
+ /** The index version the adoption file names. */
43
+ index: IndexDocument;
44
+ /** Logical ids already in the stack (empty for a new stack). */
45
+ existing?: ReadonlySet<string>;
46
+ manifest?: ReadonlyMap<string, ImportabilityManifestEntry>;
47
+ }
48
+ /** The import identifier: the type's primary identifier properties zipped with the native id split on `|`. */
49
+ export declare function cfnResourceIdentifier(primaryIdentifier: readonly string[], nativeId: string): Record<string, string> | undefined;
50
+ /** Plan an adoption. Pure: reads nothing from the cloud. */
51
+ export declare function planCfnAdoption(input: PlanCfnAdoptionInput): CfnAdoptionPlan;
52
+ /** What the IMPORT change set's review shows, one line per resource. */
53
+ export declare function cfnImportReview(plan: CfnAdoptionPlan): string[];
54
+ /** What the marker UPDATE's review shows: each marker write, as tags only. */
55
+ export declare function cfnMarkerReview(plan: CfnAdoptionPlan): string[];
56
+ /** Why an IMPORT change set is not the one planned, or empty. */
57
+ export declare function importChangeSetProblems(changeSet: CfnChangeSet, plan: CfnAdoptionPlan): string[];
58
+ /** Why the marker UPDATE is not tags only on the imported resources, or empty. */
59
+ export declare function markerChangeSetProblems(changeSet: CfnChangeSet, plan: CfnAdoptionPlan): string[];
60
+ /** One change-set review: the step, the change set and what it writes. Approving executes it. */
61
+ export type CfnAdoptionReview = (step: "import" | "markers", changeSet: CfnChangeSet, lines: string[]) => Promise<boolean>;
62
+ export type CfnAdoptionOutcome = {
63
+ status: "adopted";
64
+ resources: CfnAdoptedResource[];
65
+ importChangeSet: string;
66
+ markerChangeSet?: string;
67
+ } | {
68
+ status: "refused";
69
+ step: "plan" | "import" | "markers";
70
+ refusals: string[];
71
+ } | {
72
+ status: "declined";
73
+ step: "import" | "markers";
74
+ } | {
75
+ status: "failed";
76
+ step: "import" | "markers";
77
+ stackStatus: string;
78
+ };
79
+ export interface AdoptIntoStackOptions {
80
+ cfn: CloudFormationClient;
81
+ stackName: string;
82
+ plan: CfnAdoptionPlan;
83
+ review: CfnAdoptionReview;
84
+ parameters?: Record<string, string>;
85
+ waitOptions?: {
86
+ intervalMs?: number;
87
+ timeoutMs?: number;
88
+ };
89
+ }
90
+ /**
91
+ * Run a planned adoption: the IMPORT change set, its review and execution,
92
+ * then the marker UPDATE, its tags-only check, review and execution. A
93
+ * refusal or a declined review deletes the change set it made and stops. A
94
+ * marker step that stops leaves the resources imported with Retain and
95
+ * without markers; running the adoption's UPDATE again (a plain deploy of
96
+ * the same template) writes them.
97
+ */
98
+ export declare function adoptIntoStack(opts: AdoptIntoStackOptions): Promise<CfnAdoptionOutcome>;
99
+ //# sourceMappingURL=cfn-adopt.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cfn-adopt.d.ts","sourceRoot":"","sources":["../../src/components/cfn-adopt.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,278 @@
1
+ /**
2
+ * Adopting indexed resources into a CloudFormation stack (#4536; design and
3
+ * ruling on INTENTIUS/choudoufu#2284, 2026-10-26).
4
+ *
5
+ * The aws lexicon deploys CREATE and UPDATE change sets. A CREATE of a
6
+ * template that declares an existing bucket fails, so adoption takes two
7
+ * change sets:
8
+ *
9
+ * 1. IMPORT, with the template's markers left out. CloudFormation import
10
+ * brings a resource under the stack without applying the template's
11
+ * property values, so markers in the import template would never be
12
+ * written, and the next UPDATE would then see no change and not write them
13
+ * either. The import template also carries no stack tags.
14
+ * 2. UPDATE, with the template as built: it adds chant's marker tags to each
15
+ * imported resource and the stack's own tags. It must be tags only: a
16
+ * change to anything but the tag property, a replace, an add or a remove
17
+ * refuses the run before it executes.
18
+ *
19
+ * The gate is chant's change-set review (Q3): each change set is shown before
20
+ * it is executed, through the caller's `review`, and the marker review lists
21
+ * each marker write as tags only. Nothing here asks a question of its own.
22
+ *
23
+ * Refused before any change set is made ({@link planCfnAdoption}): an
24
+ * adoption file that does not match its index version, a node the template
25
+ * does not declare under the file's `as`, a type CloudFormation cannot import
26
+ * or whose tags can only be set at create (../importable.ts), a resource with
27
+ * no DeletionPolicy (import needs one; `chant import --index` writes Retain,
28
+ * Q4), a resource already in the stack, and a template that would also create
29
+ * a resource. The index proposes; the live read decides: CloudFormation reads
30
+ * each object again when it imports it, by the identifier given here.
31
+ */
32
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
33
+ import { tmpdir } from "node:os";
34
+ import { join } from "node:path";
35
+ import { adoptionFileProblemsAgainst } from "@intentius/chant/lifecycle/adoption-file";
36
+ import { adoptionRefusal, importabilityManifest } from "../importable.js";
37
+ import { AWS_TAG_OWNERSHIP_KEYS, OWNERSHIP_METADATA_KEY } from "../ownership.js";
38
+ import { isNoopChangeSet } from "./apply.js";
39
+ const MARKER_KEYS = new Set(Object.values(AWS_TAG_OWNERSHIP_KEYS));
40
+ /** The marker tags in a tag property's value, and the value without them. */
41
+ function splitMarkers(tags, shape) {
42
+ const markers = {};
43
+ if (Array.isArray(tags) && shape !== "map") {
44
+ const rest = tags.filter((t) => {
45
+ const key = t?.Key;
46
+ const value = t?.Value;
47
+ if (typeof key === "string" && MARKER_KEYS.has(key)) {
48
+ if (typeof value === "string")
49
+ markers[key] = value;
50
+ return false;
51
+ }
52
+ return true;
53
+ });
54
+ return { markers, rest };
55
+ }
56
+ if (tags && typeof tags === "object" && !Array.isArray(tags)) {
57
+ const rest = {};
58
+ for (const [k, v] of Object.entries(tags)) {
59
+ if (MARKER_KEYS.has(k)) {
60
+ if (typeof v === "string")
61
+ markers[k] = v;
62
+ }
63
+ else
64
+ rest[k] = v;
65
+ }
66
+ return { markers, rest };
67
+ }
68
+ return { markers, rest: tags };
69
+ }
70
+ const isEmpty = (v) => v === undefined || (Array.isArray(v) && v.length === 0) || (typeof v === "object" && v !== null && !Array.isArray(v) && Object.keys(v).length === 0);
71
+ /** The import identifier: the type's primary identifier properties zipped with the native id split on `|`. */
72
+ export function cfnResourceIdentifier(primaryIdentifier, nativeId) {
73
+ const parts = nativeId.split("|");
74
+ if (primaryIdentifier.length === 0 || parts.length !== primaryIdentifier.length)
75
+ return undefined;
76
+ return Object.fromEntries(primaryIdentifier.map((p, i) => [p, parts[i]]));
77
+ }
78
+ /** Plan an adoption. Pure: reads nothing from the cloud. */
79
+ export function planCfnAdoption(input) {
80
+ const manifest = input.manifest ?? importabilityManifest();
81
+ const existing = input.existing ?? new Set();
82
+ const refusals = adoptionFileProblemsAgainst(input.adoption, input.index);
83
+ const nodes = new Map(input.index.graph.nodes.map((n) => [n.id, n]));
84
+ const importTemplate = JSON.parse(JSON.stringify(input.template));
85
+ const markerTemplate = JSON.parse(JSON.stringify(input.template));
86
+ const resources = [];
87
+ const adopted = new Set();
88
+ for (const sel of input.adoption.select ?? []) {
89
+ const node = nodes.get(sel.node);
90
+ if (!node)
91
+ continue; // adoptionFileProblemsAgainst names it
92
+ const logicalId = sel.as;
93
+ adopted.add(logicalId);
94
+ const resource = input.template.Resources?.[logicalId];
95
+ if (!resource) {
96
+ refusals.push(`${sel.node}: the template declares no resource ${logicalId}`);
97
+ continue;
98
+ }
99
+ const type = resource.Type;
100
+ if (type !== node.kind)
101
+ refusals.push(`${logicalId}: the template declares ${type}, but ${sel.node} is ${node.kind}`);
102
+ if (sel.type !== undefined && sel.type !== type)
103
+ refusals.push(`${logicalId}: the adoption file names type ${sel.type}, but the template declares ${type}`);
104
+ const refusal = adoptionRefusal(type, manifest);
105
+ if (refusal)
106
+ refusals.push(`${logicalId}: ${refusal}`);
107
+ if (existing.has(logicalId))
108
+ refusals.push(`${logicalId} is already in the stack; nothing to import`);
109
+ const deletionPolicy = resource.DeletionPolicy;
110
+ if (typeof deletionPolicy !== "string") {
111
+ refusals.push(`${logicalId} declares no DeletionPolicy; CloudFormation import needs one (chant import --index writes Retain)`);
112
+ }
113
+ const entry = manifest.get(type);
114
+ const identifier = cfnResourceIdentifier(entry?.primaryIdentifier ?? [], node.nativeId ?? "");
115
+ if (!identifier) {
116
+ refusals.push(`${logicalId}: native id "${node.nativeId ?? ""}" does not split into ${type}'s primary identifier (${(entry?.primaryIdentifier ?? []).join(", ") || "none"})`);
117
+ }
118
+ const tagProperty = entry?.tagging?.taggable ? entry.tagging.tagProperty : undefined;
119
+ let markers = {};
120
+ if (tagProperty) {
121
+ const props = importTemplate.Resources[logicalId].Properties ?? {};
122
+ const split = splitMarkers(props[tagProperty], entry?.tagging?.tagShape);
123
+ markers = split.markers;
124
+ if (isEmpty(split.rest))
125
+ delete props[tagProperty];
126
+ else
127
+ props[tagProperty] = split.rest;
128
+ importTemplate.Resources[logicalId].Properties = props;
129
+ }
130
+ resources.push({
131
+ node: sel.node,
132
+ logicalId,
133
+ type,
134
+ identifier: identifier ?? {},
135
+ deletionPolicy: typeof deletionPolicy === "string" ? deletionPolicy : "",
136
+ ...(tagProperty ? { tagProperty } : {}),
137
+ markers,
138
+ });
139
+ }
140
+ for (const id of Object.keys(input.template.Resources ?? {}).sort()) {
141
+ if (!adopted.has(id) && !existing.has(id)) {
142
+ refusals.push(`the template also creates ${id}; adopt first, then add it in a later deploy (the marker UPDATE must be tags only)`);
143
+ }
144
+ }
145
+ // No stack tags on the IMPORT: the UPDATE writes them with the resource markers.
146
+ if (importTemplate.Metadata && OWNERSHIP_METADATA_KEY in importTemplate.Metadata) {
147
+ delete importTemplate.Metadata[OWNERSHIP_METADATA_KEY];
148
+ if (Object.keys(importTemplate.Metadata).length === 0)
149
+ delete importTemplate.Metadata;
150
+ }
151
+ resources.sort((a, b) => (a.logicalId < b.logicalId ? -1 : a.logicalId > b.logicalId ? 1 : 0));
152
+ return { refusals, resources, importTemplate, markerTemplate };
153
+ }
154
+ /** What the IMPORT change set's review shows, one line per resource. */
155
+ export function cfnImportReview(plan) {
156
+ return plan.resources.map((r) => `import ${r.logicalId} (${r.type}) ${Object.entries(r.identifier).map(([k, v]) => `${k}=${v}`).join(" ")}, DeletionPolicy ${r.deletionPolicy}`);
157
+ }
158
+ /** What the marker UPDATE's review shows: each marker write, as tags only. */
159
+ export function cfnMarkerReview(plan) {
160
+ return plan.resources.map((r) => Object.keys(r.markers).length > 0 && r.tagProperty
161
+ ? `marker write, tags only: ${r.logicalId} (${r.type}) ${r.tagProperty} += ${Object.entries(r.markers)
162
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
163
+ .map(([k, v]) => `${k}=${v}`)
164
+ .join(", ")}`
165
+ : r.tagProperty
166
+ ? `no marker: ${r.logicalId} (${r.type}) carries no chant marker in the template; the stack owns it on its template alone`
167
+ : `no marker: ${r.logicalId} (${r.type}) has no tags chant can stamp; the stack owns it on its template alone`);
168
+ }
169
+ /** Why an IMPORT change set is not the one planned, or empty. */
170
+ export function importChangeSetProblems(changeSet, plan) {
171
+ const problems = [];
172
+ if (changeSet.status === "FAILED")
173
+ problems.push(`the IMPORT change set failed: ${changeSet.statusReason ?? "no reason given"}`);
174
+ const want = new Set(plan.resources.map((r) => r.logicalId));
175
+ const seen = new Set();
176
+ for (const c of changeSet.changes) {
177
+ if (c.action !== "Import" || !want.has(c.logicalResourceId)) {
178
+ problems.push(`the IMPORT change set would ${c.action.toLowerCase()} ${c.logicalResourceId} (${c.resourceType}), which the adoption does not import`);
179
+ }
180
+ seen.add(c.logicalResourceId);
181
+ }
182
+ for (const id of want)
183
+ if (!seen.has(id) && changeSet.status !== "FAILED")
184
+ problems.push(`the IMPORT change set does not import ${id}`);
185
+ return problems;
186
+ }
187
+ function changeReplaces(c) {
188
+ return c.replacement || c.replacementMode === "True" || c.replacementMode === "Conditional";
189
+ }
190
+ /** Why the marker UPDATE is not tags only on the imported resources, or empty. */
191
+ export function markerChangeSetProblems(changeSet, plan) {
192
+ const problems = [];
193
+ if (changeSet.status === "FAILED" && !isNoopChangeSet(changeSet)) {
194
+ problems.push(`the marker change set failed: ${changeSet.statusReason ?? "no reason given"}`);
195
+ }
196
+ const byId = new Map(plan.resources.map((r) => [r.logicalId, r]));
197
+ for (const c of changeSet.changes) {
198
+ const r = byId.get(c.logicalResourceId);
199
+ if (c.action !== "Modify" || !r) {
200
+ problems.push(`the marker change set would ${c.action.toLowerCase()} ${c.logicalResourceId} (${c.resourceType}); it may only add tags to the imported resources`);
201
+ continue;
202
+ }
203
+ if (changeReplaces(c))
204
+ problems.push(`the marker change set would replace ${c.logicalResourceId} (${c.resourceType})`);
205
+ const other = (c.details ?? []).filter((d) => d !== r.tagProperty);
206
+ if (other.length > 0)
207
+ problems.push(`the marker change set changes ${c.logicalResourceId}'s ${other.join(", ")}, not only its tags`);
208
+ }
209
+ return problems;
210
+ }
211
+ /**
212
+ * Run a planned adoption: the IMPORT change set, its review and execution,
213
+ * then the marker UPDATE, its tags-only check, review and execution. A
214
+ * refusal or a declined review deletes the change set it made and stops. A
215
+ * marker step that stops leaves the resources imported with Retain and
216
+ * without markers; running the adoption's UPDATE again (a plain deploy of
217
+ * the same template) writes them.
218
+ */
219
+ export async function adoptIntoStack(opts) {
220
+ const { cfn, stackName, plan, review } = opts;
221
+ if (plan.refusals.length > 0)
222
+ return { status: "refused", step: "plan", refusals: plan.refusals };
223
+ if (plan.resources.length === 0)
224
+ return { status: "refused", step: "plan", refusals: ["the adoption names no resource"] };
225
+ if (!cfn.createImportChangeSet) {
226
+ return { status: "refused", step: "plan", refusals: ["this CloudFormation client cannot create an IMPORT change set"] };
227
+ }
228
+ const dir = mkdtempSync(join(tmpdir(), "chant-cfn-adopt-"));
229
+ try {
230
+ const importPath = join(dir, "import.template.json");
231
+ writeFileSync(importPath, JSON.stringify(plan.importTemplate, null, 2));
232
+ const resourcesToImport = plan.resources.map((r) => ({
233
+ resourceType: r.type,
234
+ logicalResourceId: r.logicalId,
235
+ resourceIdentifier: r.identifier,
236
+ }));
237
+ const imported = await cfn.createImportChangeSet({ stackName, templatePath: importPath, resourcesToImport, ...(opts.parameters ? { parameters: opts.parameters } : {}) });
238
+ const ref = { stackName, changeSetName: imported.changeSetName };
239
+ const importProblems = importChangeSetProblems(imported, plan);
240
+ if (importProblems.length > 0) {
241
+ await cfn.deleteChangeSet(ref);
242
+ return { status: "refused", step: "import", refusals: importProblems };
243
+ }
244
+ if (!(await review("import", imported, cfnImportReview(plan)))) {
245
+ await cfn.deleteChangeSet(ref);
246
+ return { status: "declined", step: "import" };
247
+ }
248
+ await cfn.executeChangeSet(ref);
249
+ const afterImport = await cfn.waitForStack(stackName, opts.waitOptions);
250
+ if (afterImport.stackStatus !== "IMPORT_COMPLETE")
251
+ return { status: "failed", step: "import", stackStatus: afterImport.stackStatus };
252
+ const markerPath = join(dir, "markers.template.json");
253
+ writeFileSync(markerPath, JSON.stringify(plan.markerTemplate, null, 2));
254
+ const marked = await cfn.createChangeSet({ stackName, templatePath: markerPath, ...(opts.parameters ? { parameters: opts.parameters } : {}) });
255
+ const markerRef = { stackName, changeSetName: marked.changeSetName };
256
+ if (isNoopChangeSet(marked)) {
257
+ await cfn.deleteChangeSet(markerRef);
258
+ return { status: "adopted", resources: plan.resources, importChangeSet: imported.changeSetName };
259
+ }
260
+ const markerProblems = markerChangeSetProblems(marked, plan);
261
+ if (markerProblems.length > 0) {
262
+ await cfn.deleteChangeSet(markerRef);
263
+ return { status: "refused", step: "markers", refusals: markerProblems };
264
+ }
265
+ if (!(await review("markers", marked, cfnMarkerReview(plan)))) {
266
+ await cfn.deleteChangeSet(markerRef);
267
+ return { status: "declined", step: "markers" };
268
+ }
269
+ await cfn.executeChangeSet(markerRef);
270
+ const afterMarkers = await cfn.waitForStack(stackName, opts.waitOptions);
271
+ if (afterMarkers.stackStatus !== "UPDATE_COMPLETE")
272
+ return { status: "failed", step: "markers", stackStatus: afterMarkers.stackStatus };
273
+ return { status: "adopted", resources: plan.resources, importChangeSet: imported.changeSetName, markerChangeSet: marked.changeSetName };
274
+ }
275
+ finally {
276
+ rmSync(dir, { recursive: true, force: true });
277
+ }
278
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * `Fn::ImportValue` between stacks (#4466): which exports a template
3
+ * declares, which it imports, and the names they go by.
4
+ *
5
+ * A stack that imports another stack's export reads that stack, the way a
6
+ * CDK app wires its stacks. The read sits inside the template, so core does
7
+ * not see it in the step. `cfn-deploy`'s `declaredReads` finds it here: an
8
+ * import of a name another `cfn-deploy` step's template exports is a read of
9
+ * `<component>.<output>`, which core writes into the importer's step under
10
+ * `imports` (`CfnDeployInput.imports`) and orders, holds and lists like any
11
+ * other read.
12
+ *
13
+ * A name is a string, or an `Fn::Sub` (or `Fn::Join`, or `Ref`) over
14
+ * parameters and pseudo parameters. It resolves where the values it reads
15
+ * are known: a parameter with a plain value in the step's `inputs` or a
16
+ * `Default` in the template, `AWS::StackName`, and the region's pseudo
17
+ * parameters when `AWS_REGION` or `AWS_DEFAULT_REGION` names the region.
18
+ * Anything else (`AWS::AccountId`, a resource attribute, a parameter wired
19
+ * from another stack, an intrinsic this does not follow) leaves the name
20
+ * unresolved, and the plan says so in a hole.
21
+ */
22
+ /** A name an export or import goes by: resolved, or why not. */
23
+ export type CfnName = {
24
+ name: string;
25
+ } | {
26
+ unresolved: string;
27
+ };
28
+ /** One export a template declares: the output it is, and its name. */
29
+ export type CfnExport = {
30
+ output: string;
31
+ } & CfnName;
32
+ /** One import in a template: where it sits, as a dotted path, the logical id it sits under, and its name. */
33
+ export type CfnImport = {
34
+ at: string;
35
+ logicalId: string;
36
+ } & CfnName;
37
+ /** The template at `path`, read against `root` when relative, as JSON; `undefined` for one that cannot be read or parsed, or an archive reference. */
38
+ export declare function readCfnTemplate(path: unknown, root?: string): Record<string, unknown> | undefined;
39
+ /**
40
+ * The pseudo parameters this plan knows for `stack`: `AWS::StackName`, and
41
+ * `AWS::Region`, `AWS::Partition` and `AWS::URLSuffix` when the region is
42
+ * named. `AWS::AccountId` and `AWS::StackId` are not known before apply.
43
+ */
44
+ export declare function cfnPseudoParameters(stack: string, env?: NodeJS.ProcessEnv): Record<string, string>;
45
+ /**
46
+ * The values a name in `template` can read for `stack`: each parameter's
47
+ * `Default`, overlaid by the step's plain inputs (a string, a number or a
48
+ * boolean; a wired reference or `undefined` is not known), and the pseudo
49
+ * parameters.
50
+ */
51
+ export declare function cfnKnownValues(template: Record<string, unknown> | undefined, stack: string, inputs: Record<string, unknown> | undefined, env?: NodeJS.ProcessEnv): Record<string, string>;
52
+ /**
53
+ * The name `value` resolves to against `known`, or why it does not. A
54
+ * string is itself; `Fn::Sub` substitutes `${X}` from `known` and its own
55
+ * variables (`${!X}` is the literal `${X}`); `Ref` reads `known`;
56
+ * `Fn::Join` joins what it resolves. Any other shape is unresolved.
57
+ */
58
+ export declare function resolveCfnName(value: unknown, known: Record<string, string>): CfnName;
59
+ /** Every export `template` declares (`Outputs.<name>.Export.Name`), by output, sorted. */
60
+ export declare function cfnExports(template: Record<string, unknown> | undefined, known: Record<string, string>): CfnExport[];
61
+ /**
62
+ * Every `Fn::ImportValue` in `template`'s resources, outputs and conditions,
63
+ * with where it sits and the name it imports, in template order. An import
64
+ * whose argument is itself an import is listed once, unresolved.
65
+ */
66
+ export declare function cfnImports(template: Record<string, unknown> | undefined, known: Record<string, string>): CfnImport[];
67
+ /** The JSON Pointer `CfnDeployInput.imports[name]` sits at in a `cfn-deploy` step. */
68
+ export declare function cfnImportPointer(name: string): string;
69
+ /** The stand-in parameter that takes an import's place in a preview: `ChantImport`, the name's letters and digits, and a short digest so two names never share one. */
70
+ export declare function cfnStandInParameter(name: string): string;
71
+ /**
72
+ * A copy of `template` for a preview (#4466b): each `Fn::ImportValue` of a
73
+ * name in `names` is a `Ref` to a stand-in `String` parameter, which the
74
+ * preview fills with `cfnPlaceholder`. CloudFormation refuses a change set
75
+ * that imports an export that does not exist yet, so the importer of a stack
76
+ * the same change creates is planned against this copy. `standIns` maps each
77
+ * stand-in parameter to the export it stands for; a name the template never
78
+ * imports gets none.
79
+ */
80
+ export declare function cfnTemplateWithStandIns(template: Record<string, unknown>, names: readonly string[], known: Record<string, string>): {
81
+ template: Record<string, unknown>;
82
+ standIns: Record<string, string>;
83
+ };
84
+ //# sourceMappingURL=cfn-imports.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cfn-imports.d.ts","sourceRoot":"","sources":["../../src/components/cfn-imports.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAMH,gEAAgE;AAChE,MAAM,MAAM,OAAO,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC;AAEhE,sEAAsE;AACtE,MAAM,MAAM,SAAS,GAAG;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC;AAErD,6GAA6G;AAC7G,MAAM,MAAM,SAAS,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC;AAEpE,sJAAsJ;AACtJ,wBAAgB,eAAe,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAQjG;AAOD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAS/G;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EAC7C,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EAC3C,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAcxB;AAUD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CA6CrF;AAED,0FAA0F;AAC1F,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,EAAE,CAQpH;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,EAAE,CAmBpH;AAED,sFAAsF;AACtF,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAErD;AAED,uKAAuK;AACvK,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAExD;AAED;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,KAAK,EAAE,SAAS,MAAM,EAAE,EACxB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC5B;IAAE,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,CA8BzE"}