UNPKG

aws-cdk

Version:

CDK Toolkit, the command line tool for CDK apps

1,079 lines (1,051 loc) 117 kB
"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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