Skip to content

Commit

Permalink
Merge branch 'main' into epolon/core-codeowners
Browse files Browse the repository at this point in the history
  • Loading branch information
mergify[bot] authored Nov 19, 2024
2 parents c73ddfb + 49b5afd commit 4a73b89
Show file tree
Hide file tree
Showing 7 changed files with 149 additions and 67 deletions.
34 changes: 34 additions & 0 deletions packages/aws-cdk/test/api/assembly-versions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import * as fs from 'fs';
import * as cxapi from '@aws-cdk/cx-api';
import { CloudAssembly } from '../../lib/api/cxapp/cloud-assembly';

/**
* The cloud-assembly-schema in the new monorepo will use its own package version as the schema version, which is always `0.0.0` when tests are running.
*
* If we want to test the CLI's behavior when presented with specific schema versions, we will have to
* mutate `manifest.json` on disk after writing it, and write the schema version that we want to test for in there.
*
* After we raise the schema version in the file on disk from `0.0.0` to
* `30.0.0`, `cx-api` will refuse to load `manifest.json` back, because the
* version is higher than its own package version ("Maximum schema version
* supported is 0.x.x, but found 30.0.0"), so we have to turn on `skipVersionCheck`.
*/
export function cxapiAssemblyWithForcedVersion(asm: cxapi.CloudAssembly, version: string) {
rewriteManifestVersion(asm.directory, version);
return new cxapi.CloudAssembly(asm.directory, { skipVersionCheck: true });
}

/**
* The CLI has its own CloudAssembly class which wraps the cxapi CloudAssembly class
*/
export function cliAssemblyWithForcedVersion(asm: CloudAssembly, version: string) {
rewriteManifestVersion(asm.directory, version);
return new CloudAssembly(new cxapi.CloudAssembly(asm.directory, { skipVersionCheck: true }));
}

export function rewriteManifestVersion(directory: string, version: string) {
const manifestFile = `${directory}/manifest.json`;
const contents = JSON.parse(fs.readFileSync(`${directory}/manifest.json`, 'utf-8'));
contents.version = version;
fs.writeFileSync(manifestFile, JSON.stringify(contents, undefined, 2));
}
4 changes: 3 additions & 1 deletion packages/aws-cdk/test/api/cloud-assembly.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import * as cxschema from '@aws-cdk/cloud-assembly-schema';
import { DefaultSelection } from '../../lib/api/cxapp/cloud-assembly';
import { MockCloudExecutable } from '../util';
import { cliAssemblyWithForcedVersion } from './assembly-versions';

// behave like v2
process.env.CXAPI_DISABLE_SELECT_BY_ID = '1';
Expand Down Expand Up @@ -261,5 +262,6 @@ async function testNestedCloudAssembly({ env }: { env?: string; versionReporting
}],
});

return cloudExec.synthesize();
const asm = await cloudExec.synthesize();
return cliAssemblyWithForcedVersion(asm, '30.0.0');
}
114 changes: 60 additions & 54 deletions packages/aws-cdk/test/api/cloud-executable.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,64 +5,76 @@ import { DefaultSelection } from '../../lib/api/cxapp/cloud-assembly';
import { registerContextProvider } from '../../lib/context-providers';
import { MockCloudExecutable } from '../util';

// Apps on this version of the cxschema don't emit their own metadata resources
// yet, so rely on the CLI to add the Metadata resource in.
const SCHEMA_VERSION_THAT_DOESNT_INCLUDE_METADATA_ITSELF = '2.0.0';

