aws-cdk
Version:
CDK Toolkit, the command line tool for CDK apps
1,079 lines (1,051 loc) • 117 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const fs = require("fs");
const os = require("os");
const path = require("path");
const stream_1 = require("stream");
const string_decoder_1 = require("string_decoder");
const cxschema = require("@aws-cdk/cloud-assembly-schema");
const util_1 = require("./util");
const deployments_1 = require("../lib/api/deployments");
const cfn = require("../lib/api/deployments/cloudformation");
const cdk_toolkit_1 = require("../lib/cli/cdk-toolkit");
let cloudExecutable;
let cloudFormation;
let toolkit;
let oldDir;
let tmpDir;
beforeAll(() => {
// The toolkit writes and checks for temporary files in the current directory,
// so run these tests in a tempdir so they don't interfere with each other
// and other tests.
oldDir = process.cwd();
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'aws-cdk-test'));
process.chdir(tmpDir);
});
afterAll(() => {
process.chdir(oldDir);
fs.rmSync(tmpDir, { recursive: true, force: true });
});
afterEach(() => {
jest.restoreAllMocks();
});
describe('fixed template', () => {
const templatePath = 'oldTemplate.json';
beforeEach(() => {
const oldTemplate = {
Resources: {
SomeResource: {
Type: 'AWS::SomeService::SomeResource',
Properties: {
Something: 'old-value',
},
},
},
};
cloudExecutable = new util_1.MockCloudExecutable({
stacks: [
{
stackName: 'A',
template: {
Resources: {
SomeResource: {
Type: 'AWS::SomeService::SomeResource',
Properties: {
Something: 'new-value',
},
},
},
},
},
],
});
toolkit = new cdk_toolkit_1.CdkToolkit({
cloudExecutable,
deployments: cloudFormation,
configuration: cloudExecutable.configuration,
sdkProvider: cloudExecutable.sdkProvider,
});
fs.writeFileSync(templatePath, JSON.stringify(oldTemplate));
});
afterEach(() => fs.rmSync(templatePath));
test('fixed template with valid templates', async () => {
// GIVEN
const buffer = new StringWritable();
// WHEN
const exitCode = await toolkit.diff({
stackNames: ['A'],
stream: buffer,
changeSet: undefined,
templatePath,
});
// THEN
const plainTextOutput = buffer.data.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, '');
expect(exitCode).toBe(0);
expect(plainTextOutput.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, '')).toContain(`Resources
[~] AWS::SomeService::SomeResource SomeResource
└─ [~] Something
├─ [-] old-value
└─ [+] new-value
✨ Number of stacks with differences: 1
`);
});
});
describe('imports', () => {
let createDiffChangeSet;
beforeEach(() => {
const outputToJson = {
'//': 'This file is generated by cdk migrate. It will be automatically deleted after the first successful deployment of this app to the environment of the original resources.',
'Source': 'localfile',
'Resources': [],
};
fs.writeFileSync('migrate.json', JSON.stringify(outputToJson, null, 2));
createDiffChangeSet = jest.spyOn(cfn, 'createDiffChangeSet').mockImplementationOnce(async () => {
return {
$metadata: {},
Changes: [
{
ResourceChange: {
Action: 'Import',
LogicalResourceId: 'Queue',
},
},
{
ResourceChange: {
Action: 'Import',
LogicalResourceId: 'Bucket',
},
},
{
ResourceChange: {
Action: 'Import',
LogicalResourceId: 'Queue2',
},
},
],
};
});
cloudExecutable = new util_1.MockCloudExecutable({
stacks: [
{
stackName: 'A',
template: {
Resources: {
Queue: {
Type: 'AWS::SQS::Queue',
},
Queue2: {
Type: 'AWS::SQS::Queue',
},
Bucket: {
Type: 'AWS::S3::Bucket',
},
},
},
},
],
});
cloudFormation = (0, util_1.instanceMockFrom)(deployments_1.Deployments);
toolkit = new cdk_toolkit_1.CdkToolkit({
cloudExecutable,
deployments: cloudFormation,
configuration: cloudExecutable.configuration,
sdkProvider: cloudExecutable.sdkProvider,
});
// Default implementations
cloudFormation.readCurrentTemplateWithNestedStacks.mockImplementation((_stackArtifact) => {
return Promise.resolve({
deployedRootTemplate: {},
nestedStacks: {},
});
});
cloudFormation.deployStack.mockImplementation((options) => Promise.resolve({
type: 'did-deploy-stack',
noOp: true,
outputs: {},
stackArn: '',
stackArtifact: options.stack,
}));
});
afterEach(() => {
fs.rmSync('migrate.json');
});
test('imports render correctly for a nonexistant stack without creating a changeset', async () => {
// GIVEN
const buffer = new StringWritable();
// WHEN
const exitCode = await toolkit.diff({
stackNames: ['A'],
stream: buffer,
changeSet: true,
});
// THEN
const plainTextOutput = buffer.data.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, '');
expect(createDiffChangeSet).not.toHaveBeenCalled();
expect(plainTextOutput).toContain(`Stack A
Parameters and rules created during migration do not affect resource configuration.
Resources
[←] AWS::SQS::Queue Queue import
[←] AWS::SQS::Queue Queue2 import
[←] AWS::S3::Bucket Bucket import
`);
expect(buffer.data.trim()).toContain('✨ Number of stacks with differences: 1');
expect(exitCode).toBe(0);
});
test('imports render correctly for an existing stack and diff creates a changeset', async () => {
// GIVEN
const buffer = new StringWritable();
cloudFormation.stackExists = jest.fn().mockReturnValue(Promise.resolve(true));
// WHEN
const exitCode = await toolkit.diff({
stackNames: ['A'],
stream: buffer,
changeSet: true,
});
// THEN
const plainTextOutput = buffer.data.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, '');
expect(createDiffChangeSet).toHaveBeenCalled();
expect(plainTextOutput).toContain(`Stack A
Parameters and rules created during migration do not affect resource configuration.
Resources
[←] AWS::SQS::Queue Queue import
[←] AWS::SQS::Queue Queue2 import
[←] AWS::S3::Bucket Bucket import
`);
expect(buffer.data.trim()).toContain('✨ Number of stacks with differences: 1');
expect(exitCode).toBe(0);
});
});
describe('non-nested stacks', () => {
beforeEach(() => {
cloudExecutable = new util_1.MockCloudExecutable({
stacks: [
{
stackName: 'A',
template: { resource: 'A' },
},
{
stackName: 'B',
depends: ['A'],
template: { resource: 'B' },
},
{
stackName: 'C',
depends: ['A'],
template: { resource: 'C' },
metadata: {
'/resource': [
{
type: cxschema.ArtifactMetadataEntryType.ERROR,
data: 'this is an error',
},
],
},
},
{
stackName: 'D',
template: { resource: 'D' },
},
],
});
cloudFormation = (0, util_1.instanceMockFrom)(deployments_1.Deployments);
toolkit = new cdk_toolkit_1.CdkToolkit({
cloudExecutable,
deployments: cloudFormation,
configuration: cloudExecutable.configuration,
sdkProvider: cloudExecutable.sdkProvider,
});
// Default implementations
cloudFormation.readCurrentTemplateWithNestedStacks.mockImplementation((stackArtifact) => {
if (stackArtifact.stackName === 'D') {
return Promise.resolve({
deployedRootTemplate: { resource: 'D' },
nestedStacks: {},
});
}
return Promise.resolve({
deployedRootTemplate: {},
nestedStacks: {},
});
});
cloudFormation.deployStack.mockImplementation((options) => Promise.resolve({
type: 'did-deploy-stack',
noOp: true,
outputs: {},
stackArn: '',
stackArtifact: options.stack,
}));
});
test('diff can diff multiple stacks', async () => {
// GIVEN
const buffer = new StringWritable();
// WHEN
const exitCode = await toolkit.diff({
stackNames: ['B'],
stream: buffer,
});
// THEN
const plainTextOutput = buffer.data.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, '');
expect(plainTextOutput).toContain('Stack A');
expect(plainTextOutput).toContain('Stack B');
expect(buffer.data.trim()).toContain('✨ Number of stacks with differences: 2');
expect(exitCode).toBe(0);
});
test('diff number of stack diffs, not resource diffs', async () => {
// GIVEN
cloudExecutable = new util_1.MockCloudExecutable({
stacks: [
{
stackName: 'A',
template: { resourceA: 'A', resourceB: 'B' },
},
{
stackName: 'B',
template: { resourceC: 'C' },
},
],
});
toolkit = new cdk_toolkit_1.CdkToolkit({
cloudExecutable,
deployments: cloudFormation,
configuration: cloudExecutable.configuration,
sdkProvider: cloudExecutable.sdkProvider,
});
const buffer = new StringWritable();
// WHEN
const exitCode = await toolkit.diff({
stackNames: ['A', 'B'],
stream: buffer,
});
// THEN
const plainTextOutput = buffer.data.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, '');
expect(plainTextOutput).toContain('Stack A');
expect(plainTextOutput).toContain('Stack B');
expect(buffer.data.trim()).toContain('✨ Number of stacks with differences: 2');
expect(exitCode).toBe(0);
});
test('exits with 1 with diffs and fail set to true', async () => {
// GIVEN
const buffer = new StringWritable();
// WHEN
const exitCode = await toolkit.diff({
stackNames: ['A'],
stream: buffer,
fail: true,
});
// THEN
expect(buffer.data.trim()).toContain('✨ Number of stacks with differences: 1');
expect(exitCode).toBe(1);
});
test('throws an error if no valid stack names given', async () => {
const buffer = new StringWritable();
// WHEN
await expect(() => toolkit.diff({
stackNames: ['X', 'Y', 'Z'],
stream: buffer,
})).rejects.toThrow('No stacks match the name(s) X,Y,Z');
});
test('exits with 1 with diff in first stack, but not in second stack and fail set to true', async () => {
// GIVEN
const buffer = new StringWritable();
// WHEN
const exitCode = await toolkit.diff({
stackNames: ['A', 'D'],
stream: buffer,
fail: true,
});
// THEN
expect(buffer.data.trim()).toContain('✨ Number of stacks with differences: 1');
expect(exitCode).toBe(1);
});
test('throws an error during diffs on stack with error metadata', async () => {
const buffer = new StringWritable();
// WHEN
await expect(() => toolkit.diff({
stackNames: ['C'],
stream: buffer,
})).rejects.toThrow(/Found errors/);
});
test('when quiet mode is enabled, stacks with no diffs should not print stack name & no differences to stdout', async () => {
// GIVEN
const buffer = new StringWritable();
// WHEN
const exitCode = await toolkit.diff({
stackNames: ['D'],
stream: buffer,
fail: false,
quiet: true,
});
// THEN
const plainTextOutput = buffer.data.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, '');
expect(plainTextOutput).not.toContain('Stack D');
expect(plainTextOutput).not.toContain('There were no differences');
expect(buffer.data.trim()).toContain('✨ Number of stacks with differences: 0');
expect(exitCode).toBe(0);
});
test('when quiet mode is enabled, stacks with diffs should print stack name to stdout', async () => {
// GIVEN
const buffer = new StringWritable();
// WHEN
const exitCode = await toolkit.diff({
stackNames: ['A'],
stream: buffer,
fail: false,
quiet: true,
});
// THEN
const plainTextOutput = buffer.data.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, '');
expect(plainTextOutput).toContain('Stack A');
expect(plainTextOutput).not.toContain('There were no differences');
expect(buffer.data.trim()).toContain('✨ Number of stacks with differences: 1');
expect(exitCode).toBe(0);
});
});
describe('stack exists checks', () => {
beforeEach(() => {
jest.resetAllMocks();
cloudExecutable = new util_1.MockCloudExecutable({
stacks: [
{
stackName: 'A',
template: { resource: 'A' },
},
{
stackName: 'B',
depends: ['A'],
template: { resource: 'B' },
},
{
stackName: 'C',
depends: ['A'],
template: { resource: 'C' },
metadata: {
'/resource': [
{
type: cxschema.ArtifactMetadataEntryType.ERROR,
data: 'this is an error',
},
],
},
},
{
stackName: 'D',
template: { resource: 'D' },
},
],
});
cloudFormation = (0, util_1.instanceMockFrom)(deployments_1.Deployments);
toolkit = new cdk_toolkit_1.CdkToolkit({
cloudExecutable,
deployments: cloudFormation,
configuration: cloudExecutable.configuration,
sdkProvider: cloudExecutable.sdkProvider,
});
// Default implementations
cloudFormation.readCurrentTemplateWithNestedStacks.mockImplementation((stackArtifact) => {
if (stackArtifact.stackName === 'D') {
return Promise.resolve({
deployedRootTemplate: { resource: 'D' },
nestedStacks: {},
});
}
return Promise.resolve({
deployedRootTemplate: {},
nestedStacks: {},
});
});
cloudFormation.deployStack.mockImplementation((options) => Promise.resolve({
type: 'did-deploy-stack',
noOp: true,
outputs: {},
stackArn: '',
stackArtifact: options.stack,
}));
});
test('diff does not check for stack existence when --no-change-set is passed', async () => {
// GIVEN
const buffer = new StringWritable();
// WHEN
const exitCode = await toolkit.diff({
stackNames: ['A', 'A'],
stream: buffer,
fail: false,
quiet: true,
changeSet: false,
});
// THEN
expect(exitCode).toBe(0);
expect(cloudFormation.stackExists).not.toHaveBeenCalled();
});
test('diff falls back to classic diff when stack does not exist', async () => {
// GIVEN
const buffer = new StringWritable();
const stackExists = jest.spyOn(cloudFormation, 'stackExists').mockReturnValue(Promise.resolve(false));
const createDiffChangeSet = jest.spyOn(cfn, 'createDiffChangeSet');
// WHEN
const exitCode = await toolkit.diff({
stackNames: ['A', 'A'],
stream: buffer,
fail: false,
quiet: true,
changeSet: true,
});
// THEN
expect(exitCode).toBe(0);
expect(stackExists).toHaveBeenCalled();
expect(createDiffChangeSet).not.toHaveBeenCalled();
});
test('diff falls back to classic diff when stackExists call fails', async () => {
// GIVEN
const buffer = new StringWritable();
const stackExists = jest.spyOn(cloudFormation, 'stackExists');
const createDiffChangeSet = jest.spyOn(cfn, 'createDiffChangeSet');
stackExists.mockImplementation(() => {
throw new Error('Fail fail fail');
});
// WHEN
const exitCode = await toolkit.diff({
stackNames: ['A', 'A'],
stream: buffer,
fail: false,
quiet: true,
changeSet: true,
});
// THEN
expect(exitCode).toBe(0);
expect(stackExists).toHaveBeenCalled();
expect(createDiffChangeSet).not.toHaveBeenCalled();
});
});
describe('nested stacks', () => {
beforeEach(() => {
cloudExecutable = new util_1.MockCloudExecutable({
stacks: [
{
stackName: 'Parent',
template: {},
},
{
stackName: 'UnchangedParent',
template: {},
},
],
});
cloudFormation = (0, util_1.instanceMockFrom)(deployments_1.Deployments);
toolkit = new cdk_toolkit_1.CdkToolkit({
cloudExecutable,
deployments: cloudFormation,
configuration: cloudExecutable.configuration,
sdkProvider: cloudExecutable.sdkProvider,
});
cloudFormation.readCurrentTemplateWithNestedStacks.mockImplementation((stackArtifact) => {
if (stackArtifact.stackName === 'Parent') {
stackArtifact.template.Resources = {
AdditionChild: {
Type: 'AWS::CloudFormation::Stack',
Properties: {
TemplateURL: 'addition-child-url-old',
},
},
DeletionChild: {
Type: 'AWS::CloudFormation::Stack',
Properties: {
TemplateURL: 'deletion-child-url-old',
},
},
ChangedChild: {
Type: 'AWS::CloudFormation::Stack',
Properties: {
TemplateURL: 'changed-child-url-old',
},
},
UnchangedChild: {
Type: 'AWS::CloudFormation::Stack',
Properties: {
TemplateURL: 'changed-child-url-constant',
},
},
};
return Promise.resolve({
deployedRootTemplate: {
Resources: {
AdditionChild: {
Type: 'AWS::CloudFormation::Stack',
Properties: {
TemplateURL: 'addition-child-url-new',
},
},
DeletionChild: {
Type: 'AWS::CloudFormation::Stack',
Properties: {
TemplateURL: 'deletion-child-url-new',
},
},
ChangedChild: {
Type: 'AWS::CloudFormation::Stack',
Properties: {
TemplateURL: 'changed-child-url-new',
},
},
UnchangedChild: {
Type: 'AWS::CloudFormation::Stack',
Properties: {
TemplateURL: 'changed-child-url-constant',
},
},
},
},
nestedStacks: {
AdditionChild: {
deployedTemplate: {
Resources: {
SomeResource: {
Type: 'AWS::Something',
},
},
},
generatedTemplate: {
Resources: {
SomeResource: {
Type: 'AWS::Something',
Properties: {
Prop: 'added-value',
},
},
},
},
nestedStackTemplates: {},
physicalName: 'AdditionChild',
},
DeletionChild: {
deployedTemplate: {
Resources: {
SomeResource: {
Type: 'AWS::Something',
Properties: {
Prop: 'value-to-be-removed',
},
},
},
},
generatedTemplate: {
Resources: {
SomeResource: {
Type: 'AWS::Something',
},
},
},
nestedStackTemplates: {},
physicalName: 'DeletionChild',
},
ChangedChild: {
deployedTemplate: {
Resources: {
SomeResource: {
Type: 'AWS::Something',
Properties: {
Prop: 'old-value',
},
},
},
},
generatedTemplate: {
Resources: {
SomeResource: {
Type: 'AWS::Something',
Properties: {
Prop: 'new-value',
},
},
},
},
nestedStackTemplates: {},
physicalName: 'ChangedChild',
},
newChild: {
deployedTemplate: {},
generatedTemplate: {
Resources: {
SomeResource: {
Type: 'AWS::Something',
Properties: {
Prop: 'new-value',
},
},
},
},
nestedStackTemplates: {
newGrandChild: {
deployedTemplate: {},
generatedTemplate: {
Resources: {
SomeResource: {
Type: 'AWS::Something',
Properties: {
Prop: 'new-value',
},
},
},
},
physicalName: undefined,
nestedStackTemplates: {},
},
},
physicalName: undefined,
},
UnChangedChild: {
deployedTemplate: {
Resources: {
SomeResource: {
Type: 'AWS::Something',
Properties: {
Prop: 'unchanged',
},
},
},
},
generatedTemplate: {
Resources: {
SomeResource: {
Type: 'AWS::Something',
Properties: {
Prop: 'unchanged',
},
},
},
},
nestedStackTemplates: {},
physicalName: 'UnChangedChild',
},
},
});
}
if (stackArtifact.stackName === 'UnchangedParent') {
stackArtifact.template.Resources = {
UnchangedChild: {
Type: 'AWS::CloudFormation::Stack',
Properties: {
TemplateURL: 'child-url',
},
},
};
return Promise.resolve({
deployedRootTemplate: {
Resources: {
UnchangedChild: {
Type: 'AWS::CloudFormation::Stack',
Properties: {
TemplateURL: 'child-url',
},
},
},
},
nestedStacks: {
UnchangedChild: {
deployedTemplate: {
Resources: {
SomeResource: {
Type: 'AWS::Something',
Properties: {
Prop: 'unchanged',
},
},
},
},
generatedTemplate: {
Resources: {
SomeResource: {
Type: 'AWS::Something',
Properties: {
Prop: 'unchanged',
},
},
},
},
nestedStackTemplates: {},
physicalName: 'UnchangedChild',
},
},
});
}
return Promise.resolve({
deployedRootTemplate: {},
nestedStacks: {},
});
});
});
test('diff can diff nested stacks and display the nested stack logical ID if has not been deployed or otherwise has no physical name', async () => {
// GIVEN
const buffer = new StringWritable();
// WHEN
const exitCode = await toolkit.diff({
stackNames: ['Parent'],
stream: buffer,
changeSet: false,
});
// THEN
const plainTextOutput = buffer.data.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, '').replace(/[ \t]+$/gm, '');
expect(plainTextOutput.trim()).toEqual(`Stack Parent
Resources
[~] AWS::CloudFormation::Stack AdditionChild
└─ [~] TemplateURL
├─ [-] addition-child-url-new
└─ [+] addition-child-url-old
[~] AWS::CloudFormation::Stack DeletionChild
└─ [~] TemplateURL
├─ [-] deletion-child-url-new
└─ [+] deletion-child-url-old
[~] AWS::CloudFormation::Stack ChangedChild
└─ [~] TemplateURL
├─ [-] changed-child-url-new
└─ [+] changed-child-url-old
Stack AdditionChild
Resources
[~] AWS::Something SomeResource
└─ [+] Prop
└─ added-value
Stack DeletionChild
Resources
[~] AWS::Something SomeResource
└─ [-] Prop
└─ value-to-be-removed
Stack ChangedChild
Resources
[~] AWS::Something SomeResource
└─ [~] Prop
├─ [-] old-value
└─ [+] new-value
Stack newChild
Resources
[+] AWS::Something SomeResource
Stack newGrandChild
Resources
[+] AWS::Something SomeResource
Stack UnChangedChild
✨ Number of stacks with differences: 6`);
expect(exitCode).toBe(0);
});
test('diff falls back to non-changeset diff for nested stacks', async () => {
// GIVEN
const changeSetSpy = jest.spyOn(cfn, 'waitForChangeSet');
const buffer = new StringWritable();
// WHEN
const exitCode = await toolkit.diff({
stackNames: ['Parent'],
stream: buffer,
changeSet: true,
});
// THEN
const plainTextOutput = buffer.data.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, '').replace(/[ \t]+$/gm, '');
expect(plainTextOutput.trim()).toEqual(`Stack Parent
Resources
[~] AWS::CloudFormation::Stack AdditionChild
└─ [~] TemplateURL
├─ [-] addition-child-url-new
└─ [+] addition-child-url-old
[~] AWS::CloudFormation::Stack DeletionChild
└─ [~] TemplateURL
├─ [-] deletion-child-url-new
└─ [+] deletion-child-url-old
[~] AWS::CloudFormation::Stack ChangedChild
└─ [~] TemplateURL
├─ [-] changed-child-url-new
└─ [+] changed-child-url-old
Stack AdditionChild
Resources
[~] AWS::Something SomeResource
└─ [+] Prop
└─ added-value
Stack DeletionChild
Resources
[~] AWS::Something SomeResource
└─ [-] Prop
└─ value-to-be-removed
Stack ChangedChild
Resources
[~] AWS::Something SomeResource
└─ [~] Prop
├─ [-] old-value
└─ [+] new-value
Stack newChild
Resources
[+] AWS::Something SomeResource
Stack newGrandChild
Resources
[+] AWS::Something SomeResource
Stack UnChangedChild
✨ Number of stacks with differences: 6`);
expect(exitCode).toBe(0);
expect(changeSetSpy).not.toHaveBeenCalled();
});
test('when quiet mode is enabled, nested stacks with no diffs should not print stack name & no differences to stdout', async () => {
// GIVEN
const buffer = new StringWritable();
// WHEN
const exitCode = await toolkit.diff({
stackNames: ['UnchangedParent'],
stream: buffer,
fail: false,
quiet: true,
});
// THEN
const plainTextOutput = buffer.data.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, '').replace(/[ \t]+$/gm, '');
expect(plainTextOutput).not.toContain('Stack UnchangedParent');
expect(plainTextOutput).not.toContain('There were no differences');
expect(buffer.data.trim()).toContain('✨ Number of stacks with differences: 0');
expect(exitCode).toBe(0);
});
test('when quiet mode is enabled, nested stacks with diffs should print stack name to stdout', async () => {
// GIVEN
const buffer = new StringWritable();
// WHEN
const exitCode = await toolkit.diff({
stackNames: ['Parent'],
stream: buffer,
fail: false,
quiet: true,
});
// THEN
const plainTextOutput = buffer.data.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, '').replace(/[ \t]+$/gm, '');
expect(plainTextOutput).toContain(`Stack Parent
Resources
[~] AWS::CloudFormation::Stack AdditionChild
└─ [~] TemplateURL
├─ [-] addition-child-url-new
└─ [+] addition-child-url-old
[~] AWS::CloudFormation::Stack DeletionChild
└─ [~] TemplateURL
├─ [-] deletion-child-url-new
└─ [+] deletion-child-url-old
[~] AWS::CloudFormation::Stack ChangedChild
└─ [~] TemplateURL
├─ [-] changed-child-url-new
└─ [+] changed-child-url-old
Stack AdditionChild
Resources
[~] AWS::Something SomeResource
└─ [+] Prop
└─ added-value
Stack DeletionChild
Resources
[~] AWS::Something SomeResource
└─ [-] Prop
└─ value-to-be-removed
Stack ChangedChild
Resources
[~] AWS::Something SomeResource
└─ [~] Prop
├─ [-] old-value
└─ [+] new-value
Stack newChild
Resources
[+] AWS::Something SomeResource
Stack newGrandChild
Resources
[+] AWS::Something SomeResource
✨ Number of stacks with differences: 6`);
expect(plainTextOutput).not.toContain('Stack UnChangedChild');
expect(exitCode).toBe(0);
});
});
describe('--strict', () => {
const templatePath = 'oldTemplate.json';
beforeEach(() => {
const oldTemplate = {};
cloudFormation = (0, util_1.instanceMockFrom)(deployments_1.Deployments);
cloudFormation.readCurrentTemplateWithNestedStacks.mockImplementation((_stackArtifact) => {
return Promise.resolve({
deployedRootTemplate: {},
nestedStacks: {},
});
});
cloudExecutable = new util_1.MockCloudExecutable({
stacks: [
{
stackName: 'A',
template: {
Resources: {
MetadataResource: {
Type: 'AWS::CDK::Metadata',
Properties: {
newMeta: 'newData',
},
},
SomeOtherResource: {
Type: 'AWS::Something::Amazing',
},
},
Rules: {
CheckBootstrapVersion: {
newCheck: 'newBootstrapVersion',
},
},
},
},
],
});
toolkit = new cdk_toolkit_1.CdkToolkit({
cloudExecutable,
deployments: cloudFormation,
configuration: cloudExecutable.configuration,
sdkProvider: cloudExecutable.sdkProvider,
});
fs.writeFileSync(templatePath, JSON.stringify(oldTemplate));
});
afterEach(() => fs.rmSync(templatePath));
test('--strict does not obscure CDK::Metadata or CheckBootstrapVersion', async () => {
// GIVEN
const buffer = new StringWritable();
// WHEN
const exitCode = await toolkit.diff({
stackNames: ['A'],
stream: buffer,
strict: true,
});
// THEN
const plainTextOutput = buffer.data.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, '');
expect(plainTextOutput.trim()).toEqual(`Stack A
Resources
[+] AWS::CDK::Metadata MetadataResource
[+] AWS::Something::Amazing SomeOtherResource
Other Changes
[+] Unknown Rules: {\"CheckBootstrapVersion\":{\"newCheck\":\"newBootstrapVersion\"}}
✨ Number of stacks with differences: 1`);
expect(exitCode).toBe(0);
});
test('--no-strict obscures CDK::Metadata and CheckBootstrapVersion', async () => {
// GIVEN
const buffer = new StringWritable();
// WHEN
const exitCode = await toolkit.diff({
stackNames: ['A'],
stream: buffer,
});
// THEN
const plainTextOutput = buffer.data.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, '');
expect(plainTextOutput.trim()).toEqual(`Stack A
Resources
[+] AWS::Something::Amazing SomeOtherResource
✨ Number of stacks with differences: 1`);
expect(exitCode).toBe(0);
});
});
class StringWritable extends stream_1.Writable {
constructor(options = {}) {
super(options);
this._decoder = new string_decoder_1.StringDecoder(options && options.defaultEncoding);
this.data = '';
}
_write(chunk, encoding, callback) {
if (encoding === 'buffer') {
chunk = this._decoder.write(chunk);
}
this.data += chunk;
callback();
}
_final(callback) {
this.data += this._decoder.end();
callback();
}
}
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZGlmZi50ZXN0LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiZGlmZi50ZXN0LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7O0FBQUEseUJBQXlCO0FBQ3pCLHlCQUF5QjtBQUN6Qiw2QkFBNkI7QUFDN0IsbUNBQWtDO0FBQ2xDLG1EQUErQztBQUMvQywyREFBMkQ7QUFHM0QsaUNBQStEO0FBQy9ELHdEQUF5RztBQUN6Ryw2REFBNkQ7QUFDN0Qsd0RBQW9EO0FBRXBELElBQUksZUFBb0MsQ0FBQztBQUN6QyxJQUFJLGNBQXdDLENBQUM7QUFDN0MsSUFBSSxPQUFtQixDQUFDO0FBQ3hCLElBQUksTUFBYyxDQUFDO0FBQ25CLElBQUksTUFBYyxDQUFDO0FBRW5CLFNBQVMsQ0FBQyxHQUFHLEVBQUU7SUFDYiw4RUFBOEU7SUFDOUUsMEVBQTBFO0lBQzFFLG1CQUFtQjtJQUNuQixNQUFNLEdBQUcsT0FBTyxDQUFDLEdBQUcsRUFBRSxDQUFDO0lBQ3ZCLE1BQU0sR0FBRyxFQUFFLENBQUMsV0FBVyxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsRUFBRSxDQUFDLE1BQU0sRUFBRSxFQUFFLGNBQWMsQ0FBQyxDQUFDLENBQUM7SUFDaEUsT0FBTyxDQUFDLEtBQUssQ0FBQyxNQUFNLENBQUMsQ0FBQztBQUN4QixDQUFDLENBQUMsQ0FBQztBQUVILFFBQVEsQ0FBQyxHQUFHLEVBQUU7SUFDWixPQUFPLENBQUMsS0FBSyxDQUFDLE1BQU0sQ0FBQyxDQUFDO0lBQ3RCLEVBQUUsQ0FBQyxNQUFNLENBQUMsTUFBTSxFQUFFLEVBQUUsU0FBUyxFQUFFLElBQUksRUFBRSxLQUFLLEVBQUUsSUFBSSxFQUFFLENBQUMsQ0FBQztBQUN0RCxDQUFDLENBQUMsQ0FBQztBQUVILFNBQVMsQ0FBQyxHQUFHLEVBQUU7SUFDYixJQUFJLENBQUMsZUFBZSxFQUFFLENBQUM7QUFDekIsQ0FBQyxDQUFDLENBQUM7QUFFSCxRQUFRLENBQUMsZ0JBQWdCLEVBQUUsR0FBRyxFQUFFO0lB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