From ededa21d1880dd69eee7f36728fcf1891c5584ad Mon Sep 17 00:00:00 2001 From: Momo Kornher Date: Fri, 17 Jan 2025 21:40:19 +0000 Subject: [PATCH] fix(cx-api): cannot detect CloudAssembly across different library versions --- packages/@aws-cdk/cx-api/package.json | 2 +- .../cloud-assembly/private/source-builder.ts | 15 ++++++++++++++- .../lib/api/cloud-assembly/source-builder.ts | 4 ++-- .../test/_fixtures/external-context/index.ts | 2 +- .../test/_fixtures/stack-with-bucket/index.js | 2 +- .../test/_fixtures/stack-with-bucket/index.ts | 2 +- .../test/_fixtures/two-empty-stacks/index.js | 3 +-- .../test/_fixtures/two-empty-stacks/index.ts | 3 +-- .../aws-cdk-lib/cx-api/lib/cloud-assembly.ts | 19 ++++++++++++++++++- .../cx-api/test/cloud-assembly.test.ts | 9 +++++++++ 10 files changed, 49 insertions(+), 12 deletions(-) diff --git a/packages/@aws-cdk/cx-api/package.json b/packages/@aws-cdk/cx-api/package.json index 14285457d9b96..82cec57ce73f1 100644 --- a/packages/@aws-cdk/cx-api/package.json +++ b/packages/@aws-cdk/cx-api/package.json @@ -62,7 +62,7 @@ "gen": "cdk-copy cx-api", "watch": "cdk-watch", "lint": "cdk-lint && madge --circular --extensions js lib", - "test": "cdk-test", + "test": "jest", "pkglint": "pkglint -f", "package": "cdk-package", "awslint": "cdk-awslint", diff --git a/packages/@aws-cdk/toolkit/lib/api/cloud-assembly/private/source-builder.ts b/packages/@aws-cdk/toolkit/lib/api/cloud-assembly/private/source-builder.ts index bcd2b4f807091..695e736dbbe7e 100644 --- a/packages/@aws-cdk/toolkit/lib/api/cloud-assembly/private/source-builder.ts +++ b/packages/@aws-cdk/toolkit/lib/api/cloud-assembly/private/source-builder.ts @@ -1,3 +1,4 @@ +import * as cxapi from '@aws-cdk/cx-api'; import * as fs from 'fs-extra'; import type { ICloudAssemblySource } from '../'; import { ContextAwareCloudAssembly, ContextAwareCloudAssemblyProps } from './context-aware-source'; @@ -9,6 +10,12 @@ import { ToolkitError } from '../../errors'; import { debug } from '../../io/private'; import { AssemblyBuilder, CdkAppSourceProps } from '../source-builder'; +// bypass loading from disk if we already have a supported object +const CLOUD_ASSEMBLY_SYMBOL = Symbol.for('@aws-cdk/cx-api.CloudAssembly'); +function isCloudAssembly(x: any): x is cxapi.CloudAssembly { + return x !== null && typeof(x) === 'object' && CLOUD_ASSEMBLY_SYMBOL in x; +} + export abstract class CloudAssemblySourceBuilder { /** @@ -40,13 +47,19 @@ export abstract class CloudAssemblySourceBuilder { produce: async () => { const outdir = determineOutputDirectory(props.outdir); const env = await prepareDefaultEnvironment(services, { outdir }); - return changeDir(async () => + const assembly = await changeDir(async () => withContext(context.all, env, props.synthOptions ?? {}, async (envWithContext, ctx) => withEnv(envWithContext, () => builder({ outdir, context: ctx, })), ), props.workingDirectory); + + if (isCloudAssembly(assembly)) { + return assembly; + } + + return new cxapi.CloudAssembly(assembly.directory); }, }, contextAssemblyProps, diff --git a/packages/@aws-cdk/toolkit/lib/api/cloud-assembly/source-builder.ts b/packages/@aws-cdk/toolkit/lib/api/cloud-assembly/source-builder.ts index 9f87860ad30d8..bd748c65be5f7 100644 --- a/packages/@aws-cdk/toolkit/lib/api/cloud-assembly/source-builder.ts +++ b/packages/@aws-cdk/toolkit/lib/api/cloud-assembly/source-builder.ts @@ -1,4 +1,4 @@ -import type * as cxapi from '@aws-cdk/cx-api'; +import type * as cxschema from "@aws-cdk/cloud-assembly-schema"; export interface AppProps { /** @@ -12,7 +12,7 @@ export interface AppProps { readonly context?: { [key: string]: any }; } -export type AssemblyBuilder = (props: AppProps) => Promise; +export type AssemblyBuilder = (props: AppProps) => Promise; /** * Configuration for creating a CLI from an AWS CDK App directory diff --git a/packages/@aws-cdk/toolkit/test/_fixtures/external-context/index.ts b/packages/@aws-cdk/toolkit/test/_fixtures/external-context/index.ts index 5f505989fcde6..a676b442d3fa6 100644 --- a/packages/@aws-cdk/toolkit/test/_fixtures/external-context/index.ts +++ b/packages/@aws-cdk/toolkit/test/_fixtures/external-context/index.ts @@ -7,5 +7,5 @@ export default async () => { new s3.Bucket(stack, 'MyBucket', { bucketName: app.node.tryGetContext('externally-provided-bucket-name'), }); - return app.synth() as any; + return app.synth(); }; diff --git a/packages/@aws-cdk/toolkit/test/_fixtures/stack-with-bucket/index.js b/packages/@aws-cdk/toolkit/test/_fixtures/stack-with-bucket/index.js index 3e530df5c98e1..1d74c329f4920 100644 --- a/packages/@aws-cdk/toolkit/test/_fixtures/stack-with-bucket/index.js +++ b/packages/@aws-cdk/toolkit/test/_fixtures/stack-with-bucket/index.js @@ -8,4 +8,4 @@ exports.default = async () => { new s3.Bucket(stack, 'MyBucket'); return app.synth(); }; -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJpbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOztBQUFBLHlDQUF5QztBQUN6Qyx5Q0FBeUM7QUFFekMsa0JBQWUsS0FBSyxJQUFJLEVBQUU7SUFDeEIsTUFBTSxHQUFHLEdBQUcsSUFBSSxJQUFJLENBQUMsR0FBRyxFQUFFLENBQUM7SUFDM0IsTUFBTSxLQUFLLEdBQUcsSUFBSSxJQUFJLENBQUMsS0FBSyxDQUFDLEdBQUcsRUFBRSxRQUFRLENBQUMsQ0FBQztJQUM1QyxJQUFJLEVBQUUsQ0FBQyxNQUFNLENBQUMsS0FBSyxFQUFFLFVBQVUsQ0FBQyxDQUFDO0lBQ2pDLE9BQU8sR0FBRyxDQUFDLEtBQUssRUFBUyxDQUFDO0FBQzVCLENBQUMsQ0FBQyJ9 \ No newline at end of file +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJpbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOztBQUFBLHlDQUF5QztBQUN6Qyx5Q0FBeUM7QUFFekMsa0JBQWUsS0FBSyxJQUFJLEVBQUU7SUFDeEIsTUFBTSxHQUFHLEdBQUcsSUFBSSxJQUFJLENBQUMsR0FBRyxFQUFFLENBQUM7SUFDM0IsTUFBTSxLQUFLLEdBQUcsSUFBSSxJQUFJLENBQUMsS0FBSyxDQUFDLEdBQUcsRUFBRSxRQUFRLENBQUMsQ0FBQztJQUM1QyxJQUFJLEVBQUUsQ0FBQyxNQUFNLENBQUMsS0FBSyxFQUFFLFVBQVUsQ0FBQyxDQUFDO0lBQ2pDLE9BQU8sR0FBRyxDQUFDLEtBQUssRUFBRSxDQUFDO0FBQ3JCLENBQUMsQ0FBQyJ9 \ No newline at end of file diff --git a/packages/@aws-cdk/toolkit/test/_fixtures/stack-with-bucket/index.ts b/packages/@aws-cdk/toolkit/test/_fixtures/stack-with-bucket/index.ts index 05f7ec0ad6bfc..bf3ebfa486d9b 100644 --- a/packages/@aws-cdk/toolkit/test/_fixtures/stack-with-bucket/index.ts +++ b/packages/@aws-cdk/toolkit/test/_fixtures/stack-with-bucket/index.ts @@ -5,5 +5,5 @@ export default async () => { const app = new core.App(); const stack = new core.Stack(app, 'Stack1'); new s3.Bucket(stack, 'MyBucket'); - return app.synth() as any; + return app.synth(); }; diff --git a/packages/@aws-cdk/toolkit/test/_fixtures/two-empty-stacks/index.js b/packages/@aws-cdk/toolkit/test/_fixtures/two-empty-stacks/index.js index 547c2827c2181..edd0b40c17620 100644 --- a/packages/@aws-cdk/toolkit/test/_fixtures/two-empty-stacks/index.js +++ b/packages/@aws-cdk/toolkit/test/_fixtures/two-empty-stacks/index.js @@ -5,7 +5,6 @@ exports.default = async () => { const app = new core.App(); new core.Stack(app, 'Stack1'); new core.Stack(app, 'Stack2'); - // @todo fix api return app.synth(); }; -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJpbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOztBQUFBLHlDQUF5QztBQUV6QyxrQkFBZSxLQUFLLElBQUksRUFBRTtJQUN4QixNQUFNLEdBQUcsR0FBRyxJQUFJLElBQUksQ0FBQyxHQUFHLEVBQUUsQ0FBQztJQUMzQixJQUFJLElBQUksQ0FBQyxLQUFLLENBQUMsR0FBRyxFQUFFLFFBQVEsQ0FBQyxDQUFDO0lBQzlCLElBQUksSUFBSSxDQUFDLEtBQUssQ0FBQyxHQUFHLEVBQUUsUUFBUSxDQUFDLENBQUM7SUFFOUIsZ0JBQWdCO0lBQ2hCLE9BQU8sR0FBRyxDQUFDLEtBQUssRUFBUyxDQUFDO0FBQzVCLENBQUMsQ0FBQyJ9 \ No newline at end of file +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJpbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOztBQUFBLHlDQUF5QztBQUV6QyxrQkFBZSxLQUFLLElBQUksRUFBRTtJQUN4QixNQUFNLEdBQUcsR0FBRyxJQUFJLElBQUksQ0FBQyxHQUFHLEVBQUUsQ0FBQztJQUMzQixJQUFJLElBQUksQ0FBQyxLQUFLLENBQUMsR0FBRyxFQUFFLFFBQVEsQ0FBQyxDQUFDO0lBQzlCLElBQUksSUFBSSxDQUFDLEtBQUssQ0FBQyxHQUFHLEVBQUUsUUFBUSxDQUFDLENBQUM7SUFFOUIsT0FBTyxHQUFHLENBQUMsS0FBSyxFQUFFLENBQUM7QUFDckIsQ0FBQyxDQUFDIn0= \ No newline at end of file diff --git a/packages/@aws-cdk/toolkit/test/_fixtures/two-empty-stacks/index.ts b/packages/@aws-cdk/toolkit/test/_fixtures/two-empty-stacks/index.ts index 5502e0b07d22c..9d4c1df80e12c 100644 --- a/packages/@aws-cdk/toolkit/test/_fixtures/two-empty-stacks/index.ts +++ b/packages/@aws-cdk/toolkit/test/_fixtures/two-empty-stacks/index.ts @@ -5,6 +5,5 @@ export default async () => { new core.Stack(app, 'Stack1'); new core.Stack(app, 'Stack2'); - // @todo fix api - return app.synth() as any; + return app.synth(); }; diff --git a/packages/aws-cdk-lib/cx-api/lib/cloud-assembly.ts b/packages/aws-cdk-lib/cx-api/lib/cloud-assembly.ts index b170e6c0df5ac..af1601ba25860 100644 --- a/packages/aws-cdk-lib/cx-api/lib/cloud-assembly.ts +++ b/packages/aws-cdk-lib/cx-api/lib/cloud-assembly.ts @@ -1,6 +1,10 @@ import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; +// This is deliberately importing the interface from the external package. +// We want this, so that jsii language packages can depend on @aws-cdk/cloud-assembly-schema +// instead of being forced to take a dependency on the much larger aws-cdk-lib. +import type { ICloudAssembly } from '@aws-cdk/cloud-assembly-schema'; import { CloudFormationStackArtifact } from './artifacts/cloudformation-artifact'; import { NestedCloudAssemblyArtifact } from './artifacts/nested-cloud-assembly-artifact'; import { TreeCloudArtifact } from './artifacts/tree-cloud-artifact'; @@ -8,6 +12,8 @@ import { CloudArtifact } from './cloud-artifact'; import { topologicalSort } from './toposort'; import * as cxschema from '../../cloud-assembly-schema'; +const CLOUD_ASSEMBLY_SYMBOL = Symbol.for('@aws-cdk/cx-api.CloudAssembly'); + /** * The name of the root manifest file of the assembly. */ @@ -16,7 +22,16 @@ const MANIFEST_FILE = 'manifest.json'; /** * Represents a deployable cloud application. */ -export class CloudAssembly { +export class CloudAssembly implements ICloudAssembly { + /** + * Return whether the given object is a CloudAssembly. + * + * We do attribute detection since we can't reliably use 'instanceof'. + */ + public static isCloudAssembly(x: any): x is CloudAssembly { + return x !== null && typeof(x) === 'object' && CLOUD_ASSEMBLY_SYMBOL in x; + } + /** * The root directory of the cloud assembly. */ @@ -54,6 +69,8 @@ export class CloudAssembly { this.artifacts = this.renderArtifacts(loadOptions?.topoSort ?? true); this.runtime = this.manifest.runtime || { libraries: { } }; + Object.defineProperty(this, CLOUD_ASSEMBLY_SYMBOL, { value: true }); + // force validation of deps by accessing 'depends' on all artifacts this.validateDeps(); } diff --git a/packages/aws-cdk-lib/cx-api/test/cloud-assembly.test.ts b/packages/aws-cdk-lib/cx-api/test/cloud-assembly.test.ts index b3965026a98fb..8fc2dcc000100 100644 --- a/packages/aws-cdk-lib/cx-api/test/cloud-assembly.test.ts +++ b/packages/aws-cdk-lib/cx-api/test/cloud-assembly.test.ts @@ -185,3 +185,12 @@ test('getStackArtifact retrieves a stack by artifact id from a nested assembly', expect(assembly.getStackArtifact('stack1').id).toEqual('stack1'); expect(assembly.getStackArtifact('stack2').id).toEqual('stack2'); }); + +test('isCloudAssembly correctly detects Cloud Assemblies', () => { + const assembly = new CloudAssembly(path.join(FIXTURES, 'nested-assemblies')); + const inheritedAssembly = new (class extends CloudAssembly {})(path.join(FIXTURES, 'nested-assemblies')); + + expect(CloudAssembly.isCloudAssembly(assembly)).toBe(true); + expect(CloudAssembly.isCloudAssembly(inheritedAssembly)).toBe(true); + expect(CloudAssembly.isCloudAssembly({})).toBe(false); +});