Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cli): cdk diff works for Nested Stacks #18207

Merged
merged 16 commits into from
Feb 3, 2022
135 changes: 118 additions & 17 deletions packages/aws-cdk/lib/api/cloudformation-deployments.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import * as path from 'path';
import * as cxapi from '@aws-cdk/cx-api';
import { AssetManifest } from 'cdk-assets';
import * as fs from 'fs-extra';
import { Tag } from '../cdk-toolkit';
import { debug, warning } from '../logging';
import { publishAssets } from '../util/asset-publishing';
import { Mode, SdkProvider, ISDK } from './aws-auth';
import { deployStack, DeployStackResult, destroyStack } from './deploy-stack';
import { LazyListStackResources, ListStackResources } from './evaluate-cloudformation-template';
import { ToolkitInfo } from './toolkit-info';
import { CloudFormationStack, Template } from './util/cloudformation';
import { StackActivityProgress } from './util/cloudformation/stack-activity-monitor';
Expand Down Expand Up @@ -293,25 +296,23 @@ export class CloudFormationDeployments {
this.sdkProvider = props.sdkProvider;
}

public async readCurrentTemplate(stackArtifact: cxapi.CloudFormationStackArtifact): Promise<Template> {
debug(`Reading existing template for stack ${stackArtifact.displayName}.`);
let stackSdk: ISDK | undefined = undefined;
// try to assume the lookup role and fallback to the deploy role
try {
const result = await prepareSdkWithLookupRoleFor(this.sdkProvider, stackArtifact);
if (result.didAssumeRole) {
stackSdk = result.sdk;
}
} catch { }
public async readCurrentTemplateWithNestedStacks(rootStackArtifact: cxapi.CloudFormationStackArtifact): Promise<Template> {
const sdk = await this.prepareSdkWithLookupOrDeployRole(rootStackArtifact);
const deployedTemplate = await this.readCurrentTemplate(rootStackArtifact, sdk);
await this.addNestedTemplatesToGeneratedAndDeployedStacks(rootStackArtifact, sdk, {
generatedTemplate: rootStackArtifact.template,
deployedTemplate: deployedTemplate,
deployedStackName: rootStackArtifact.stackName,
});
return deployedTemplate;
}

if (!stackSdk) {
stackSdk = (await this.prepareSdkFor(stackArtifact, undefined, Mode.ForReading)).stackSdk;
public async readCurrentTemplate(stackArtifact: cxapi.CloudFormationStackArtifact, sdk?: ISDK): Promise<Template> {
debug(`Reading existing template for stack ${stackArtifact.displayName}.`);
if (!sdk) {
sdk = await this.prepareSdkWithLookupOrDeployRole(stackArtifact);
}

const cfn = stackSdk.cloudFormation();

const stack = await CloudFormationStack.lookup(cfn, stackArtifact.stackName);
return stack.template();
return this.readCurrentStackTemplate(stackArtifact.stackName, sdk);
}

public async deployStack(options: DeployStackOptions): Promise<DeployStackResult> {
Expand Down Expand Up @@ -372,6 +373,95 @@ export class CloudFormationDeployments {
return stack.exists;
}

private async prepareSdkWithLookupOrDeployRole(stackArtifact: cxapi.CloudFormationStackArtifact): Promise<ISDK> {
// try to assume the lookup role
try {
const result = await prepareSdkWithLookupRoleFor(this.sdkProvider, stackArtifact);
if (result.didAssumeRole) {
return result.sdk;
}
} catch { }
// fall back to the deploy role
return (await this.prepareSdkFor(stackArtifact, undefined, Mode.ForReading)).stackSdk;
}

private async readCurrentStackTemplate(stackName: string, stackSdk: ISDK) : Promise<Template> {
const cfn = stackSdk.cloudFormation();
const stack = await CloudFormationStack.lookup(cfn, stackName);
return stack.template();
}

private async addNestedTemplatesToGeneratedAndDeployedStacks(
rootStackArtifact: cxapi.CloudFormationStackArtifact,
sdk: ISDK,
parentTemplates: StackTemplates,
): Promise<void> {
const listStackResources = parentTemplates.deployedStackName ? new LazyListStackResources(sdk, parentTemplates.deployedStackName) : undefined;
for (const [nestedStackLogicalId, generatedNestedStackResource] of Object.entries(parentTemplates.generatedTemplate.Resources ?? {})) {
if (!this.isCdkManagedNestedStack(generatedNestedStackResource)) {
continue;
}

const assetPath = generatedNestedStackResource.Metadata['aws:asset:path'];
const nestedStackTemplates = await this.getNestedStackTemplates(rootStackArtifact, assetPath, nestedStackLogicalId, listStackResources, sdk);

generatedNestedStackResource.Properties.NestedTemplate = nestedStackTemplates.generatedTemplate;

const deployedParentTemplate = parentTemplates.deployedTemplate;
deployedParentTemplate.Resources = deployedParentTemplate.Resources ?? {};
const deployedNestedStackResource = deployedParentTemplate.Resources[nestedStackLogicalId] ?? {};
deployedParentTemplate.Resources[nestedStackLogicalId] = deployedNestedStackResource;
deployedNestedStackResource.Type = deployedNestedStackResource.Type ?? 'AWS::CloudFormation::Stack';
deployedNestedStackResource.Properties = deployedNestedStackResource.Properties ?? {};
deployedNestedStackResource.Properties.NestedTemplate = nestedStackTemplates.deployedTemplate;

await this.addNestedTemplatesToGeneratedAndDeployedStacks(
rootStackArtifact,
sdk,
nestedStackTemplates,
);
}
}

private async getNestedStackTemplates(
rootStackArtifact: cxapi.CloudFormationStackArtifact, nestedTemplateAssetPath: string, nestedStackLogicalId: string,
listStackResources: ListStackResources | undefined, sdk: ISDK,
): Promise<StackTemplates> {
const nestedTemplatePath = path.join(rootStackArtifact.assembly.directory, nestedTemplateAssetPath);

// CFN generates the nested stack name in the form `ParentStackName-NestedStackLogicalID-SomeHashWeCan'tCompute,
// the arn is of the form: arn:aws:cloudformation:region:123456789012:stack/NestedStackName/AnotherHashWeDon'tNeed
// so we get the ARN and manually extract the name.
const nestedStackArn = await this.getNestedStackArn(nestedStackLogicalId, listStackResources);
const deployedStackName = nestedStackArn?.slice(nestedStackArn.indexOf('/') + 1, nestedStackArn.lastIndexOf('/'));

return {
generatedTemplate: JSON.parse(fs.readFileSync(nestedTemplatePath, 'utf-8')),
deployedTemplate: deployedStackName
? await this.readCurrentStackTemplate(deployedStackName, sdk)
: {},
deployedStackName,
};
}

private async getNestedStackArn(
nestedStackLogicalId: string, listStackResources?: ListStackResources,
): Promise<string | undefined> {
try {
const stackResources = await listStackResources?.listStackResources();
return stackResources?.find(sr => sr.LogicalResourceId === nestedStackLogicalId)?.PhysicalResourceId;
} catch (e) {
if (e.message.startsWith('Stack with id ') && e.message.endsWith(' does not exist')) {
return;
}
throw e;
}
}

private isCdkManagedNestedStack(stackResource: any): stackResource is NestedStackResource {
return stackResource.Type === 'AWS::CloudFormation::Stack' && stackResource.Metadata && stackResource.Metadata['aws:asset:path'];
comcalvi marked this conversation as resolved.
Show resolved Hide resolved
}
comcalvi marked this conversation as resolved.
Show resolved Hide resolved

/**
* Get the environment necessary for touching the given stack
*
Expand Down Expand Up @@ -453,3 +543,14 @@ export class CloudFormationDeployments {
function isAssetManifestArtifact(art: cxapi.CloudArtifact): art is cxapi.AssetManifestArtifact {
return art instanceof cxapi.AssetManifestArtifact;
}

interface StackTemplates {
readonly generatedTemplate: any;
readonly deployedTemplate: any;
readonly deployedStackName: string | undefined;
}

interface NestedStackResource {
readonly Metadata: { 'aws:asset:path': string };
readonly Properties: any;
}
1 change: 0 additions & 1 deletion packages/aws-cdk/lib/api/hotswap-deployments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,4 +234,3 @@ async function applyHotswappableChange(sdk: ISDK, hotswapOperation: HotswapOpera
sdk.removeCustomUserAgent(customUserAgent);
}
}

comcalvi marked this conversation as resolved.
Show resolved Hide resolved
4 changes: 2 additions & 2 deletions packages/aws-cdk/lib/cdk-toolkit.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import * as path from 'path';
import { format } from 'util';
import * as cxapi from '@aws-cdk/cx-api';
import * as chokidar from 'chokidar';
import * as chalk from 'chalk';
import * as chokidar from 'chokidar';
import * as fs from 'fs-extra';
import * as promptly from 'promptly';
import { environmentsFromDescriptors, globEnvironmentsFromStacks, looksLikeGlob } from '../lib/api/cxapp/environments';
Expand Down Expand Up @@ -104,7 +104,7 @@ export class CdkToolkit {
// Compare N stacks against deployed templates
for (const stack of stacks.stackArtifacts) {
stream.write(format('Stack %s\n', chalk.bold(stack.displayName)));
const currentTemplate = await this.props.cloudFormation.readCurrentTemplate(stack);
const currentTemplate = await this.props.cloudFormation.readCurrentTemplateWithNestedStacks(stack);
diffs += options.securityOnly
? numberFromBool(printSecurityDiff(currentTemplate, stack, RequireApproval.Broadening))
: printStackDiff(currentTemplate, stack, strict, contextLines, stream);
Expand Down
Loading