|
| 1 | +import * as path from 'path'; |
1 | 2 | import * as cxapi from '@aws-cdk/cx-api';
|
2 | 3 | import { AssetManifest } from 'cdk-assets';
|
| 4 | +import * as fs from 'fs-extra'; |
3 | 5 | import { Tag } from '../cdk-toolkit';
|
4 | 6 | import { debug, warning } from '../logging';
|
5 | 7 | import { publishAssets } from '../util/asset-publishing';
|
6 | 8 | import { Mode, SdkProvider, ISDK } from './aws-auth';
|
7 | 9 | import { deployStack, DeployStackResult, destroyStack } from './deploy-stack';
|
| 10 | +import { LazyListStackResources, ListStackResources } from './evaluate-cloudformation-template'; |
8 | 11 | import { ToolkitInfo } from './toolkit-info';
|
9 | 12 | import { CloudFormationStack, Template } from './util/cloudformation';
|
10 | 13 | import { StackActivityProgress } from './util/cloudformation/stack-activity-monitor';
|
@@ -293,25 +296,23 @@ export class CloudFormationDeployments {
|
293 | 296 | this.sdkProvider = props.sdkProvider;
|
294 | 297 | }
|
295 | 298 |
|
296 |
| - public async readCurrentTemplate(stackArtifact: cxapi.CloudFormationStackArtifact): Promise<Template> { |
297 |
| - debug(`Reading existing template for stack ${stackArtifact.displayName}.`); |
298 |
| - let stackSdk: ISDK | undefined = undefined; |
299 |
| - // try to assume the lookup role and fallback to the deploy role |
300 |
| - try { |
301 |
| - const result = await prepareSdkWithLookupRoleFor(this.sdkProvider, stackArtifact); |
302 |
| - if (result.didAssumeRole) { |
303 |
| - stackSdk = result.sdk; |
304 |
| - } |
305 |
| - } catch { } |
| 299 | + public async readCurrentTemplateWithNestedStacks(rootStackArtifact: cxapi.CloudFormationStackArtifact): Promise<Template> { |
| 300 | + const sdk = await this.prepareSdkWithLookupOrDeployRole(rootStackArtifact); |
| 301 | + const deployedTemplate = await this.readCurrentTemplate(rootStackArtifact, sdk); |
| 302 | + await this.addNestedTemplatesToGeneratedAndDeployedStacks(rootStackArtifact, sdk, { |
| 303 | + generatedTemplate: rootStackArtifact.template, |
| 304 | + deployedTemplate: deployedTemplate, |
| 305 | + deployedStackName: rootStackArtifact.stackName, |
| 306 | + }); |
| 307 | + return deployedTemplate; |
| 308 | + } |
306 | 309 |
|
307 |
| - if (!stackSdk) { |
308 |
| - stackSdk = (await this.prepareSdkFor(stackArtifact, undefined, Mode.ForReading)).stackSdk; |
| 310 | + public async readCurrentTemplate(stackArtifact: cxapi.CloudFormationStackArtifact, sdk?: ISDK): Promise<Template> { |
| 311 | + debug(`Reading existing template for stack ${stackArtifact.displayName}.`); |
| 312 | + if (!sdk) { |
| 313 | + sdk = await this.prepareSdkWithLookupOrDeployRole(stackArtifact); |
309 | 314 | }
|
310 |
| - |
311 |
| - const cfn = stackSdk.cloudFormation(); |
312 |
| - |
313 |
| - const stack = await CloudFormationStack.lookup(cfn, stackArtifact.stackName); |
314 |
| - return stack.template(); |
| 315 | + return this.readCurrentStackTemplate(stackArtifact.stackName, sdk); |
315 | 316 | }
|
316 | 317 |
|
317 | 318 | public async deployStack(options: DeployStackOptions): Promise<DeployStackResult> {
|
@@ -372,6 +373,95 @@ export class CloudFormationDeployments {
|
372 | 373 | return stack.exists;
|
373 | 374 | }
|
374 | 375 |
|
| 376 | + private async prepareSdkWithLookupOrDeployRole(stackArtifact: cxapi.CloudFormationStackArtifact): Promise<ISDK> { |
| 377 | + // try to assume the lookup role |
| 378 | + try { |
| 379 | + const result = await prepareSdkWithLookupRoleFor(this.sdkProvider, stackArtifact); |
| 380 | + if (result.didAssumeRole) { |
| 381 | + return result.sdk; |
| 382 | + } |
| 383 | + } catch { } |
| 384 | + // fall back to the deploy role |
| 385 | + return (await this.prepareSdkFor(stackArtifact, undefined, Mode.ForReading)).stackSdk; |
| 386 | + } |
| 387 | + |
| 388 | + private async readCurrentStackTemplate(stackName: string, stackSdk: ISDK) : Promise<Template> { |
| 389 | + const cfn = stackSdk.cloudFormation(); |
| 390 | + const stack = await CloudFormationStack.lookup(cfn, stackName); |
| 391 | + return stack.template(); |
| 392 | + } |
| 393 | + |
| 394 | + private async addNestedTemplatesToGeneratedAndDeployedStacks( |
| 395 | + rootStackArtifact: cxapi.CloudFormationStackArtifact, |
| 396 | + sdk: ISDK, |
| 397 | + parentTemplates: StackTemplates, |
| 398 | + ): Promise<void> { |
| 399 | + const listStackResources = parentTemplates.deployedStackName ? new LazyListStackResources(sdk, parentTemplates.deployedStackName) : undefined; |
| 400 | + for (const [nestedStackLogicalId, generatedNestedStackResource] of Object.entries(parentTemplates.generatedTemplate.Resources ?? {})) { |
| 401 | + if (!this.isCdkManagedNestedStack(generatedNestedStackResource)) { |
| 402 | + continue; |
| 403 | + } |
| 404 | + |
| 405 | + const assetPath = generatedNestedStackResource.Metadata['aws:asset:path']; |
| 406 | + const nestedStackTemplates = await this.getNestedStackTemplates(rootStackArtifact, assetPath, nestedStackLogicalId, listStackResources, sdk); |
| 407 | + |
| 408 | + generatedNestedStackResource.Properties.NestedTemplate = nestedStackTemplates.generatedTemplate; |
| 409 | + |
| 410 | + const deployedParentTemplate = parentTemplates.deployedTemplate; |
| 411 | + deployedParentTemplate.Resources = deployedParentTemplate.Resources ?? {}; |
| 412 | + const deployedNestedStackResource = deployedParentTemplate.Resources[nestedStackLogicalId] ?? {}; |
| 413 | + deployedParentTemplate.Resources[nestedStackLogicalId] = deployedNestedStackResource; |
| 414 | + deployedNestedStackResource.Type = deployedNestedStackResource.Type ?? 'AWS::CloudFormation::Stack'; |
| 415 | + deployedNestedStackResource.Properties = deployedNestedStackResource.Properties ?? {}; |
| 416 | + deployedNestedStackResource.Properties.NestedTemplate = nestedStackTemplates.deployedTemplate; |
| 417 | + |
| 418 | + await this.addNestedTemplatesToGeneratedAndDeployedStacks( |
| 419 | + rootStackArtifact, |
| 420 | + sdk, |
| 421 | + nestedStackTemplates, |
| 422 | + ); |
| 423 | + } |
| 424 | + } |
| 425 | + |
| 426 | + private async getNestedStackTemplates( |
| 427 | + rootStackArtifact: cxapi.CloudFormationStackArtifact, nestedTemplateAssetPath: string, nestedStackLogicalId: string, |
| 428 | + listStackResources: ListStackResources | undefined, sdk: ISDK, |
| 429 | + ): Promise<StackTemplates> { |
| 430 | + const nestedTemplatePath = path.join(rootStackArtifact.assembly.directory, nestedTemplateAssetPath); |
| 431 | + |
| 432 | + // CFN generates the nested stack name in the form `ParentStackName-NestedStackLogicalID-SomeHashWeCan'tCompute, |
| 433 | + // the arn is of the form: arn:aws:cloudformation:region:123456789012:stack/NestedStackName/AnotherHashWeDon'tNeed |
| 434 | + // so we get the ARN and manually extract the name. |
| 435 | + const nestedStackArn = await this.getNestedStackArn(nestedStackLogicalId, listStackResources); |
| 436 | + const deployedStackName = nestedStackArn?.slice(nestedStackArn.indexOf('/') + 1, nestedStackArn.lastIndexOf('/')); |
| 437 | + |
| 438 | + return { |
| 439 | + generatedTemplate: JSON.parse(fs.readFileSync(nestedTemplatePath, 'utf-8')), |
| 440 | + deployedTemplate: deployedStackName |
| 441 | + ? await this.readCurrentStackTemplate(deployedStackName, sdk) |
| 442 | + : {}, |
| 443 | + deployedStackName, |
| 444 | + }; |
| 445 | + } |
| 446 | + |
| 447 | + private async getNestedStackArn( |
| 448 | + nestedStackLogicalId: string, listStackResources?: ListStackResources, |
| 449 | + ): Promise<string | undefined> { |
| 450 | + try { |
| 451 | + const stackResources = await listStackResources?.listStackResources(); |
| 452 | + return stackResources?.find(sr => sr.LogicalResourceId === nestedStackLogicalId)?.PhysicalResourceId; |
| 453 | + } catch (e) { |
| 454 | + if (e.message.startsWith('Stack with id ') && e.message.endsWith(' does not exist')) { |
| 455 | + return; |
| 456 | + } |
| 457 | + throw e; |
| 458 | + } |
| 459 | + } |
| 460 | + |
| 461 | + private isCdkManagedNestedStack(stackResource: any): stackResource is NestedStackResource { |
| 462 | + return stackResource.Type === 'AWS::CloudFormation::Stack' && stackResource.Metadata && stackResource.Metadata['aws:asset:path']; |
| 463 | + } |
| 464 | + |
375 | 465 | /**
|
376 | 466 | * Get the environment necessary for touching the given stack
|
377 | 467 | *
|
@@ -453,3 +543,14 @@ export class CloudFormationDeployments {
|
453 | 543 | function isAssetManifestArtifact(art: cxapi.CloudArtifact): art is cxapi.AssetManifestArtifact {
|
454 | 544 | return art instanceof cxapi.AssetManifestArtifact;
|
455 | 545 | }
|
| 546 | + |
| 547 | +interface StackTemplates { |
| 548 | + readonly generatedTemplate: any; |
| 549 | + readonly deployedTemplate: any; |
| 550 | + readonly deployedStackName: string | undefined; |
| 551 | +} |
| 552 | + |
| 553 | +interface NestedStackResource { |
| 554 | + readonly Metadata: { 'aws:asset:path': string }; |
| 555 | + readonly Properties: any; |
| 556 | +} |
0 commit comments