diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/private/prepare-source.ts b/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/private/prepare-source.ts index dddb9fae2..fc8e8e046 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/private/prepare-source.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/private/prepare-source.ts @@ -11,7 +11,7 @@ import { ToolkitServices } from '../../../toolkit/private'; import { CODES } from '../../io/private'; import { ActionAwareIoHost } from '../../shared-private'; import { ToolkitError } from '../../shared-public'; -import type { AppSynthOptions } from '../source-builder'; +import type { AppSynthOptions, LoadAssemblyOptions } from '../source-builder'; export { guessExecutable } from '../../../api/aws-cdk'; @@ -149,9 +149,11 @@ export async function checkContextOverflowSupport(assembly: cxapi.CloudAssembly, /** * Safely create an assembly from a cloud assembly directory */ -export async function assemblyFromDirectory(assemblyDir: string, ioHost: ActionAwareIoHost) { +export async function assemblyFromDirectory(assemblyDir: string, ioHost: ActionAwareIoHost, loadOptions: LoadAssemblyOptions = {}) { try { const assembly = new cxapi.CloudAssembly(assemblyDir, { + skipVersionCheck: !(loadOptions.checkVersion ?? true), + skipEnumCheck: !(loadOptions.checkEnums ?? true), // We sort as we deploy topoSort: false, }); diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/private/source-builder.ts b/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/private/source-builder.ts index 3250992da..574e93af0 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/private/source-builder.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/private/source-builder.ts @@ -1,6 +1,6 @@ import * as cxapi from '@aws-cdk/cx-api'; import * as fs from 'fs-extra'; -import type { AssemblySourceProps, ICloudAssemblySource } from '../'; +import type { AssemblyDirectoryProps, AssemblySourceProps, ICloudAssemblySource } from '../'; import { ContextAwareCloudAssembly, ContextAwareCloudAssemblyProps } from './context-aware-source'; import { execInChildProcess } from './exec'; import { assemblyFromDirectory, changeDir, determineOutputDirectory, guessExecutable, prepareDefaultEnvironment, withContext, withEnv } from './prepare-source'; @@ -52,7 +52,7 @@ export abstract class CloudAssemblySourceBuilder { return assembly; } - return new cxapi.CloudAssembly(assembly.directory); + return assemblyFromDirectory(assembly.directory, services.ioHost, props.loadAssemblyOptions); }, }, contextAssemblyProps, @@ -64,7 +64,7 @@ export abstract class CloudAssemblySourceBuilder { * @param directory the directory of a already produced Cloud Assembly. * @returns the CloudAssembly source */ - public async fromAssemblyDirectory(directory: string): Promise { + public async fromAssemblyDirectory(directory: string, props: AssemblyDirectoryProps = {}): Promise { const services: ToolkitServices = await this.sourceBuilderServices(); const contextAssemblyProps: ContextAwareCloudAssemblyProps = { services, @@ -77,7 +77,7 @@ export abstract class CloudAssemblySourceBuilder { produce: async () => { // @todo build await services.ioHost.notify(CODES.CDK_ASSEMBLY_I0150.msg('--app points to a cloud assembly, so we bypass synth')); - return assemblyFromDirectory(directory, services.ioHost); + return assemblyFromDirectory(directory, services.ioHost, props.loadAssemblyOptions); }, }, contextAssemblyProps, @@ -136,7 +136,7 @@ export abstract class CloudAssemblySourceBuilder { extraEnv: envWithContext, cwd: props.workingDirectory, }); - return assemblyFromDirectory(outdir, services.ioHost); + return assemblyFromDirectory(outdir, services.ioHost, props.loadAssemblyOptions); }); } finally { await lock?.release(); diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/source-builder.ts b/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/source-builder.ts index 063c35be0..9ddcb8403 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/source-builder.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/source-builder.ts @@ -17,6 +17,16 @@ export interface AssemblyBuilderProps { export type AssemblyBuilder = (props: AssemblyBuilderProps) => Promise; +/** + * Configuration for creating a CLI from an AWS CDK App directory + */ +export interface AssemblyDirectoryProps { + /** + * Options to configure loading of the assembly after it has been synthesized + */ + readonly loadAssemblyOptions?: LoadAssemblyOptions; +} + /** * Configuration for creating a CLI from an AWS CDK App directory */ @@ -59,6 +69,11 @@ export interface AssemblySourceProps { * Options that are passed through the context to a CDK app on synth */ readonly synthOptions?: AppSynthOptions; + + /** + * Options to configure loading of the assembly after it has been synthesized + */ + readonly loadAssemblyOptions?: LoadAssemblyOptions; } /** @@ -123,3 +138,28 @@ export interface AppSynthOptions { */ readonly bundlingForStacks?: string; } + +/** + * Options to configure loading of the assembly after it has been synthesized + */ +export interface LoadAssemblyOptions { + /** + * Check the Toolkit supports the Cloud Assembly Schema version + * + * When disabled, allows to Toolkit to read a newer cloud assembly than the CX API is designed + * to support. Your application may not be aware of all features that in use in the Cloud Assembly. + * + * @default true + */ + readonly checkVersion?: boolean; + + /** + * Validate enums to only have known values + * + * When disabled, the Toolkit may read enum values it doesn't know about yet. + * You will have to make sure to always check the values of enums you encounter in the manifest. + * + * @default true + */ + readonly checkEnums?: boolean; +} diff --git a/packages/@aws-cdk/toolkit-lib/test/_fixtures/manifest-from-the-future/cdk.out/Stack1.assets.json b/packages/@aws-cdk/toolkit-lib/test/_fixtures/manifest-from-the-future/cdk.out/Stack1.assets.json new file mode 100644 index 000000000..8d05af80a --- /dev/null +++ b/packages/@aws-cdk/toolkit-lib/test/_fixtures/manifest-from-the-future/cdk.out/Stack1.assets.json @@ -0,0 +1,19 @@ +{ + "version": "39.0.0", + "files": { + "6cf0c6d5d33f7914e048b4435b7ffc1909cdec43efb95fcde227762c2f0effd1": { + "source": { + "path": "Stack1.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "6cf0c6d5d33f7914e048b4435b7ffc1909cdec43efb95fcde227762c2f0effd1.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk/toolkit-lib/test/_fixtures/manifest-from-the-future/cdk.out/Stack1.template.json b/packages/@aws-cdk/toolkit-lib/test/_fixtures/manifest-from-the-future/cdk.out/Stack1.template.json new file mode 100644 index 000000000..20847c180 --- /dev/null +++ b/packages/@aws-cdk/toolkit-lib/test/_fixtures/manifest-from-the-future/cdk.out/Stack1.template.json @@ -0,0 +1,322 @@ +{ + "Resources": { + "MyBucketF68F3FF0": { + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain", + "Metadata": { + "aws:cdk:path": "Stack1/MyBucket/Resource" + } + }, + "CDKMetadata": { + "Type": "AWS::CDK::Metadata", + "Properties": { + "Analytics": "v2:deflate64:H4sIAAAAAAAA/8vLT0nVyyrWLzMy0DM01jNUzCrOzNQtKs0rycxN1QuC0ADPiCvwJQAAAA==" + }, + "Metadata": { + "aws:cdk:path": "Stack1/CDKMetadata/Default" + }, + "Condition": "CDKMetadataAvailable" + } + }, + "Conditions": { + "CDKMetadataAvailable": { + "Fn::Or": [ + { + "Fn::Or": [ + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "af-south-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ap-east-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ap-northeast-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ap-northeast-2" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ap-northeast-3" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ap-south-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ap-south-2" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ap-southeast-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ap-southeast-2" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ap-southeast-3" + ] + } + ] + }, + { + "Fn::Or": [ + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ap-southeast-4" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ca-central-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "ca-west-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "cn-north-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "cn-northwest-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "eu-central-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "eu-central-2" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "eu-north-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "eu-south-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "eu-south-2" + ] + } + ] + }, + { + "Fn::Or": [ + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "eu-west-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "eu-west-2" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "eu-west-3" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "il-central-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "me-central-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "me-south-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "sa-east-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "us-east-1" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "us-east-2" + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "us-west-1" + ] + } + ] + }, + { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "us-west-2" + ] + } + ] + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/toolkit-lib/test/_fixtures/manifest-from-the-future/cdk.out/cdk.out b/packages/@aws-cdk/toolkit-lib/test/_fixtures/manifest-from-the-future/cdk.out/cdk.out new file mode 100644 index 000000000..ede0bba86 --- /dev/null +++ b/packages/@aws-cdk/toolkit-lib/test/_fixtures/manifest-from-the-future/cdk.out/cdk.out @@ -0,0 +1 @@ +{"version":"99999.0.0"} diff --git a/packages/@aws-cdk/toolkit-lib/test/_fixtures/manifest-from-the-future/cdk.out/manifest.json b/packages/@aws-cdk/toolkit-lib/test/_fixtures/manifest-from-the-future/cdk.out/manifest.json new file mode 100644 index 000000000..5db93883d --- /dev/null +++ b/packages/@aws-cdk/toolkit-lib/test/_fixtures/manifest-from-the-future/cdk.out/manifest.json @@ -0,0 +1,73 @@ +{ + "version": "99999.0.0", + "artifacts": { + "Stack1.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "Stack1.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "Stack1": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "Stack1.template.json", + "terminationProtection": false, + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/6cf0c6d5d33f7914e048b4435b7ffc1909cdec43efb95fcde227762c2f0effd1.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": ["Stack1.assets"], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": ["Stack1.assets"], + "metadata": { + "/Stack1/MyBucket/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "MyBucketF68F3FF0" + } + ], + "/Stack1/CDKMetadata/Default": [ + { + "type": "aws:cdk:logicalId", + "data": "CDKMetadata" + } + ], + "/Stack1/CDKMetadata/Condition": [ + { + "type": "aws:cdk:logicalId", + "data": "CDKMetadataAvailable" + } + ], + "/Stack1/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/Stack1/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "Stack1" + }, + "Tree": { + "type": "cdk:tree", + "properties": { + "file": "tree.json" + } + } + } +} diff --git a/packages/@aws-cdk/toolkit-lib/test/_fixtures/manifest-from-the-future/cdk.out/tree.json b/packages/@aws-cdk/toolkit-lib/test/_fixtures/manifest-from-the-future/cdk.out/tree.json new file mode 100644 index 000000000..8416e84df --- /dev/null +++ b/packages/@aws-cdk/toolkit-lib/test/_fixtures/manifest-from-the-future/cdk.out/tree.json @@ -0,0 +1,95 @@ +{ + "version": "tree-0.1", + "tree": { + "id": "App", + "path": "", + "children": { + "Stack1": { + "id": "Stack1", + "path": "Stack1", + "children": { + "MyBucket": { + "id": "MyBucket", + "path": "Stack1/MyBucket", + "children": { + "Resource": { + "id": "Resource", + "path": "Stack1/MyBucket/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::S3::Bucket", + "aws:cdk:cloudformation:props": {} + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.4.2" + } + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.4.2" + } + }, + "CDKMetadata": { + "id": "CDKMetadata", + "path": "Stack1/CDKMetadata", + "children": { + "Default": { + "id": "Default", + "path": "Stack1/CDKMetadata/Default", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.4.2" + } + }, + "Condition": { + "id": "Condition", + "path": "Stack1/CDKMetadata/Condition", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.4.2" + } + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.4.2" + } + }, + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "Stack1/BootstrapVersion", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.4.2" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "Stack1/CheckBootstrapVersion", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.4.2" + } + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.4.2" + } + }, + "Tree": { + "id": "Tree", + "path": "Tree", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.4.2" + } + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.4.2" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/toolkit-lib/test/_helpers/index.ts b/packages/@aws-cdk/toolkit-lib/test/_helpers/index.ts index 300451457..94fd8b470 100644 --- a/packages/@aws-cdk/toolkit-lib/test/_helpers/index.ts +++ b/packages/@aws-cdk/toolkit-lib/test/_helpers/index.ts @@ -1,6 +1,6 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; -import { Toolkit, ToolkitError } from '../../lib'; +import { AssemblyDirectoryProps, Toolkit, ToolkitError } from '../../lib'; import { determineOutputDirectory } from '../../lib/api/cloud-assembly/private'; export * from './test-io-host'; @@ -31,10 +31,10 @@ export function builderFixture(toolkit: Toolkit, name: string, context?: { [key: }); } -export function cdkOutFixture(toolkit: Toolkit, name: string) { +export function cdkOutFixture(toolkit: Toolkit, name: string, props: AssemblyDirectoryProps = {}) { const outdir = path.join(__dirname, '..', '_fixtures', name, 'cdk.out'); if (!fs.existsSync(outdir)) { throw new ToolkitError(`Assembly Dir Fixture ${name} does not exist in ${outdir}`); } - return toolkit.fromAssemblyDirectory(outdir); + return toolkit.fromAssemblyDirectory(outdir, props); } diff --git a/packages/@aws-cdk/toolkit-lib/test/api/cloud-assembly/source-builder.test.ts b/packages/@aws-cdk/toolkit-lib/test/api/cloud-assembly/source-builder.test.ts index 5f3c3f781..8faaf301c 100644 --- a/packages/@aws-cdk/toolkit-lib/test/api/cloud-assembly/source-builder.test.ts +++ b/packages/@aws-cdk/toolkit-lib/test/api/cloud-assembly/source-builder.test.ts @@ -96,4 +96,25 @@ describe('fromAssemblyDirectory', () => { // THEN expect(assembly.stacksRecursively.map(s => s.hierarchicalId)).toEqual(['Stack1', 'Stack2']); }); + + test('validates manifest version', async () => { + // WHEN + const cx = await cdkOutFixture(toolkit, 'manifest-from-the-future'); + + // THEN + await expect(() => cx.produce()).rejects.toThrow('This AWS CDK Toolkit is not compatible with the AWS CDK library used by your application'); + }); + + test('can disable manifest version validation', async () => { + // WHEN + const cx = await cdkOutFixture(toolkit, 'manifest-from-the-future', { + loadAssemblyOptions: { + checkVersion: false, + }, + }); + const assembly = await cx.produce(); + + // THEN + expect(assembly.stacksRecursively.map(s => s.hierarchicalId)).toEqual(['Stack1']); + }); });