@intentius/chant-lexicon-aws 0.124.52 → 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.
- package/dist/codegen/generate-lexicon.d.ts +2 -0
- package/dist/codegen/generate-lexicon.d.ts.map +1 -1
- package/dist/codegen/generate-lexicon.js +1 -0
- package/dist/components/__tests__/mock-cloud-executor.d.ts +4 -0
- package/dist/components/__tests__/mock-cloud-executor.d.ts.map +1 -1
- package/dist/components/__tests__/mock-cloud-executor.js +18 -1
- package/dist/components/apply.d.ts +44 -2
- package/dist/components/apply.d.ts.map +1 -1
- package/dist/components/apply.js +260 -29
- package/dist/components/cfn-adopt.d.ts +99 -0
- package/dist/components/cfn-adopt.d.ts.map +1 -0
- package/dist/components/cfn-adopt.js +278 -0
- package/dist/components/cfn-imports.d.ts +84 -0
- package/dist/components/cfn-imports.d.ts.map +1 -0
- package/dist/components/cfn-imports.js +239 -0
- package/dist/components/cloud-executor.d.ts +31 -2
- package/dist/components/cloud-executor.d.ts.map +1 -1
- package/dist/components/cloud-executor.js +64 -2
- package/dist/importable.d.ts +30 -0
- package/dist/importable.d.ts.map +1 -0
- package/dist/importable.js +72 -0
- package/dist/index-identity.d.ts +19 -0
- package/dist/index-identity.d.ts.map +1 -0
- package/dist/index-identity.js +79 -0
- package/dist/index-import.d.ts +17 -0
- package/dist/index-import.d.ts.map +1 -0
- package/dist/index-import.js +115 -0
- package/dist/integrity.json +3 -3
- package/dist/listed-types.d.ts +11 -0
- package/dist/listed-types.d.ts.map +1 -0
- package/dist/listed-types.js +52 -0
- package/dist/manifest.json +1 -1
- package/dist/meta.json +13206 -1237
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +9 -0
- package/dist/reference-catalog.d.ts.map +1 -1
- package/dist/reference-catalog.js +62 -20
- package/dist/spec/fetch.d.ts +5 -0
- package/dist/spec/fetch.d.ts.map +1 -1
- package/dist/spec/parse.d.ts +8 -0
- package/dist/spec/parse.d.ts.map +1 -1
- package/dist/spec/parse.js +3 -0
- package/package.json +2 -2
- package/src/codegen/generate-lexicon.ts +3 -0
- package/src/components/__tests__/mock-cloud-executor.ts +20 -1
- package/src/components/apply.ts +282 -29
- package/src/components/cfn-adopt.test.ts +284 -0
- package/src/components/cfn-adopt.ts +340 -0
- package/src/components/cfn-exports-in-use.test.ts +146 -0
- package/src/components/cfn-imports-preview.test.ts +178 -0
- package/src/components/cfn-imports-three-stacks.test.ts +107 -0
- package/src/components/cfn-imports.test.ts +213 -0
- package/src/components/cfn-imports.ts +246 -0
- package/src/components/cfn-plan-shape-conformance.test.ts +83 -0
- package/src/components/cloud-executor.ts +102 -5
- package/src/components/demo-composed-stacks-floci.e2e.test.ts +235 -0
- package/src/derived-edges.test.ts +142 -0
- package/src/generated/lexicon-aws.json +13206 -1237
- package/src/importable.test.ts +86 -0
- package/src/importable.ts +83 -0
- package/src/index-identity.test.ts +134 -0
- package/src/index-identity.ts +98 -0
- package/src/index-import.test.ts +242 -0
- package/src/index-import.ts +140 -0
- package/src/index-search.test.ts +87 -0
- package/src/listed-types.test.ts +97 -0
- package/src/listed-types.ts +56 -0
- package/src/plugin.ts +10 -0
- package/src/reference-catalog.ts +63 -20
- package/src/spec/fetch.ts +5 -0
- package/src/spec/parse.test.ts +15 -0
- package/src/spec/parse.ts +12 -0
- package/src/testdata/indexed/identity.listing.json +65 -0
- package/src/testdata/indexed/import.listing.json +126 -0
- package/src/testdata/indexed/listing.json +69 -0
- package/src/testdata/indexed/topology.listing.json +350 -0
- package/src/testdata/three-cloud/dns.graph.json +336 -0
- package/src/three-cloud-parity.test.ts +49 -0
|
@@ -26,6 +26,8 @@ export interface LexiconEntry {
|
|
|
26
26
|
conditionalCreateOnly?: string[];
|
|
27
27
|
replacementStrategy?: "delete_then_create" | "create_then_delete";
|
|
28
28
|
tagging?: ResourceTagging;
|
|
29
|
+
/** The registry schema's handler operations, sorted (#4536). Absent for a legacy type. */
|
|
30
|
+
handlers?: string[];
|
|
29
31
|
runtimeDeprecations?: Record<string, string>;
|
|
30
32
|
}
|
|
31
33
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generate-lexicon.d.ts","sourceRoot":"","sources":["../../src/codegen/generate-lexicon.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAExE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC/C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACxD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sCAAsC,CAAC;AAOhF,MAAM,WAAW,YAAY;IAC3B,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,UAAU,GAAG,UAAU,CAAC;IAC9B,OAAO,EAAE,KAAK,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,WAAW,CAAC,EAAE,mBAAmB,EAAE,CAAC;IACpC,mBAAmB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;IAC1D,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC7B,oBAAoB,CAAC,EAAE,MAAM,EAAE,CAAC;IAChC;;;OAGG;IACH,oBAAoB,CAAC,EAAE,MAAM,EAAE,CAAC;IAChC,qBAAqB,CAAC,EAAE,MAAM,EAAE,CAAC;IACjC,mBAAmB,CAAC,EAAE,oBAAoB,GAAG,oBAAoB,CAAC;IAClE,OAAO,CAAC,EAAE,eAAe,CAAC;IAC1B,mBAAmB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC9C;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,iBAAiB,EAAE,EAC5B,MAAM,EAAE,cAAc,EACtB,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,mBAAmB,EAAE,CAAC,EAC/C,mBAAmB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,GACjD,MAAM,
|
|
1
|
+
{"version":3,"file":"generate-lexicon.d.ts","sourceRoot":"","sources":["../../src/codegen/generate-lexicon.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAExE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC/C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACxD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sCAAsC,CAAC;AAOhF,MAAM,WAAW,YAAY;IAC3B,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,UAAU,GAAG,UAAU,CAAC;IAC9B,OAAO,EAAE,KAAK,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,WAAW,CAAC,EAAE,mBAAmB,EAAE,CAAC;IACpC,mBAAmB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;IAC1D,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC7B,oBAAoB,CAAC,EAAE,MAAM,EAAE,CAAC;IAChC;;;OAGG;IACH,oBAAoB,CAAC,EAAE,MAAM,EAAE,CAAC;IAChC,qBAAqB,CAAC,EAAE,MAAM,EAAE,CAAC;IACjC,mBAAmB,CAAC,EAAE,oBAAoB,GAAG,oBAAoB,CAAC;IAClE,OAAO,CAAC,EAAE,eAAe,CAAC;IAC1B,0FAA0F;IAC1F,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,mBAAmB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC9C;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,iBAAiB,EAAE,EAC5B,MAAM,EAAE,cAAc,EACtB,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,mBAAmB,EAAE,CAAC,EAC/C,mBAAmB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,GACjD,MAAM,CAgDR;AAED;;GAEG;AACH,wBAAgB,yBAAyB,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAYlE"}
|
|
@@ -41,6 +41,7 @@ export function generateLexiconJSON(results, naming, constraints, runtimeDepreca
|
|
|
41
41
|
...(r.resource.conditionalCreateOnly?.length && { conditionalCreateOnly: r.resource.conditionalCreateOnly }),
|
|
42
42
|
...(r.resource.replacementStrategy && { replacementStrategy: r.resource.replacementStrategy }),
|
|
43
43
|
...(r.resource.tagging && { tagging: r.resource.tagging }),
|
|
44
|
+
...(r.resource.handlers?.length && { handlers: r.resource.handlers }),
|
|
44
45
|
...(runtimeDepr && { runtimeDeprecations: runtimeDepr }),
|
|
45
46
|
};
|
|
46
47
|
},
|
|
@@ -47,6 +47,10 @@ export interface FakeStackConfig {
|
|
|
47
47
|
missing?: boolean;
|
|
48
48
|
/** Set by the mock: the empty `REVIEW_IN_PROGRESS` stack a CREATE change set made for a missing stack. */
|
|
49
49
|
shell?: boolean;
|
|
50
|
+
/** The stack's exports (#4466c), export name to the output it is; the value is the output's. */
|
|
51
|
+
exports?: Record<string, string>;
|
|
52
|
+
/** The stacks that import each of this stack's exports now, by export name (#4466c). */
|
|
53
|
+
importedBy?: Record<string, string[]>;
|
|
50
54
|
}
|
|
51
55
|
export interface FakeDeploymentConfig {
|
|
52
56
|
/** Terminal status CodeDeploy reports for this deployment. Default: "Succeeded". */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mock-cloud-executor.d.ts","sourceRoot":"","sources":["../../../src/components/__tests__/mock-cloud-executor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EACV,SAAS,EAIT,aAAa,EAKb,eAAe,EAWhB,MAAM,mBAAmB,CAAC;AAK3B,OAAO,KAAK,EAMV,kBAAkB,EAEnB,MAAM,kDAAkD,CAAC;AAE1D,2LAA2L;AAC3L,KAAK,YAAY,GAAG,aAAa,GAAG;IAAE,KAAK,EAAE,kBAAkB,CAAA;CAAE,CAAC;AAElE,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,OAAO,CAAC;CACf;AAED,2DAA2D;AAC3D,MAAM,WAAW,eAAe;IAC9B,kIAAkI;IAClI,OAAO,CAAC,EAAE,SAAS,EAAE,CAAC;IACtB,+CAA+C;IAC/C,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,kIAAkI;IAClI,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,+FAA+F;IAC/F,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;yBAGqB;IACrB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,0GAA0G;IAC1G,KAAK,CAAC,EAAE,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"mock-cloud-executor.d.ts","sourceRoot":"","sources":["../../../src/components/__tests__/mock-cloud-executor.ts"],"names":[],"mappings":"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"}
|
|
@@ -32,7 +32,10 @@ export function createMockCloudExecutor(options = {}) {
|
|
|
32
32
|
let stepCounter = 0;
|
|
33
33
|
function stackStatus(name) {
|
|
34
34
|
const config = stacks.get(name);
|
|
35
|
-
|
|
35
|
+
const exports = config?.exports
|
|
36
|
+
? Object.fromEntries(Object.entries(config.exports).map(([exportName, output]) => [exportName, { output, value: config.outputs?.[output] ?? "" }]))
|
|
37
|
+
: undefined;
|
|
38
|
+
return { stackStatus: config?.terminalStatus ?? "UPDATE_COMPLETE", outputs: config?.outputs ?? {}, ...(exports ? { exports } : {}) };
|
|
36
39
|
}
|
|
37
40
|
const docker = {
|
|
38
41
|
async build(args) {
|
|
@@ -124,6 +127,20 @@ export function createMockCloudExecutor(options = {}) {
|
|
|
124
127
|
stacks.set(stackName, { ...config, shell: false, missing: true });
|
|
125
128
|
return true;
|
|
126
129
|
},
|
|
130
|
+
// Only a mock some stack's `importedBy` was given has ListImports, so the
|
|
131
|
+
// plans of every other test make the calls they always made (#4466c).
|
|
132
|
+
...(Object.values(options.stacks ?? {}).some((c) => c.importedBy !== undefined)
|
|
133
|
+
? {
|
|
134
|
+
async listImports(exportName) {
|
|
135
|
+
record("cloudformation", "listImports", exportName);
|
|
136
|
+
const out = new Set();
|
|
137
|
+
for (const config of stacks.values())
|
|
138
|
+
for (const s of config.importedBy?.[exportName] ?? [])
|
|
139
|
+
out.add(s);
|
|
140
|
+
return [...out].sort();
|
|
141
|
+
},
|
|
142
|
+
}
|
|
143
|
+
: {}),
|
|
127
144
|
async waitForStack(stackName) {
|
|
128
145
|
record("cloudformation", "waitForStack", stackName);
|
|
129
146
|
return stackStatus(stackName);
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* `run-migration` dispatches on its target transport (ECS one-off task, Lambda
|
|
19
19
|
* invoke, or SSM host command).
|
|
20
20
|
*/
|
|
21
|
-
import type { Capability, PlannedOutputs } from "@intentius/chant/components/capability";
|
|
21
|
+
import type { Capability, DeclaredRead, PlannedOutputs } from "@intentius/chant/components/capability";
|
|
22
22
|
import type { ChangeSetEntry } from "@intentius/chant/change-set";
|
|
23
23
|
import { type PlanShape, type PlanShapeArtifact } from "@intentius/chant/components/covered-round";
|
|
24
24
|
import { type CfnChange, type CfnChangeSet, type CloudExecutor } from "./cloud-executor.js";
|
|
@@ -115,9 +115,25 @@ export declare function cfnPlanShape(stack: string, changeSet: CfnChangeSet, unk
|
|
|
115
115
|
* known at that value (`given`, the inputs of this plan). Both are what
|
|
116
116
|
* the stack's outputs will be, whatever they are now.
|
|
117
117
|
*
|
|
118
|
+
* An output that passes an import through (`Fn::ImportValue` of a name
|
|
119
|
+
* `imports` holds, #4466) is known at the value the exporting stack's plan
|
|
120
|
+
* gave it, which is the value core read for the import.
|
|
121
|
+
*
|
|
118
122
|
* `undefined` when there is no template or no standing stack to read from.
|
|
119
123
|
*/
|
|
120
|
-
export declare function cfnPlannedOutputs(template: unknown, changes: readonly CfnChange[], standing: Record<string, string> | undefined, given?: Record<string, string>): PlannedOutputs | undefined;
|
|
124
|
+
export declare function cfnPlannedOutputs(template: unknown, changes: readonly CfnChange[], standing: Record<string, string> | undefined, given?: Record<string, string>, imports?: Record<string, string>): PlannedOutputs | undefined;
|
|
125
|
+
/**
|
|
126
|
+
* `cfn-deploy`'s `declaredReads` (#4466): each `Fn::ImportValue` of a name
|
|
127
|
+
* exactly one other `cfn-deploy` step's template exports is a read of that
|
|
128
|
+
* component's output, placed under the step's `imports`. A name two steps
|
|
129
|
+
* export is left alone (CloudFormation refuses the second export), and so
|
|
130
|
+
* is a name no step in the project exports: it reads a stack outside it.
|
|
131
|
+
*/
|
|
132
|
+
export declare function cfnDeclaredReads(root: string, members: readonly {
|
|
133
|
+
component: string;
|
|
134
|
+
index: number;
|
|
135
|
+
step: Record<string, unknown>;
|
|
136
|
+
}[]): DeclaredRead[];
|
|
121
137
|
/**
|
|
122
138
|
* How `cfn-deploy` handles a changeset that requires resource replacement.
|
|
123
139
|
* - `block`: refuse to apply; surface the changeset for human review.
|
|
@@ -146,6 +162,14 @@ export interface CfnDeployInput {
|
|
|
146
162
|
onReplace?: CfnReplacePolicy;
|
|
147
163
|
/** Stage DynamoDB GSI changes (add-then-backfill-then-remove) instead of an in-place replace. Default: false. */
|
|
148
164
|
stageGsi?: boolean;
|
|
165
|
+
/**
|
|
166
|
+
* The exports the template imports (`Fn::ImportValue`) from another
|
|
167
|
+
* `cfn-deploy` step of the project, by export name (#4466). Core fills it
|
|
168
|
+
* from `declaredReads` with a `stackOutput()` of the exporting component's
|
|
169
|
+
* output, so a value is `undefined` while that stack's plan cannot say it.
|
|
170
|
+
* Never passed to CloudFormation: the template imports the export itself.
|
|
171
|
+
*/
|
|
172
|
+
imports?: Record<string, string | undefined>;
|
|
149
173
|
}
|
|
150
174
|
export interface CfnDeployOutput {
|
|
151
175
|
/** Final stack status (`CREATE_COMPLETE`, `UPDATE_COMPLETE`, ...). */
|
|
@@ -155,6 +179,24 @@ export interface CfnDeployOutput {
|
|
|
155
179
|
/** Snapshot id captured before an `onReplace: "snapshot-first"` replacement, if one was taken. */
|
|
156
180
|
snapshotId?: string;
|
|
157
181
|
}
|
|
182
|
+
/** One export a plan changes or removes while other stacks import it (#4466c). */
|
|
183
|
+
export interface CfnExportInUse {
|
|
184
|
+
exportName: string;
|
|
185
|
+
/** What the plan does to it, as the refusal says it: "removes", or "changes ... from A to B". */
|
|
186
|
+
change: string;
|
|
187
|
+
/** The stacks that import it now, sorted. */
|
|
188
|
+
importers: string[];
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Thrown at plan time when a change set would change or remove an export
|
|
192
|
+
* that another stack still imports (#4466c). CloudFormation refuses that
|
|
193
|
+
* update at apply; the plan refuses it first, naming both stacks.
|
|
194
|
+
*/
|
|
195
|
+
export declare class CfnExportInUseError extends Error {
|
|
196
|
+
readonly stack: string;
|
|
197
|
+
readonly uses: CfnExportInUse[];
|
|
198
|
+
constructor(stack: string, uses: CfnExportInUse[]);
|
|
199
|
+
}
|
|
158
200
|
/**
|
|
159
201
|
* Thrown when a changeset proposes a resource replacement and `onReplace` is
|
|
160
202
|
* `"block"` (the default) — refusing to apply rather than silently destroying
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"apply.d.ts","sourceRoot":"","sources":["../../src/components/apply.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;
|
|
1
|
+
{"version":3,"file":"apply.d.ts","sourceRoot":"","sources":["../../src/components/apply.ts"],"names":[],"mappings":"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"}
|
package/dist/components/apply.js
CHANGED
|
@@ -19,10 +19,13 @@
|
|
|
19
19
|
* invoke, or SSM host command).
|
|
20
20
|
*/
|
|
21
21
|
import { createHash } from "node:crypto";
|
|
22
|
-
import { readFileSync } from "node:fs";
|
|
22
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
23
|
+
import { tmpdir } from "node:os";
|
|
24
|
+
import { join } from "node:path";
|
|
23
25
|
import { computePlanDigest } from "@intentius/chant/lifecycle/plan-digest";
|
|
24
26
|
import { normalizePlanShape, PlanShapeMovedError, planShapeDifferences, planShapeDigest, } from "@intentius/chant/components/covered-round";
|
|
25
27
|
import { defaultCloudExecutor } from "./cloud-executor.js";
|
|
28
|
+
import { cfnExports, cfnImportPointer, cfnImports, cfnKnownValues, cfnTemplateWithStandIns, readCfnTemplate, resolveCfnName } from "./cfn-imports.js";
|
|
26
29
|
/** CloudFormation's wording when a change set has nothing to apply — the stack
|
|
27
30
|
* is already current. AWS phrases it "The submitted information didn't contain
|
|
28
31
|
* changes."; `aws cloudformation deploy` reports "No updates are to be
|
|
@@ -157,6 +160,8 @@ preview) {
|
|
|
157
160
|
templateDigest: templateDigest(input.template),
|
|
158
161
|
parameters,
|
|
159
162
|
unresolved,
|
|
163
|
+
// #4466: the exports the template imports from stacks in the change, as resolved.
|
|
164
|
+
...(input.imports && Object.keys(input.imports).length > 0 ? { imports: importValues(input.imports) } : {}),
|
|
160
165
|
...(preview ? { preview } : {}),
|
|
161
166
|
});
|
|
162
167
|
}
|
|
@@ -167,10 +172,14 @@ preview) {
|
|
|
167
172
|
* exist yet gets a CREATE change set, so every resource reads as a create,
|
|
168
173
|
* and the empty `REVIEW_IN_PROGRESS` stack CloudFormation made to hold it is
|
|
169
174
|
* deleted too. A change set with nothing to apply (`isNoopChangeSet`) gives
|
|
170
|
-
* a member with no entries. A step whose parameters read outputs
|
|
171
|
-
* not known yet, from a stack the same run applies first,
|
|
172
|
-
*
|
|
173
|
-
*
|
|
175
|
+
* a member with no entries. A step whose parameters or imports read outputs
|
|
176
|
+
* that are not known yet, from a stack the same run applies first, is
|
|
177
|
+
* previewed: its change set is made against a stand-in for each unknown
|
|
178
|
+
* value (#4423), an import standing in as a parameter of a template copy
|
|
179
|
+
* (#4466b), and `run` checks the real change set's shape against the
|
|
180
|
+
* preview's before it executes. When no preview can be made, its member gets
|
|
181
|
+
* a hole saying whose outputs it waits on, and no artifact, so `run` plans
|
|
182
|
+
* it afresh.
|
|
174
183
|
*
|
|
175
184
|
* With `onReplace: "block"` (the default), a change set that replaces a
|
|
176
185
|
* resource fails the plan with {@link CfnReplacementBlockedError}, so the
|
|
@@ -202,31 +211,51 @@ async function planCfnDeploy(executor, ctx, input) {
|
|
|
202
211
|
}
|
|
203
212
|
const unresolved = [...(ctx.unresolvedInputs ?? [])].sort();
|
|
204
213
|
let planDigest = cfnPlanDigest(ctx, stack, input, parameters, unresolved);
|
|
214
|
+
// #4466: an import whose name the plan cannot resolve is a hole that says why.
|
|
215
|
+
const importHoles = unresolvedImportHoles(stack, input, knownParameters(parameters));
|
|
216
|
+
// #4466c: an export another stack imports that the plan may change, said where it cannot tell.
|
|
217
|
+
const exportHoles = [];
|
|
205
218
|
const member = (holes) => ({
|
|
206
219
|
member: stack,
|
|
207
220
|
lexicon: "aws",
|
|
208
221
|
planner: "chant",
|
|
209
222
|
status: "planned",
|
|
210
223
|
planDigest,
|
|
211
|
-
holes,
|
|
224
|
+
holes: [...holes, ...importHoles, ...exportHoles],
|
|
212
225
|
});
|
|
226
|
+
const standing = standingReader(executor, stack);
|
|
213
227
|
if (unresolved.length > 0 || missing.length > 0) {
|
|
214
228
|
const hole = (why) => ({
|
|
215
229
|
member: stack,
|
|
216
230
|
planDigest,
|
|
217
231
|
changeSet: { member: member([{ address: stack, type: "AWS::CloudFormation::Stack", reason: why }]), entries: [] },
|
|
218
232
|
});
|
|
233
|
+
const waiting = unknownImports(input);
|
|
219
234
|
const fallback = unresolved.length > 0
|
|
220
|
-
?
|
|
235
|
+
? missing.length === 0 && waiting.length > 0
|
|
236
|
+
? `imports ${waiting.join(", ")}, exported by ${unresolved.join(", ")}, which apply first; re-checked on apply`
|
|
237
|
+
: `inputs from ${unresolved.join(", ")}; re-checked on apply`
|
|
221
238
|
: `inputs ${[...missing].sort().join(", ")} are known only at apply; re-checked on apply`;
|
|
222
|
-
// Which parameters are unknown: the inputs that hold nothing yet
|
|
223
|
-
//
|
|
224
|
-
|
|
239
|
+
// Which parameters are unknown: the inputs that hold nothing yet, and a
|
|
240
|
+
// stand-in for each import of an export not known yet (#4466b). When the
|
|
241
|
+
// upstream is unresolved but neither says which, nothing can be previewed.
|
|
242
|
+
if (missing.length === 0 && waiting.length === 0)
|
|
225
243
|
return hole(fallback);
|
|
226
|
-
const
|
|
244
|
+
const copy = waiting.length > 0 ? standInTemplate(stack, input, waiting, knownParameters(parameters)) : undefined;
|
|
245
|
+
if (typeof copy === "string")
|
|
246
|
+
return hole(`${fallback}; no preview: ${copy}`);
|
|
247
|
+
let preview;
|
|
248
|
+
try {
|
|
249
|
+
const previewed = copy ? { ...input, template: copy.path } : input;
|
|
250
|
+
preview = await previewCfnDeploy(executor, stack, previewed, [...missing, ...(copy?.parameters ?? [])].sort(), unresolved.length > 0 ? unresolved : undefined);
|
|
251
|
+
}
|
|
252
|
+
finally {
|
|
253
|
+
copy?.remove();
|
|
254
|
+
}
|
|
227
255
|
if (typeof preview === "string")
|
|
228
256
|
return hole(`${fallback}; no preview: ${preview}`);
|
|
229
257
|
const { entries, shape, outputs } = preview;
|
|
258
|
+
exportHoles.push(...(await exportsInUse(executor, stack, input, knownParameters(parameters), outputs, standing)));
|
|
230
259
|
const artifact = { kind: "cfn-deploy-shape", member: stack, shape, digest: planShapeDigest(shape) };
|
|
231
260
|
// The preview's shape enters the digest (#4423); `member` reads it when called below.
|
|
232
261
|
planDigest = cfnPlanDigest(ctx, stack, input, parameters, unresolved, artifact.digest);
|
|
@@ -241,8 +270,8 @@ async function planCfnDeploy(executor, ctx, input) {
|
|
|
241
270
|
if (isNoopChangeSet(changeSet)) {
|
|
242
271
|
const artifact = { kind: "cfn-deploy-plan", stack, changesDigest: cfnChangesDigest(stack, changeSet) };
|
|
243
272
|
// Nothing to apply, so the outputs the stack has are the ones it will have (#3999).
|
|
244
|
-
const
|
|
245
|
-
const outputs =
|
|
273
|
+
const now = await standing();
|
|
274
|
+
const outputs = now ? { known: { ...now.outputs }, unknown: [] } : undefined;
|
|
246
275
|
return { member: stack, planDigest, artifact, changeSet: { member: member([]), entries: [] }, ...(outputs ? { outputs } : {}) };
|
|
247
276
|
}
|
|
248
277
|
if (changeSet.status === "FAILED") {
|
|
@@ -253,7 +282,12 @@ async function planCfnDeploy(executor, ctx, input) {
|
|
|
253
282
|
throw new CfnReplacementBlockedError(stack, blocked);
|
|
254
283
|
const entries = changeSet.changes.map((c) => cfnChangeEntry(stack, c));
|
|
255
284
|
const artifact = { kind: "cfn-deploy-plan", stack, changesDigest: cfnChangesDigest(stack, changeSet) };
|
|
256
|
-
const outputs = changeSet.stackCreated
|
|
285
|
+
const outputs = changeSet.stackCreated
|
|
286
|
+
? undefined
|
|
287
|
+
: await plannedStackOutputs(standing, input.template, changeSet.changes, knownParameters(parameters), importValues(input.imports ?? {}));
|
|
288
|
+
// #4466c: CloudFormation refuses to change or remove an export another stack imports; say so now, not at apply.
|
|
289
|
+
if (!changeSet.stackCreated)
|
|
290
|
+
exportHoles.push(...(await exportsInUse(executor, stack, input, knownParameters(parameters), outputs, standing)));
|
|
257
291
|
// The real change set's shape, with the replacement CloudFormation reports,
|
|
258
292
|
// for a round that checks it against a preview (#4419).
|
|
259
293
|
const shape = cfnPlanShape(stack, changeSet, []);
|
|
@@ -388,7 +422,9 @@ export function cfnPlanShape(stack, changeSet, unknown, templatePath, refs = unk
|
|
|
388
422
|
* Floci accepts a change set that real CloudFormation refuses (a missing
|
|
389
423
|
* export, an id that does not exist), reports `Parameters: null`, and says
|
|
390
424
|
* `Replacement: False` for a change that replaces. So a cross-stack value
|
|
391
|
-
* goes through a parameter here, never an export
|
|
425
|
+
* goes through a parameter here, never an export: an import of an export
|
|
426
|
+
* not known yet is planned from a copy of the template in which it is a
|
|
427
|
+
* stand-in parameter (#4466b, `cfnTemplateWithStandIns`). The replacement a
|
|
392
428
|
* preview reports is whatever the backend says; the shape check at apply
|
|
393
429
|
* compares like with like.
|
|
394
430
|
*/
|
|
@@ -414,7 +450,7 @@ async function previewCfnDeploy(executor, stack, input, unknown, refs = unknown)
|
|
|
414
450
|
// output that reads one of the unknown parameters is unknown, one that
|
|
415
451
|
// reads a known parameter or states its value is not (#4419).
|
|
416
452
|
const given = Object.fromEntries(Object.entries(parameters).filter(([k]) => !unknown.includes(k)));
|
|
417
|
-
const outputs = noop || changeSet.stackCreated ? undefined : await plannedStackOutputs(executor, stack, input.template, changeSet.changes, given);
|
|
453
|
+
const outputs = noop || changeSet.stackCreated ? undefined : await plannedStackOutputs(standingReader(executor, stack), input.template, changeSet.changes, given);
|
|
418
454
|
return { entries, shape: cfnPlanShape(stack, changeSet, unknown, input.template, refs), ...(outputs ? { outputs } : {}) };
|
|
419
455
|
}
|
|
420
456
|
finally {
|
|
@@ -443,26 +479,23 @@ async function cleanUpPlanChangeSet(executor, changeSet) {
|
|
|
443
479
|
}
|
|
444
480
|
}
|
|
445
481
|
}
|
|
446
|
-
/** The stack
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
}
|
|
451
|
-
catch {
|
|
452
|
-
return undefined;
|
|
453
|
-
}
|
|
482
|
+
/** The stack as it stands, read once however often it is asked for, or `undefined` when it cannot be read. */
|
|
483
|
+
function standingReader(executor, stack) {
|
|
484
|
+
let read;
|
|
485
|
+
return () => (read ?? (read = executor.cloudformation.describeStack(stack).then((s) => s, () => undefined)));
|
|
454
486
|
}
|
|
455
487
|
/**
|
|
456
488
|
* What the plan says of the stack's outputs once `changes` apply: none to
|
|
457
489
|
* say for a template with no `Outputs`, which needs no `DescribeStacks`.
|
|
458
490
|
*/
|
|
459
|
-
async function plannedStackOutputs(
|
|
491
|
+
async function plannedStackOutputs(standing, templatePath, changes, parameters = {}, imports = {}) {
|
|
460
492
|
const template = readTemplate(templatePath);
|
|
461
493
|
if (!template || typeof template !== "object")
|
|
462
494
|
return undefined;
|
|
463
495
|
if (Object.keys(template.Outputs ?? {}).length === 0)
|
|
464
496
|
return { known: {}, unknown: [] };
|
|
465
|
-
|
|
497
|
+
const now = await standing();
|
|
498
|
+
return cfnPlannedOutputs(template, changes, now ? { ...now.outputs } : undefined, parameters, imports);
|
|
466
499
|
}
|
|
467
500
|
/** The template at `path` as JSON, or `undefined` when it cannot be read or parsed here. */
|
|
468
501
|
function readTemplate(path) {
|
|
@@ -551,9 +584,13 @@ function valueReads(value) {
|
|
|
551
584
|
* known at that value (`given`, the inputs of this plan). Both are what
|
|
552
585
|
* the stack's outputs will be, whatever they are now.
|
|
553
586
|
*
|
|
587
|
+
* An output that passes an import through (`Fn::ImportValue` of a name
|
|
588
|
+
* `imports` holds, #4466) is known at the value the exporting stack's plan
|
|
589
|
+
* gave it, which is the value core read for the import.
|
|
590
|
+
*
|
|
554
591
|
* `undefined` when there is no template or no standing stack to read from.
|
|
555
592
|
*/
|
|
556
|
-
export function cfnPlannedOutputs(template, changes, standing, given = {}) {
|
|
593
|
+
export function cfnPlannedOutputs(template, changes, standing, given = {}, imports = {}) {
|
|
557
594
|
if (!standing || template === null || typeof template !== "object")
|
|
558
595
|
return undefined;
|
|
559
596
|
const t = template;
|
|
@@ -568,7 +605,7 @@ export function cfnPlannedOutputs(template, changes, standing, given = {}) {
|
|
|
568
605
|
const known = {};
|
|
569
606
|
const unknown = [];
|
|
570
607
|
for (const [name, output] of Object.entries(t.Outputs ?? {}).sort(([a], [b]) => a.localeCompare(b))) {
|
|
571
|
-
const stated = statedOutput(output, given, t.Parameters);
|
|
608
|
+
const stated = statedOutput(output, given, t.Parameters, imports);
|
|
572
609
|
if (stated !== undefined) {
|
|
573
610
|
known[name] = stated;
|
|
574
611
|
continue;
|
|
@@ -592,7 +629,7 @@ function knownParameters(parameters) {
|
|
|
592
629
|
* parameter this plan has a value for. `undefined` for anything that reads a
|
|
593
630
|
* resource, an import or a condition, which the standing outputs speak for.
|
|
594
631
|
*/
|
|
595
|
-
function statedOutput(output, parameters, declared) {
|
|
632
|
+
function statedOutput(output, parameters, declared, imports = {}) {
|
|
596
633
|
if (output?.Condition !== undefined)
|
|
597
634
|
return undefined;
|
|
598
635
|
const value = output?.Value;
|
|
@@ -603,11 +640,202 @@ function statedOutput(output, parameters, declared) {
|
|
|
603
640
|
const [key, arg] = entries[0] ?? [];
|
|
604
641
|
if (entries.length === 1 && key === "Ref" && typeof arg === "string" && declared && arg in declared && arg in parameters)
|
|
605
642
|
return parameters[arg];
|
|
643
|
+
if (entries.length === 1 && key === "Fn::ImportValue") {
|
|
644
|
+
const name = resolveCfnName(arg, parameters);
|
|
645
|
+
if ("name" in name && name.name in imports)
|
|
646
|
+
return imports[name.name];
|
|
647
|
+
}
|
|
606
648
|
}
|
|
607
649
|
return undefined;
|
|
608
650
|
}
|
|
651
|
+
// ── Fn::ImportValue between stacks (#4466) ──────────────────────────────────
|
|
652
|
+
/** The imports core read a value for, as strings; one still unknown is left out. */
|
|
653
|
+
function importValues(imports) {
|
|
654
|
+
const out = {};
|
|
655
|
+
for (const [name, v] of Object.entries(imports).sort(([a], [b]) => a.localeCompare(b))) {
|
|
656
|
+
if (v === undefined || v === null)
|
|
657
|
+
continue;
|
|
658
|
+
out[name] = typeof v === "string" ? v : JSON.stringify(v);
|
|
659
|
+
}
|
|
660
|
+
return out;
|
|
661
|
+
}
|
|
662
|
+
/** The export names the step imports from a stack in the change whose value is not known yet, sorted. */
|
|
663
|
+
function unknownImports(input) {
|
|
664
|
+
return Object.entries(input.imports ?? {})
|
|
665
|
+
.filter(([, v]) => v === undefined || v === null)
|
|
666
|
+
.map(([name]) => name)
|
|
667
|
+
.sort();
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* A hole for each `Fn::ImportValue` in the template whose name the plan
|
|
671
|
+
* cannot resolve (#4466): it may name an export of a stack in this change,
|
|
672
|
+
* and then nothing orders the two, so the plan says it cannot tell.
|
|
673
|
+
*/
|
|
674
|
+
function unresolvedImportHoles(stack, input, parameters) {
|
|
675
|
+
const template = readCfnTemplate(input.template);
|
|
676
|
+
if (!template)
|
|
677
|
+
return [];
|
|
678
|
+
const resources = (template.Resources ?? {});
|
|
679
|
+
const holes = [];
|
|
680
|
+
for (const i of cfnImports(template, cfnKnownValues(template, stack, parameters))) {
|
|
681
|
+
if (!("unresolved" in i))
|
|
682
|
+
continue;
|
|
683
|
+
const type = typeof resources[i.logicalId]?.Type === "string" ? resources[i.logicalId].Type : undefined;
|
|
684
|
+
holes.push({
|
|
685
|
+
address: i.logicalId,
|
|
686
|
+
...(type ? { type } : {}),
|
|
687
|
+
reason: `the Fn::ImportValue at ${i.at} ${i.unresolved}, so the plan cannot say which export it reads or whether a stack in this change exports it`,
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
return holes;
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* The importer's template with each import of `names` as a stand-in
|
|
694
|
+
* parameter (#4466b), written to a scratch file the preview creates its
|
|
695
|
+
* change set from; or why there is none. `remove` deletes the file.
|
|
696
|
+
*/
|
|
697
|
+
function standInTemplate(stack, input, names, parameters) {
|
|
698
|
+
const template = readCfnTemplate(input.template);
|
|
699
|
+
if (!template)
|
|
700
|
+
return "the template could not be read as JSON to stand in for its imports";
|
|
701
|
+
const { template: copy, standIns } = cfnTemplateWithStandIns(template, names, cfnKnownValues(template, stack, parameters));
|
|
702
|
+
const params = Object.keys(standIns).sort();
|
|
703
|
+
if (params.length === 0)
|
|
704
|
+
return `the template imports none of ${names.join(", ")} by a name the plan resolves`;
|
|
705
|
+
const dir = mkdtempSync(join(tmpdir(), "chant-cfn-preview-"));
|
|
706
|
+
const path = join(dir, `${stack}.preview.template.json`);
|
|
707
|
+
writeFileSync(path, JSON.stringify(copy, null, 2));
|
|
708
|
+
return { path, parameters: params, remove: () => rmSync(dir, { recursive: true, force: true }) };
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* `cfn-deploy`'s `declaredReads` (#4466): each `Fn::ImportValue` of a name
|
|
712
|
+
* exactly one other `cfn-deploy` step's template exports is a read of that
|
|
713
|
+
* component's output, placed under the step's `imports`. A name two steps
|
|
714
|
+
* export is left alone (CloudFormation refuses the second export), and so
|
|
715
|
+
* is a name no step in the project exports: it reads a stack outside it.
|
|
716
|
+
*/
|
|
717
|
+
export function cfnDeclaredReads(root, members) {
|
|
718
|
+
const seen = members.map((m) => {
|
|
719
|
+
const template = readCfnTemplate(m.step.template, root);
|
|
720
|
+
const stack = typeof m.step.stack === "string" ? m.step.stack : m.component;
|
|
721
|
+
const inputs = (m.step.inputs ?? {});
|
|
722
|
+
const known = cfnKnownValues(template, stack, typeof m.step.imageRef === "string" ? { ...inputs, ImageRef: m.step.imageRef } : inputs);
|
|
723
|
+
return { member: m, template, known };
|
|
724
|
+
});
|
|
725
|
+
const exporters = new Map();
|
|
726
|
+
for (const { member, template, known } of seen) {
|
|
727
|
+
for (const e of cfnExports(template, known)) {
|
|
728
|
+
if (!("name" in e))
|
|
729
|
+
continue;
|
|
730
|
+
(exporters.get(e.name) ?? exporters.set(e.name, []).get(e.name)).push({ component: member.component, output: e.output });
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
const out = [];
|
|
734
|
+
for (const { member, template, known } of seen) {
|
|
735
|
+
const done = new Set();
|
|
736
|
+
for (const i of cfnImports(template, known)) {
|
|
737
|
+
if (!("name" in i) || done.has(i.name))
|
|
738
|
+
continue;
|
|
739
|
+
done.add(i.name);
|
|
740
|
+
const owners = (exporters.get(i.name) ?? []).filter((o) => o.component !== member.component);
|
|
741
|
+
if (owners.length !== 1 || (exporters.get(i.name) ?? []).length !== 1)
|
|
742
|
+
continue;
|
|
743
|
+
out.push({ reader: member.component, index: member.index, upstream: owners[0].component, output: owners[0].output, at: cfnImportPointer(i.name) });
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
return out;
|
|
747
|
+
}
|
|
609
748
|
/** Whether a CloudFormation error says the stack does not exist. */
|
|
610
749
|
const STACK_MISSING = /does not exist/i;
|
|
750
|
+
/**
|
|
751
|
+
* Thrown at plan time when a change set would change or remove an export
|
|
752
|
+
* that another stack still imports (#4466c). CloudFormation refuses that
|
|
753
|
+
* update at apply; the plan refuses it first, naming both stacks.
|
|
754
|
+
*/
|
|
755
|
+
export class CfnExportInUseError extends Error {
|
|
756
|
+
constructor(stack, uses) {
|
|
757
|
+
const importers = [...new Set(uses.flatMap((u) => u.importers))].sort();
|
|
758
|
+
super(`cfn-deploy "${stack}": refusing the plan: ` +
|
|
759
|
+
uses
|
|
760
|
+
.map((u) => `it ${u.change.replace("{export}", `export ${u.exportName}`)}, which ${u.importers.length === 1 ? "stack" : "stacks"} ${u.importers.join(", ")} still ${u.importers.length === 1 ? "imports" : "import"}`)
|
|
761
|
+
.join("; ") +
|
|
762
|
+
`. CloudFormation refuses to change or remove an export another stack imports. Change ${importers.join(", ")} to stop importing it first, then change ${stack}.`);
|
|
763
|
+
this.stack = stack;
|
|
764
|
+
this.uses = uses;
|
|
765
|
+
this.name = "CfnExportInUseError";
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* The exports `stack` has now that this plan changes or removes while
|
|
770
|
+
* another stack imports them (#4466c). An export the new template no longer
|
|
771
|
+
* declares is removed; one whose planned value differs from the value it
|
|
772
|
+
* has is changed: both throw {@link CfnExportInUseError}. One the plan
|
|
773
|
+
* cannot say about (its output's value is known only after apply, or an
|
|
774
|
+
* export name in the new template does not resolve) is a hole naming the
|
|
775
|
+
* importers. Asks `ListImports` only for exports at stake, and nothing at
|
|
776
|
+
* all when the executor cannot list imports or the stack exports nothing.
|
|
777
|
+
*/
|
|
778
|
+
async function exportsInUse(executor, stack, input, parameters, planned, standing) {
|
|
779
|
+
const listImports = executor.cloudformation.listImports?.bind(executor.cloudformation);
|
|
780
|
+
if (!listImports)
|
|
781
|
+
return [];
|
|
782
|
+
const was = (await standing())?.exports;
|
|
783
|
+
if (!was || Object.keys(was).length === 0)
|
|
784
|
+
return [];
|
|
785
|
+
const template = readCfnTemplate(input.template);
|
|
786
|
+
if (!template)
|
|
787
|
+
return [];
|
|
788
|
+
const declared = cfnExports(template, cfnKnownValues(template, stack, parameters));
|
|
789
|
+
const now = new Map(declared.filter((e) => "name" in e).map((e) => [e.name, e.output]));
|
|
790
|
+
const unresolvedNow = declared.some((e) => "unresolved" in e);
|
|
791
|
+
const refused = [];
|
|
792
|
+
const holes = [];
|
|
793
|
+
for (const [exportName, { output, value }] of Object.entries(was).sort(([a], [b]) => a.localeCompare(b))) {
|
|
794
|
+
const at = now.get(exportName);
|
|
795
|
+
let change;
|
|
796
|
+
let maybe;
|
|
797
|
+
if (at === undefined) {
|
|
798
|
+
if (unresolvedNow)
|
|
799
|
+
maybe = `may remove export ${exportName}: an export name the template declares does not resolve at plan`;
|
|
800
|
+
else
|
|
801
|
+
change = "removes {export}";
|
|
802
|
+
}
|
|
803
|
+
else {
|
|
804
|
+
const next = planned?.known[at];
|
|
805
|
+
if (next === undefined)
|
|
806
|
+
maybe = `may change export ${exportName}: the value of output ${at} is known only after apply`;
|
|
807
|
+
else if ((typeof next === "string" ? next : JSON.stringify(next)) !== value)
|
|
808
|
+
change = `changes {export} from ${value} to ${typeof next === "string" ? next : JSON.stringify(next)}`;
|
|
809
|
+
}
|
|
810
|
+
if (change === undefined && maybe === undefined)
|
|
811
|
+
continue;
|
|
812
|
+
let importers;
|
|
813
|
+
try {
|
|
814
|
+
importers = (await listImports(exportName)).filter((s) => s !== stack);
|
|
815
|
+
}
|
|
816
|
+
catch (err) {
|
|
817
|
+
// A backend without ListImports (an emulator, say) cannot say who imports it; the plan says so instead of failing.
|
|
818
|
+
holes.push({
|
|
819
|
+
address: `Outputs.${at ?? output}`,
|
|
820
|
+
reason: `the plan ${change?.replace("{export}", `export ${exportName}`) ?? maybe}, and the stacks that import it could not be listed: ${err instanceof Error ? err.message : String(err)}`,
|
|
821
|
+
});
|
|
822
|
+
continue;
|
|
823
|
+
}
|
|
824
|
+
if (importers.length === 0)
|
|
825
|
+
continue;
|
|
826
|
+
if (change !== undefined)
|
|
827
|
+
refused.push({ exportName, change, importers });
|
|
828
|
+
else {
|
|
829
|
+
holes.push({
|
|
830
|
+
address: `Outputs.${at ?? output}`,
|
|
831
|
+
reason: `the plan ${maybe}, and ${importers.join(", ")} still ${importers.length === 1 ? "imports" : "import"} it; CloudFormation refuses the apply if it changes`,
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
if (refused.length > 0)
|
|
836
|
+
throw new CfnExportInUseError(stack, refused);
|
|
837
|
+
return holes;
|
|
838
|
+
}
|
|
611
839
|
/**
|
|
612
840
|
* Thrown when a changeset proposes a resource replacement and `onReplace` is
|
|
613
841
|
* `"block"` (the default) — refusing to apply rather than silently destroying
|
|
@@ -655,6 +883,9 @@ export function createCfnDeployCapability(executor = defaultCloudExecutor()) {
|
|
|
655
883
|
plan(ctx, input) {
|
|
656
884
|
return planCfnDeploy(executor, ctx, input);
|
|
657
885
|
},
|
|
886
|
+
declaredReads(ctx, members) {
|
|
887
|
+
return cfnDeclaredReads(ctx.root, members);
|
|
888
|
+
},
|
|
658
889
|
async outputs(ctx, input) {
|
|
659
890
|
// The outputs the stack has now, for a dependent planned before it
|
|
660
891
|
// applies (#3183). A stack that does not exist yet has none.
|