diff --git a/src/converters/app-converter.ts b/src/converters/app-converter.ts index cc7c35fc..13534b16 100644 --- a/src/converters/app-converter.ts +++ b/src/converters/app-converter.ts @@ -117,7 +117,7 @@ export class StackConverter extends ArtifactConverter { for (const n of dependencyGraphNodes) { if (n.construct.id === this.stack.id) { this._stackResource = new CdkConstruct(`${this.app.name}/${n.construct.path}`, n.construct.id, { - parent: this.app, + parent: this.app.component, // NOTE: Currently we make the stack depend on all the assets and then all resources // have the parent as the stack. This means we deploy all assets before we deploy any resources // we might be able better and have individual resources depend on individual assets, but CDK @@ -170,7 +170,7 @@ export class StackConverter extends ArtifactConverter { private stackDependsOn(dependencies: Set): pulumi.Resource[] { const dependsOn: pulumi.Resource[] = []; - dependsOn.push(...this.host.dependencies); + dependsOn.push(...this.app.dependencies); for (const d of dependencies) { if (d instanceof StackConverter) { dependsOn.push(d.stackResource); @@ -202,7 +202,7 @@ export class StackConverter extends ArtifactConverter { return key; } - this.parameters.set(logicalId, parameterValue(this.app)); + this.parameters.set(logicalId, parameterValue(this.app.component)); } private mapResource( @@ -363,15 +363,15 @@ export class StackConverter extends ArtifactConverter { switch (target) { case 'AWS::AccountId': - return getAccountId({ parent: this.app }).then((r) => r.accountId); + return getAccountId({ parent: this.app.component }).then((r) => r.accountId); case 'AWS::NoValue': return undefined; case 'AWS::Partition': - return getPartition({ parent: this.app }).then((p) => p.partition); + return getPartition({ parent: this.app.component }).then((p) => p.partition); case 'AWS::Region': - return getRegion({ parent: this.app }).then((r) => r.region); + return getRegion({ parent: this.app.component }).then((r) => r.region); case 'AWS::URLSuffix': - return getUrlSuffix({ parent: this.app }).then((r) => r.urlSuffix); + return getUrlSuffix({ parent: this.app.component }).then((r) => r.urlSuffix); case 'AWS::NotificationARNs': case 'AWS::StackId': case 'AWS::StackName': diff --git a/src/converters/artifact-converter.ts b/src/converters/artifact-converter.ts index 0bc2bee6..1415f9e9 100644 --- a/src/converters/artifact-converter.ts +++ b/src/converters/artifact-converter.ts @@ -1,6 +1,6 @@ import * as cx from 'aws-cdk-lib/cx-api'; import { getAccountId, getPartition, getRegion } from '@pulumi/aws-native'; -import { StackComponentResource } from '../types'; +import { AppComponent } from '../types'; /** * ArtifactConverter diff --git a/src/stack.ts b/src/stack.ts index 99379c0b..34c47018 100644 --- a/src/stack.ts +++ b/src/stack.ts @@ -14,65 +14,64 @@ import * as cdk from 'aws-cdk-lib'; import * as cx from 'aws-cdk-lib/cx-api'; import * as pulumi from '@pulumi/pulumi'; -import { AppComponent, AppOptions, PulumiStack } from './types'; +import { AppComponent, AppOptions, AppResourceOptions, PulumiStack } from './types'; import { AppConverter, StackConverter } from './converters/app-converter'; -import { PulumiSynthesizer } from './synthesizer'; -import { CdkConstruct } from './interop'; +import { PulumiSynthesizer, PulumiSynthesizerBase } from './synthesizer'; import { AwsCdkCli, ICloudAssemblyDirectoryProducer } from '@aws-cdk/cli-lib-alpha'; import { error } from '@pulumi/pulumi/log'; +import { CdkConstruct } from './interop'; export type AppOutputs = { [outputId: string]: pulumi.Output }; const STACK_SYMBOL = Symbol.for('@pulumi/cdk.Stack'); -export type create = (scope: App) => AppOutputs; -export class App extends AppComponent implements ICloudAssemblyDirectoryProducer { - public name: string; +interface AppResource { + converter: AppConverter; +} - /** @internal */ - public converter: Promise; +export class App + extends pulumi.ComponentResource + implements ICloudAssemblyDirectoryProducer, AppComponent +{ + public readonly name: string; + public readonly component: pulumi.ComponentResource; + public readonly stacks: { [artifactId: string]: PulumiStack } = {}; /** - * @internal + * The collection of outputs from the AWS CDK Stack represented as Pulumi Outputs. + * Each CfnOutput defined in the AWS CDK Stack will populate a value in the outputs. */ - readonly component: pulumi.ComponentResource; + public outputs: { [outputId: string]: pulumi.Output } = {}; - /** - * The directory to which cdk synthesizes the CloudAssembly - * @internal - */ - public assemblyDir: string; - private _app?: cdk.App; + /** @internal */ + public converter: Promise; /** * @internal */ public appOptions?: AppOptions; - public get app(): cdk.App { - if (!this._app) { - throw new Error('cdk.App has not been created yet'); - } - return this._app!; - } - /** - * The collection of outputs from the AWS CDK Stack represented as Pulumi Outputs. - * Each CfnOutput defined in the AWS CDK Stack will populate a value in the outputs. + * The directory to which cdk synthesizes the CloudAssembly + * @internal */ - public outputs: { [outputId: string]: pulumi.Output } = {}; + public assemblyDir!: string; + readonly dependencies: CdkConstruct[] = []; private readonly createFunc: (scope: App) => AppOutputs | void; + private _app?: cdk.App; private appProps?: cdk.AppProps; - constructor(id: string, createFunc: (scope: App) => void | AppOutputs, props?: AppOptions) { - super(id, props); - this.appOptions = props; + constructor(id: string, createFunc: (scope: App) => void | AppOutputs, props?: AppResourceOptions) { + super('cdk:index:App', id, props?.appOptions, props); + this.appOptions = props?.appOptions; this.createFunc = createFunc; + this.component = this; this.name = id; - this.appProps = props?.props; - this.converter = this.getData(); + this.appProps = props?.appOptions?.props; + const data = this.getData(); + this.converter = data.then((d) => d.converter); const outputs = this.converter.then((converter) => { const stacks = Array.from(converter.stacks.values()); @@ -94,8 +93,21 @@ export class App extends AppComponent implements ICloudAssemblyDir this.registerOutputs(this.outputs); } - protected async initialize(): Promise { + public get app(): cdk.App { + if (!this._app) { + throw new Error('cdk.App has not been created yet'); + } + return this._app!; + } + + protected async initialize(props: { + name: string; + args?: AppOptions; + opts?: pulumi.ComponentResourceOptions; + }): Promise { const cli = AwsCdkCli.fromCloudAssemblyDirectoryProducer(this); + this.appProps = props.args?.props; + this.appOptions = props.args; try { await cli.synth({ quiet: true, lookups: false }); } catch (e: any) { @@ -110,25 +122,34 @@ export class App extends AppComponent implements ICloudAssemblyDir this, ); } else { - error(e.message, this); + error(e, this); } } const converter = new AppConverter(this); converter.convert(); - return converter; + return { + converter, + }; } async produce(context: Record): Promise { + const appId = this.appOptions?.appId ?? generateAppId(); + const synthesizer = this.appProps?.defaultStackSynthesizer ?? new PulumiSynthesizer({ appId, parent: this }); + + if (synthesizer instanceof PulumiSynthesizerBase) { + this.dependencies.push(synthesizer.stagingStack); + } + const app = new cdk.App({ ...(this.appProps ?? {}), autoSynth: false, analyticsReporting: false, context, + defaultStackSynthesizer: synthesizer, }); this._app = app; - this.assemblyDir = app.outdir; const outputs = this.createFunc(this); this.outputs = outputs ?? {}; @@ -138,7 +159,9 @@ export class App extends AppComponent implements ICloudAssemblyDir } }); - return app.synth().directory; + const dir = app.synth().directory; + this.assemblyDir = dir; + return dir; } } @@ -188,11 +211,6 @@ export class Stack extends PulumiStack { private pulumiApp: App; - /** - * @internal - */ - public readonly pulumiSynthesizer: PulumiSynthesizer; - /** * Create and register an AWS CDK stack deployed with Pulumi. * diff --git a/src/synthesizer.ts b/src/synthesizer.ts index 9a9be1eb..7997a22a 100644 --- a/src/synthesizer.ts +++ b/src/synthesizer.ts @@ -68,6 +68,17 @@ export interface PulumiSynthesizerOptions { * @default true */ readonly autoDeleteStagingAssets?: boolean; + + readonly parent?: pulumi.ComponentResource; +} + +export abstract class PulumiSynthesizerBase extends cdk.StackSynthesizer { + /** + * The Pulumi ComponentResource wrapper which contains all of the + * staging resources. This can be added to the `dependsOn` of the main + * stack to ensure the staging assets are created first + */ + public abstract readonly stagingStack: CdkConstruct; } /** @@ -83,7 +94,7 @@ export interface PulumiSynthesizerOptions { * @see Recommended reading https://github.com/aws/aws-cdk/wiki/Security-And-Safety-Dev-Guide#controlling-the-permissions-used-by-cdk-deployments * @see https://docs.aws.amazon.com/cdk/api/v2/docs/app-staging-synthesizer-alpha-readme.html */ -export class PulumiSynthesizer extends cdk.StackSynthesizer implements cdk.IReusableStackSynthesizer { +export class PulumiSynthesizer extends PulumiSynthesizerBase implements cdk.IReusableStackSynthesizer { /** * The Pulumi ComponentResource wrapper which contains all of the * staging resources. This can be added to the `dependsOn` of the main @@ -159,14 +170,13 @@ export class PulumiSynthesizer extends cdk.StackSynthesizer implements cdk.IReus this.autoDeleteStagingAssets = props.autoDeleteStagingAssets ?? true; this.appId = this.validateAppId(props.appId); - // TODO: inherit the provider from the app component https://github.com/pulumi/pulumi-cdk/issues/181 - const account = aws.getCallerIdentity().then((id) => id.accountId); + const account = aws.getCallerIdentity({}, { parent: props.parent }).then((id) => id.accountId); this.pulumiAccount = pulumi.output(account); - const region = aws.getRegion().then((r) => r.name); + const region = aws.getRegion({}, { parent: props.parent }).then((r) => r.name); this.pulumiRegion = pulumi.output(region); const id = `${stackPrefix}-${this.appId}`; // create a wrapper component resource that we can depend on - this.stagingStack = new CdkConstruct(id, 'StagingStack', {}); + this.stagingStack = new CdkConstruct(id, 'StagingStack', { parent: props.parent }); this.stagingStack.done(); } diff --git a/src/types.ts b/src/types.ts index 1ff12951..7d0c3bf0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,10 +1,18 @@ import * as pulumi from '@pulumi/pulumi'; -import { Stack, StackProps } from 'aws-cdk-lib/core'; -import { CdkConstruct, ResourceMapping } from './interop'; import { Stack, StackProps, AppProps, App } from 'aws-cdk-lib/core'; -import { ResourceMapping } from './interop'; +import { CdkConstruct, ResourceMapping } from './interop'; +const STACK_SYMBOL = Symbol.for('@pulumi/cdk.Stack'); export abstract class PulumiStack extends Stack { + /** + * Return whether the given object is a Stack. + * + * We do attribute detection since we can't reliably use 'instanceof'. + * @internal + */ + public static isPulumiStack(x: any): x is Stack { + return x !== null && typeof x === 'object' && STACK_SYMBOL in x; + } /** * The collection of outputs from the AWS CDK Stack represented as Pulumi Outputs. * Each CfnOutput defined in the AWS CDK Stack will populate a value in the outputs. @@ -13,51 +21,19 @@ export abstract class PulumiStack extends Stack { constructor(app: App, name: string, options?: StackProps) { super(app, name, options); + Object.defineProperty(this, STACK_SYMBOL, { value: true }); } /** @internal */ registerOutput(outputId: string, output: any) { this.outputs[outputId] = pulumi.output(output); } } -/** - * Options specific to the Stack component. - */ -export interface AppOptions extends pulumi.ComponentResourceOptions { - /** - * Specify the CDK Stack properties to asociate with the stack. - */ - props?: AppProps; - /** - * Defines a mapping to override and/or provide an implementation for a CloudFormation resource - * type that is not (yet) implemented in the AWS Cloud Control API (and thus not yet available in - * the Pulumi AWS Native provider). Pulumi code can override this method to provide a custom mapping - * of CloudFormation elements and their properties into Pulumi CustomResources, commonly by using the - * AWS Classic provider to implement the missing resource. - * - * @param logicalId The logical ID of the resource being mapped. - * @param typeName The CloudFormation type name of the resource being mapped. - * @param props The bag of input properties to the CloudFormation resource being mapped. - * @param options The set of Pulumi ResourceOptions to apply to the resource being mapped. - * @returns An object containing one or more logical IDs mapped to Pulumi resources that must be - * created to implement the mapped CloudFormation resource, or else undefined if no mapping is - * implemented. - */ - remapCloudControlResource?( - logicalId: string, - typeName: string, - props: any, - options: pulumi.ResourceOptions, - ): ResourceMapping | undefined; -} -/** - * Options specific to the Stack component. - */ -export interface StackOptions extends pulumi.ComponentResourceOptions { +export interface AppOptions { /** * Specify the CDK Stack properties to asociate with the stack. */ - props?: StackProps; + props?: AppProps; /** * A unique identifier for the application that the asset staging stack belongs to. @@ -94,6 +70,12 @@ export interface StackOptions extends pulumi.ComponentResourceOptions { options: pulumi.ResourceOptions, ): ResourceMapping[] | undefined; } +/** + * Options specific to the Stack component. + */ +export interface AppResourceOptions extends pulumi.ComponentResourceOptions { + appOptions?: AppOptions; +} /** * The pulumi provider to read the schema from @@ -108,9 +90,8 @@ export enum PulumiProvider { * This exists because pulumicdk.Stack needs to extend cdk.Stack, but we also want it to represent a * pulumi ComponentResource so we create this `StackComponentResource` to hold the pulumi logic */ -export abstract class AppComponent extends pulumi.ComponentResource { - public abstract name: string; - +export interface AppComponent { + readonly name: string; /** * The directory to which cdk synthesizes the CloudAssembly * @internal @@ -120,16 +101,25 @@ export abstract class AppComponent extends pulumi.ComponentResource /** * The CDK stack associated with the component resource */ - public readonly stacks: { [artifactId: string]: PulumiStack } = {}; + readonly stacks: { [artifactId: string]: PulumiStack }; /** * @internal */ - public abstract appOptions?: AppOptions; + readonly component: pulumi.ComponentResource; - constructor(id: string, options?: AppOptions) { - super('cdk:index:App', id, options?.props, options); - } + /** + * @internal + */ + appOptions?: AppOptions; + + /** + * The Resources that the component resource depends on + * This will typically be the staging resources + * + * @internal + */ + readonly dependencies: CdkConstruct[]; } export type Mapping = { diff --git a/tests/assembly/manifest.test.ts b/tests/assembly/manifest.test.ts index dd500d05..ccce3fd0 100644 --- a/tests/assembly/manifest.test.ts +++ b/tests/assembly/manifest.test.ts @@ -147,7 +147,7 @@ describe('cloud assembly manifest reader', () => { 'test-stack/MyFunction1/Resource': 'MyFunction12A744C2E', 'test-stack/MyFunction1/ServiceRole/Resource': 'MyFunction1ServiceRole9852B06B', }, - outputs: {}, + outputs: undefined, parameters: undefined, resources: { MyFunction12A744C2E: { Properties: {}, Type: 'AWS::Lambda::Function' }, diff --git a/tests/basic.test.ts b/tests/basic.test.ts index bbb1cc4e..1970347d 100644 --- a/tests/basic.test.ts +++ b/tests/basic.test.ts @@ -13,25 +13,23 @@ // limitations under the License. import * as pulumi from '@pulumi/pulumi'; import * as s3 from 'aws-cdk-lib/aws-s3'; -import { App, Stack } from '../src/stack'; import * as output from '../src/output'; -import { promiseOf, setMocks } from './mocks'; +import { setMocks, testApp } from './mocks'; import { ApplicationLoadBalancer } from 'aws-cdk-lib/aws-elasticloadbalancingv2'; import { Vpc } from 'aws-cdk-lib/aws-ec2'; import { aws_ssm } from 'aws-cdk-lib'; +import { Construct } from 'constructs'; describe('Basic tests', () => { + setMocks(); test('Checking single resource registration', async () => { - const app = new App('testapp', (scope: App) => { - const s = new Stack(scope, 'teststack'); - new s3.Bucket(s, 'MyFirstBucket', { versioned: true }); + await testApp((scope: Construct) => { + new s3.Bucket(scope, 'MyFirstBucket', { versioned: true }); }); - const urn = await promiseOf(app.urn); - expect(urn).toEqual('urn:pulumi:stack::project::cdk:index:App::testapp'); }); test('LoadBalancer dnsName attribute does not throw', async () => { - const stack = testStack('test3', (scope) => { + await testApp((scope: Construct) => { const vpc = new Vpc(scope, 'vpc'); const alb = new ApplicationLoadBalancer(scope, 'alb', { vpc, @@ -44,16 +42,11 @@ describe('Basic tests', () => { stringValue: alb.loadBalancerDnsName, }); }); - const urn = await promiseOf(stack.urn); - expect(urn).toEqual('urn:pulumi:stack::project::cdk:index:Stack::test3'); }); test('Supports Output', async () => { const o = pulumi.output('the-bucket-name'); - const app = new App('testapp', (scope: App) => { - const s = new Stack(scope, 'teststack'); - new s3.Bucket(s, 'MyFirstBucket', { bucketName: output.asString(o) }); + await testApp((scope: Construct) => { + new s3.Bucket(scope, 'MyFirstBucket', { bucketName: output.asString(o) }); }); - const urn = await promiseOf(app.urn); - expect(urn).toEqual('urn:pulumi:stack::project::cdk:index:App::testapp'); }); }); diff --git a/tests/cdk-resource.test.ts b/tests/cdk-resource.test.ts index fdfa6523..9eb520da 100644 --- a/tests/cdk-resource.test.ts +++ b/tests/cdk-resource.test.ts @@ -1,14 +1,9 @@ import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'; import { TableArgs } from '@pulumi/aws-native/dynamodb'; import { Key } from 'aws-cdk-lib/aws-kms'; -import { setMocks, testStack } from './mocks'; +import { setMocks, testApp } from './mocks'; import { MockResourceArgs } from '@pulumi/pulumi/runtime'; -import { App, Stack } from '../src/stack'; -import { Key } from 'aws-cdk-lib/aws-kms'; -import { ApplicationLoadBalancer } from 'aws-cdk-lib/aws-elasticloadbalancingv2'; -import { Vpc } from 'aws-cdk-lib/aws-ec2'; -import { aws_ssm } from 'aws-cdk-lib'; -import { promiseOf, setMocks } from './mocks'; +import { Construct } from 'constructs'; describe('CDK Construct tests', () => { // DynamoDB table was previously mapped to the `aws` provider @@ -18,58 +13,9 @@ describe('CDK Construct tests', () => { const resources: MockResourceArgs[] = []; setMocks(resources); - setMocks((args) => { - if (args.type === 'aws-native:dynamodb:Table') { - expect(args.inputs).toEqual({ - keySchema: [ - { attributeName: 'pk', keyType: 'HASH' }, - { attributeName: 'sort', keyType: 'RANGE' }, - ], - sseSpecification: { - kmsMasterKeyId: 'arn:aws:kms:us-west-2:123456789012:key/abcdefg', - sseEnabled: true, - sseType: 'KMS', - }, - attributeDefinitions: [ - { attributeName: 'pk', attributeType: 'S' }, - { attributeName: 'sort', attributeType: 'S' }, - { attributeName: 'lsiSort', attributeType: 'S' }, - { attributeName: 'gsiKey', attributeType: 'S' }, - ], - provisionedThroughput: { - readCapacityUnits: 5, - writeCapacityUnits: 5, - }, - globalSecondaryIndexes: [ - { - provisionedThroughput: { - readCapacityUnits: 5, - writeCapacityUnits: 5, - }, - indexName: 'gsi', - keySchema: [{ attributeName: 'gsiKey', keyType: 'HASH' }], - projection: { - projectionType: 'ALL', - }, - }, - ], - localSecondaryIndexes: [ - { - projection: { projectionType: 'ALL' }, - keySchema: [ - { attributeName: 'pk', keyType: 'HASH' }, - { attributeName: 'lsiSort', keyType: 'RANGE' }, - ], - indexName: 'lsi', - }, - ], - } as TableArgs); - } - }); - const app = new App('testapp', (scope) => { - const stack = new Stack(scope, 'teststack'); - const key = Key.fromKeyArn(stack, 'key', 'arn:aws:kms:us-west-2:123456789012:key/abcdefg'); - const table = new dynamodb.Table(stack, 'Table', { + await testApp((scope: Construct) => { + const key = Key.fromKeyArn(scope, 'key', 'arn:aws:kms:us-west-2:123456789012:key/abcdefg'); + const table = new dynamodb.Table(scope, 'Table', { encryption: dynamodb.TableEncryption.CUSTOMER_MANAGED, encryptionKey: key, sortKey: { @@ -96,27 +42,51 @@ describe('CDK Construct tests', () => { }, }); }); - const urn = await promiseOf(app.urn); - expect(urn).toEqual('urn:pulumi:stack::project::cdk:index:App::testapp'); - }); - - test('LoadBalancer dnsName attribute does not throw', async () => { - setMocks((_args) => {}); - const app = new App('testapp', (scope) => { - const stack = new Stack(scope, 'teststack'); - const vpc = new Vpc(stack, 'vpc'); - const alb = new ApplicationLoadBalancer(stack, 'alb', { - vpc, - }); - - new aws_ssm.StringParameter(stack, 'param', { - // Referencing the `dnsName` attribute of the LoadBalancer resource. - // This tests that the reference is correctly mapped, otherwise this test - // throws an error - stringValue: alb.loadBalancerDnsName, - }); - }); - const urn = await promiseOf(app.urn); - expect(urn).toEqual('urn:pulumi:stack::project::cdk:index:App::testapp'); + const db = resources.find((res) => res.type === 'aws-native:dynamodb:Table'); + expect(db).toBeDefined(); + expect(db!.inputs).toEqual({ + keySchema: [ + { attributeName: 'pk', keyType: 'HASH' }, + { attributeName: 'sort', keyType: 'RANGE' }, + ], + sseSpecification: { + kmsMasterKeyId: 'arn:aws:kms:us-west-2:123456789012:key/abcdefg', + sseEnabled: true, + sseType: 'KMS', + }, + attributeDefinitions: [ + { attributeName: 'pk', attributeType: 'S' }, + { attributeName: 'sort', attributeType: 'S' }, + { attributeName: 'lsiSort', attributeType: 'S' }, + { attributeName: 'gsiKey', attributeType: 'S' }, + ], + provisionedThroughput: { + readCapacityUnits: 5, + writeCapacityUnits: 5, + }, + globalSecondaryIndexes: [ + { + provisionedThroughput: { + readCapacityUnits: 5, + writeCapacityUnits: 5, + }, + indexName: 'gsi', + keySchema: [{ attributeName: 'gsiKey', keyType: 'HASH' }], + projection: { + projectionType: 'ALL', + }, + }, + ], + localSecondaryIndexes: [ + { + projection: { projectionType: 'ALL' }, + keySchema: [ + { attributeName: 'pk', keyType: 'HASH' }, + { attributeName: 'lsiSort', keyType: 'RANGE' }, + ], + indexName: 'lsi', + }, + ], + } as TableArgs); }); }); diff --git a/tests/converters/app-converter.test.ts b/tests/converters/app-converter.test.ts index 5a140086..e76c8bbf 100644 --- a/tests/converters/app-converter.test.ts +++ b/tests/converters/app-converter.test.ts @@ -1,21 +1,25 @@ import { AppConverter, StackConverter } from '../../src/converters/app-converter'; import { Stack } from 'aws-cdk-lib/core'; -import { AppComponent, AppOptions } from '../../src/types'; +import { AppComponent, AppOptions, PulumiStack } from '../../src/types'; import * as path from 'path'; import * as mockfs from 'mock-fs'; import * as pulumi from '@pulumi/pulumi'; import { BucketPolicy } from '@pulumi/aws-native/s3'; import { createStackManifest } from '../utils'; import { promiseOf, setMocks } from '../mocks'; +import { CdkConstruct } from '../../src/interop'; -class MockStackComponent extends AppComponent { +class MockAppComponent extends pulumi.ComponentResource implements AppComponent { public readonly name = 'stack'; public readonly assemblyDir: string; + stacks: { [artifactId: string]: PulumiStack } = {}; + dependencies: CdkConstruct[] = []; + component: pulumi.ComponentResource; public stack: Stack; public appOptions?: AppOptions | undefined; constructor(dir: string) { - super('stack'); + super('cdk:index:App', 'stack'); this.assemblyDir = dir; this.registerOutputs(); } @@ -178,7 +182,7 @@ describe('App Converter', () => { mockfs.restore(); }); test('can convert', async () => { - const mockStackComponent = new MockStackComponent('/tmp/foo/bar/does/not/exist'); + const mockStackComponent = new MockAppComponent('/tmp/foo/bar/does/not/exist'); const converter = new AppConverter(mockStackComponent); converter.convert(); const stacks = Array.from(converter.stacks.values()); @@ -246,7 +250,7 @@ describe('App Converter', () => { ])( 'intrinsics %s', async (_name, stackManifest, expected) => { - const mockStackComponent = new MockStackComponent('/tmp/foo/bar/does/not/exist'); + const mockStackComponent = new MockAppComponent('/tmp/foo/bar/does/not/exist'); const converter = new StackConverter(mockStackComponent, stackManifest); converter.convert(new Set()); const promises = Array.from(converter.resources.values()).flatMap((res) => promiseOf(res.resource.urn)); diff --git a/tests/converters/artifact-converter.test.ts b/tests/converters/artifact-converter.test.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/mocks.ts b/tests/mocks.ts index 792ea2cd..50048113 100644 --- a/tests/mocks.ts +++ b/tests/mocks.ts @@ -1,12 +1,46 @@ import * as pulumi from '@pulumi/pulumi'; -import { MockCallArgs, MockResourceArgs, setMockOptions } from '@pulumi/pulumi/runtime'; -import { MockMonitor } from '@pulumi/pulumi/runtime/mocks'; +import { MockCallArgs, MockResourceArgs } from '@pulumi/pulumi/runtime'; +import { Construct } from 'constructs'; +import { App, Stack } from '../src/stack'; // Convert a pulumi.Output to a promise of the same type. export function promiseOf(output: pulumi.Output): Promise { return new Promise((resolve) => output.apply(resolve)); } +export async function testApp(fn: (scope: Construct) => void) { + class TestStack extends Stack { + constructor(app: App, id: string) { + super(app, id, { + props: { + env: { + region: 'us-east-1', + account: '12345678912', + }, + }, + }); + + fn(this); + } + + get availabilityZones(): string[] { + return ['us-east-1a', 'us-east-1b']; + } + } + + const app = new App('testapp', (scope: App) => { + new TestStack(scope, 'teststack'); + }); + const converter = await app.converter; + await Promise.all( + Array.from(converter.stacks.values()).flatMap((stackConverter) => { + return Array.from(stackConverter.constructs.values()).flatMap((v) => promiseOf(v.urn)); + }), + ); + await promiseOf(app.urn); + await Promise.all(app.dependencies.flatMap((d) => promiseOf(d.urn))); +} + export function setMocks(resources?: MockResourceArgs[]) { const mocks: pulumi.runtime.Mocks = { call: (args: MockCallArgs): { [id: string]: any } => { diff --git a/tests/synthesizer.test.ts b/tests/synthesizer.test.ts index b9082868..4ef2d54c 100644 --- a/tests/synthesizer.test.ts +++ b/tests/synthesizer.test.ts @@ -1,6 +1,6 @@ import * as path from 'path'; import { Asset } from 'aws-cdk-lib/aws-s3-assets'; -import { setMocks, testStack } from './mocks'; +import { setMocks, testApp } from './mocks'; import { MockResourceArgs } from '@pulumi/pulumi/runtime'; import { CfnBucket } from 'aws-cdk-lib/aws-s3'; @@ -9,7 +9,7 @@ describe('Synthesizer', () => { const resources: MockResourceArgs[] = []; setMocks(resources); - await testStack((scope) => { + await testApp((scope) => { new CfnBucket(scope, 'Bucket'); }); expect(resources).toEqual([ @@ -28,7 +28,7 @@ describe('Synthesizer', () => { const resources: MockResourceArgs[] = []; setMocks(resources); - await testStack((scope) => { + await testApp((scope) => { new CfnBucket(scope, 'Bucket'); new Asset(scope, 'asset', { path: path.join(__dirname, 'synthesizer.test.ts'), @@ -74,7 +74,7 @@ describe('Synthesizer', () => { const resources: MockResourceArgs[] = []; setMocks(resources); - await testStack((scope) => { + await testApp((scope) => { new CfnBucket(scope, 'Bucket'); new Asset(scope, 'deploy-time-asset', { deployTime: true,