sst 2.48.4 → 2.49.0

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.
@@ -1,408 +0,0 @@
1
- // Copied from https://github.com/aws/aws-cdk/blob/main/packages/aws-cdk/lib/api/cloudformation-deployments.ts
2
- import { randomUUID } from "crypto";
3
- import * as cxapi from "@aws-cdk/cx-api";
4
- import * as cdk_assets from "cdk-assets";
5
- import { AssetManifestBuilder } from "sst-aws-cdk/lib/api/deployments/asset-manifest-builder.js";
6
- import { EVENT_TO_LOGGER, PublishingAws, } from "sst-aws-cdk/lib/api/deployments/asset-publishing.js";
7
- import { determineAllowCrossAccountAssetPublishing } from "sst-aws-cdk/lib/api/deployments/checks.js";
8
- import { CloudFormationStack, stabilizeStack, uploadStackTemplateAssets, } from "sst-aws-cdk/lib/api/deployments/cloudformation.js";
9
- import { deployStack, destroyStack } from "./deploy-stack.js";
10
- import { loadCurrentTemplate, loadCurrentTemplateWithNestedStacks, } from "sst-aws-cdk/lib/api/deployments/nested-stack-helpers.js";
11
- import { debug, warning } from "sst-aws-cdk/lib/logging.js";
12
- import { ToolkitError } from "sst-aws-cdk/lib/toolkit/error.js";
13
- import { formatErrorMessage } from "sst-aws-cdk/lib/util/error.js";
14
- import { EnvironmentAccess } from "sst-aws-cdk/lib/api/environment-access.js";
15
- import { DEFAULT_TOOLKIT_STACK_NAME } from "sst-aws-cdk/lib/api/toolkit-info.js";
16
- import { StackActivityMonitor, } from "sst-aws-cdk/lib/api/util/cloudformation/stack-activity-monitor.js";
17
- import { StackEventPoller } from "sst-aws-cdk/lib/api/util/cloudformation/stack-event-poller.js";
18
- import { RollbackChoice } from "sst-aws-cdk/lib/api/util/cloudformation/stack-status.js";
19
- import { makeBodyParameter } from "sst-aws-cdk/lib/api/util/template-body-parameter.js";
20
- import { AssetManifest } from "cdk-assets";
21
- const BOOTSTRAP_STACK_VERSION_FOR_ROLLBACK = 23;
22
- /**
23
- * Scope for a single set of deployments from a set of Cloud Assembly Artifacts
24
- *
25
- * Manages lookup of SDKs, Bootstrap stacks, etc.
26
- */
27
- export class Deployments {
28
- props;
29
- envs;
30
- /**
31
- * SDK provider for asset publishing (do not use for anything else).
32
- *
33
- * This SDK provider is only allowed to be used for that purpose, nothing else.
34
- *
35
- * It's not a different object, but the field name should imply that this
36
- * object should not be used directly, except to pass to asset handling routines.
37
- */
38
- assetSdkProvider;
39
- /**
40
- * SDK provider for passing to deployStack
41
- *
42
- * This SDK provider is only allowed to be used for that purpose, nothing else.
43
- *
44
- * It's not a different object, but the field name should imply that this
45
- * object should not be used directly, except to pass to `deployStack`.
46
- */
47
- deployStackSdkProvider;
48
- publisherCache = new Map();
49
- _allowCrossAccountAssetPublishing;
50
- constructor(props) {
51
- this.props = props;
52
- this.assetSdkProvider = props.sdkProvider;
53
- this.deployStackSdkProvider = props.sdkProvider;
54
- this.envs = new EnvironmentAccess(props.sdkProvider, props.toolkitStackName ?? DEFAULT_TOOLKIT_STACK_NAME);
55
- }
56
- /**
57
- * Resolves the environment for a stack.
58
- */
59
- async resolveEnvironment(stack) {
60
- return this.envs.resolveStackEnvironment(stack);
61
- }
62
- async readCurrentTemplateWithNestedStacks(rootStackArtifact, retrieveProcessedTemplate = false) {
63
- const env = await this.envs.accessStackForLookupBestEffort(rootStackArtifact);
64
- return loadCurrentTemplateWithNestedStacks(rootStackArtifact, env.sdk, retrieveProcessedTemplate);
65
- }
66
- async readCurrentTemplate(stackArtifact) {
67
- debug(`Reading existing template for stack ${stackArtifact.displayName}.`);
68
- const env = await this.envs.accessStackForLookupBestEffort(stackArtifact);
69
- return loadCurrentTemplate(stackArtifact, env.sdk);
70
- }
71
- async resourceIdentifierSummaries(stackArtifact) {
72
- debug(`Retrieving template summary for stack ${stackArtifact.displayName}.`);
73
- // Currently, needs to use `deploy-role` since it may need to read templates in the staging
74
- // bucket which have been encrypted with a KMS key (and lookup-role may not read encrypted things)
75
- const env = await this.envs.accessStackForReadOnlyStackOperations(stackArtifact);
76
- const cfn = env.sdk.cloudFormation();
77
- // @ts-ignore
78
- await uploadStackTemplateAssets(stackArtifact, this);
79
- // Upload the template, if necessary, before passing it to CFN
80
- const builder = new AssetManifestBuilder();
81
- const cfnParam = await makeBodyParameter(stackArtifact, env.resolvedEnvironment, builder, env.resources);
82
- // If the `makeBodyParameter` before this added assets, make sure to publish them before
83
- // calling the API.
84
- const addedAssets = builder.toManifest(stackArtifact.assembly.directory);
85
- for (const entry of addedAssets.entries) {
86
- await this.buildSingleAsset("no-version-validation", addedAssets, entry, {
87
- stack: stackArtifact,
88
- });
89
- await this.publishSingleAsset(addedAssets, entry, {
90
- stack: stackArtifact,
91
- });
92
- }
93
- const response = await cfn.getTemplateSummary(cfnParam);
94
- if (!response.ResourceIdentifierSummaries) {
95
- debug('GetTemplateSummary API call did not return "ResourceIdentifierSummaries"');
96
- }
97
- return response.ResourceIdentifierSummaries ?? [];
98
- }
99
- async deployStack(options) {
100
- let deploymentMethod = options.deploymentMethod;
101
- if (options.changeSetName || options.execute !== undefined) {
102
- if (deploymentMethod) {
103
- throw new ToolkitError("You cannot supply both 'deploymentMethod' and 'changeSetName/execute'. Supply one or the other.");
104
- }
105
- deploymentMethod = {
106
- method: "change-set",
107
- changeSetName: options.changeSetName,
108
- execute: options.execute,
109
- };
110
- }
111
- const env = await this.envs.accessStackForMutableStackOperations(options.stack);
112
- // Do a verification of the bootstrap stack version
113
- await this.validateBootstrapStackVersion(options.stack.stackName, options.stack.requiresBootstrapStackVersion, options.stack.bootstrapStackVersionSsmParameter, env.resources);
114
- const executionRoleArn = await env.replacePlaceholders(options.roleArn ?? options.stack.cloudFormationExecutionRoleArn);
115
- // Deploy assets
116
- const assetArtifacts = options.stack.dependencies.filter(cxapi.AssetManifestArtifact.isAssetManifestArtifact);
117
- for (const asset of assetArtifacts) {
118
- const manifest = AssetManifest.fromFile(asset.file);
119
- await publishAssets(manifest, this.deployStackSdkProvider, env.resolvedEnvironment, { quiet: options.quiet });
120
- }
121
- return deployStack({
122
- stack: options.stack,
123
- noMonitor: true,
124
- resolvedEnvironment: env.resolvedEnvironment,
125
- deployName: options.deployName,
126
- notificationArns: options.notificationArns,
127
- quiet: options.quiet,
128
- sdk: env.sdk,
129
- sdkProvider: this.deployStackSdkProvider,
130
- roleArn: executionRoleArn,
131
- reuseAssets: options.reuseAssets,
132
- envResources: env.resources,
133
- tags: options.tags,
134
- deploymentMethod,
135
- force: options.force,
136
- parameters: options.parameters,
137
- usePreviousParameters: options.usePreviousParameters,
138
- progress: options.progress,
139
- ci: options.ci,
140
- rollback: options.rollback,
141
- hotswap: options.hotswap,
142
- hotswapPropertyOverrides: options.hotswapPropertyOverrides,
143
- extraUserAgent: options.extraUserAgent,
144
- resourcesToImport: options.resourcesToImport,
145
- overrideTemplate: options.overrideTemplate,
146
- assetParallelism: options.assetParallelism,
147
- });
148
- }
149
- async rollbackStack(options) {
150
- let resourcesToSkip = options.orphanLogicalIds ?? [];
151
- if (options.force && resourcesToSkip.length > 0) {
152
- throw new ToolkitError("Cannot combine --force with --orphan");
153
- }
154
- const env = await this.envs.accessStackForMutableStackOperations(options.stack);
155
- if (options.validateBootstrapStackVersion ?? true) {
156
- // Do a verification of the bootstrap stack version
157
- await this.validateBootstrapStackVersion(options.stack.stackName, BOOTSTRAP_STACK_VERSION_FOR_ROLLBACK, options.stack.bootstrapStackVersionSsmParameter, env.resources);
158
- }
159
- const cfn = env.sdk.cloudFormation();
160
- const deployName = options.stack.stackName;
161
- // We loop in case of `--force` and the stack ends up in `CONTINUE_UPDATE_ROLLBACK`.
162
- let maxLoops = 10;
163
- while (maxLoops--) {
164
- let cloudFormationStack = await CloudFormationStack.lookup(cfn, deployName);
165
- const executionRoleArn = await env.replacePlaceholders(options.roleArn ?? options.stack.cloudFormationExecutionRoleArn);
166
- switch (cloudFormationStack.stackStatus.rollbackChoice) {
167
- case RollbackChoice.NONE:
168
- warning(`Stack ${deployName} does not need a rollback: ${cloudFormationStack.stackStatus}`);
169
- return { notInRollbackableState: true };
170
- case RollbackChoice.START_ROLLBACK:
171
- debug(`Initiating rollback of stack ${deployName}`);
172
- await cfn.rollbackStack({
173
- StackName: deployName,
174
- RoleARN: executionRoleArn,
175
- ClientRequestToken: randomUUID(),
176
- // Enabling this is just the better overall default, the only reason it isn't the upstream default is backwards compatibility
177
- RetainExceptOnCreate: true,
178
- });
179
- break;
180
- case RollbackChoice.CONTINUE_UPDATE_ROLLBACK:
181
- if (options.force) {
182
- // Find the failed resources from the deployment and automatically skip them
183
- // (Using deployment log because we definitely have `DescribeStackEvents` permissions, and we might not have
184
- // `DescribeStackResources` permissions).
185
- const poller = new StackEventPoller(cfn, {
186
- stackName: deployName,
187
- stackStatuses: [
188
- "ROLLBACK_IN_PROGRESS",
189
- "UPDATE_ROLLBACK_IN_PROGRESS",
190
- ],
191
- });
192
- await poller.poll();
193
- resourcesToSkip = poller.resourceErrors
194
- .filter((r) => !r.isStackEvent && r.parentStackLogicalIds.length === 0)
195
- .map((r) => r.event.LogicalResourceId ?? "");
196
- }
197
- const skipDescription = resourcesToSkip.length > 0
198
- ? ` (orphaning: ${resourcesToSkip.join(", ")})`
199
- : "";
200
- warning(`Continuing rollback of stack ${deployName}${skipDescription}`);
201
- await cfn.continueUpdateRollback({
202
- StackName: deployName,
203
- ClientRequestToken: randomUUID(),
204
- RoleARN: executionRoleArn,
205
- ResourcesToSkip: resourcesToSkip,
206
- });
207
- break;
208
- case RollbackChoice.ROLLBACK_FAILED:
209
- warning(`Stack ${deployName} failed creation and rollback. This state cannot be rolled back. You can recreate this stack by running 'cdk deploy'.`);
210
- return { notInRollbackableState: true };
211
- default:
212
- throw new ToolkitError(`Unexpected rollback choice: ${cloudFormationStack.stackStatus.rollbackChoice}`);
213
- }
214
- const monitor = options.quiet
215
- ? undefined
216
- : StackActivityMonitor.withDefaultPrinter(cfn, deployName, options.stack, {
217
- ci: options.ci,
218
- }).start();
219
- let stackErrorMessage = undefined;
220
- let finalStackState = cloudFormationStack;
221
- try {
222
- const successStack = await stabilizeStack(cfn, deployName);
223
- // This shouldn't really happen, but catch it anyway. You never know.
224
- if (!successStack) {
225
- throw new ToolkitError("Stack deploy failed (the stack disappeared while we were rolling it back)");
226
- }
227
- finalStackState = successStack;
228
- const errors = monitor?.errors?.join(", ");
229
- if (errors) {
230
- stackErrorMessage = errors;
231
- }
232
- }
233
- catch (e) {
234
- stackErrorMessage = suffixWithErrors(formatErrorMessage(e), monitor?.errors);
235
- }
236
- finally {
237
- await monitor?.stop();
238
- }
239
- if (finalStackState.stackStatus.isRollbackSuccess || !stackErrorMessage) {
240
- return { success: true };
241
- }
242
- // Either we need to ignore some resources to continue the rollback, or something went wrong
243
- if (finalStackState.stackStatus.rollbackChoice ===
244
- RollbackChoice.CONTINUE_UPDATE_ROLLBACK &&
245
- options.force) {
246
- // Do another loop-de-loop
247
- continue;
248
- }
249
- throw new ToolkitError(`${stackErrorMessage} (fix problem and retry, or orphan these resources using --orphan or --force)`);
250
- }
251
- throw new ToolkitError("Rollback did not finish after a large number of iterations; stopping because it looks like we're not making progress anymore. You can retry if rollback was progressing as expected.");
252
- }
253
- async destroyStack(options) {
254
- const env = await this.envs.accessStackForMutableStackOperations(options.stack);
255
- const executionRoleArn = await env.replacePlaceholders(options.roleArn ?? options.stack.cloudFormationExecutionRoleArn);
256
- return destroyStack({
257
- sdk: env.sdk,
258
- roleArn: executionRoleArn,
259
- stack: options.stack,
260
- deployName: options.deployName,
261
- quiet: options.quiet,
262
- ci: options.ci,
263
- });
264
- }
265
- async stackExists(options) {
266
- let env;
267
- if (options.tryLookupRole) {
268
- env = await this.envs.accessStackForLookupBestEffort(options.stack);
269
- }
270
- else {
271
- env = await this.envs.accessStackForReadOnlyStackOperations(options.stack);
272
- }
273
- const stack = await CloudFormationStack.lookup(env.sdk.cloudFormation(), options.deployName ?? options.stack.stackName);
274
- return stack.exists;
275
- }
276
- /**
277
- * Build a single asset from an asset manifest
278
- *
279
- * If an assert manifest artifact is given, the bootstrap stack version
280
- * will be validated according to the constraints in that manifest artifact.
281
- * If that is not necessary, `'no-version-validation'` can be passed.
282
- */
283
- // eslint-disable-next-line max-len
284
- async buildSingleAsset(assetArtifact, assetManifest, asset, options) {
285
- if (assetArtifact !== "no-version-validation") {
286
- const env = await this.envs.accessStackForReadOnlyStackOperations(options.stack);
287
- await this.validateBootstrapStackVersion(options.stack.stackName, assetArtifact.requiresBootstrapStackVersion, assetArtifact.bootstrapStackVersionSsmParameter, env.resources);
288
- }
289
- const resolvedEnvironment = await this.envs.resolveStackEnvironment(options.stack);
290
- const publisher = this.cachedPublisher(assetManifest, resolvedEnvironment, options.stackName);
291
- await publisher.buildEntry(asset);
292
- }
293
- /**
294
- * Publish a single asset from an asset manifest
295
- */
296
- async publishSingleAsset(assetManifest, asset, options) {
297
- const stackEnv = await this.envs.resolveStackEnvironment(options.stack);
298
- // No need to validate anymore, we already did that during build
299
- const publisher = this.cachedPublisher(assetManifest, stackEnv, options.stackName);
300
- await publisher.publishEntry(asset, {
301
- allowCrossAccount: await this.allowCrossAccountAssetPublishingForEnv(options.stack),
302
- });
303
- if (publisher.hasFailures) {
304
- throw new ToolkitError(`Failed to publish asset ${asset.id}`);
305
- }
306
- }
307
- async allowCrossAccountAssetPublishingForEnv(stack) {
308
- if (this._allowCrossAccountAssetPublishing === undefined) {
309
- const env = await this.envs.accessStackForReadOnlyStackOperations(stack);
310
- this._allowCrossAccountAssetPublishing =
311
- await determineAllowCrossAccountAssetPublishing(env.sdk, this.props.toolkitStackName);
312
- }
313
- return this._allowCrossAccountAssetPublishing;
314
- }
315
- /**
316
- * Return whether a single asset has been published already
317
- */
318
- async isSingleAssetPublished(assetManifest, asset, options) {
319
- const stackEnv = await this.envs.resolveStackEnvironment(options.stack);
320
- const publisher = this.cachedPublisher(assetManifest, stackEnv, options.stackName);
321
- return publisher.isEntryPublished(asset);
322
- }
323
- /**
324
- * Validate that the bootstrap stack has the right version for this stack
325
- *
326
- * Call into envResources.validateVersion, but prepend the stack name in case of failure.
327
- */
328
- async validateBootstrapStackVersion(stackName, requiresBootstrapStackVersion, bootstrapStackVersionSsmParameter, envResources) {
329
- try {
330
- await envResources.validateVersion(requiresBootstrapStackVersion, bootstrapStackVersionSsmParameter);
331
- }
332
- catch (e) {
333
- throw new ToolkitError(`${stackName}: ${formatErrorMessage(e)}`);
334
- }
335
- }
336
- cachedPublisher(assetManifest, env, stackName) {
337
- const existing = this.publisherCache.get(assetManifest);
338
- if (existing) {
339
- return existing;
340
- }
341
- const prefix = stackName ? `${stackName}: ` : "";
342
- const publisher = new cdk_assets.AssetPublishing(assetManifest, {
343
- // The AssetPublishing class takes care of role assuming etc, so it's okay to
344
- // give it a direct `SdkProvider`.
345
- aws: new PublishingAws(this.assetSdkProvider, env),
346
- progressListener: new ParallelSafeAssetProgress(prefix, this.props.quiet ?? false),
347
- });
348
- this.publisherCache.set(assetManifest, publisher);
349
- return publisher;
350
- }
351
- }
352
- /**
353
- * Asset progress that doesn't do anything with percentages (currently)
354
- */
355
- class ParallelSafeAssetProgress {
356
- prefix;
357
- quiet;
358
- constructor(prefix, quiet) {
359
- this.prefix = prefix;
360
- this.quiet = quiet;
361
- }
362
- onPublishEvent(type, event) {
363
- const handler = this.quiet && type !== "fail" ? debug : EVENT_TO_LOGGER[type];
364
- handler(`${this.prefix}${type}: ${event.message}`);
365
- }
366
- }
367
- function suffixWithErrors(msg, errors) {
368
- return errors && errors.length > 0 ? `${msg}: ${errors.join(", ")}` : msg;
369
- }
370
- //////////////////////
371
- // Manually copied over functions
372
- //////////////////////
373
- /*
374
- * Copy over deprecated `publishAssets` from `lib/api/deployments/asset-publishing.ts`
375
- * to be used in `deployments-wrapper.ts`
376
- */
377
- class PublishingProgressListener {
378
- constructor() { }
379
- onPublishEvent(type, event) {
380
- const handler = EVENT_TO_LOGGER[type];
381
- handler(`[${event.percentComplete}%] ${type}: ${event.message}`);
382
- }
383
- }
384
- export async function publishAssets(manifest, sdk, targetEnv, options) {
385
- // This shouldn't really happen (it's a programming error), but we don't have
386
- // the types here to guide us. Do an runtime validation to be super super sure.
387
- if (targetEnv.account === undefined ||
388
- targetEnv.account === cxapi.UNKNOWN_ACCOUNT ||
389
- targetEnv.region === undefined ||
390
- targetEnv.account === cxapi.UNKNOWN_REGION) {
391
- throw new ToolkitError(`Asset publishing requires resolved account and region, got ${JSON.stringify(targetEnv)}`);
392
- }
393
- const publisher = new cdk_assets.AssetPublishing(manifest, {
394
- aws: new PublishingAws(sdk, targetEnv),
395
- progressListener: options.quiet
396
- ? undefined
397
- : new PublishingProgressListener(),
398
- throwOnError: false,
399
- publishInParallel: true,
400
- buildAssets: true,
401
- publishAssets: true,
402
- quiet: options.quiet,
403
- });
404
- await publisher.publish({ allowCrossAccount: true });
405
- if (publisher.hasFailures) {
406
- throw new ToolkitError("Failed to publish one or more assets. See the error messages above for more information.");
407
- }
408
- }