describe('AWS::CDK::Metadata', () => {
test('is generated for relocatable stacks from old frameworks', async () => {
await withFakeCurrentCxVersion('2.0.0', async () => {
const cx = await testCloudExecutable({ env: `aws://${cxapi.UNKNOWN_ACCOUNT}/${cxapi.UNKNOWN_REGION}`, versionReporting: true });
const cxasm = await cx.synthesize();

const result = cxasm.stackById('withouterrors').firstStack;
const metadata = result.template.Resources && result.template.Resources.CDKMetadata;
expect(metadata).toEqual({
Type: 'AWS::CDK::Metadata',
Properties: {
// eslint-disable-next-line @typescript-eslint/no-require-imports
Modules: `${require('../../package.json').name}=${require('../../package.json').version}`,
},
Condition: 'CDKMetadataAvailable',
});

expect(result.template.Conditions?.CDKMetadataAvailable).toBeDefined();
const cx = await testCloudExecutable({
env: `aws://${cxapi.UNKNOWN_ACCOUNT}/${cxapi.UNKNOWN_REGION}`,
versionReporting: true,
schemaVersion: SCHEMA_VERSION_THAT_DOESNT_INCLUDE_METADATA_ITSELF,
});
const cxasm = await cx.synthesize();

const result = cxasm.stackById('withouterrors').firstStack;
const metadata = result.template.Resources && result.template.Resources.CDKMetadata;
expect(metadata).toEqual({
Type: 'AWS::CDK::Metadata',
Properties: {
// eslint-disable-next-line @typescript-eslint/no-require-imports
Modules: `${require('../../package.json').name}=${require('../../package.json').version}`,
},
Condition: 'CDKMetadataAvailable',
});

expect(result.template.Conditions?.CDKMetadataAvailable).toBeDefined();
});

test('is generated for stacks in supported regions from old frameworks', async () => {
await withFakeCurrentCxVersion('2.0.0', async () => {
const cx = await testCloudExecutable({ env: 'aws://012345678912/us-east-1', versionReporting: true });
const cxasm = await cx.synthesize();

const result = cxasm.stackById('withouterrors').firstStack;
const metadata = result.template.Resources && result.template.Resources.CDKMetadata;
expect(metadata).toEqual({
Type: 'AWS::CDK::Metadata',
Properties: {
// eslint-disable-next-line @typescript-eslint/no-require-imports
Modules: `${require('../../package.json').name}=${require('../../package.json').version}`,
},
});
const cx = await testCloudExecutable({
env: 'aws://012345678912/us-east-1',
versionReporting: true,
schemaVersion: SCHEMA_VERSION_THAT_DOESNT_INCLUDE_METADATA_ITSELF,
});
const cxasm = await cx.synthesize();

const result = cxasm.stackById('withouterrors').firstStack;
const metadata = result.template.Resources && result.template.Resources.CDKMetadata;
expect(metadata).toEqual({
Type: 'AWS::CDK::Metadata',
Properties: {
// eslint-disable-next-line @typescript-eslint/no-require-imports
Modules: `${require('../../package.json').name}=${require('../../package.json').version}`,
},
});
});

test('is not generated for stacks in unsupported regions from old frameworks', async () => {
await withFakeCurrentCxVersion('2.0.0', async () => {
const cx = await testCloudExecutable({ env: 'aws://012345678912/bermuda-triangle-1337', versionReporting: true });
const cxasm = await cx.synthesize();

const result = cxasm.stackById('withouterrors').firstStack;
const metadata = result.template.Resources && result.template.Resources.CDKMetadata;
expect(metadata).toBeUndefined();
const cx = await testCloudExecutable({
env: 'aws://012345678912/bermuda-triangle-1337',
versionReporting: true,
schemaVersion: SCHEMA_VERSION_THAT_DOESNT_INCLUDE_METADATA_ITSELF,
});
const cxasm = await cx.synthesize();

const result = cxasm.stackById('withouterrors').firstStack;
const metadata = result.template.Resources && result.template.Resources.CDKMetadata;
expect(metadata).toBeUndefined();
});

test('is not generated for new frameworks', async () => {
await withFakeCurrentCxVersion('8.0.0', async () => {
const cx = await testCloudExecutable({ env: 'aws://012345678912/us-east-1', versionReporting: true });
const cxasm = await cx.synthesize();

const result = cxasm.stackById('withouterrors').firstStack;
const metadata = result.template.Resources && result.template.Resources.CDKMetadata;
expect(metadata).toBeUndefined();
const cx = await testCloudExecutable({
env: 'aws://012345678912/us-east-1',
versionReporting: true,
schemaVersion: '8.0.0',
});
const cxasm = await cx.synthesize();

const result = cxasm.stackById('withouterrors').firstStack;
const metadata = result.template.Resources && result.template.Resources.CDKMetadata;
expect(metadata).toBeUndefined();
});
});

Expand Down Expand Up @@ -109,7 +121,10 @@ test('fails if lookups are disabled and missing context is synthesized', async (
await expect(cloudExecutable.synthesize()).rejects.toThrow(/Context lookups have been disabled/);
});

async function testCloudExecutable({ env, versionReporting = true }: { env?: string; versionReporting?: boolean } = {}) {
async function testCloudExecutable(
{ env, versionReporting = true, schemaVersion }:
{ env?: string; versionReporting?: boolean; schemaVersion?: string } = {},
) {
const cloudExec = new MockCloudExecutable({
stacks: [{
stackName: 'withouterrors',
Expand All @@ -129,18 +144,9 @@ async function testCloudExecutable({ env, versionReporting = true }: { env?: str
],
},
}],
schemaVersion,
});
cloudExec.configuration.settings.set(['versionReporting'], versionReporting);

return cloudExec;
}

async function withFakeCurrentCxVersion<A>(version: string, block: () => Promise<A>): Promise<A> {
const currentVersionFn = cxschema.Manifest.version;
cxschema.Manifest.version = () => version;
try {
return await block();
} finally {
cxschema.Manifest.version = currentVersionFn;
}
}
33 changes: 31 additions & 2 deletions packages/aws-cdk/test/api/exec.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable import/order */
jest.mock('child_process');
import { bockfs } from '@aws-cdk/cdk-build-tools';
import * as cxschema from 'aws-cdk-lib/cloud-assembly-schema';
import * as cxschema from '@aws-cdk/cloud-assembly-schema';
import * as cdk from 'aws-cdk-lib';
import * as semver from 'semver';
import * as sinon from 'sinon';
Expand All @@ -13,6 +13,7 @@ import { testAssembly } from '../util';
import { mockSpawn } from '../util/mock-child_process';
import { MockSdkProvider } from '../util/mock-sdk';
import { RWLock } from '../../lib/api/util/rwlock';
import { rewriteManifestVersion } from './assembly-versions';

let sdkProvider: MockSdkProvider;
let config: Configuration;
Expand Down Expand Up @@ -76,6 +77,8 @@ test('cli throws when manifest version > schema version', async () => {
mockVersionNumber.restore();
}

rewriteManifestVersion('cdk.out', `${mockManifestVersion}`);

const expectedError = 'This CDK CLI is not compatible with the CDK library used by your application. Please upgrade the CLI to the latest version.'
+ `\n(Cloud assembly schema version mismatch: Maximum schema version supported is ${semver.major(currentSchemaVersion)}.x.x, but found ${mockManifestVersion})`;

Expand All @@ -90,20 +93,31 @@ test('cli does not throw when manifest version = schema version', async () => {
const app = createApp();
app.synth();

rewriteManifestVersionToOurs();

config.settings.set(['app'], 'cdk.out');

const { lock } = await execProgram(sdkProvider, config);
await lock.release();

}, TEN_SECOND_TIMEOUT);

test('cli does not throw when manifest version < schema version', async () => {
// Why do we have to do something here at all? Because `aws-cdk-lib` has its own version of `cloud-assembly-schema`,
// which will have real version `38.0.0`, different from the `0.0.0` version of `cloud-assembly-schema` that the CLI
// uses.
//
// Since our Cloud Assembly Schema version will be `0.0.0` and there is no such thing as `-1.0.0`, this test doesn't
// make any sense anymore.
// eslint-disable-next-line jest/no-disabled-tests
test.skip('cli does not throw when manifest version < schema version', async () => {

const app = createApp();
const currentSchemaVersion = cxschema.Manifest.version();

app.synth();

rewriteManifestVersionToOurs();

config.settings.set(['app'], 'cdk.out');

// this mock will cause the cli to think its exepcted schema version is
Expand All @@ -130,6 +144,7 @@ test('bypasses synth when app points to a cloud assembly', async () => {
// GIVEN
config.settings.set(['app'], 'cdk.out');
writeOutputAssembly();
rewriteManifestVersionToOurs();

// WHEN
const { assembly: cloudAssembly, lock } = await execProgram(sdkProvider, config);
Expand Down Expand Up @@ -259,4 +274,18 @@ function writeOutputAssembly() {
stacks: [],
});
bockfs.write('/home/project/cdk.out/manifest.json', JSON.stringify(asm.manifest));
rewriteManifestVersionToOurs(bockfs.path('/home/project/cdk.out'));
}

/**
* Rewrite the manifest schema version in the given directory to match the version number we expect (probably `0.0.0`).
*
* Why do we have to do this? Because `aws-cdk-lib` has its own version of `cloud-assembly-schema`,
* which will have real version `38.0.0`, different from the `0.0.0` version of `cloud-assembly-schema` that the CLI
* uses.
*
* If we don't do this, every time we load a Cloud Assembly the code will say "Maximum schema version supported is 0.x.x, but found 30.0.0".0
*/
function rewriteManifestVersionToOurs(dir: string = 'cdk.out') {
rewriteManifestVersion(dir, cxschema.Manifest.version());
}
6 changes: 3 additions & 3 deletions packages/aws-cdk/test/build.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ describe('buildAllStackAssets', () => {
.toBeUndefined();

expect(buildStackAssets).toBeCalledTimes(3);
expect(buildStackAssets).toBeCalledWith(A);
expect(buildStackAssets).toBeCalledWith(B);
expect(buildStackAssets).toBeCalledWith(C);
expect(buildStackAssets).toHaveBeenCalledWith(A);
expect(buildStackAssets).toHaveBeenCalledWith(B);
expect(buildStackAssets).toHaveBeenCalledWith(C);
});

test('errors', async () => {
Expand Down
18 changes: 12 additions & 6 deletions packages/aws-cdk/test/cdk-toolkit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1062,7 +1062,7 @@ describe('watch', () => {
});
fakeChokidarWatcherOn.readyCallback();

expect(cdkDeployMock).toBeCalledWith(expect.objectContaining({ concurrency: 3 }));
expect(cdkDeployMock).toHaveBeenCalledWith(expect.objectContaining({ concurrency: 3 }));
});

describe.each([HotswapMode.FALL_BACK, HotswapMode.HOTSWAP_ONLY])('%p mode', (hotswapMode) => {
Expand All @@ -1078,7 +1078,7 @@ describe('watch', () => {
});
fakeChokidarWatcherOn.readyCallback();

expect(cdkDeployMock).toBeCalledWith(expect.objectContaining({ hotswap: hotswapMode }));
expect(cdkDeployMock).toHaveBeenCalledWith(expect.objectContaining({ hotswap: hotswapMode }));
});
});

Expand All @@ -1094,7 +1094,7 @@ describe('watch', () => {
});
fakeChokidarWatcherOn.readyCallback();

expect(cdkDeployMock).toBeCalledWith(expect.objectContaining({ hotswap: HotswapMode.HOTSWAP_ONLY }));
expect(cdkDeployMock).toHaveBeenCalledWith(expect.objectContaining({ hotswap: HotswapMode.HOTSWAP_ONLY }));
});

test('respects HotswapMode.FALL_BACK', async () => {
Expand All @@ -1109,7 +1109,7 @@ describe('watch', () => {
});
fakeChokidarWatcherOn.readyCallback();

expect(cdkDeployMock).toBeCalledWith(expect.objectContaining({ hotswap: HotswapMode.FALL_BACK }));
expect(cdkDeployMock).toHaveBeenCalledWith(expect.objectContaining({ hotswap: HotswapMode.FALL_BACK }));
});

test('respects HotswapMode.FULL_DEPLOYMENT', async () => {
Expand All @@ -1124,7 +1124,7 @@ describe('watch', () => {
});
fakeChokidarWatcherOn.readyCallback();

expect(cdkDeployMock).toBeCalledWith(expect.objectContaining({ hotswap: HotswapMode.FULL_DEPLOYMENT }));
expect(cdkDeployMock).toHaveBeenCalledWith(expect.objectContaining({ hotswap: HotswapMode.FULL_DEPLOYMENT }));
});

describe('with file change events', () => {
Expand Down Expand Up @@ -1677,7 +1677,13 @@ class FakeCloudFormation extends Deployments {
expect(options.tags).toEqual(this.expectedTags[options.stack.stackName]);
}

expect(options.notificationArns).toEqual(this.expectedNotificationArns);
// In these tests, we don't make a distinction here between `undefined` and `[]`.
//
// In tests `deployStack` itself we do treat `undefined` and `[]` differently,
// and in `aws-cdk-lib` we emit them under different conditions. But this test
// without normalization depends on a version of `aws-cdk-lib` that hasn't been
// released yet.
expect(options.notificationArns ?? []).toEqual(this.expectedNotificationArns ?? []);
return Promise.resolve({
type: 'did-deploy-stack',
stackArn: `arn:aws:cloudformation:::stack/${options.stack.stackName}/MockedOut`,
Expand Down
7 changes: 6 additions & 1 deletion packages/aws-cdk/test/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ import { type CloudAssembly, CloudAssemblyBuilder, type CloudFormationStackArtif
import { MockSdkProvider } from './util/mock-sdk';
import { CloudExecutable } from '../lib/api/cxapp/cloud-executable';
import { Configuration } from '../lib/settings';
import { cxapiAssemblyWithForcedVersion } from './api/assembly-versions';

export const DEFAULT_FAKE_TEMPLATE = { No: 'Resources' };

const SOME_RECENT_SCHEMA_VERSION = '30.0.0';

export interface TestStackArtifact {
stackName: string;
template?: any;
Expand All @@ -30,6 +33,7 @@ export interface TestAssembly {
stacks: TestStackArtifact[];
missing?: MissingContext[];
nestedAssemblies?: TestAssembly[];
schemaVersion?: string;
}

export class MockCloudExecutable extends CloudExecutable {
Expand Down Expand Up @@ -136,7 +140,8 @@ export function testAssembly(assembly: TestAssembly): CloudAssembly {
});
}

return builder.buildAssembly();
const asm = builder.buildAssembly();
return cxapiAssemblyWithForcedVersion(asm, assembly.schemaVersion ?? SOME_RECENT_SCHEMA_VERSION);
}

/**
Expand Down

0 comments on commit 4a73b89

Please sign in to comment.